@reconcrap/boss-recommend-mcp 2.0.11 → 2.0.13

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.11",
3
+ "version": "2.0.13",
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
@@ -71,6 +71,43 @@ const TERMINAL_STATUSES = new Set([
71
71
  RUN_STATUS_CANCELED
72
72
  ]);
73
73
 
74
+ const ARTIFACT_STATUSES = new Set([
75
+ RUN_STATUS_COMPLETED,
76
+ RUN_STATUS_FAILED,
77
+ RUN_STATUS_CANCELED,
78
+ RUN_STATUS_PAUSED
79
+ ]);
80
+
81
+ const STALE_PROCESS_STATUSES = new Set([
82
+ "queued",
83
+ "running",
84
+ RUN_STATUS_CANCELING
85
+ ]);
86
+
87
+ const CHAT_REQUEST_RESUME_ACTIONS = new Set([
88
+ "request_cv",
89
+ "ask_cv",
90
+ "request_resume",
91
+ "求简历",
92
+ "索要简历"
93
+ ]);
94
+
95
+ const CHAT_DISABLE_REQUEST_RESUME_ACTIONS = new Set([
96
+ "none",
97
+ "no",
98
+ "false",
99
+ "off",
100
+ "skip",
101
+ "do_nothing",
102
+ "nothing",
103
+ "不做",
104
+ "什么都不做",
105
+ "无",
106
+ "不用",
107
+ "不求简历",
108
+ "不请求简历"
109
+ ]);
110
+
74
111
  let chatWorkflowImpl = runChatWorkflow;
75
112
  let chatConnectorImpl = connectChatChromeSession;
76
113
  let chatJobReaderImpl = readChatJobOptionsFromSession;
@@ -156,8 +193,13 @@ function readJsonFile(filePath) {
156
193
  }
157
194
  }
158
195
 
159
- function selectedChatJobForCsv(meta = {}) {
160
- const job = normalizeText(meta.normalized?.job || meta.args?.job || "");
196
+ function selectedChatJobForCsv(meta = {}, snapshot = {}) {
197
+ const job = normalizeText(
198
+ meta.normalized?.job
199
+ || meta.args?.job
200
+ || snapshot.context?.job
201
+ || ""
202
+ );
161
203
  return {
162
204
  value: job,
163
205
  title: job,
@@ -167,31 +209,32 @@ function selectedChatJobForCsv(meta = {}) {
167
209
 
168
210
  function buildChatCsvInputRows(snapshot = {}, meta = {}) {
169
211
  const normalized = meta.normalized || {};
170
- const postAction = shouldRequestChatResume(meta.args)
212
+ const context = snapshot.context || {};
213
+ const postAction = shouldRequestChatResume(meta.args, context)
171
214
  ? "request_cv"
172
215
  : normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
173
216
  const searchParams = {
174
- job: normalized.job || meta.args?.job || "",
175
- start_from: normalized.startFrom || meta.args?.start_from || "",
217
+ job: normalized.job || meta.args?.job || context.job || "",
218
+ start_from: normalized.startFrom || meta.args?.start_from || context.start_from || "",
176
219
  target_count: normalized.publicTargetCount ?? normalized.targetCount ?? snapshot.progress?.target_count ?? "",
177
- detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || snapshot.context?.detail_source || ""
220
+ detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || context.detail_source || ""
178
221
  };
179
222
  return buildLegacyScreenInputRows({
180
223
  instruction: meta.args?.instruction || "启动boss聊天任务",
181
224
  selectedPage: "chat",
182
- selectedJob: selectedChatJobForCsv(meta),
225
+ selectedJob: selectedChatJobForCsv(meta, snapshot),
183
226
  userSearchParams: cloneReportInput(searchParams, {}),
184
227
  effectiveSearchParams: cloneReportInput(searchParams, {}),
185
228
  screenParams: {
186
- criteria: normalized.criteria || meta.args?.criteria || "",
229
+ criteria: normalized.criteria || meta.args?.criteria || context.criteria || "",
187
230
  target_count: searchParams.target_count,
188
231
  post_action: postAction,
189
232
  max_greet_count: meta.args?.max_greet_count ?? ""
190
233
  },
191
234
  followUp: meta.args?.follow_up || null,
192
235
  extraRows: [
193
- ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT],
194
- ["chat_params.profile", normalized.profile || meta.args?.profile || "default"]
236
+ ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
237
+ ["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"]
195
238
  ]
196
239
  });
197
240
  }
@@ -314,6 +357,12 @@ function ensureChatRunArtifacts(snapshot) {
314
357
  partial: true,
315
358
  partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
316
359
  results: checkpointResults
360
+ } : ARTIFACT_STATUSES.has(snapshot?.status || snapshot?.state) ? {
361
+ domain: "chat",
362
+ partial: (snapshot?.status || snapshot?.state) !== RUN_STATUS_COMPLETED,
363
+ partial_reason: snapshot?.status || snapshot?.state || "unknown",
364
+ completion_reason: completionReason(snapshot?.status || snapshot?.state),
365
+ results: []
317
366
  } : null);
318
367
  if (artifactSummary) {
319
368
  const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
@@ -337,6 +386,143 @@ function ensureChatRunArtifacts(snapshot) {
337
386
  return artifacts;
338
387
  }
339
388
 
389
+ function isPidAlive(pid) {
390
+ const numericPid = Number(pid);
391
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
392
+ if (numericPid === process.pid) return true;
393
+ try {
394
+ process.kill(numericPid, 0);
395
+ return true;
396
+ } catch (error) {
397
+ return error?.code === "EPERM";
398
+ }
399
+ }
400
+
401
+ function snapshotFromPersistedChatRun(persisted = {}) {
402
+ return {
403
+ runId: persisted.run_id || persisted.runId,
404
+ name: persisted.name || persisted.run_id || persisted.runId,
405
+ status: persisted.status || persisted.state,
406
+ phase: persisted.stage || persisted.phase,
407
+ progress: persisted.progress || {},
408
+ context: persisted.context || {},
409
+ checkpoint: persisted.checkpoint || {},
410
+ startedAt: persisted.started_at || persisted.startedAt,
411
+ updatedAt: persisted.updated_at || persisted.updatedAt,
412
+ completedAt: persisted.completed_at || persisted.completedAt || null,
413
+ error: persisted.error || null,
414
+ summary: persisted.summary || null
415
+ };
416
+ }
417
+
418
+ function persistDiskChatRun(runId, payload) {
419
+ const artifacts = getChatRunArtifacts(runId);
420
+ if (!artifacts) return payload;
421
+ writeJsonAtomic(artifacts.run_state_path, payload);
422
+ return payload;
423
+ }
424
+
425
+ function attachLegacyArtifactsToPersistedChatRun(persisted = {}) {
426
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
427
+ if (!runId) return persisted;
428
+ const snapshot = snapshotFromPersistedChatRun(persisted);
429
+ const result = buildLegacyChatResult(snapshot);
430
+ const artifacts = getChatRunArtifacts(runId);
431
+ const next = {
432
+ ...persisted,
433
+ result,
434
+ resume: {
435
+ ...(persisted.resume || {}),
436
+ checkpoint_path: result?.checkpoint_path || persisted.resume?.checkpoint_path || artifacts?.checkpoint_path || null,
437
+ output_csv: result?.output_csv || persisted.resume?.output_csv || artifacts?.output_csv || null
438
+ },
439
+ artifacts: artifacts || persisted.artifacts || null
440
+ };
441
+ return persistDiskChatRun(runId, next);
442
+ }
443
+
444
+ function finalizePersistedChatRun(persisted = {}, {
445
+ status = RUN_STATUS_FAILED,
446
+ error = null,
447
+ message = ""
448
+ } = {}) {
449
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
450
+ if (!runId) return persisted;
451
+ const now = new Date().toISOString();
452
+ const normalizedError = status === RUN_STATUS_FAILED
453
+ ? {
454
+ name: error?.name || "Error",
455
+ code: error?.code || "STALE_RUN_PROCESS_EXITED",
456
+ message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
457
+ }
458
+ : null;
459
+ const next = {
460
+ ...persisted,
461
+ run_id: runId,
462
+ state: status,
463
+ status,
464
+ stage: persisted.stage || persisted.phase || "chat:stale",
465
+ updated_at: now,
466
+ heartbeat_at: now,
467
+ completed_at: persisted.completed_at || now,
468
+ last_message: normalizedError?.message || message || status,
469
+ control: {
470
+ ...(persisted.control || {}),
471
+ cancel_requested: false
472
+ },
473
+ error: normalizedError,
474
+ summary: persisted.summary || null
475
+ };
476
+ return attachLegacyArtifactsToPersistedChatRun(next);
477
+ }
478
+
479
+ function persistedChatRunArtifactMissing(persisted = {}) {
480
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
481
+ const artifacts = getChatRunArtifacts(runId);
482
+ const outputCsv = persisted.result?.output_csv
483
+ || persisted.resume?.output_csv
484
+ || persisted.artifacts?.output_csv
485
+ || artifacts?.output_csv;
486
+ const reportJson = persisted.result?.report_json
487
+ || persisted.artifacts?.report_json
488
+ || artifacts?.report_json;
489
+ return Boolean(
490
+ !outputCsv
491
+ || !reportJson
492
+ || !fs.existsSync(outputCsv)
493
+ || !fs.existsSync(reportJson)
494
+ );
495
+ }
496
+
497
+ function reconcilePersistedChatRun(persisted = {}, { cancelStale = false } = {}) {
498
+ const status = persisted.status || persisted.state;
499
+ if (STALE_PROCESS_STATUSES.has(status) && !isPidAlive(persisted.pid)) {
500
+ const shouldCancel = cancelStale || status === RUN_STATUS_CANCELING || persisted.control?.cancel_requested === true;
501
+ return {
502
+ run: finalizePersistedChatRun(persisted, {
503
+ status: shouldCancel ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
504
+ error: shouldCancel ? null : {
505
+ code: "STALE_RUN_PROCESS_EXITED",
506
+ message: `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
507
+ },
508
+ message: shouldCancel
509
+ ? "Boss chat run was canceled after its worker process was no longer active."
510
+ : `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
511
+ }),
512
+ stale_finalized: true
513
+ };
514
+ }
515
+ if (ARTIFACT_STATUSES.has(status) && persistedChatRunArtifactMissing(persisted)) {
516
+ return {
517
+ run: attachLegacyArtifactsToPersistedChatRun(persisted),
518
+ artifacts_repaired: true
519
+ };
520
+ }
521
+ return {
522
+ run: persisted
523
+ };
524
+ }
525
+
340
526
  function buildLegacyChatResult(snapshot) {
341
527
  if (!snapshot) return null;
342
528
  const artifacts = ensureChatRunArtifacts(snapshot);
@@ -791,14 +977,32 @@ async function buildNeedInputResponse({ args, missingFields, normalized }) {
791
977
  };
792
978
  }
793
979
 
794
- function shouldRequestChatResume(args = {}) {
795
- return (
980
+ function shouldRequestChatResume(args = {}, context = {}) {
981
+ const action = normalizeText(args.post_action || args.action).toLowerCase();
982
+ if (
983
+ args.request_cv === false
984
+ || args.request_resume === false
985
+ || args.ask_cv === false
986
+ || args.execute_post_action === false
987
+ || args.no_request_cv === true
988
+ || args.no_request_resume === true
989
+ || CHAT_DISABLE_REQUEST_RESUME_ACTIONS.has(action)
990
+ ) {
991
+ return false;
992
+ }
993
+ if (
796
994
  args.request_cv === true
797
995
  || args.request_resume === true
798
996
  || args.ask_cv === true
799
997
  || args.execute_post_action === true
800
- || ["request_cv", "ask_cv", "request_resume", "求简历", "索要简历"].includes(normalizeText(args.post_action || args.action))
801
- );
998
+ || CHAT_REQUEST_RESUME_ACTIONS.has(action)
999
+ ) {
1000
+ return true;
1001
+ }
1002
+ if (typeof context.request_resume_for_passed === "boolean") {
1003
+ return context.request_resume_for_passed;
1004
+ }
1005
+ return true;
802
1006
  }
803
1007
 
804
1008
  function isDebugTestMode(args = {}) {
@@ -1300,12 +1504,15 @@ export function getBossChatRunTool({ args = {} } = {}) {
1300
1504
  } catch {
1301
1505
  const persisted = readChatRunState(runId);
1302
1506
  if (persisted) {
1507
+ const reconciled = reconcilePersistedChatRun(persisted);
1303
1508
  return {
1304
1509
  status: "RUN_STATUS",
1305
- run: persisted,
1510
+ run: reconciled.run,
1306
1511
  persistence: {
1307
1512
  source: "disk",
1308
- active_control_available: false
1513
+ active_control_available: false,
1514
+ stale_finalized: reconciled.stale_finalized === true,
1515
+ artifacts_repaired: reconciled.artifacts_repaired === true
1309
1516
  },
1310
1517
  runtime_evaluate_used: false,
1311
1518
  method_summary: {},
@@ -1354,9 +1561,10 @@ export function pauseBossChatRunTool({ args = {} } = {}) {
1354
1561
  } catch {
1355
1562
  const persisted = readChatRunState(runId);
1356
1563
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1564
+ const reconciled = reconcilePersistedChatRun(persisted);
1357
1565
  return {
1358
1566
  status: "PAUSE_IGNORED",
1359
- run: persisted,
1567
+ run: reconciled.run,
1360
1568
  message: "目标任务已结束,无需暂停。",
1361
1569
  runtime_evaluate_used: false,
1362
1570
  method_summary: {},
@@ -1412,19 +1620,23 @@ export function resumeBossChatRunTool({ args = {} } = {}) {
1412
1620
  } catch {
1413
1621
  const persisted = readChatRunState(runId);
1414
1622
  if (persisted) {
1623
+ const reconciled = reconcilePersistedChatRun(persisted);
1624
+ const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
1415
1625
  return {
1416
1626
  status: "FAILED",
1417
1627
  error: {
1418
- code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1419
- message: TERMINAL_STATUSES.has(persisted.state)
1628
+ code: TERMINAL_STATUSES.has(reconciledStatus) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1629
+ message: TERMINAL_STATUSES.has(reconciledStatus)
1420
1630
  ? "目标任务已结束,无法继续。"
1421
1631
  : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1422
- retryable: !TERMINAL_STATUSES.has(persisted.state)
1632
+ retryable: !TERMINAL_STATUSES.has(reconciledStatus)
1423
1633
  },
1424
- run: persisted,
1634
+ run: reconciled.run,
1425
1635
  persistence: {
1426
1636
  source: "disk",
1427
- active_control_available: false
1637
+ active_control_available: false,
1638
+ stale_finalized: reconciled.stale_finalized === true,
1639
+ artifacts_repaired: reconciled.artifacts_repaired === true
1428
1640
  },
1429
1641
  runtime_evaluate_used: false,
1430
1642
  method_summary: {},
@@ -1458,9 +1670,10 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
1458
1670
  } catch {
1459
1671
  const persisted = readChatRunState(runId);
1460
1672
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1673
+ const reconciled = reconcilePersistedChatRun(persisted);
1461
1674
  return {
1462
1675
  status: "CANCEL_IGNORED",
1463
- run: persisted,
1676
+ run: reconciled.run,
1464
1677
  message: "目标任务已结束,无需取消。",
1465
1678
  runtime_evaluate_used: false,
1466
1679
  method_summary: {},
@@ -1468,6 +1681,26 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
1468
1681
  chrome: null
1469
1682
  };
1470
1683
  }
1684
+ if (persisted) {
1685
+ const reconciled = reconcilePersistedChatRun(persisted, { cancelStale: true });
1686
+ if (reconciled.stale_finalized) {
1687
+ return {
1688
+ status: "CANCEL_REQUESTED",
1689
+ run: reconciled.run,
1690
+ message: "该 run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
1691
+ persistence: {
1692
+ source: "disk",
1693
+ active_control_available: false,
1694
+ stale_finalized: true,
1695
+ artifacts_repaired: reconciled.artifacts_repaired === true
1696
+ },
1697
+ runtime_evaluate_used: false,
1698
+ method_summary: {},
1699
+ method_log: [],
1700
+ chrome: null
1701
+ };
1702
+ }
1703
+ }
1471
1704
  return getBossChatRunTool({ args });
1472
1705
  }
1473
1706
  }
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
9
  assertNoForbiddenCdpCalls,
10
+ buildBossChromeLaunchArgs,
10
11
  bringPageToFront,
11
12
  connectToChromeTarget,
12
13
  enableDomains,
@@ -1994,14 +1995,7 @@ async function launchChrome(options = {}) {
1994
1995
  }
1995
1996
 
1996
1997
  const userDataDir = getChromeUserDataDir(port);
1997
- const args = [
1998
- `--remote-debugging-port=${port}`,
1999
- `--user-data-dir=${userDataDir}`,
2000
- "--no-first-run",
2001
- "--no-default-browser-check",
2002
- "--new-window",
2003
- bossUrl
2004
- ];
1998
+ const args = buildBossChromeLaunchArgs({ port, userDataDir, url: bossUrl });
2005
1999
  const child = spawn(chromePath, args, {
2006
2000
  detached: true,
2007
2001
  stdio: "ignore",
@@ -7,6 +7,12 @@ import CDP from "chrome-remote-interface";
7
7
  export const DEFAULT_CHROME_HOST = "127.0.0.1";
8
8
  export const DEFAULT_CHROME_PORT = 9222;
9
9
  export const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
+ export const LID_CLOSED_SAFE_CHROME_ARGS = [
11
+ "--disable-backgrounding-occluded-windows",
12
+ "--disable-background-timer-throttling",
13
+ "--disable-renderer-backgrounding",
14
+ "--disable-features=CalculateNativeWinOcclusion"
15
+ ];
10
16
 
11
17
  export const ALLOWED_CDP_DOMAINS = new Set([
12
18
  "Accessibility",
@@ -208,6 +214,33 @@ export function getBossChromeUserDataDir(port = DEFAULT_CHROME_PORT) {
208
214
  return sharedPath;
209
215
  }
210
216
 
217
+ function parseExtraChromeArgs(value = "") {
218
+ return String(value || "")
219
+ .split(/\s+/)
220
+ .map((item) => item.trim())
221
+ .filter(Boolean);
222
+ }
223
+
224
+ export function buildBossChromeLaunchArgs({
225
+ port = DEFAULT_CHROME_PORT,
226
+ userDataDir = "",
227
+ url = "about:blank",
228
+ extraArgs = []
229
+ } = {}) {
230
+ const args = [
231
+ `--remote-debugging-port=${port}`,
232
+ `--user-data-dir=${userDataDir}`,
233
+ "--no-first-run",
234
+ "--no-default-browser-check",
235
+ ...LID_CLOSED_SAFE_CHROME_ARGS,
236
+ ...parseExtraChromeArgs(process.env.BOSS_MCP_EXTRA_CHROME_ARGS),
237
+ ...extraArgs,
238
+ "--new-window",
239
+ url
240
+ ];
241
+ return Array.from(new Set(args.filter(Boolean)));
242
+ }
243
+
211
244
  export async function waitForChromeDebugPort({
212
245
  host = DEFAULT_CHROME_HOST,
213
246
  port = DEFAULT_CHROME_PORT,
@@ -250,14 +283,7 @@ export async function launchChromeDebugInstance({
250
283
  throw new Error("Chrome executable not found. Set BOSS_MCP_CHROME_PATH or BOSS_RECOMMEND_CHROME_PATH.");
251
284
  }
252
285
  const userDataDir = getBossChromeUserDataDir(port);
253
- const args = [
254
- `--remote-debugging-port=${port}`,
255
- `--user-data-dir=${userDataDir}`,
256
- "--no-first-run",
257
- "--no-default-browser-check",
258
- "--new-window",
259
- url
260
- ];
286
+ const args = buildBossChromeLaunchArgs({ port, userDataDir, url });
261
287
  const child = spawn(chromePath, args, {
262
288
  detached: true,
263
289
  stdio: "ignore",
@@ -277,6 +303,7 @@ export async function launchChromeDebugInstance({
277
303
  launched: true,
278
304
  chrome_path: chromePath,
279
305
  user_data_dir: userDataDir,
306
+ launch_args: args,
280
307
  port,
281
308
  url,
282
309
  readiness: {