@reconcrap/boss-recommend-mcp 2.0.20 → 2.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/chat-mcp.js CHANGED
@@ -114,7 +114,8 @@ let chatConnectorImpl = connectChatChromeSession;
114
114
  let chatJobReaderImpl = readChatJobOptionsFromSession;
115
115
  let chatRunService = createChatRunService({
116
116
  idPrefix: "mcp_chat",
117
- workflow: (...args) => chatWorkflowImpl(...args)
117
+ workflow: (...args) => chatWorkflowImpl(...args),
118
+ onSnapshot: persistChatLifecycleSnapshot
118
119
  });
119
120
  const chatRunMeta = new Map();
120
121
 
@@ -387,6 +388,17 @@ function ensureChatRunArtifacts(snapshot) {
387
388
  return artifacts;
388
389
  }
389
390
 
391
+ function persistChatCheckpointSnapshot(normalized) {
392
+ const artifacts = getChatRunArtifacts(normalized?.run_id || normalized?.runId);
393
+ if (!artifacts) return;
394
+ const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
395
+ ? normalized.checkpoint
396
+ : {};
397
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
398
+ const meta = getChatRunMeta(normalized?.run_id || normalized?.runId);
399
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
400
+ }
401
+
390
402
  function isPidAlive(pid) {
391
403
  const numericPid = Number(pid);
392
404
  if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
@@ -565,6 +577,7 @@ function buildLegacyChatResult(snapshot) {
565
577
  function normalizeRunSnapshot(snapshot) {
566
578
  if (!snapshot) return null;
567
579
  const meta = getChatRunMeta(snapshot.runId);
580
+ const artifacts = getChatRunArtifacts(snapshot.runId);
568
581
  const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
569
582
  const progress = normalizeLegacyProgress(snapshot.progress, summary);
570
583
  const legacyResult = (
@@ -606,23 +619,28 @@ function normalizeRunSnapshot(snapshot) {
606
619
  cancel_requested: snapshot.status === RUN_STATUS_CANCELING
607
620
  },
608
621
  resume: {
609
- checkpoint_path: legacyResult?.checkpoint_path || null,
610
- pause_control_path: getChatRunArtifacts(snapshot.runId)?.run_state_path || null,
622
+ checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
623
+ pause_control_path: artifacts?.run_state_path || null,
611
624
  output_csv: legacyResult?.output_csv || null,
612
625
  resume_count: meta.resumeCount || 0,
613
626
  last_resumed_at: meta.lastResumedAt || null,
614
627
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
615
628
  },
616
629
  result: legacyResult,
617
- artifacts: getChatRunArtifacts(snapshot.runId)
630
+ artifacts
618
631
  };
619
632
  }
620
633
 
621
- function persistChatRunSnapshot(snapshot) {
634
+ function persistChatRunSnapshot(snapshot, {
635
+ persistActiveCheckpoint = false
636
+ } = {}) {
622
637
  const normalized = normalizeRunSnapshot(snapshot);
623
638
  if (!normalized?.run_id) return normalized;
624
639
  const artifacts = getChatRunArtifacts(normalized.run_id);
625
640
  if (!artifacts) return normalized;
641
+ if (persistActiveCheckpoint) {
642
+ persistChatCheckpointSnapshot(normalized);
643
+ }
626
644
  const payload = {
627
645
  run_id: normalized.run_id,
628
646
  mode: normalized.mode,
@@ -648,6 +666,12 @@ function persistChatRunSnapshot(snapshot) {
648
666
  return normalized;
649
667
  }
650
668
 
669
+ function persistChatLifecycleSnapshot(snapshot, event = {}) {
670
+ return persistChatRunSnapshot(snapshot, {
671
+ persistActiveCheckpoint: event?.type === "checkpoint"
672
+ });
673
+ }
674
+
651
675
  function attachMethodEvidence(payload, runId) {
652
676
  const meta = getChatRunMeta(runId);
653
677
  assertNoForbiddenCdpCalls(meta.methodLog || []);
@@ -1729,7 +1753,8 @@ export function __setChatMcpWorkflowForTests(nextWorkflow) {
1729
1753
  chatWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runChatWorkflow;
1730
1754
  chatRunService = createChatRunService({
1731
1755
  idPrefix: "mcp_chat",
1732
- workflow: (...args) => chatWorkflowImpl(...args)
1756
+ workflow: (...args) => chatWorkflowImpl(...args),
1757
+ onSnapshot: persistChatLifecycleSnapshot
1733
1758
  });
1734
1759
  }
1735
1760
 
@@ -65,10 +65,24 @@ function snapshotFromEntry(entry) {
65
65
 
66
66
  export function createRunLifecycleManager({
67
67
  idPrefix = "run",
68
- now = nowIso
68
+ now = nowIso,
69
+ onSnapshot = null
69
70
  } = {}) {
70
71
  const runs = new Map();
71
72
 
73
+ function emitSnapshot(entry, event = {}) {
74
+ if (typeof onSnapshot !== "function") return;
75
+ try {
76
+ onSnapshot(snapshotFromEntry(entry), {
77
+ type: event.type || "update",
78
+ at: now(),
79
+ ...event
80
+ });
81
+ } catch {
82
+ // Snapshot hooks must never interrupt an active browser run.
83
+ }
84
+ }
85
+
72
86
  function getEntry(runId) {
73
87
  const entry = runs.get(runId);
74
88
  if (!entry) throw new Error(`Unknown runId: ${runId}`);
@@ -83,6 +97,7 @@ export function createRunLifecycleManager({
83
97
  entry.run.status = status;
84
98
  Object.assign(entry.run, patch);
85
99
  touch(entry);
100
+ emitSnapshot(entry, { type: "status", status });
86
101
  }
87
102
 
88
103
  function createControls(entry) {
@@ -104,6 +119,7 @@ export function createRunLifecycleManager({
104
119
  ...progressPatch
105
120
  };
106
121
  touch(entry);
122
+ emitSnapshot(entry, { type: "progress", progressPatch });
107
123
  return snapshotFromEntry(entry);
108
124
  },
109
125
  checkpoint(checkpointPatch = {}) {
@@ -113,6 +129,7 @@ export function createRunLifecycleManager({
113
129
  updatedAt: now()
114
130
  };
115
131
  touch(entry);
132
+ emitSnapshot(entry, { type: "checkpoint", checkpointPatch });
116
133
  return snapshotFromEntry(entry);
117
134
  },
118
135
  async waitIfPaused() {
@@ -231,6 +248,7 @@ export function createRunLifecycleManager({
231
248
  entry.pauseRequested = true;
232
249
  if (entry.run.status === RUN_STATUS_RUNNING) {
233
250
  touch(entry);
251
+ emitSnapshot(entry, { type: "pause_requested" });
234
252
  }
235
253
  return snapshotFromEntry(entry);
236
254
  }
@@ -246,6 +264,7 @@ export function createRunLifecycleManager({
246
264
  setStatus(entry, RUN_STATUS_RUNNING);
247
265
  } else {
248
266
  touch(entry);
267
+ emitSnapshot(entry, { type: "resume_requested" });
249
268
  }
250
269
  return snapshotFromEntry(entry);
251
270
  }
@@ -328,12 +328,25 @@ function isRequestedResumeText(text = "") {
328
328
  return Boolean(
329
329
  normalized === "已求简历"
330
330
  || normalized === "已索要简历"
331
- || normalized === "已申请"
332
- || normalized === "已发送"
333
331
  || normalized.includes("已求简历")
334
332
  || normalized.includes("已索要简历")
335
- || normalized.includes("已申请")
336
- || normalized.includes("已发送")
333
+ || normalized.includes("简历请求已发送")
334
+ || normalized.includes("已发送简历")
335
+ || (normalized.includes("已申请") && normalized.includes("简历"))
336
+ );
337
+ }
338
+
339
+ function isRequestedResumeControlTarget(target = {}) {
340
+ const label = normalizeDetailText(target.label);
341
+ const className = String(target.attributes?.class || "");
342
+ const selector = String(target.selector || "");
343
+ const controlLike = /\boperate-btn\b|operate|resume|button|btn/i.test(`${selector} ${className}`);
344
+ if (isRequestedResumeText(label)) return true;
345
+ return controlLike && Boolean(
346
+ label === "已申请"
347
+ || label === "已发送"
348
+ || label.includes("已申请")
349
+ || label.includes("已发送")
337
350
  );
338
351
  }
339
352
 
@@ -861,7 +874,7 @@ export async function readChatConversationReadyState(client) {
861
874
  client,
862
875
  rootState.roots,
863
876
  CHAT_ASK_RESUME_BUTTON_SELECTORS,
864
- (target) => isRequestedResumeText(target.label)
877
+ (target) => isRequestedResumeControlTarget(target)
865
878
  );
866
879
  const editor = await findVisibleMatchingTarget(
867
880
  client,
@@ -1564,9 +1564,10 @@ export async function runChatWorkflow({
1564
1564
  export function createChatRunService({
1565
1565
  lifecycle,
1566
1566
  idPrefix = "chat",
1567
- workflow = runChatWorkflow
1567
+ workflow = runChatWorkflow,
1568
+ onSnapshot = null
1568
1569
  } = {}) {
1569
- const manager = lifecycle || createRunLifecycleManager({ idPrefix });
1570
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
1570
1571
 
1571
1572
  function startChatRun({
1572
1573
  client,
@@ -957,9 +957,10 @@ export async function runRecommendWorkflow({
957
957
  export function createRecommendRunService({
958
958
  lifecycle,
959
959
  idPrefix = "recommend",
960
- workflow = runRecommendWorkflow
960
+ workflow = runRecommendWorkflow,
961
+ onSnapshot = null
961
962
  } = {}) {
962
- const manager = lifecycle || createRunLifecycleManager({ idPrefix });
963
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
963
964
 
964
965
  function startRecommendRun({
965
966
  client,
@@ -619,9 +619,10 @@ export async function runRecruitWorkflow({
619
619
  export function createRecruitRunService({
620
620
  lifecycle,
621
621
  idPrefix = "recruit",
622
- workflow = runRecruitWorkflow
622
+ workflow = runRecruitWorkflow,
623
+ onSnapshot = null
623
624
  } = {}) {
624
- const manager = lifecycle || createRunLifecycleManager({ idPrefix });
625
+ const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
625
626
 
626
627
  function startRecruitRun({
627
628
  client,
@@ -52,7 +52,7 @@ import {
52
52
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
53
53
  const DEFAULT_RECOMMEND_PORT = 9222;
54
54
  const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
55
- const TARGET_COUNT_SEMANTICS = "target_count means processed recommend candidates, not passed candidates";
55
+ const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
56
56
  const RUN_MODE_ASYNC = "async";
57
57
 
58
58
  const TERMINAL_STATUSES = new Set([
@@ -66,7 +66,8 @@ let recommendConnectorImpl = connectRecommendChromeSession;
66
66
  let recommendJobReaderImpl = readRecommendJobOptionsFromSession;
67
67
  let recommendRunService = createRecommendRunService({
68
68
  idPrefix: "mcp_recommend",
69
- workflow: (...args) => recommendWorkflowImpl(...args)
69
+ workflow: (...args) => recommendWorkflowImpl(...args),
70
+ onSnapshot: persistRecommendLifecycleSnapshot
70
71
  });
71
72
  const recommendRunMeta = new Map();
72
73
 
@@ -328,6 +329,17 @@ function ensureRecommendRunArtifacts(snapshot) {
328
329
  return artifacts;
329
330
  }
330
331
 
332
+ function persistRecommendCheckpointSnapshot(normalized) {
333
+ const artifacts = getRecommendRunArtifacts(normalized?.run_id || normalized?.runId);
334
+ if (!artifacts) return;
335
+ const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
336
+ ? normalized.checkpoint
337
+ : {};
338
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
339
+ const meta = getRecommendRunMeta(normalized?.run_id || normalized?.runId);
340
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
341
+ }
342
+
331
343
  function buildLegacyRecommendResult(snapshot) {
332
344
  if (!snapshot) return null;
333
345
  const artifacts = ensureRecommendRunArtifacts(snapshot);
@@ -390,6 +402,7 @@ function buildLegacyRecommendResult(snapshot) {
390
402
  function normalizeRunSnapshot(snapshot) {
391
403
  if (!snapshot) return null;
392
404
  const meta = getRecommendRunMeta(snapshot.runId);
405
+ const artifacts = getRecommendRunArtifacts(snapshot.runId);
393
406
  const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
394
407
  const progress = normalizeLegacyProgress(snapshot.progress, summary);
395
408
  const legacyResult = (
@@ -429,23 +442,28 @@ function normalizeRunSnapshot(snapshot) {
429
442
  cancel_requested: snapshot.status === RUN_STATUS_CANCELING
430
443
  },
431
444
  resume: {
432
- checkpoint_path: legacyResult?.checkpoint_path || null,
433
- pause_control_path: getRecommendRunArtifacts(snapshot.runId)?.run_state_path || null,
445
+ checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
446
+ pause_control_path: artifacts?.run_state_path || null,
434
447
  output_csv: legacyResult?.output_csv || null,
435
448
  resume_count: meta.resumeCount || 0,
436
449
  last_resumed_at: meta.lastResumedAt || null,
437
450
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
438
451
  },
439
452
  result: legacyResult,
440
- artifacts: getRecommendRunArtifacts(snapshot.runId)
453
+ artifacts
441
454
  };
442
455
  }
443
456
 
444
- function persistRecommendRunSnapshot(snapshot) {
457
+ function persistRecommendRunSnapshot(snapshot, {
458
+ persistActiveCheckpoint = false
459
+ } = {}) {
445
460
  const normalized = normalizeRunSnapshot(snapshot);
446
461
  if (!normalized?.run_id) return normalized;
447
462
  const artifacts = getRecommendRunArtifacts(normalized.run_id);
448
463
  if (!artifacts) return normalized;
464
+ if (persistActiveCheckpoint) {
465
+ persistRecommendCheckpointSnapshot(normalized);
466
+ }
449
467
  const payload = {
450
468
  run_id: normalized.run_id,
451
469
  mode: normalized.mode,
@@ -471,6 +489,12 @@ function persistRecommendRunSnapshot(snapshot) {
471
489
  return normalized;
472
490
  }
473
491
 
492
+ function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
493
+ return persistRecommendRunSnapshot(snapshot, {
494
+ persistActiveCheckpoint: event?.type === "checkpoint"
495
+ });
496
+ }
497
+
474
498
  function attachMethodEvidence(payload, runId) {
475
499
  const meta = getRecommendRunMeta(runId);
476
500
  assertNoForbiddenCdpCalls(meta.methodLog || []);
@@ -1440,7 +1464,8 @@ export function __setRecommendMcpWorkflowForTests(nextWorkflow) {
1440
1464
  recommendWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecommendWorkflow;
1441
1465
  recommendRunService = createRecommendRunService({
1442
1466
  idPrefix: "mcp_recommend",
1443
- workflow: (...args) => recommendWorkflowImpl(...args)
1467
+ workflow: (...args) => recommendWorkflowImpl(...args),
1468
+ onSnapshot: persistRecommendLifecycleSnapshot
1444
1469
  });
1445
1470
  }
1446
1471
 
@@ -42,7 +42,7 @@ const RUN_MODE_SYNC = "sync";
42
42
  const DEFAULT_RECRUIT_POLL_AFTER_SEC = 10;
43
43
  const DEFAULT_RECRUIT_HOST = "127.0.0.1";
44
44
  const DEFAULT_RECRUIT_PORT = 9222;
45
- const TARGET_COUNT_SEMANTICS = "target_count means processed candidate count, not passed candidate count";
45
+ const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
46
46
  const DEFAULT_RECRUIT_HOME_DIR = ".boss-recruit-mcp";
47
47
 
48
48
  const TERMINAL_STATUSES = new Set([
@@ -55,7 +55,8 @@ let recruitWorkflowImpl = runRecruitWorkflow;
55
55
  let recruitConnectorImpl = connectRecruitChromeSession;
56
56
  let recruitRunService = createRecruitRunService({
57
57
  idPrefix: "mcp_recruit",
58
- workflow: (...args) => recruitWorkflowImpl(...args)
58
+ workflow: (...args) => recruitWorkflowImpl(...args),
59
+ onSnapshot: persistRecruitLifecycleSnapshot
59
60
  });
60
61
  const recruitRunMeta = new Map();
61
62
 
@@ -253,6 +254,17 @@ function ensureRecruitRunArtifacts(snapshot) {
253
254
  return artifacts;
254
255
  }
255
256
 
257
+ function persistRecruitCheckpointSnapshot(normalized) {
258
+ const artifacts = getRecruitRunArtifacts(normalized?.run_id || normalized?.runId);
259
+ if (!artifacts) return;
260
+ const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
261
+ ? normalized.checkpoint
262
+ : {};
263
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
264
+ const meta = getRecruitRunMeta(normalized?.run_id || normalized?.runId);
265
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
266
+ }
267
+
256
268
  function toIsoOrNull(value) {
257
269
  const normalized = normalizeText(value);
258
270
  return normalized || null;
@@ -565,6 +577,7 @@ function evaluateRecruitPipelineGate(parsed) {
565
577
  function normalizeRunSnapshot(snapshot) {
566
578
  if (!snapshot) return null;
567
579
  const meta = getRecruitRunMeta(snapshot.runId);
580
+ const artifacts = getRecruitRunArtifacts(snapshot.runId);
568
581
  const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
569
582
  const progress = normalizeLegacyProgress(snapshot.progress, summary);
570
583
  const legacyResult = (
@@ -603,27 +616,33 @@ function normalizeRunSnapshot(snapshot) {
603
616
  cancel_requested: snapshot.status === RUN_STATUS_CANCELING
604
617
  },
605
618
  resume: {
606
- checkpoint_path: legacyResult?.checkpoint_path || null,
607
- pause_control_path: getRecruitRunArtifacts(snapshot.runId)?.run_state_path || null,
619
+ checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
620
+ pause_control_path: artifacts?.run_state_path || null,
608
621
  output_csv: legacyResult?.output_csv || null,
609
622
  resume_count: meta.resumeCount || 0,
610
623
  last_resumed_at: meta.lastResumedAt || null,
611
624
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
612
625
  },
613
626
  result: legacyResult,
614
- artifacts: getRecruitRunArtifacts(snapshot.runId)
627
+ artifacts
615
628
  };
616
629
  }
617
630
 
618
- function persistRecruitRunSnapshot(snapshot) {
631
+ function persistRecruitRunSnapshot(snapshot, {
632
+ persistActiveCheckpoint = false
633
+ } = {}) {
619
634
  const normalized = normalizeRunSnapshot(snapshot);
620
635
  if (!normalized?.run_id) return normalized;
621
636
  const artifacts = getRecruitRunArtifacts(normalized.run_id);
622
637
  if (!artifacts) return normalized;
638
+ if (persistActiveCheckpoint) {
639
+ persistRecruitCheckpointSnapshot(normalized);
640
+ }
623
641
  const payload = {
624
642
  run_id: normalized.run_id,
625
643
  mode: normalized.mode,
626
644
  state: normalized.state,
645
+ status: normalized.status,
627
646
  stage: normalized.stage,
628
647
  started_at: normalized.started_at,
629
648
  updated_at: normalized.updated_at,
@@ -644,6 +663,12 @@ function persistRecruitRunSnapshot(snapshot) {
644
663
  return normalized;
645
664
  }
646
665
 
666
+ function persistRecruitLifecycleSnapshot(snapshot, event = {}) {
667
+ return persistRecruitRunSnapshot(snapshot, {
668
+ persistActiveCheckpoint: event?.type === "checkpoint"
669
+ });
670
+ }
671
+
647
672
  function getRecruitRunMeta(runId) {
648
673
  return recruitRunMeta.get(runId) || {};
649
674
  }
@@ -1248,7 +1273,8 @@ export function __setRecruitMcpWorkflowForTests(nextWorkflow) {
1248
1273
  recruitWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecruitWorkflow;
1249
1274
  recruitRunService = createRecruitRunService({
1250
1275
  idPrefix: "mcp_recruit",
1251
- workflow: (...args) => recruitWorkflowImpl(...args)
1276
+ workflow: (...args) => recruitWorkflowImpl(...args),
1277
+ onSnapshot: persistRecruitLifecycleSnapshot
1252
1278
  });
1253
1279
  }
1254
1280