@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 +1 -1
- package/src/domains/recommend/detail.js +61 -18
- package/src/index.js +148 -9
- package/src/recommend-mcp.js +4 -0
- package/src/run-state.js +2 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
135
|
-
|
|
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 =
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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 {
|
package/src/recommend-mcp.js
CHANGED
|
@@ -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 || ""),
|