@reconcrap/boss-recommend-mcp 2.0.37 → 2.0.38

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.37",
3
+ "version": "2.0.38",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -131,21 +131,49 @@ export async function waitForRecommendDetail(client, {
131
131
  const started = Date.now();
132
132
  let lastState = null;
133
133
  while (Date.now() - started <= timeoutMs) {
134
- const rootState = await getRecommendRoots(client);
135
- const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
136
- const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
137
- lastState = {
138
- iframe: rootState.iframe,
139
- roots: rootState.roots,
140
- popup,
141
- resumeIframe
142
- };
143
- if (popup || resumeIframe) return lastState;
134
+ lastState = await readRecommendDetailState(client);
135
+ if (lastState?.popup || lastState?.resumeIframe) return lastState;
144
136
  await sleep(intervalMs);
145
137
  }
146
138
  return lastState;
147
139
  }
148
140
 
141
+ async function readRecommendDetailState(client) {
142
+ const rootState = await getRecommendRoots(client);
143
+ const popup = await findVisibleDetailTarget(client, rootState.roots, DETAIL_POPUP_SELECTORS);
144
+ const resumeIframe = await findVisibleDetailTarget(client, rootState.roots, DETAIL_RESUME_IFRAME_SELECTORS);
145
+ return {
146
+ iframe: rootState.iframe,
147
+ roots: rootState.roots,
148
+ popup,
149
+ resumeIframe
150
+ };
151
+ }
152
+
153
+ export async function waitForRecommendDetailClosed(client, {
154
+ timeoutMs = 4000,
155
+ intervalMs = 250
156
+ } = {}) {
157
+ const started = Date.now();
158
+ let lastState = null;
159
+ while (Date.now() - started <= timeoutMs) {
160
+ lastState = await readRecommendDetailState(client);
161
+ if (!lastState?.popup && !lastState?.resumeIframe) {
162
+ return {
163
+ closed: true,
164
+ elapsed_ms: Date.now() - started,
165
+ state: lastState
166
+ };
167
+ }
168
+ await sleep(intervalMs);
169
+ }
170
+ return {
171
+ closed: false,
172
+ elapsed_ms: Date.now() - started,
173
+ state: lastState
174
+ };
175
+ }
176
+
149
177
  async function findVisibleDetailTarget(client, roots, selectors) {
150
178
  for (const root of roots) {
151
179
  if (!root?.nodeId) continue;
@@ -397,7 +425,9 @@ export async function openRecommendCardDetailWithFreshRetry(client, {
397
425
  }
398
426
 
399
427
  export async function closeRecommendDetail(client, {
400
- attemptsLimit = 3
428
+ attemptsLimit = 4,
429
+ closeWaitMs = 5000,
430
+ escapeWaitMs = 3500
401
431
  } = {}) {
402
432
  const attempts = [];
403
433
  for (let index = 0; index < attemptsLimit; index += 1) {
@@ -433,15 +463,21 @@ export async function closeRecommendDetail(client, {
433
463
  await pressEscape(client);
434
464
  attempts.push({ mode: "Escape-after-close-selector-error" });
435
465
  }
436
- await sleep(700);
437
466
  } else {
438
467
  await pressEscape(client);
439
468
  attempts.push({ mode: "Escape" });
440
- await sleep(700);
441
469
  }
442
470
 
443
- let state = await waitForRecommendDetail(client, { timeoutMs: 1000 });
444
- if (!state?.popup && !state?.resumeIframe) {
471
+ const closedAfterClick = await waitForRecommendDetailClosed(client, {
472
+ timeoutMs: closeWaitMs,
473
+ intervalMs: 250
474
+ });
475
+ attempts.push({
476
+ mode: "wait-closed-after-primary",
477
+ closed: closedAfterClick.closed,
478
+ elapsed_ms: closedAfterClick.elapsed_ms
479
+ });
480
+ if (closedAfterClick.closed) {
445
481
  return {
446
482
  closed: true,
447
483
  attempts
@@ -450,10 +486,17 @@ export async function closeRecommendDetail(client, {
450
486
 
451
487
  await pressEscape(client);
452
488
  attempts.push({ mode: "Escape-fallback" });
453
- await sleep(700);
454
489
 
455
- state = await waitForRecommendDetail(client, { timeoutMs: 1000 });
456
- if (!state?.popup && !state?.resumeIframe) {
490
+ const closedAfterEscape = await waitForRecommendDetailClosed(client, {
491
+ timeoutMs: escapeWaitMs,
492
+ intervalMs: 250
493
+ });
494
+ attempts.push({
495
+ mode: "wait-closed-after-escape",
496
+ closed: closedAfterEscape.closed,
497
+ elapsed_ms: closedAfterEscape.elapsed_ms
498
+ });
499
+ if (closedAfterEscape.closed) {
457
500
  return {
458
501
  closed: true,
459
502
  attempts
package/src/index.js CHANGED
@@ -295,7 +295,9 @@ function getRunArtifacts(runId) {
295
295
  const normalizedRunId = normalizeText(runId);
296
296
  return {
297
297
  run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
298
- checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
298
+ checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`),
299
+ worker_stdout_path: path.join(getRunsDir(), `${normalizedRunId}.worker.stdout.log`),
300
+ worker_stderr_path: path.join(getRunsDir(), `${normalizedRunId}.worker.stderr.log`)
299
301
  };
300
302
  }
301
303
 
@@ -1554,18 +1556,145 @@ function launchDetachedRunWorker({ runId, resumeRun = false }) {
1554
1556
  if (resumeRun) {
1555
1557
  childArgs.push(DETACHED_WORKER_RESUME_FLAG);
1556
1558
  }
1557
- const child = spawnProcessImpl(process.execPath, childArgs, {
1558
- detached: true,
1559
- stdio: "ignore",
1560
- windowsHide: true,
1561
- env: process.env
1562
- });
1559
+ const artifacts = getRunArtifacts(runId);
1560
+ fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
1561
+ const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
1562
+ const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
1563
+ let child;
1564
+ try {
1565
+ child = spawnProcessImpl(process.execPath, childArgs, {
1566
+ detached: true,
1567
+ stdio: ["ignore", stdoutFd, stderrFd],
1568
+ windowsHide: true,
1569
+ env: process.env
1570
+ });
1571
+ } finally {
1572
+ fs.closeSync(stdoutFd);
1573
+ fs.closeSync(stderrFd);
1574
+ }
1575
+ child.workerLogPaths = {
1576
+ stdoutPath: artifacts.worker_stdout_path,
1577
+ stderrPath: artifacts.worker_stderr_path
1578
+ };
1563
1579
  if (typeof child?.unref === "function") {
1564
1580
  child.unref();
1565
1581
  }
1566
1582
  return child;
1567
1583
  }
1568
1584
 
1585
+ function errorToDetachedWorkerPayload(error, fallbackMessage = "detached recommend worker exited unexpectedly") {
1586
+ const message = normalizeText(error?.message || error || fallbackMessage) || fallbackMessage;
1587
+ const payload = {
1588
+ code: normalizeText(error?.code || "") || "RUN_WORKER_UNHANDLED_EXCEPTION",
1589
+ message,
1590
+ retryable: true
1591
+ };
1592
+ if (normalizeText(error?.name || "")) {
1593
+ payload.name = normalizeText(error.name);
1594
+ }
1595
+ if (normalizeText(error?.stack || "")) {
1596
+ payload.stack = String(error.stack).slice(0, 8000);
1597
+ }
1598
+ return payload;
1599
+ }
1600
+
1601
+ function markDetachedWorkerFailed(runId, error, options = {}) {
1602
+ const normalizedRunId = normalizeText(runId);
1603
+ if (!normalizedRunId) return null;
1604
+ const existing = readRawRunState(normalizedRunId) || {};
1605
+ const existingState = normalizeText(existing.state || existing.status);
1606
+ if (TERMINAL_RUN_STATES.has(existingState)) return existing;
1607
+
1608
+ const now = new Date().toISOString();
1609
+ const artifacts = getRunArtifacts(normalizedRunId);
1610
+ const errorPayload = {
1611
+ ...errorToDetachedWorkerPayload(error, options.message),
1612
+ ...(options.code ? { code: options.code } : {})
1613
+ };
1614
+ const previousResult = existing.result && typeof existing.result === "object" ? existing.result : {};
1615
+ const result = {
1616
+ ...previousResult,
1617
+ status: "FAILED",
1618
+ completion_reason: "failed",
1619
+ error: errorPayload,
1620
+ worker_stdout_path: artifacts.worker_stdout_path,
1621
+ worker_stderr_path: artifacts.worker_stderr_path
1622
+ };
1623
+
1624
+ return writeRawRunState(normalizedRunId, {
1625
+ ...existing,
1626
+ run_id: normalizedRunId,
1627
+ mode: existing.mode || RUN_MODE_ASYNC,
1628
+ state: RUN_STATE_FAILED,
1629
+ status: RUN_STATE_FAILED,
1630
+ stage: existing.stage || RUN_STAGE_PREFLIGHT,
1631
+ started_at: existing.started_at || now,
1632
+ updated_at: now,
1633
+ heartbeat_at: now,
1634
+ completed_at: now,
1635
+ pid: Number.isInteger(existing.pid) && existing.pid > 0 ? existing.pid : process.pid,
1636
+ progress: existing.progress || {},
1637
+ last_message: errorPayload.message,
1638
+ context: existing.context || {},
1639
+ control: existing.control || {
1640
+ pause_requested: false,
1641
+ pause_requested_at: null,
1642
+ pause_requested_by: null,
1643
+ cancel_requested: false
1644
+ },
1645
+ resume: {
1646
+ ...(existing.resume && typeof existing.resume === "object" ? existing.resume : {}),
1647
+ checkpoint_path: existing.resume?.checkpoint_path || artifacts.checkpoint_path,
1648
+ pause_control_path: existing.resume?.pause_control_path || artifacts.run_state_path,
1649
+ worker_stdout_path: artifacts.worker_stdout_path,
1650
+ worker_stderr_path: artifacts.worker_stderr_path
1651
+ },
1652
+ artifacts: {
1653
+ ...(existing.artifacts && typeof existing.artifacts === "object" ? existing.artifacts : {}),
1654
+ worker_stdout_path: artifacts.worker_stdout_path,
1655
+ worker_stderr_path: artifacts.worker_stderr_path
1656
+ },
1657
+ error: errorPayload,
1658
+ result
1659
+ });
1660
+ }
1661
+
1662
+ function installDetachedWorkerFailureHandlers(runId) {
1663
+ let handled = false;
1664
+ const failOnce = (error, options = {}) => {
1665
+ if (handled) return;
1666
+ handled = true;
1667
+ try {
1668
+ markDetachedWorkerFailed(runId, error, options);
1669
+ } catch (markError) {
1670
+ console.error("[boss-recommend-mcp] failed to persist detached worker failure", markError);
1671
+ }
1672
+ };
1673
+
1674
+ process.on("uncaughtException", (error) => {
1675
+ console.error("[boss-recommend-mcp] detached worker uncaught exception", error);
1676
+ failOnce(error, { code: "RUN_WORKER_UNCAUGHT_EXCEPTION" });
1677
+ process.exit(1);
1678
+ });
1679
+
1680
+ process.on("unhandledRejection", (reason) => {
1681
+ console.error("[boss-recommend-mcp] detached worker unhandled rejection", reason);
1682
+ const error = reason instanceof Error ? reason : new Error(normalizeText(reason) || "Unhandled promise rejection");
1683
+ failOnce(error, { code: "RUN_WORKER_UNHANDLED_REJECTION" });
1684
+ process.exit(1);
1685
+ });
1686
+
1687
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
1688
+ process.on(signal, () => {
1689
+ const error = new Error(`detached recommend worker received ${signal}`);
1690
+ console.error("[boss-recommend-mcp] detached worker received signal", signal);
1691
+ failOnce(error, { code: "RUN_WORKER_SIGNAL" });
1692
+ const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
1693
+ process.exit(signalExitCodes[signal] || 1);
1694
+ });
1695
+ }
1696
+ }
1697
+
1569
1698
  function buildWorkerLaunchFailedPayload(message) {
1570
1699
  return {
1571
1700
  status: "FAILED",
@@ -1993,7 +2122,14 @@ async function handleStartRunTool({ workspaceRoot, args }) {
1993
2122
  let worker;
1994
2123
  try {
1995
2124
  worker = launchDetachedRunWorker({ runId });
1996
- safeUpdateRunState(runId, { pid: worker.pid || process.pid });
2125
+ const workerLogPaths = worker.workerLogPaths || {};
2126
+ safeUpdateRunState(runId, {
2127
+ pid: worker.pid || process.pid,
2128
+ resume: {
2129
+ worker_stdout_path: workerLogPaths.stdoutPath || getRunArtifacts(runId).worker_stdout_path,
2130
+ worker_stderr_path: workerLogPaths.stderrPath || getRunArtifacts(runId).worker_stderr_path
2131
+ }
2132
+ });
1997
2133
  } catch (error) {
1998
2134
  const failed = buildWorkerLaunchFailedPayload(error?.message || "无法启动 detached recommend worker。");
1999
2135
  safeUpdateRunState(runId, {
@@ -2602,6 +2738,7 @@ const thisFilePath = fileURLToPath(import.meta.url);
2602
2738
  if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
2603
2739
  const detachedWorkerOptions = parseDetachedWorkerOptions(process.argv.slice(2));
2604
2740
  if (detachedWorkerOptions) {
2741
+ installDetachedWorkerFailureHandlers(detachedWorkerOptions.runId);
2605
2742
  runDetachedWorker({
2606
2743
  runId: detachedWorkerOptions.runId,
2607
2744
  resumeRun: detachedWorkerOptions.resumeRun
@@ -2609,7 +2746,9 @@ if (process.argv[1] && path.resolve(process.argv[1]) === thisFilePath) {
2609
2746
  if (!result?.ok) {
2610
2747
  process.exitCode = 1;
2611
2748
  }
2612
- }).catch(() => {
2749
+ }).catch((error) => {
2750
+ console.error("[boss-recommend-mcp] detached worker failed", error);
2751
+ markDetachedWorkerFailed(detachedWorkerOptions.runId, error);
2613
2752
  process.exitCode = 1;
2614
2753
  });
2615
2754
  } else {
@@ -165,6 +165,8 @@ function getRecommendRunArtifacts(runId) {
165
165
  output_dir: outputDir,
166
166
  run_state_path: path.join(runsDir, `${normalized}.json`),
167
167
  checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
168
+ worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
169
+ worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
168
170
  output_csv: path.join(outputDir, `${normalized}.results.csv`),
169
171
  report_json: path.join(outputDir, `${normalized}.report.json`)
170
172
  };
@@ -557,6 +559,8 @@ function normalizeRunSnapshot(snapshot) {
557
559
  checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
558
560
  pause_control_path: artifacts?.run_state_path || null,
559
561
  output_csv: legacyResult?.output_csv || null,
562
+ worker_stdout_path: artifacts?.worker_stdout_path || null,
563
+ worker_stderr_path: artifacts?.worker_stderr_path || null,
560
564
  resume_count: meta.resumeCount || 0,
561
565
  last_resumed_at: meta.lastResumedAt || null,
562
566
  last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
package/src/run-state.js CHANGED
@@ -160,6 +160,8 @@ function defaultResume(resume = {}) {
160
160
  checkpoint_path: normalizeMessage(resume?.checkpoint_path || ""),
161
161
  pause_control_path: normalizeMessage(resume?.pause_control_path || ""),
162
162
  output_csv: normalizeMessage(resume?.output_csv || ""),
163
+ worker_stdout_path: normalizeMessage(resume?.worker_stdout_path || ""),
164
+ worker_stderr_path: normalizeMessage(resume?.worker_stderr_path || ""),
163
165
  follow_up_phase: normalizeMessage(resume?.follow_up_phase || ""),
164
166
  chat_run_id: normalizeMessage(resume?.chat_run_id || ""),
165
167
  chat_state: normalizeMessage(resume?.chat_state || ""),