@reconcrap/boss-recommend-mcp 2.1.8 → 2.1.10
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/README.md +11 -9
- package/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +20 -9
- package/src/cli.js +38 -5
- package/src/core/run/index.js +32 -25
- package/src/index.js +282 -54
- package/src/recommend-mcp.js +195 -60
package/README.md
CHANGED
|
@@ -82,12 +82,13 @@ curl -fsSL https://raw.githubusercontent.com/reconcrap-cpu/boss-recommend-mcp/ma
|
|
|
82
82
|
MCP 工具:
|
|
83
83
|
|
|
84
84
|
- `list_recommend_jobs`(只读读取推荐页岗位下拉框,返回可直接用于 cron/一次性任务的 `job_names`)
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
85
|
+
- `run_recommend`(`start_recommend_pipeline_run` 的短别名;用户已经确认且要现在启动时优先调用)
|
|
86
|
+
- `start_recommend_pipeline_run`(异步启动;先经过前置门禁,通过后返回 `ACCEPTED + run_id`)
|
|
87
|
+
- `prepare_recommend_pipeline_run`(只校验完整 payload;不启动筛选。主要用于显式预检或定时任务前校验;若现在运行,READY 后继续调用 `run_recommend` / `start_recommend_pipeline_run`)
|
|
88
|
+
- `schedule_recommend_pipeline_run`(只用于用户明确要求稍后/cron/定时;保存已 READY 的完整 payload,启动 detached scheduler,到点后直接调用 `start_recommend_pipeline_run`)
|
|
87
89
|
- `get_recommend_scheduled_run`(查询 package-owned 定时任务;到点后会显示内层 `run_id` 和 run 快照)
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
90
|
-
- `get_recommend_pipeline_run`(轮询 run_id 状态)
|
|
90
|
+
- `get_recommend_pipeline_run`(用已知 `run_id` 轮询状态)
|
|
91
|
+
- `list_recommend_pipeline_runs`(只读列出最近 run 摘要并返回 `latest_run`;忘记 `run_id` 时用它恢复,不要读磁盘 JSON)
|
|
91
92
|
- `cancel_recommend_pipeline_run`(取消运行中任务)
|
|
92
93
|
- `pause_recommend_pipeline_run`(请求暂停 run;会在当前候选人处理完成后进入 paused)
|
|
93
94
|
- `resume_recommend_pipeline_run`(继续 paused run;沿用同 run_id 与同 CSV)
|
|
@@ -265,7 +266,7 @@ BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列
|
|
|
265
266
|
BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
|
|
266
267
|
```
|
|
267
268
|
|
|
268
|
-
推荐运行入口是 MCP 工具 `run_recommend` / `start_recommend_pipeline_run`。在 Trae/Trae-CN 这类普通 MCP
|
|
269
|
+
推荐运行入口是 MCP 工具 `run_recommend` / `start_recommend_pipeline_run`。在 Trae/Trae-CN 这类普通 MCP 宿主中,用户完成总确认并要求现在运行时,应直接调用 `run_recommend` 或 `start_recommend_pipeline_run`。`prepare_recommend_pipeline_run` 只做显式预检或定时任务前校验;如果已经调用过 prepare 且返回 `READY + cron_ready=true`,下一步仍然必须调用 `run_recommend` / `start_recommend_pipeline_run`,不要改用 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC,也不要用短延迟 `schedule_recommend_pipeline_run` 冒充立即启动。`prepare` 能返回结果就证明该宿主已经可以调用本 MCP server。
|
|
269
270
|
|
|
270
271
|
只有宿主是 QClaw 这类真正 shell-only agent、没有把 MCP tools 暴露给模型、且当前会话从未成功调用过 `boss-recommend/prepare_recommend_pipeline_run` 时,才使用 CDP-only CLI fallback。CLI fallback 也必须显式传本次用户确认的 rest level:
|
|
271
272
|
|
|
@@ -275,7 +276,7 @@ npx -y @reconcrap/boss-recommend-mcp@latest run --detached --instruction-file bo
|
|
|
275
276
|
|
|
276
277
|
`--detached` 会让父进程输出 `ACCEPTED + run_id` 后退出,子进程继续持有 Chrome DevTools 会话并执行长任务。岗位发现可以使用只读 CLI:
|
|
277
278
|
|
|
278
|
-
|
|
279
|
+
如果用户明确要求稍后启动/cron/定时任务,不要手写系统 cron;用 package-owned scheduler:
|
|
279
280
|
|
|
280
281
|
```bash
|
|
281
282
|
npx -y @reconcrap/boss-recommend-mcp@latest prepare-run --instruction-file boss-recommend-instruction.txt --overrides-file boss-recommend-overrides.json --confirmation-file boss-recommend-confirmation.json --rest-level <low|medium|high> --slow-live --port 9222
|
|
@@ -477,10 +478,11 @@ Trae-CN / 长对话防循环建议:
|
|
|
477
478
|
|
|
478
479
|
说明:
|
|
479
480
|
|
|
480
|
-
- `run_recommend` 与 `start_recommend_pipeline_run` 是同一个异步 MCP
|
|
481
|
-
- `prepare_recommend_pipeline_run` / `boss-recommend-mcp prepare-run` 只做参数门禁;它不启动筛选。普通 MCP
|
|
481
|
+
- `run_recommend` 与 `start_recommend_pipeline_run` 是同一个异步 MCP 启动入口,但不会跳过同步确认流程;普通 MCP 宿主现在运行时优先直接调用它们。
|
|
482
|
+
- `prepare_recommend_pipeline_run` / `boss-recommend-mcp prepare-run` 只做参数门禁;它不启动筛选。普通 MCP 宿主只有在显式预检或定时任务准备时才需要先调用 prepare;prepare READY 后继续调用 `run_recommend` / `start_recommend_pipeline_run`,不要改用 CLI fallback。
|
|
482
483
|
- `prepare_recommend_pipeline_run` 的 READY 响应会带 `prepared_only=true`、`run_started=false`、`recommended_next_tool=start_recommend_pipeline_run`、`alternate_next_tool=run_recommend` 和 `next_action.do_not_call_prepare_again=true`;agent 应直接照这个字段继续下一步。
|
|
483
484
|
- `schedule_recommend_pipeline_run` / `boss-recommend-mcp schedule-run` 是推荐页定时启动的唯一推荐路径;它创建真实 package-owned detached scheduler,并返回 `schedule_id`。
|
|
485
|
+
- 如果忘记了 `run_id`,调用 `list_recommend_pipeline_runs` 获取 `latest_run.run_id`;不要用 PowerShell、`Get-Content`、terminal、CLI 或手动读取 `~/.boss-recommend-mcp/runs/*.json` 来恢复状态。
|
|
484
486
|
- 定时心跳默认 120 秒一次;`updated_at` 仍会在阶段或进度变化时刷新。
|
|
485
487
|
- 每个 run 会持久化到 `~/.boss-recommend-mcp/runs/<run_id>.json`(可通过 `BOSS_RECOMMEND_HOME` 覆盖)。
|
|
486
488
|
- screen 阶段会持久化 checkpoint:`~/.boss-recommend-mcp/runs/<run_id>.checkpoint.json`。
|
package/package.json
CHANGED
|
@@ -18,9 +18,11 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
18
18
|
- recommend 失败时(如 `JOB_TRIGGER_NOT_FOUND/NO_RECOMMEND_IFRAME/BOSS_LOGIN_REQUIRED`)禁止降级到 recruit;先修 recommend 页面就绪/登录态。
|
|
19
19
|
|
|
20
20
|
- **Trae-CN / 原生 MCP 启动强制规则**
|
|
21
|
+
- 用户完成总确认并要求“现在启动”时,优先直接调用 `boss-recommend/run_recommend` 或 `boss-recommend/start_recommend_pipeline_run`;不要为了即时启动先调用 prepare。
|
|
21
22
|
- 如果当前会话里已经成功调用过 `boss-recommend/prepare_recommend_pipeline_run`,说明 Trae-CN 已经具备原生 MCP tool call 能力;下一步必须继续调用 `boss-recommend/start_recommend_pipeline_run` 或 `boss-recommend/run_recommend`。
|
|
22
23
|
- `prepare_recommend_pipeline_run` 返回 `READY` 只代表参数校验通过,不代表已经启动;严禁再次调用 prepare 试图启动。
|
|
23
24
|
- 在 Trae-CN、Trae、Codex、Cursor、Claude Code 等普通 MCP 宿主中,严禁用 `run_command`、终端、PowerShell、`npx ... run --detached`、手写 `tools/call` JSON-RPC 或任何 CLI fallback 启动 recommend run。
|
|
25
|
+
- 用户要“现在启动”时,严禁用 `schedule_recommend_pipeline_run` 加短延迟冒充立即启动;schedule 只用于用户明确要求稍后/cron/定时。
|
|
24
26
|
- 不要说“prepare 覆盖了 MCP run 调用”。正确说法是:prepare 没有启动,下一步是原生 MCP tool call。
|
|
25
27
|
|
|
26
28
|
- **确认不可代填(强制)**
|
|
@@ -99,24 +101,32 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
99
101
|
- 用途:当用户需要为 cron / 一次性自动任务提前填写完整参数时,先用它读取推荐页岗位下拉框的全部可用岗位名;默认会复用/自动打开本机 9222 Chrome 并导航到推荐页。
|
|
100
102
|
- 输出:优先把 `job_names` 里的值作为后续 `overrides.job` / `confirmation.job_value`。
|
|
101
103
|
- 限制:只读岗位列表,不启动筛选任务;若返回 `BOSS_LOGIN_REQUIRED`,必须让用户在自动打开的 Chrome 完成登录后重试,本次 cron 不得创建。
|
|
104
|
+
- 主工具:`run_recommend` / `start_recommend_pipeline_run`
|
|
105
|
+
- 用途:用户完成最终总确认并要“现在启动”时调用;这是即时运行的首选入口,不需要先 prepare。
|
|
106
|
+
- 必填:`instruction`
|
|
107
|
+
- 关键输入:
|
|
108
|
+
- `confirmation`:新流程只需要 `{ "final_confirmed": true }`;旧版 `page_confirmed/page_value/.../job_confirmed/job_value` 仍兼容但不要主动制造逐项确认。
|
|
109
|
+
- `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
|
|
110
|
+
- `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
|
|
111
|
+
- 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
|
|
112
|
+
- 若返回 `ACCEPTED + run_id`:记录 `run_id` 并停止本轮,不自动轮询。
|
|
113
|
+
- 若返回 `NEED_INPUT` 或 `NEED_CONFIRMATION`:只追问 `pending_questions`。
|
|
102
114
|
- 准备/门禁工具:`prepare_recommend_pipeline_run`
|
|
103
|
-
-
|
|
115
|
+
- 用途:只校验参数是否完整,不启动筛选任务;主要用于用户明确要求预检,或 cron/稍后/定时启动前校验。
|
|
104
116
|
- 要求:若用户要“现在启动”,返回 `status=READY` 且 `cron_ready=true` 后,下一步必须调用 MCP 工具 `run_recommend` 或 `start_recommend_pipeline_run`,禁止改用 terminal/shell/run_command/CLI/manual JSON-RPC。只有用户要“稍后/cron/定时启动”时,才继续创建定时任务。
|
|
105
117
|
- READY 响应会带 `prepared_only=true`、`run_started=false`、`recommended_next_tool=start_recommend_pipeline_run`、`alternate_next_tool=run_recommend`、`next_action.do_not_call_prepare_again=true`、`agent_guidance.native_mcp_required_after_prepare=true`;必须照这些字段继续,不得再次调用 prepare。
|
|
106
118
|
- 若返回 `NEED_INPUT` / `NEED_CONFIRMATION` / `FAILED`:继续补齐 `pending_questions` 或修复登录/页面/config;不得先创建 cron。
|
|
107
119
|
- Cron 创建工具:`schedule_recommend_pipeline_run`
|
|
108
|
-
-
|
|
120
|
+
- 用途:只在用户明确要求“稍后/cron/定时启动”时保存已经 READY 的完整 payload,并启动 package-owned detached scheduler;到点后由包内 worker 直接调用 `start_recommend_pipeline_run`。
|
|
121
|
+
- 禁止:用户要“现在启动”时,不得用短延迟 schedule 作为 `run_recommend` / `start_recommend_pipeline_run` 的替代。
|
|
109
122
|
- 必填:同 `start_recommend_pipeline_run` 的完整 payload,另加 `schedule_delay_minutes` / `schedule_delay_seconds` / `schedule_run_at` 之一。
|
|
110
123
|
- 成功标准:必须返回 `status=SCHEDULED`、`schedule_created=true`、`schedule_id`、`run_at`。只有这个返回后,才可以告诉用户定时任务已创建。
|
|
111
124
|
- Cron 查询工具:`get_recommend_scheduled_run`
|
|
112
125
|
- 用途:用户问“任务是否启动/进度”时,先查 `schedule_id`。若到点后已启动,会返回内层 `run_id` 和 run 快照。
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
- `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
|
|
118
|
-
- `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
|
|
119
|
-
- 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
|
|
126
|
+
- 状态恢复工具:`list_recommend_pipeline_runs`
|
|
127
|
+
- 用途:忘记 `run_id` 或用户问“刚才那个任务怎么样”但上下文里没有 run_id 时调用;读取最近 run 摘要并返回 `latest_run`。
|
|
128
|
+
- 限制:只返回紧凑摘要,不包含大体积候选人 `results`;拿到 `latest_run.run_id` 后再用 `get_recommend_pipeline_run` / `cancel_recommend_pipeline_run`。
|
|
129
|
+
- 在 Trae-CN 中禁止用 `run_command`、PowerShell、`Get-Content`、CLI 或直接读取 `~/.boss-recommend-mcp/runs/*.json` 来恢复 run_id 或状态。
|
|
120
130
|
|
|
121
131
|
最小策略:
|
|
122
132
|
|
|
@@ -162,6 +172,7 @@ npx -y @reconcrap/boss-recommend-mcp@latest schedule-status --schedule-id <sched
|
|
|
162
172
|
|
|
163
173
|
- 用户未明确要求“持续跟进”时,不自动 `sleep + get_recommend_pipeline_run`。
|
|
164
174
|
- 用户要求查进度时,再用 `get_recommend_pipeline_run`。
|
|
175
|
+
- 如果当前对话已经丢失 `run_id`,先调用 `list_recommend_pipeline_runs`,再用 `latest_run.run_id` 调用 `get_recommend_pipeline_run`;不要改用终端读 JSON。
|
|
165
176
|
- **长任务轮询节奏(强制)**:
|
|
166
177
|
- 推荐任务可能运行数小时,禁止高频轮询。
|
|
167
178
|
- 默认最小轮询间隔为 **30 分钟**(除非用户明确要求更频繁)。
|
package/src/cli.js
CHANGED
|
@@ -24,8 +24,12 @@ import {
|
|
|
24
24
|
resumeBossChatRunTool
|
|
25
25
|
} from "./chat-mcp.js";
|
|
26
26
|
import {
|
|
27
|
+
cancelRecommendPipelineRunTool,
|
|
28
|
+
getRecommendPipelineRunTool,
|
|
27
29
|
listRecommendJobsTool,
|
|
30
|
+
pauseRecommendPipelineRunTool,
|
|
28
31
|
prepareRecommendPipelineRunTool,
|
|
32
|
+
resumeRecommendPipelineRunTool,
|
|
29
33
|
startRecommendPipelineRunTool
|
|
30
34
|
} from "./recommend-mcp.js";
|
|
31
35
|
import {
|
|
@@ -2971,6 +2975,33 @@ function scheduleStatusCli(options = {}) {
|
|
|
2971
2975
|
}
|
|
2972
2976
|
}
|
|
2973
2977
|
|
|
2978
|
+
function isTerminalRecommendCliState(state) {
|
|
2979
|
+
return ["completed", "failed", "canceled"].includes(String(state || "").trim().toLowerCase());
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
async function monitorDetachedRecommendCliRunControls(runId) {
|
|
2983
|
+
const normalizedRunId = String(runId || "").trim();
|
|
2984
|
+
if (!normalizedRunId) return;
|
|
2985
|
+
while (true) {
|
|
2986
|
+
const payload = getRecommendPipelineRunTool({
|
|
2987
|
+
args: {
|
|
2988
|
+
run_id: normalizedRunId
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
const state = String(payload?.run?.state || payload?.run?.status || "").trim().toLowerCase();
|
|
2992
|
+
if (isTerminalRecommendCliState(state)) return;
|
|
2993
|
+
const control = payload?.run?.control || {};
|
|
2994
|
+
if (control.cancel_requested === true) {
|
|
2995
|
+
cancelRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2996
|
+
} else if (control.pause_requested === true && state === "running") {
|
|
2997
|
+
pauseRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
2998
|
+
} else if (control.pause_requested === false && state === "paused") {
|
|
2999
|
+
resumeRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
3000
|
+
}
|
|
3001
|
+
await sleepMs(1000);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2974
3005
|
async function runPipelineOnce(options = {}) {
|
|
2975
3006
|
const workspaceRoot = getWorkspaceRoot(options);
|
|
2976
3007
|
const { args, port } = buildRecommendRunCliInput(options);
|
|
@@ -2987,11 +3018,13 @@ async function runPipelineOnce(options = {}) {
|
|
|
2987
3018
|
workspace_root: workspaceRoot,
|
|
2988
3019
|
port
|
|
2989
3020
|
}
|
|
2990
|
-
});
|
|
2991
|
-
if (result.status !== "ACCEPTED") {
|
|
2992
|
-
process.exitCode = 1;
|
|
2993
|
-
}
|
|
2994
|
-
|
|
3021
|
+
});
|
|
3022
|
+
if (result.status !== "ACCEPTED") {
|
|
3023
|
+
process.exitCode = 1;
|
|
3024
|
+
} else if (process.env[detachedRecommendRunChildEnv] === "1") {
|
|
3025
|
+
await monitorDetachedRecommendCliRunControls(result.run_id);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2995
3028
|
|
|
2996
3029
|
function buildRecommendJobListCliInput(options = {}) {
|
|
2997
3030
|
const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
|
package/src/core/run/index.js
CHANGED
|
@@ -28,14 +28,24 @@ function createRunId(prefix = "run") {
|
|
|
28
28
|
return `${prefix}_${Date.now().toString(36)}_${random}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function clone(value) {
|
|
32
|
-
return JSON.parse(JSON.stringify(value));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
function clone(value) {
|
|
32
|
+
return JSON.parse(JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function errorDiagnostic(error) {
|
|
36
|
+
if (!error) return null;
|
|
37
|
+
const diagnostic = {
|
|
38
|
+
name: error?.name || "Error",
|
|
39
|
+
message: error?.message || String(error)
|
|
40
|
+
};
|
|
41
|
+
if (error?.code) diagnostic.code = error.code;
|
|
42
|
+
return diagnostic;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createDeferred() {
|
|
46
|
+
let resolve;
|
|
47
|
+
let reject;
|
|
48
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
39
49
|
resolve = promiseResolve;
|
|
40
50
|
reject = promiseReject;
|
|
41
51
|
});
|
|
@@ -188,23 +198,20 @@ export function createRunLifecycleManager({
|
|
|
188
198
|
summary: summary || entry.run.summary
|
|
189
199
|
});
|
|
190
200
|
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
|
|
193
|
-
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
194
|
-
completedAt: now(),
|
|
195
|
-
error: null
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
setStatus(entry, RUN_STATUS_FAILED, {
|
|
200
|
-
completedAt: now(),
|
|
201
|
-
error:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
|
|
203
|
+
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
204
|
+
completedAt: now(),
|
|
205
|
+
error: error instanceof RunCanceledError ? null : errorDiagnostic(error)
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
setStatus(entry, RUN_STATUS_FAILED, {
|
|
210
|
+
completedAt: now(),
|
|
211
|
+
error: errorDiagnostic(error)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
208
215
|
|
|
209
216
|
function startRun({ runId: requestedRunId = "", name, pid = process.pid, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
210
217
|
if (typeof task !== "function") {
|
package/src/index.js
CHANGED
|
@@ -101,6 +101,7 @@ const TOOL_GET_SCHEDULED_RUN = "get_recommend_scheduled_run";
|
|
|
101
101
|
const TOOL_RUN_RECOMMEND = "run_recommend";
|
|
102
102
|
const TOOL_START_RUN = "start_recommend_pipeline_run";
|
|
103
103
|
const TOOL_GET_RUN = "get_recommend_pipeline_run";
|
|
104
|
+
const TOOL_LIST_RUNS = "list_recommend_pipeline_runs";
|
|
104
105
|
const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
|
|
105
106
|
const TOOL_PAUSE_RUN = "pause_recommend_pipeline_run";
|
|
106
107
|
const TOOL_RESUME_RUN = "resume_recommend_pipeline_run";
|
|
@@ -371,6 +372,68 @@ function getRunArtifacts(runId) {
|
|
|
371
372
|
};
|
|
372
373
|
}
|
|
373
374
|
|
|
375
|
+
function isShutdownLikeError(error = {}) {
|
|
376
|
+
const text = normalizeText([
|
|
377
|
+
error?.code || "",
|
|
378
|
+
error?.message || error || ""
|
|
379
|
+
].join(" "));
|
|
380
|
+
return /socket hang up|ECONNREFUSED|ECONNRESET|WebSocket is not open|Target closed|Session closed|Connection closed|RUN_PROCESS_EXITED|DETACHED_WORKER|RUN_WORKER/i.test(text);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function buildCanceledResultFromExisting(existing = {}, errorPayload = null, message = "流水线已取消。") {
|
|
384
|
+
const previousResult = existing.result && typeof existing.result === "object" ? existing.result : {};
|
|
385
|
+
const previousError = previousResult.error || existing.error || errorPayload || null;
|
|
386
|
+
return {
|
|
387
|
+
...previousResult,
|
|
388
|
+
status: "CANCELED",
|
|
389
|
+
completion_reason: "canceled_by_user",
|
|
390
|
+
error: {
|
|
391
|
+
code: "PIPELINE_CANCELED",
|
|
392
|
+
message,
|
|
393
|
+
retryable: true,
|
|
394
|
+
shutdown_error: previousError || undefined
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function finalizeRawRunStateAsCanceled(runId, existing = {}, {
|
|
400
|
+
errorPayload = null,
|
|
401
|
+
message = "流水线已取消。"
|
|
402
|
+
} = {}) {
|
|
403
|
+
const normalizedRunId = normalizeText(runId);
|
|
404
|
+
if (!normalizedRunId) return null;
|
|
405
|
+
const now = new Date().toISOString();
|
|
406
|
+
const current = existing && typeof existing === "object" ? existing : {};
|
|
407
|
+
const result = buildCanceledResultFromExisting(current, errorPayload, message);
|
|
408
|
+
return writeRawRunState(normalizedRunId, {
|
|
409
|
+
...current,
|
|
410
|
+
run_id: normalizedRunId,
|
|
411
|
+
mode: current.mode || RUN_MODE_ASYNC,
|
|
412
|
+
state: RUN_STATE_CANCELED,
|
|
413
|
+
status: RUN_STATE_CANCELED,
|
|
414
|
+
stage: current.stage || RUN_STAGE_PREFLIGHT,
|
|
415
|
+
started_at: current.started_at || now,
|
|
416
|
+
updated_at: now,
|
|
417
|
+
heartbeat_at: now,
|
|
418
|
+
completed_at: current.completed_at || now,
|
|
419
|
+
pid: Number.isInteger(current.pid) && current.pid > 0 ? current.pid : process.pid,
|
|
420
|
+
progress: current.progress || {},
|
|
421
|
+
last_message: message,
|
|
422
|
+
context: current.context || {},
|
|
423
|
+
control: {
|
|
424
|
+
...(current.control || {}),
|
|
425
|
+
pause_requested: false,
|
|
426
|
+
pause_requested_at: null,
|
|
427
|
+
pause_requested_by: null,
|
|
428
|
+
cancel_requested: false
|
|
429
|
+
},
|
|
430
|
+
resume: current.resume || {},
|
|
431
|
+
artifacts: current.artifacts || undefined,
|
|
432
|
+
error: result.error,
|
|
433
|
+
result
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
374
437
|
function writeRawRunState(runId, payload) {
|
|
375
438
|
const artifacts = getRunArtifacts(runId);
|
|
376
439
|
fs.mkdirSync(path.dirname(artifacts.run_state_path), { recursive: true });
|
|
@@ -390,6 +453,110 @@ function readRawRunState(runId) {
|
|
|
390
453
|
}
|
|
391
454
|
}
|
|
392
455
|
|
|
456
|
+
function getRunSortTime(run = {}, fallbackMs = 0) {
|
|
457
|
+
for (const key of ["updated_at", "heartbeat_at", "completed_at", "started_at", "updatedAt", "completedAt", "startedAt"]) {
|
|
458
|
+
const ms = Date.parse(run?.[key] || "");
|
|
459
|
+
if (Number.isFinite(ms)) return ms;
|
|
460
|
+
}
|
|
461
|
+
return fallbackMs;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function compactRunForList(run = {}) {
|
|
465
|
+
const state = normalizeText(run.state || run.status);
|
|
466
|
+
const result = run.result && typeof run.result === "object" ? run.result : null;
|
|
467
|
+
const error = run.error || result?.error || null;
|
|
468
|
+
return {
|
|
469
|
+
run_id: normalizeText(run.run_id || run.runId),
|
|
470
|
+
state,
|
|
471
|
+
status: state,
|
|
472
|
+
stage: normalizeText(run.stage || run.phase),
|
|
473
|
+
mode: normalizeText(run.mode),
|
|
474
|
+
started_at: run.started_at || run.startedAt || null,
|
|
475
|
+
updated_at: run.updated_at || run.updatedAt || null,
|
|
476
|
+
heartbeat_at: run.heartbeat_at || null,
|
|
477
|
+
completed_at: run.completed_at || run.completedAt || null,
|
|
478
|
+
pid: Number.isInteger(run.pid) && run.pid > 0 ? run.pid : null,
|
|
479
|
+
progress: run.progress || {},
|
|
480
|
+
last_message: normalizeText(run.last_message || error?.message || ""),
|
|
481
|
+
control: {
|
|
482
|
+
pause_requested: run.control?.pause_requested === true,
|
|
483
|
+
cancel_requested: run.control?.cancel_requested === true
|
|
484
|
+
},
|
|
485
|
+
error: error ? {
|
|
486
|
+
code: normalizeText(error.code || ""),
|
|
487
|
+
message: normalizeText(error.message || error || "")
|
|
488
|
+
} : null,
|
|
489
|
+
result: result ? {
|
|
490
|
+
status: normalizeText(result.status || ""),
|
|
491
|
+
completion_reason: normalizeText(result.completion_reason || ""),
|
|
492
|
+
output_csv: normalizeText(result.output_csv || result.result?.output_csv || ""),
|
|
493
|
+
report_json: normalizeText(result.report_json || result.result?.report_json || ""),
|
|
494
|
+
checkpoint_path: normalizeText(result.checkpoint_path || result.result?.checkpoint_path || "")
|
|
495
|
+
} : null,
|
|
496
|
+
resume: {
|
|
497
|
+
checkpoint_path: normalizeText(run.resume?.checkpoint_path || ""),
|
|
498
|
+
output_csv: normalizeText(run.resume?.output_csv || ""),
|
|
499
|
+
worker_stdout_path: normalizeText(run.resume?.worker_stdout_path || ""),
|
|
500
|
+
worker_stderr_path: normalizeText(run.resume?.worker_stderr_path || "")
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function normalizeRunStateFilter(args = {}) {
|
|
506
|
+
const rawStates = Array.isArray(args.states)
|
|
507
|
+
? args.states
|
|
508
|
+
: args.state === undefined
|
|
509
|
+
? []
|
|
510
|
+
: [args.state];
|
|
511
|
+
return new Set(rawStates.map((item) => normalizeText(item)).filter(Boolean));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function handleListRunsTool(args = {}) {
|
|
515
|
+
const limit = Math.max(1, Math.min(100, Number.parseInt(String(args.limit || 20), 10) || 20));
|
|
516
|
+
const stateFilter = normalizeRunStateFilter(args);
|
|
517
|
+
const runsDir = getRunsDir();
|
|
518
|
+
if (!fs.existsSync(runsDir)) {
|
|
519
|
+
return {
|
|
520
|
+
status: "OK",
|
|
521
|
+
runs: [],
|
|
522
|
+
latest_run: null,
|
|
523
|
+
count: 0,
|
|
524
|
+
total_matching: 0,
|
|
525
|
+
message: "No recommend run state directory exists yet."
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const entries = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
529
|
+
const runs = [];
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name.endsWith(".checkpoint.json")) continue;
|
|
532
|
+
const filePath = path.join(runsDir, entry.name);
|
|
533
|
+
const runId = entry.name.replace(/\.json$/, "");
|
|
534
|
+
const raw = readRawRunState(runId);
|
|
535
|
+
if (!raw) continue;
|
|
536
|
+
const state = normalizeText(raw.state || raw.status);
|
|
537
|
+
if (stateFilter.size > 0 && !stateFilter.has(state)) continue;
|
|
538
|
+
const stat = fs.statSync(filePath);
|
|
539
|
+
runs.push({
|
|
540
|
+
sort_ms: getRunSortTime(raw, Number(stat.mtimeMs || 0)),
|
|
541
|
+
run: compactRunForList(raw)
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
runs.sort((a, b) => b.sort_ms - a.sort_ms);
|
|
545
|
+
const compacted = runs.slice(0, limit).map((item) => item.run);
|
|
546
|
+
return {
|
|
547
|
+
status: "OK",
|
|
548
|
+
runs: compacted,
|
|
549
|
+
latest_run: compacted[0] || null,
|
|
550
|
+
count: compacted.length,
|
|
551
|
+
total_matching: runs.length,
|
|
552
|
+
limit,
|
|
553
|
+
filters: {
|
|
554
|
+
states: Array.from(stateFilter)
|
|
555
|
+
},
|
|
556
|
+
message: "Use latest_run.run_id with get_recommend_pipeline_run, cancel_recommend_pipeline_run, pause_recommend_pipeline_run, or resume_recommend_pipeline_run. In Trae-CN, do not use terminal, PowerShell, CLI, or filesystem scraping to recover run status."
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
393
560
|
function patchRawRunState(runId, patch) {
|
|
394
561
|
const current = readRawRunState(runId);
|
|
395
562
|
if (!current) return null;
|
|
@@ -1133,14 +1300,24 @@ function createToolsSchema() {
|
|
|
1133
1300
|
description: "CDP-only 读取 Boss 推荐页岗位下拉框,返回所有可用岗位完整名称,方便 cron/一次性任务提前填写 job 参数。不会启动筛选任务。",
|
|
1134
1301
|
inputSchema: createListRecommendJobsInputSchema()
|
|
1135
1302
|
},
|
|
1303
|
+
{
|
|
1304
|
+
name: TOOL_RUN_RECOMMEND,
|
|
1305
|
+
description: "立即通过原生 MCP 启动 Boss 推荐页筛选。它是 start_recommend_pipeline_run 的短别名;用户已经确认并要现在启动时,优先调用本工具,不需要先调用 prepare_recommend_pipeline_run。必须作为 MCP tool call 调用,禁止通过 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC 代替,也不要用 schedule_recommend_pipeline_run 冒充立即启动。",
|
|
1306
|
+
inputSchema: createRunInputSchema()
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
name: TOOL_START_RUN,
|
|
1310
|
+
description: "立即通过原生 MCP 异步启动 Boss 推荐页流水线(含同步门禁预检)。用户已经确认并要现在启动时,优先调用本工具或 run_recommend,不需要先调用 prepare_recommend_pipeline_run。必须作为 MCP tool call 调用,禁止通过 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC 代替,也不要用 schedule_recommend_pipeline_run 冒充立即启动。",
|
|
1311
|
+
inputSchema: createRunInputSchema()
|
|
1312
|
+
},
|
|
1136
1313
|
{
|
|
1137
1314
|
name: TOOL_PREPARE_RUN,
|
|
1138
|
-
description: "只校验 Boss
|
|
1315
|
+
description: "只校验 Boss 推荐页流水线参数是否完整;不会启动筛选任务。主要用于显式预检或稍后/cron/定时启动前校验。若用户要现在运行,READY/cron_ready=true 后必须继续调用本 MCP server 的 run_recommend 或 start_recommend_pipeline_run;prepare 能返回结果就证明原生 MCP 可用,禁止改用 terminal/shell/run_command/PowerShell/CLI/manual JSON-RPC,也禁止再次调用 prepare 试图启动。",
|
|
1139
1316
|
inputSchema: createRunInputSchema()
|
|
1140
1317
|
},
|
|
1141
1318
|
{
|
|
1142
1319
|
name: TOOL_SCHEDULE_RUN,
|
|
1143
|
-
description: "
|
|
1320
|
+
description: "只用于用户明确要求稍后/cron/定时启动的 package-owned Boss 推荐页定时任务。若用户要现在运行,必须调用 run_recommend 或 start_recommend_pipeline_run,不要用短延迟 schedule 冒充立即启动。schedule 会先校验 READY/cron_ready,再保存完整 payload,并由 detached scheduler 到点直接启动,不依赖 AI harness 自己拼 shell cron。",
|
|
1144
1321
|
inputSchema: createScheduleRunInputSchema()
|
|
1145
1322
|
},
|
|
1146
1323
|
{
|
|
@@ -1156,30 +1333,49 @@ function createToolsSchema() {
|
|
|
1156
1333
|
}
|
|
1157
1334
|
},
|
|
1158
1335
|
{
|
|
1159
|
-
name:
|
|
1160
|
-
description: "
|
|
1161
|
-
inputSchema:
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
name: TOOL_START_RUN,
|
|
1165
|
-
description: "立即通过原生 MCP 异步启动 Boss 推荐页流水线(含同步门禁预检);prepare_recommend_pipeline_run 返回 READY 后,如果用户要现在运行就调用本工具或 run_recommend。必须作为 MCP tool call 调用,禁止通过 terminal/shell/run_command/CLI/manual JSON-RPC 代替。",
|
|
1166
|
-
inputSchema: createRunInputSchema()
|
|
1167
|
-
},
|
|
1168
|
-
{
|
|
1169
|
-
name: TOOL_GET_RUN,
|
|
1170
|
-
description: "按 run_id 查询异步/同步流水线运行状态快照。",
|
|
1171
|
-
inputSchema: {
|
|
1172
|
-
type: "object",
|
|
1173
|
-
properties: {
|
|
1336
|
+
name: TOOL_GET_RUN,
|
|
1337
|
+
description: "按已知 run_id 查询异步/同步流水线运行状态快照。若忘记 run_id,请先调用 list_recommend_pipeline_runs 找 latest_run;在 Trae-CN 中禁止用 terminal/PowerShell/CLI/filesystem scraping 查看 run JSON。",
|
|
1338
|
+
inputSchema: {
|
|
1339
|
+
type: "object",
|
|
1340
|
+
properties: {
|
|
1174
1341
|
run_id: { type: "string" }
|
|
1175
1342
|
},
|
|
1176
1343
|
required: ["run_id"],
|
|
1177
|
-
additionalProperties: false
|
|
1178
|
-
}
|
|
1179
|
-
},
|
|
1180
|
-
{
|
|
1181
|
-
name:
|
|
1182
|
-
description: "
|
|
1344
|
+
additionalProperties: false
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
name: TOOL_LIST_RUNS,
|
|
1349
|
+
description: "只读列出最近的 Boss 推荐页 run 状态摘要,并返回 latest_run。用于忘记 run_id 后恢复状态/取消/暂停;摘要不会包含大体积候选人 results。Trae-CN 中必须用本工具恢复 run_id,禁止用 terminal/PowerShell/CLI/Get-Content 读取 ~/.boss-recommend-mcp/runs。",
|
|
1350
|
+
inputSchema: {
|
|
1351
|
+
type: "object",
|
|
1352
|
+
properties: {
|
|
1353
|
+
limit: {
|
|
1354
|
+
type: "integer",
|
|
1355
|
+
minimum: 1,
|
|
1356
|
+
maximum: 100,
|
|
1357
|
+
description: "最多返回多少条最近 run;默认 20,最大 100。"
|
|
1358
|
+
},
|
|
1359
|
+
state: {
|
|
1360
|
+
type: "string",
|
|
1361
|
+
enum: ["queued", "running", "paused", "completed", "failed", "canceled"],
|
|
1362
|
+
description: "可选,只返回某个状态。"
|
|
1363
|
+
},
|
|
1364
|
+
states: {
|
|
1365
|
+
type: "array",
|
|
1366
|
+
items: {
|
|
1367
|
+
type: "string",
|
|
1368
|
+
enum: ["queued", "running", "paused", "completed", "failed", "canceled"]
|
|
1369
|
+
},
|
|
1370
|
+
description: "可选,只返回这些状态;与 state 同时传时取并集。"
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
additionalProperties: false
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
name: TOOL_CANCEL_RUN,
|
|
1378
|
+
description: "取消指定 run_id 的运行中流水线。",
|
|
1183
1379
|
inputSchema: {
|
|
1184
1380
|
type: "object",
|
|
1185
1381
|
properties: {
|
|
@@ -1779,6 +1975,12 @@ function markDetachedWorkerFailed(runId, error, options = {}) {
|
|
|
1779
1975
|
...errorToDetachedWorkerPayload(error, options.message),
|
|
1780
1976
|
...(options.code ? { code: options.code } : {})
|
|
1781
1977
|
};
|
|
1978
|
+
if (existing.control?.cancel_requested === true && isShutdownLikeError(errorPayload)) {
|
|
1979
|
+
return finalizeRawRunStateAsCanceled(normalizedRunId, existing, {
|
|
1980
|
+
errorPayload,
|
|
1981
|
+
message: "流水线已取消;detached worker 在取消收尾时关闭了浏览器连接。"
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1782
1984
|
const previousResult = existing.result && typeof existing.result === "object" ? existing.result : {};
|
|
1783
1985
|
const result = {
|
|
1784
1986
|
...previousResult,
|
|
@@ -1876,7 +2078,8 @@ function buildWorkerLaunchFailedPayload(message) {
|
|
|
1876
2078
|
|
|
1877
2079
|
function finalizeCanceledRun(runId, snapshot) {
|
|
1878
2080
|
const canceledResult = {
|
|
1879
|
-
status: "
|
|
2081
|
+
status: "CANCELED",
|
|
2082
|
+
completion_reason: "canceled_by_user",
|
|
1880
2083
|
error: {
|
|
1881
2084
|
code: "PIPELINE_CANCELED",
|
|
1882
2085
|
message: "流水线已取消。",
|
|
@@ -2047,16 +2250,25 @@ async function executeTrackedPipeline({
|
|
|
2047
2250
|
}
|
|
2048
2251
|
);
|
|
2049
2252
|
} catch (error) {
|
|
2050
|
-
const canceled = Boolean(signal?.aborted)
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2253
|
+
const canceled = Boolean(signal?.aborted)
|
|
2254
|
+
|| error?.code === "PIPELINE_ABORTED"
|
|
2255
|
+
|| (isRunCancelRequested(runId) && isShutdownLikeError(error));
|
|
2256
|
+
if (canceled) {
|
|
2257
|
+
const canceledResult = {
|
|
2258
|
+
status: "CANCELED",
|
|
2259
|
+
completion_reason: "canceled_by_user",
|
|
2260
|
+
error: {
|
|
2261
|
+
code: "PIPELINE_CANCELED",
|
|
2262
|
+
message: "流水线已取消。",
|
|
2263
|
+
retryable: true,
|
|
2264
|
+
shutdown_error: isShutdownLikeError(error)
|
|
2265
|
+
? {
|
|
2266
|
+
code: error?.code || "SHUTDOWN_ERROR",
|
|
2267
|
+
message: error?.message || String(error)
|
|
2268
|
+
}
|
|
2269
|
+
: undefined
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2060
2272
|
safeUpdateRunState(runId, {
|
|
2061
2273
|
mode,
|
|
2062
2274
|
state: RUN_STATE_CANCELED,
|
|
@@ -2105,21 +2317,35 @@ async function executeTrackedPipeline({
|
|
|
2105
2317
|
};
|
|
2106
2318
|
}
|
|
2107
2319
|
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2320
|
+
const failedAfterCancel = result?.status === "FAILED"
|
|
2321
|
+
&& isRunCancelRequested(runId)
|
|
2322
|
+
&& isShutdownLikeError(result?.error || result);
|
|
2323
|
+
const terminalState = failedAfterCancel
|
|
2324
|
+
? RUN_STATE_CANCELED
|
|
2325
|
+
: result?.status === "FAILED"
|
|
2326
|
+
? RUN_STATE_FAILED
|
|
2327
|
+
: result?.status === "PAUSED"
|
|
2328
|
+
? (isRunCancelRequested(runId) ? RUN_STATE_CANCELED : RUN_STATE_PAUSED)
|
|
2329
|
+
: RUN_STATE_COMPLETED;
|
|
2113
2330
|
const outputCsv = getOutputCsvFromResult(result) || resumeConfig.output_csv;
|
|
2114
2331
|
const checkpointPath = normalizeText(result?.result?.checkpoint_path || resumeConfig.checkpoint_path);
|
|
2115
|
-
const canceledError = terminalState === RUN_STATE_CANCELED
|
|
2116
|
-
? {
|
|
2117
|
-
code: "PIPELINE_CANCELED",
|
|
2118
|
-
message: "流水线已取消。",
|
|
2119
|
-
retryable: true
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2332
|
+
const canceledError = terminalState === RUN_STATE_CANCELED
|
|
2333
|
+
? {
|
|
2334
|
+
code: "PIPELINE_CANCELED",
|
|
2335
|
+
message: "流水线已取消。",
|
|
2336
|
+
retryable: true,
|
|
2337
|
+
shutdown_error: failedAfterCancel ? (result?.error || null) : undefined
|
|
2338
|
+
}
|
|
2339
|
+
: null;
|
|
2340
|
+
const finalResult = failedAfterCancel
|
|
2341
|
+
? {
|
|
2342
|
+
...(result || {}),
|
|
2343
|
+
status: "CANCELED",
|
|
2344
|
+
completion_reason: "canceled_by_user",
|
|
2345
|
+
error: canceledError
|
|
2346
|
+
}
|
|
2347
|
+
: result || null;
|
|
2348
|
+
safeUpdateRunState(runId, {
|
|
2123
2349
|
mode,
|
|
2124
2350
|
state: terminalState,
|
|
2125
2351
|
stage: runtimeCallbacks.getLastStage(),
|
|
@@ -2147,13 +2373,13 @@ async function executeTrackedPipeline({
|
|
|
2147
2373
|
: terminalState === RUN_STATE_CANCELED
|
|
2148
2374
|
? canceledError
|
|
2149
2375
|
: null,
|
|
2150
|
-
result:
|
|
2151
|
-
});
|
|
2152
|
-
return {
|
|
2153
|
-
result,
|
|
2154
|
-
lastStage: runtimeCallbacks.getLastStage(),
|
|
2155
|
-
state: terminalState
|
|
2156
|
-
};
|
|
2376
|
+
result: finalResult
|
|
2377
|
+
});
|
|
2378
|
+
return {
|
|
2379
|
+
result: finalResult,
|
|
2380
|
+
lastStage: runtimeCallbacks.getLastStage(),
|
|
2381
|
+
state: terminalState
|
|
2382
|
+
};
|
|
2157
2383
|
}
|
|
2158
2384
|
|
|
2159
2385
|
function initializeRunStateOrThrow(runId, mode, workspaceRoot, args, pid = process.pid) {
|
|
@@ -2699,6 +2925,8 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
2699
2925
|
payload = await handleStartRunTool({ workspaceRoot, args });
|
|
2700
2926
|
} else if (toolName === TOOL_GET_RUN) {
|
|
2701
2927
|
payload = handleGetRunTool(args);
|
|
2928
|
+
} else if (toolName === TOOL_LIST_RUNS) {
|
|
2929
|
+
payload = handleListRunsTool(args);
|
|
2702
2930
|
} else if (toolName === TOOL_CANCEL_RUN) {
|
|
2703
2931
|
payload = handleCancelRunTool(args);
|
|
2704
2932
|
} else if (toolName === TOOL_PAUSE_RUN) {
|
package/src/recommend-mcp.js
CHANGED
|
@@ -345,9 +345,9 @@ function normalizeErrorText(error = {}) {
|
|
|
345
345
|
].join(" "));
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
function classifyRecommendRecovery(error = {}) {
|
|
349
|
-
const text = normalizeErrorText(error);
|
|
350
|
-
if (!text) return null;
|
|
348
|
+
function classifyRecommendRecovery(error = {}) {
|
|
349
|
+
const text = normalizeErrorText(error);
|
|
350
|
+
if (!text) return null;
|
|
351
351
|
if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
|
|
352
352
|
if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
|
|
353
353
|
return "transient_stale_dom";
|
|
@@ -358,8 +358,13 @@ function classifyRecommendRecovery(error = {}) {
|
|
|
358
358
|
if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
|
|
359
359
|
return "transient_network_or_llm";
|
|
360
360
|
}
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isCancelShutdownError(error = {}) {
|
|
365
|
+
const text = normalizeErrorText(error);
|
|
366
|
+
return /socket hang up|ECONNREFUSED|ECONNRESET|WebSocket is not open|Target closed|Session closed|Connection closed|RUN_PROCESS_EXITED|DETACHED_WORKER|RUN_WORKER/i.test(text);
|
|
367
|
+
}
|
|
363
368
|
|
|
364
369
|
function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
|
|
365
370
|
const error = snapshot?.error || snapshot?.result?.error || null;
|
|
@@ -574,15 +579,31 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
574
579
|
};
|
|
575
580
|
}
|
|
576
581
|
|
|
577
|
-
function mergePersistedControlRequest(normalized, existing) {
|
|
578
|
-
const control = {
|
|
579
|
-
...(normalized?.control || {})
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
582
|
+
function mergePersistedControlRequest(normalized, existing) {
|
|
583
|
+
const control = {
|
|
584
|
+
...(normalized?.control || {})
|
|
585
|
+
};
|
|
586
|
+
const existingControl = plainRecord(existing?.control);
|
|
587
|
+
if (!normalized) return control;
|
|
588
|
+
if (TERMINAL_STATUSES.has(normalized.state)) {
|
|
589
|
+
if (
|
|
590
|
+
normalized.state === RUN_STATUS_FAILED
|
|
591
|
+
&& existingControl.cancel_requested === true
|
|
592
|
+
&& isCancelShutdownError(normalized.error || normalized.result?.error || "")
|
|
593
|
+
) {
|
|
594
|
+
return {
|
|
595
|
+
...control,
|
|
596
|
+
pause_requested: true,
|
|
597
|
+
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
598
|
+
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
|
|
599
|
+
cancel_requested: true
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return control;
|
|
603
|
+
}
|
|
604
|
+
if (existingControl.cancel_requested === true) {
|
|
605
|
+
return {
|
|
606
|
+
...control,
|
|
586
607
|
pause_requested: true,
|
|
587
608
|
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
588
609
|
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
|
|
@@ -605,22 +626,81 @@ function mergePersistedControlRequest(normalized, existing) {
|
|
|
605
626
|
pause_requested_by: null,
|
|
606
627
|
cancel_requested: false
|
|
607
628
|
};
|
|
608
|
-
}
|
|
609
|
-
return control;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
629
|
+
}
|
|
630
|
+
return control;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function cancelErrorFromShutdown(shutdownError = null) {
|
|
634
|
+
return {
|
|
635
|
+
code: "PIPELINE_CANCELED",
|
|
636
|
+
message: "流水线已取消。",
|
|
637
|
+
retryable: true,
|
|
638
|
+
shutdown_error: shutdownError || undefined
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function coerceCanceledTerminalSnapshot(normalized, existing) {
|
|
643
|
+
const existingControl = plainRecord(existing?.control);
|
|
644
|
+
const shutdownError = normalized?.error || normalized?.result?.error || null;
|
|
645
|
+
const shouldWrapCanceledShutdown = (
|
|
646
|
+
normalized
|
|
647
|
+
&& (
|
|
648
|
+
(
|
|
649
|
+
normalized.state === RUN_STATUS_FAILED
|
|
650
|
+
&& existingControl.cancel_requested === true
|
|
651
|
+
)
|
|
652
|
+
|| normalized.state === RUN_STATUS_CANCELED
|
|
653
|
+
)
|
|
654
|
+
&& isCancelShutdownError(shutdownError || "")
|
|
655
|
+
);
|
|
656
|
+
if (
|
|
657
|
+
!shouldWrapCanceledShutdown
|
|
658
|
+
) {
|
|
659
|
+
return normalized;
|
|
660
|
+
}
|
|
661
|
+
const canceledError = cancelErrorFromShutdown(shutdownError);
|
|
662
|
+
return {
|
|
663
|
+
...normalized,
|
|
664
|
+
state: RUN_STATUS_CANCELED,
|
|
665
|
+
status: RUN_STATUS_CANCELED,
|
|
666
|
+
last_message: "流水线已取消;取消收尾时浏览器连接已关闭。",
|
|
667
|
+
control: {
|
|
668
|
+
pause_requested: false,
|
|
669
|
+
pause_requested_at: null,
|
|
670
|
+
pause_requested_by: null,
|
|
671
|
+
cancel_requested: false
|
|
672
|
+
},
|
|
673
|
+
error: canceledError,
|
|
674
|
+
result: normalized.result ? {
|
|
675
|
+
...normalized.result,
|
|
676
|
+
status: "CANCELED",
|
|
677
|
+
completion_reason: "canceled_by_user",
|
|
678
|
+
error: canceledError
|
|
679
|
+
} : {
|
|
680
|
+
status: "CANCELED",
|
|
681
|
+
completion_reason: "canceled_by_user",
|
|
682
|
+
error: canceledError,
|
|
683
|
+
run_id: normalized.run_id,
|
|
684
|
+
processed_count: normalized.progress?.processed || 0,
|
|
685
|
+
screened_count: normalized.progress?.screened || normalized.progress?.processed || 0,
|
|
686
|
+
passed_count: normalized.progress?.passed || 0
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function persistRecommendRunSnapshot(snapshot, {
|
|
692
|
+
persistActiveCheckpoint = false
|
|
693
|
+
} = {}) {
|
|
694
|
+
let normalized = normalizeRunSnapshot(snapshot);
|
|
695
|
+
if (!normalized?.run_id) return normalized;
|
|
696
|
+
const artifacts = getRecommendRunArtifacts(normalized.run_id);
|
|
697
|
+
if (!artifacts) return normalized;
|
|
698
|
+
const existing = readJsonFile(artifacts.run_state_path);
|
|
699
|
+
normalized.control = mergePersistedControlRequest(normalized, existing);
|
|
700
|
+
normalized = coerceCanceledTerminalSnapshot(normalized, existing);
|
|
701
|
+
if (persistActiveCheckpoint) {
|
|
702
|
+
persistRecommendCheckpointSnapshot(normalized);
|
|
703
|
+
}
|
|
624
704
|
const payload = {
|
|
625
705
|
run_id: normalized.run_id,
|
|
626
706
|
mode: normalized.mode,
|
|
@@ -643,29 +723,59 @@ function persistRecommendRunSnapshot(snapshot, {
|
|
|
643
723
|
summary: normalized.summary,
|
|
644
724
|
artifacts: normalized.artifacts
|
|
645
725
|
};
|
|
646
|
-
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
647
|
-
return normalized;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
726
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
727
|
+
return normalized;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function patchPersistedRecommendRunControl(runId, controlPatch = {}, {
|
|
731
|
+
message = ""
|
|
732
|
+
} = {}) {
|
|
733
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
734
|
+
if (!artifacts) return null;
|
|
735
|
+
const current = readJsonFile(artifacts.run_state_path);
|
|
736
|
+
const state = normalizeText(current?.state || current?.status || "");
|
|
737
|
+
if (!current || TERMINAL_STATUSES.has(state)) return null;
|
|
738
|
+
const now = new Date().toISOString();
|
|
739
|
+
const patched = {
|
|
740
|
+
...current,
|
|
741
|
+
updated_at: now,
|
|
742
|
+
heartbeat_at: current.heartbeat_at || now,
|
|
743
|
+
last_message: message || current.last_message || "",
|
|
744
|
+
control: {
|
|
745
|
+
...(current.control || {}),
|
|
746
|
+
...controlPatch
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
writeJsonAtomic(artifacts.run_state_path, patched);
|
|
750
|
+
return patched;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function reconcilePersistedRecommendRunIfNeeded(persisted) {
|
|
754
|
+
if (!persisted || typeof persisted !== "object") return persisted;
|
|
755
|
+
const persistedState = normalizeText(persisted.state || persisted.status);
|
|
756
|
+
if (TERMINAL_STATUSES.has(persistedState)) return persisted;
|
|
654
757
|
if (isProcessAlive(persisted.pid)) return persisted;
|
|
655
758
|
|
|
656
|
-
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
657
|
-
const artifacts = getRecommendRunArtifacts(runId);
|
|
658
|
-
const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
|
|
659
|
-
const now = new Date().toISOString();
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
759
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
760
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
761
|
+
const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
|
|
762
|
+
const now = new Date().toISOString();
|
|
763
|
+
const cancelRequested = persisted.control?.cancel_requested === true;
|
|
764
|
+
const processExitedError = {
|
|
765
|
+
code: "RUN_PROCESS_EXITED",
|
|
766
|
+
message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"})。`,
|
|
767
|
+
retryable: true
|
|
768
|
+
};
|
|
769
|
+
const error = cancelRequested
|
|
770
|
+
? cancelErrorFromShutdown(processExitedError)
|
|
771
|
+
: {
|
|
772
|
+
...processExitedError,
|
|
773
|
+
message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`
|
|
774
|
+
};
|
|
775
|
+
return persistRecommendRunSnapshot({
|
|
666
776
|
runId,
|
|
667
777
|
name: persisted.name || runId,
|
|
668
|
-
status: RUN_STATUS_FAILED,
|
|
778
|
+
status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
|
|
669
779
|
phase: persisted.stage || persisted.phase || "recommend:orphaned",
|
|
670
780
|
progress: persisted.progress || {},
|
|
671
781
|
context: persisted.context || {},
|
|
@@ -1813,20 +1923,45 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
|
|
|
1813
1923
|
}, runId);
|
|
1814
1924
|
} catch {
|
|
1815
1925
|
const persisted = readRecommendRunState(runId);
|
|
1816
|
-
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1817
|
-
return {
|
|
1818
|
-
status: "CANCEL_IGNORED",
|
|
1819
|
-
run: persisted,
|
|
1820
|
-
message: "目标任务已结束,无需取消。",
|
|
1926
|
+
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1927
|
+
return {
|
|
1928
|
+
status: "CANCEL_IGNORED",
|
|
1929
|
+
run: persisted,
|
|
1930
|
+
message: "目标任务已结束,无需取消。",
|
|
1821
1931
|
runtime_evaluate_used: false,
|
|
1822
1932
|
method_summary: {},
|
|
1823
1933
|
method_log: [],
|
|
1824
|
-
chrome: null
|
|
1825
|
-
};
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1934
|
+
chrome: null
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
|
|
1938
|
+
const patched = patchPersistedRecommendRunControl(runId, {
|
|
1939
|
+
pause_requested: true,
|
|
1940
|
+
pause_requested_at: new Date().toISOString(),
|
|
1941
|
+
pause_requested_by: "cancel_recommend_pipeline_run",
|
|
1942
|
+
cancel_requested: true
|
|
1943
|
+
}, {
|
|
1944
|
+
message: cancelMessage
|
|
1945
|
+
});
|
|
1946
|
+
if (patched) {
|
|
1947
|
+
return {
|
|
1948
|
+
status: "CANCEL_REQUESTED",
|
|
1949
|
+
run: patched,
|
|
1950
|
+
message: cancelMessage,
|
|
1951
|
+
persistence: {
|
|
1952
|
+
source: "disk",
|
|
1953
|
+
active_control_available: false,
|
|
1954
|
+
detached_control_requested: true
|
|
1955
|
+
},
|
|
1956
|
+
runtime_evaluate_used: false,
|
|
1957
|
+
method_summary: {},
|
|
1958
|
+
method_log: [],
|
|
1959
|
+
chrome: null
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
return getRecommendPipelineRunTool({ args });
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1830
1965
|
|
|
1831
1966
|
export function getRecommendMcpHealthSnapshot(runId) {
|
|
1832
1967
|
const meta = getRecommendRunMeta(runId);
|