@reconcrap/boss-recommend-mcp 2.0.56 → 2.1.0
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 +5 -2
- package/config/screening-config.example.json +1 -0
- package/package.json +2 -1
- package/skills/boss-chat/README.md +2 -1
- package/skills/boss-chat/SKILL.md +9 -1
- package/skills/boss-recommend-pipeline/README.md +1 -0
- package/skills/boss-recommend-pipeline/SKILL.md +5 -1
- package/skills/boss-recruit-pipeline/README.md +2 -0
- package/skills/boss-recruit-pipeline/SKILL.md +8 -0
- package/src/chat-mcp.js +397 -3
- package/src/cli.js +92 -50
- package/src/core/browser/index.js +162 -1
- package/src/core/self-heal/viewport.js +1 -1
- package/src/detached-worker.js +99 -0
- package/src/domains/chat/run-service.js +43 -35
- package/src/domains/recommend/run-service.js +6 -1
- package/src/domains/recruit/run-service.js +8 -1
- package/src/index.js +46 -3
- package/src/recruit-mcp.js +545 -11
package/README.md
CHANGED
|
@@ -259,13 +259,16 @@ config/screening-config.example.json
|
|
|
259
259
|
- `debugPort`:未显式传 `port` 时,recommend / search / chat CDP-only MCP run 和健康检查默认连接这个 Chrome 调试端口。
|
|
260
260
|
- `outputDir`:recommend / search / chat 完成后的最终 CSV 与 report JSON 会写入这里;run state / checkpoint 仍保留在各自状态目录,方便 pause/resume/cancel。
|
|
261
261
|
- `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。
|
|
262
|
-
- `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests" }`。用于 recommend / search / chat 的可靠性实验,支持:
|
|
262
|
+
- `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests", "restLevel": "low" }`。用于 recommend / search / chat 的可靠性实验,支持:
|
|
263
263
|
- `profile: "baseline"`:关闭人类节奏,保持确定性行为。
|
|
264
264
|
- `profile: "paced"`:启用 CDP-only Bezier 鼠标移动、较大按钮的安全 inset 点击点、分块 `Input.insertText`、列表 wheel/settle jitter,以及小的动作前后读秒。
|
|
265
265
|
- `profile: "paced_with_rests"`:在 `paced` 基础上启用候选人短休和批次休息。
|
|
266
|
+
- `restLevel: "low"`:保持旧版休息策略不变,候选人短休 8% 概率暂停 3-7 秒,批次休息约每 25-32 人暂停 15-30 秒。
|
|
267
|
+
- `restLevel: "medium"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 30 分钟。
|
|
268
|
+
- `restLevel: "high"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 1 小时。
|
|
266
269
|
- `humanRestEnabled`:兼容旧配置。设为 `true` 时等价于 `humanBehavior.profile="paced_with_rests"`;设为 `false` 时不会关闭当前默认节奏。如需关闭,请显式设置 `humanBehavior.enabled=false` 或 `humanBehavior.profile="baseline"`。
|
|
267
270
|
- recommend / search / chat 图片简历 fallback 与主列表滚动都会在启用 `listScrollJitter` 时使用 coverage-safe scroll jitter:每次 wheel delta 在安全范围内变化,并保留截图重叠、重复检测、bottom-marker / stop-boundary 逻辑,实际 delta 和 settle 时间会写入 artifact metadata。
|
|
268
|
-
- chat/recommend/search run 也兼容显式参数 `safe_pacing` 与 `
|
|
271
|
+
- chat/recommend/search run 也兼容显式参数 `safe_pacing`、`batch_rest_enabled` 与 `human_behavior.restLevel`:run 参数优先于配置文件。AI harness/skill 启动每次 run 前必须让用户明确选择 `low/medium/high`,再把选择写入 `human_behavior.restLevel`。
|
|
269
272
|
|
|
270
273
|
## 常用命令
|
|
271
274
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"boss",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"src/chat-mcp.js",
|
|
89
89
|
"src/chat-runtime-config.js",
|
|
90
90
|
"src/cli.js",
|
|
91
|
+
"src/detached-worker.js",
|
|
91
92
|
"src/index.js",
|
|
92
93
|
"src/parser.js",
|
|
93
94
|
"src/recommend-mcp.js",
|
|
@@ -17,7 +17,7 @@ Please run a Boss chat-only task (do not switch to recommend flow).
|
|
|
17
17
|
Execution order:
|
|
18
18
|
1) Call boss_chat_health_check.
|
|
19
19
|
2) Call prepare_boss_chat_run once (empty params allowed) to fetch job_options and missing fields.
|
|
20
|
-
3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria.
|
|
20
|
+
3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria, rest_level (low/medium/high).
|
|
21
21
|
4) After user reply, call start_boss_chat_run exactly once to start the run.
|
|
22
22
|
5) If ACCEPTED, reply only with run_id and "task started"; no auto polling.
|
|
23
23
|
|
|
@@ -27,6 +27,7 @@ Anti-loop rules:
|
|
|
27
27
|
- Do not use start_boss_chat_run for preflight. It is only for the final start call and must include job/start_from/target_count/criteria.
|
|
28
28
|
- Do not call start_boss_chat_run repeatedly in one turn.
|
|
29
29
|
- Do not call get_boss_chat_run unless user explicitly asks for progress.
|
|
30
|
+
- Do not choose rest_level for the user. Pass the user's choice as `human_behavior.restLevel`.
|
|
30
31
|
|
|
31
32
|
target_count mapping:
|
|
32
33
|
- Positive integer means explicit cap (for example 20).
|
|
@@ -27,6 +27,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
27
27
|
- `start_from`: `unread|all`
|
|
28
28
|
- `target_count`
|
|
29
29
|
- `criteria`
|
|
30
|
+
- `rest_level`: `low|medium|high`
|
|
30
31
|
|
|
31
32
|
可选:
|
|
32
33
|
|
|
@@ -38,6 +39,12 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
38
39
|
- `safe_pacing`
|
|
39
40
|
- `batch_rest_enabled`
|
|
40
41
|
|
|
42
|
+
启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{ "human_behavior": { "restLevel": "medium" } }
|
|
46
|
+
```
|
|
47
|
+
|
|
41
48
|
`greeting_text` 默认规则:
|
|
42
49
|
|
|
43
50
|
- 本次显式传入 `greeting_text`:使用本次值
|
|
@@ -62,6 +69,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
62
69
|
- 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
|
|
63
70
|
- `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
|
|
64
71
|
- `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
|
|
72
|
+
- 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
|
|
65
73
|
- 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
|
|
66
74
|
- 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
|
|
67
75
|
- 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
|
|
@@ -89,5 +97,5 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
|
|
|
89
97
|
|
|
90
98
|
- 用结构化中文。
|
|
91
99
|
- 首轮建议先调用一次 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
|
|
92
|
-
- 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`。
|
|
100
|
+
- 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`、`rest_level`。
|
|
93
101
|
- 若健康检查失败,明确提示共享配置文件 `screening-config.json` 不可用。
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- 先确认推荐页 filters
|
|
8
8
|
- 再确认筛选 criteria
|
|
9
9
|
- 再确认本次运行统一动作 `favorite` 或 `greet`
|
|
10
|
+
- 每次运行都要让用户明确选择休息强度 `low` / `medium` / `high`,并传入 `human_behavior.restLevel`
|
|
10
11
|
- 只确认一次 `post_action`,不要逐个候选人反复确认
|
|
11
12
|
- 运行中临时中断请使用 `pause_recommend_pipeline_run`(按 run_id),不要靠自然语言“暂停/继续”指令
|
|
12
13
|
- 继续执行请使用 `resume_recommend_pipeline_run`;状态查询默认按用户指令触发,不自动轮询
|
|
@@ -20,7 +20,8 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
20
20
|
- **确认不可代填(强制)**
|
|
21
21
|
- 禁止 agent 自行“设置合理参数”并代替用户确认。
|
|
22
22
|
- 禁止在用户未明确回复前,把任意 `*_confirmed` 字段设为 `true`。
|
|
23
|
-
- 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job`。
|
|
23
|
+
- 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job/rest_level`。
|
|
24
|
+
- 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
|
|
24
25
|
- 若工具返回 `pending_questions`,必须逐项向用户提问并等待用户回复;不得跳过提问直接执行。
|
|
25
26
|
|
|
26
27
|
- **岗位确认时机**
|
|
@@ -54,6 +55,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
54
55
|
- `target_count`(可空)
|
|
55
56
|
- `post_action`:`favorite|greet|none`
|
|
56
57
|
- `max_greet_count`(仅当 `post_action=greet`)
|
|
58
|
+
- `rest_level`:`low|medium|high`
|
|
57
59
|
|
|
58
60
|
### Stage B (页面就绪后)
|
|
59
61
|
|
|
@@ -82,6 +84,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
82
84
|
- `gender`: `不限/男/女`
|
|
83
85
|
- `recent_not_view`: `不限/近14天没有`
|
|
84
86
|
- `post_action`: `favorite/greet/none`
|
|
87
|
+
- `rest_level`: `low/medium/high`
|
|
85
88
|
|
|
86
89
|
## Tool Usage
|
|
87
90
|
|
|
@@ -94,6 +97,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
|
|
|
94
97
|
- 关键输入:
|
|
95
98
|
- `confirmation`:`page_confirmed/page_value/filters_confirmed/school_tag_confirmed.../job_confirmed/job_value/final_confirmed`
|
|
96
99
|
- `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
|
|
100
|
+
- `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
|
|
97
101
|
- 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
|
|
98
102
|
|
|
99
103
|
最小策略:
|
|
@@ -15,3 +15,5 @@ This skill intentionally replaces legacy `boss-recruit-mcp` skill installs. It r
|
|
|
15
15
|
- `cancel_recruit_pipeline_run`
|
|
16
16
|
|
|
17
17
|
Do not call the old `@reconcrap/boss-recruit-mcp` package from this skill.
|
|
18
|
+
|
|
19
|
+
Each run must ask the user to choose `rest_level` (`low` / `medium` / `high`) and pass the answer as `human_behavior.restLevel`; do not pick a default for the user.
|
|
@@ -32,6 +32,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
|
|
|
32
32
|
- 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件、post action、max greet 等参数,必须逐项传入或确认。
|
|
33
33
|
- `post_action=greet` 时必须确认 `max_greet_count`;不要默认等于 `target_count`。
|
|
34
34
|
- 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
|
|
35
|
+
- 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
|
|
35
36
|
|
|
36
37
|
## Required Inputs
|
|
37
38
|
|
|
@@ -39,6 +40,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
|
|
|
39
40
|
- `keyword` 或用户明确的搜索意图
|
|
40
41
|
- `criteria`
|
|
41
42
|
- `target_count`
|
|
43
|
+
- `rest_level`: `low|medium|high`
|
|
42
44
|
|
|
43
45
|
常用可选项:
|
|
44
46
|
|
|
@@ -50,6 +52,12 @@ description: "Use when users want Boss search/recruit-page screening via the uni
|
|
|
50
52
|
- `max_greet_count`
|
|
51
53
|
- `port`
|
|
52
54
|
|
|
55
|
+
启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{ "human_behavior": { "restLevel": "medium" } }
|
|
59
|
+
```
|
|
60
|
+
|
|
53
61
|
## Response Style
|
|
54
62
|
|
|
55
63
|
- 用结构化中文确认参数。
|
package/src/chat-mcp.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import {
|
|
5
7
|
assertNoForbiddenCdpCalls,
|
|
6
8
|
bringPageToFront,
|
|
@@ -18,7 +20,8 @@ import {
|
|
|
18
20
|
RUN_STATUS_CANCELED,
|
|
19
21
|
RUN_STATUS_COMPLETED,
|
|
20
22
|
RUN_STATUS_FAILED,
|
|
21
|
-
RUN_STATUS_PAUSED
|
|
23
|
+
RUN_STATUS_PAUSED,
|
|
24
|
+
RUN_STATUS_RUNNING
|
|
22
25
|
} from "./core/run/index.js";
|
|
23
26
|
import {
|
|
24
27
|
buildLegacyScreenInputRows,
|
|
@@ -60,6 +63,8 @@ const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
|
|
|
60
63
|
const CHAT_ALL_MAX_CANDIDATES = 100000;
|
|
61
64
|
const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; numeric targets scan until that many candidates pass or the list ends; all/全部/扫到底 scans to the end";
|
|
62
65
|
const RUN_MODE_ASYNC = "async";
|
|
66
|
+
const DETACHED_WORKER_SCRIPT = fileURLToPath(new URL("./detached-worker.js", import.meta.url));
|
|
67
|
+
const DETACHED_WORKER_POLL_MS = 1000;
|
|
63
68
|
|
|
64
69
|
const CHAT_REQUIRED_FIELDS = Object.freeze([
|
|
65
70
|
"job",
|
|
@@ -170,6 +175,9 @@ function getChatRunArtifacts(runId) {
|
|
|
170
175
|
runs_dir: runsDir,
|
|
171
176
|
output_dir: outputDir,
|
|
172
177
|
run_state_path: path.join(runsDir, `${normalized}.json`),
|
|
178
|
+
detached_args_path: path.join(runsDir, `${normalized}.detached-args.json`),
|
|
179
|
+
worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
|
|
180
|
+
worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
|
|
173
181
|
checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
|
|
174
182
|
output_csv: path.join(outputDir, `${normalized}.results.csv`),
|
|
175
183
|
report_json: path.join(outputDir, `${normalized}.report.json`)
|
|
@@ -256,6 +264,156 @@ function readChatRunState(runId) {
|
|
|
256
264
|
return readJsonFile(artifacts.run_state_path);
|
|
257
265
|
}
|
|
258
266
|
|
|
267
|
+
function writeChatRunState(runId, payload) {
|
|
268
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
269
|
+
if (!artifacts) return null;
|
|
270
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
271
|
+
return payload;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createDetachedChatRunId() {
|
|
275
|
+
return `mcp_chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildInitialChatDetachedState(runId, {
|
|
279
|
+
workspaceRoot = "",
|
|
280
|
+
args = {},
|
|
281
|
+
normalized = {},
|
|
282
|
+
pid = process.pid
|
|
283
|
+
} = {}) {
|
|
284
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
285
|
+
const now = new Date().toISOString();
|
|
286
|
+
const isAllTarget = normalized.publicTargetCount === "all";
|
|
287
|
+
const processedLimit = isAllTarget ? CHAT_ALL_MAX_CANDIDATES : Math.max(1, Number(normalized.targetCount) || 1);
|
|
288
|
+
return {
|
|
289
|
+
run_id: runId,
|
|
290
|
+
mode: RUN_MODE_ASYNC,
|
|
291
|
+
state: "queued",
|
|
292
|
+
status: "queued",
|
|
293
|
+
stage: "queued",
|
|
294
|
+
started_at: now,
|
|
295
|
+
updated_at: now,
|
|
296
|
+
heartbeat_at: now,
|
|
297
|
+
completed_at: null,
|
|
298
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
|
|
299
|
+
progress: {
|
|
300
|
+
target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
|
|
301
|
+
target_pass_count: isAllTarget ? null : normalized.targetCount ?? null,
|
|
302
|
+
processed_limit: processedLimit,
|
|
303
|
+
processed: 0,
|
|
304
|
+
screened: 0,
|
|
305
|
+
detail_opened: 0,
|
|
306
|
+
llm_screened: 0,
|
|
307
|
+
passed: 0,
|
|
308
|
+
skipped: 0,
|
|
309
|
+
requested: 0,
|
|
310
|
+
request_satisfied: 0,
|
|
311
|
+
request_skipped: 0,
|
|
312
|
+
greet_count: 0
|
|
313
|
+
},
|
|
314
|
+
last_message: "Boss chat detached worker is queued.",
|
|
315
|
+
context: {
|
|
316
|
+
domain: "chat",
|
|
317
|
+
target_url: CHAT_TARGET_URL,
|
|
318
|
+
workspace_root: normalizeText(workspaceRoot) || process.cwd(),
|
|
319
|
+
profile: normalized.profile || args.profile || "default",
|
|
320
|
+
job: normalized.job || args.job || "",
|
|
321
|
+
start_from: normalized.startFrom || args.start_from || "",
|
|
322
|
+
criteria: normalized.criteria || args.criteria || "",
|
|
323
|
+
greeting_text: normalized.greetingText || args.greeting_text || args.greetingText || DEFAULT_CHAT_GREETING_TEXT,
|
|
324
|
+
target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
|
|
325
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
326
|
+
request_resume_for_passed: shouldRequestChatResume(args),
|
|
327
|
+
detached_worker: true
|
|
328
|
+
},
|
|
329
|
+
control: {
|
|
330
|
+
pause_requested: false,
|
|
331
|
+
pause_requested_at: null,
|
|
332
|
+
pause_requested_by: null,
|
|
333
|
+
cancel_requested: false
|
|
334
|
+
},
|
|
335
|
+
resume: {
|
|
336
|
+
checkpoint_path: artifacts?.checkpoint_path || null,
|
|
337
|
+
pause_control_path: artifacts?.run_state_path || null,
|
|
338
|
+
output_csv: null,
|
|
339
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
340
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
341
|
+
resume_count: 0,
|
|
342
|
+
last_resumed_at: null,
|
|
343
|
+
last_paused_at: null
|
|
344
|
+
},
|
|
345
|
+
error: null,
|
|
346
|
+
result: null,
|
|
347
|
+
summary: null,
|
|
348
|
+
artifacts
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function patchPersistedChatControl(runId, controlPatch = {}, {
|
|
353
|
+
status = "RUN_STATUS",
|
|
354
|
+
message = "",
|
|
355
|
+
lastMessage = ""
|
|
356
|
+
} = {}) {
|
|
357
|
+
const current = readChatRunState(runId);
|
|
358
|
+
if (!current) return null;
|
|
359
|
+
const state = normalizeText(current.state || current.status);
|
|
360
|
+
if (TERMINAL_STATUSES.has(state)) return null;
|
|
361
|
+
const now = new Date().toISOString();
|
|
362
|
+
const patched = {
|
|
363
|
+
...current,
|
|
364
|
+
updated_at: now,
|
|
365
|
+
heartbeat_at: now,
|
|
366
|
+
last_message: lastMessage || message || current.last_message || "",
|
|
367
|
+
control: {
|
|
368
|
+
...(current.control || {}),
|
|
369
|
+
...controlPatch
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
writeChatRunState(runId, patched);
|
|
373
|
+
return {
|
|
374
|
+
status,
|
|
375
|
+
run: patched,
|
|
376
|
+
message,
|
|
377
|
+
persistence: {
|
|
378
|
+
source: "disk",
|
|
379
|
+
active_control_available: false,
|
|
380
|
+
detached_control_requested: true
|
|
381
|
+
},
|
|
382
|
+
runtime_evaluate_used: false,
|
|
383
|
+
method_summary: {},
|
|
384
|
+
method_log: [],
|
|
385
|
+
chrome: null
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function launchDetachedChatWorker(runId) {
|
|
390
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
391
|
+
if (!artifacts) throw new Error("Invalid chat run_id");
|
|
392
|
+
fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
|
|
393
|
+
const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
|
|
394
|
+
const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
|
|
395
|
+
let child;
|
|
396
|
+
try {
|
|
397
|
+
child = spawn(process.execPath, [
|
|
398
|
+
DETACHED_WORKER_SCRIPT,
|
|
399
|
+
"--domain",
|
|
400
|
+
"chat",
|
|
401
|
+
"--run-id",
|
|
402
|
+
runId
|
|
403
|
+
], {
|
|
404
|
+
detached: true,
|
|
405
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
406
|
+
windowsHide: true,
|
|
407
|
+
env: process.env
|
|
408
|
+
});
|
|
409
|
+
} finally {
|
|
410
|
+
fs.closeSync(stdoutFd);
|
|
411
|
+
fs.closeSync(stderrFd);
|
|
412
|
+
}
|
|
413
|
+
if (typeof child?.unref === "function") child.unref();
|
|
414
|
+
return child;
|
|
415
|
+
}
|
|
416
|
+
|
|
259
417
|
function toIsoOrNull(value) {
|
|
260
418
|
const normalized = normalizeText(value);
|
|
261
419
|
return normalized || null;
|
|
@@ -491,6 +649,27 @@ function finalizePersistedChatRun(persisted = {}, {
|
|
|
491
649
|
return attachLegacyArtifactsToPersistedChatRun(next);
|
|
492
650
|
}
|
|
493
651
|
|
|
652
|
+
export function markBossChatDetachedWorkerFailed(runId, error, options = {}) {
|
|
653
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
654
|
+
if (!normalizedRunId) return null;
|
|
655
|
+
const persisted = readChatRunState(normalizedRunId) || buildInitialChatDetachedState(normalizedRunId, {});
|
|
656
|
+
const state = normalizeText(persisted.state || persisted.status);
|
|
657
|
+
if (TERMINAL_STATUSES.has(state)) return persisted;
|
|
658
|
+
const errorPayload = {
|
|
659
|
+
name: error?.name || "Error",
|
|
660
|
+
code: options.code || error?.code || "CHAT_WORKER_UNHANDLED_EXCEPTION",
|
|
661
|
+
message: normalizeText(error?.message || error || options.message) || "Boss chat detached worker exited unexpectedly."
|
|
662
|
+
};
|
|
663
|
+
if (normalizeText(error?.stack || "")) {
|
|
664
|
+
errorPayload.stack = String(error.stack).slice(0, 8000);
|
|
665
|
+
}
|
|
666
|
+
return finalizePersistedChatRun(persisted, {
|
|
667
|
+
status: RUN_STATUS_FAILED,
|
|
668
|
+
error: errorPayload,
|
|
669
|
+
message: errorPayload.message
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
494
673
|
function persistedChatRunArtifactMissing(persisted = {}) {
|
|
495
674
|
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
496
675
|
const artifacts = getChatRunArtifacts(runId);
|
|
@@ -568,6 +747,8 @@ function buildLegacyChatResult(snapshot) {
|
|
|
568
747
|
output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
|
|
569
748
|
report_json: artifacts?.report_json || meta.reportJsonPath || null,
|
|
570
749
|
checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
|
|
750
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
751
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
571
752
|
started_at: snapshot.startedAt,
|
|
572
753
|
completed_at: snapshot.completedAt || null,
|
|
573
754
|
duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
|
|
@@ -624,6 +805,8 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
624
805
|
checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
|
|
625
806
|
pause_control_path: artifacts?.run_state_path || null,
|
|
626
807
|
output_csv: legacyResult?.output_csv || null,
|
|
808
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
809
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
627
810
|
resume_count: meta.resumeCount || 0,
|
|
628
811
|
last_resumed_at: meta.lastResumedAt || null,
|
|
629
812
|
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
@@ -1177,7 +1360,7 @@ function trackChatRun(runId) {
|
|
|
1177
1360
|
});
|
|
1178
1361
|
}
|
|
1179
1362
|
|
|
1180
|
-
async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
|
|
1363
|
+
async function startBossChatRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
1181
1364
|
const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1182
1365
|
const normalized = normalizeChatStartInput(args, defaultConfigResolution);
|
|
1183
1366
|
const missingFields = getMissingChatStartFields(args, normalized);
|
|
@@ -1245,7 +1428,10 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
|
|
|
1245
1428
|
|
|
1246
1429
|
let started;
|
|
1247
1430
|
try {
|
|
1248
|
-
started = chatRunService.startChatRun(
|
|
1431
|
+
started = chatRunService.startChatRun({
|
|
1432
|
+
...getRunOptions(args, normalized, session, { workspaceRoot, configResolution }),
|
|
1433
|
+
runId
|
|
1434
|
+
});
|
|
1249
1435
|
} catch (error) {
|
|
1250
1436
|
await session.close?.();
|
|
1251
1437
|
return {
|
|
@@ -1525,6 +1711,178 @@ export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {
|
|
|
1525
1711
|
return attachMethodEvidence(started, started.run_id);
|
|
1526
1712
|
}
|
|
1527
1713
|
|
|
1714
|
+
export async function startBossChatDetachedRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1715
|
+
const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1716
|
+
const normalized = normalizeChatStartInput(args, defaultConfigResolution);
|
|
1717
|
+
const missingFields = getMissingChatStartFields(args, normalized);
|
|
1718
|
+
if (missingFields.length) {
|
|
1719
|
+
return buildNeedInputResponse({
|
|
1720
|
+
args,
|
|
1721
|
+
missingFields,
|
|
1722
|
+
normalized
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const useLlm = shouldUseChatLlm(args);
|
|
1727
|
+
const debugTestOptions = collectChatDebugTestOptions(args);
|
|
1728
|
+
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1729
|
+
return {
|
|
1730
|
+
status: "FAILED",
|
|
1731
|
+
error: {
|
|
1732
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1733
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1734
|
+
retryable: false
|
|
1735
|
+
},
|
|
1736
|
+
debug_test_options: debugTestOptions
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
|
|
1740
|
+
if (useLlm && !configResolution?.ok) {
|
|
1741
|
+
return {
|
|
1742
|
+
status: "FAILED",
|
|
1743
|
+
error: {
|
|
1744
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1745
|
+
message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
|
|
1746
|
+
retryable: true
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const runId = createDetachedChatRunId();
|
|
1752
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
1753
|
+
const initial = buildInitialChatDetachedState(runId, {
|
|
1754
|
+
workspaceRoot,
|
|
1755
|
+
args,
|
|
1756
|
+
normalized,
|
|
1757
|
+
pid: process.pid
|
|
1758
|
+
});
|
|
1759
|
+
try {
|
|
1760
|
+
writeJsonAtomic(artifacts.detached_args_path, {
|
|
1761
|
+
domain: "chat",
|
|
1762
|
+
run_id: runId,
|
|
1763
|
+
workspace_root: normalizeText(workspaceRoot) || process.cwd(),
|
|
1764
|
+
args: clonePlain(args, {})
|
|
1765
|
+
});
|
|
1766
|
+
writeChatRunState(runId, initial);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
return {
|
|
1769
|
+
status: "FAILED",
|
|
1770
|
+
error: {
|
|
1771
|
+
code: "CHAT_RUN_STATE_IO_ERROR",
|
|
1772
|
+
message: `Unable to write Boss chat detached run state: ${error?.message || error}`,
|
|
1773
|
+
retryable: false
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
let child;
|
|
1779
|
+
try {
|
|
1780
|
+
child = launchDetachedChatWorker(runId);
|
|
1781
|
+
const now = new Date().toISOString();
|
|
1782
|
+
const latest = readChatRunState(runId) || initial;
|
|
1783
|
+
const latestState = normalizeText(latest.state || latest.status);
|
|
1784
|
+
if (TERMINAL_STATUSES.has(latestState)) {
|
|
1785
|
+
return {
|
|
1786
|
+
status: "FAILED",
|
|
1787
|
+
error: latest.error || {
|
|
1788
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1789
|
+
message: "Boss chat detached worker exited during launch.",
|
|
1790
|
+
retryable: true
|
|
1791
|
+
},
|
|
1792
|
+
run: latest,
|
|
1793
|
+
runtime_evaluate_used: false,
|
|
1794
|
+
method_summary: {},
|
|
1795
|
+
method_log: [],
|
|
1796
|
+
chrome: null
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const queued = {
|
|
1800
|
+
...latest,
|
|
1801
|
+
pid: child.pid || process.pid,
|
|
1802
|
+
updated_at: now,
|
|
1803
|
+
heartbeat_at: now,
|
|
1804
|
+
last_message: "Boss chat detached worker launched."
|
|
1805
|
+
};
|
|
1806
|
+
writeChatRunState(runId, queued);
|
|
1807
|
+
return {
|
|
1808
|
+
status: "ACCEPTED",
|
|
1809
|
+
run_id: runId,
|
|
1810
|
+
state: "queued",
|
|
1811
|
+
run: queued,
|
|
1812
|
+
poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
|
|
1813
|
+
message: "Boss chat run started in a detached worker. It can continue after the MCP host returns or is recycled.",
|
|
1814
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
1815
|
+
detached_worker: true,
|
|
1816
|
+
runtime_evaluate_used: false,
|
|
1817
|
+
method_summary: {},
|
|
1818
|
+
method_log: [],
|
|
1819
|
+
chrome: null
|
|
1820
|
+
};
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
const failed = markBossChatDetachedWorkerFailed(runId, error, {
|
|
1823
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1824
|
+
message: "Unable to launch Boss chat detached worker."
|
|
1825
|
+
});
|
|
1826
|
+
return {
|
|
1827
|
+
status: "FAILED",
|
|
1828
|
+
error: failed?.error || {
|
|
1829
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1830
|
+
message: error?.message || "Unable to launch Boss chat detached worker.",
|
|
1831
|
+
retryable: true
|
|
1832
|
+
},
|
|
1833
|
+
run: failed || readChatRunState(runId),
|
|
1834
|
+
runtime_evaluate_used: false,
|
|
1835
|
+
method_summary: {},
|
|
1836
|
+
method_log: [],
|
|
1837
|
+
chrome: null
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
export async function runBossChatDetachedWorker({ runId } = {}) {
|
|
1843
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
1844
|
+
if (!normalizedRunId) return { ok: false, error: "run_id is required" };
|
|
1845
|
+
const artifacts = getChatRunArtifacts(normalizedRunId);
|
|
1846
|
+
const spec = readJsonFile(artifacts?.detached_args_path || "");
|
|
1847
|
+
if (!spec) {
|
|
1848
|
+
const error = new Error(`Boss chat detached args were not found for run_id=${normalizedRunId}`);
|
|
1849
|
+
markBossChatDetachedWorkerFailed(normalizedRunId, error, { code: "CHAT_WORKER_ARGS_MISSING" });
|
|
1850
|
+
return { ok: false, error: error.message };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const started = await startBossChatRunInternal(spec.args || {}, {
|
|
1854
|
+
workspaceRoot: spec.workspace_root || "",
|
|
1855
|
+
runId: normalizedRunId
|
|
1856
|
+
});
|
|
1857
|
+
if (started?.status !== "ACCEPTED") {
|
|
1858
|
+
const failedError = started?.error || {
|
|
1859
|
+
code: "CHAT_WORKER_START_FAILED",
|
|
1860
|
+
message: started?.status || "Boss chat detached worker failed to start.",
|
|
1861
|
+
retryable: true
|
|
1862
|
+
};
|
|
1863
|
+
markBossChatDetachedWorkerFailed(normalizedRunId, failedError, {
|
|
1864
|
+
code: failedError.code || "CHAT_WORKER_START_FAILED"
|
|
1865
|
+
});
|
|
1866
|
+
return { ok: false, error: failedError.message || "Boss chat detached worker failed to start." };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
while (true) {
|
|
1870
|
+
const payload = getBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1871
|
+
const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
|
|
1872
|
+
if (TERMINAL_STATUSES.has(state)) break;
|
|
1873
|
+
const persisted = readChatRunState(normalizedRunId);
|
|
1874
|
+
if (persisted?.control?.cancel_requested === true) {
|
|
1875
|
+
cancelBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1876
|
+
} else if (persisted?.control?.pause_requested === true && state === RUN_STATUS_RUNNING) {
|
|
1877
|
+
pauseBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1878
|
+
} else if (persisted?.control?.pause_requested === false && state === RUN_STATUS_PAUSED) {
|
|
1879
|
+
resumeBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1880
|
+
}
|
|
1881
|
+
await sleep(DETACHED_WORKER_POLL_MS);
|
|
1882
|
+
}
|
|
1883
|
+
return { ok: true };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1528
1886
|
export function getBossChatRunTool({ args = {} } = {}) {
|
|
1529
1887
|
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1530
1888
|
if (!runId) {
|
|
@@ -1615,6 +1973,20 @@ export function pauseBossChatRunTool({ args = {} } = {}) {
|
|
|
1615
1973
|
chrome: null
|
|
1616
1974
|
};
|
|
1617
1975
|
}
|
|
1976
|
+
if (persisted) {
|
|
1977
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1978
|
+
if (reconciled.stale_finalized) return getBossChatRunTool({ args });
|
|
1979
|
+
return patchPersistedChatControl(runId, {
|
|
1980
|
+
pause_requested: true,
|
|
1981
|
+
pause_requested_at: new Date().toISOString(),
|
|
1982
|
+
pause_requested_by: "pause_boss_chat_run",
|
|
1983
|
+
cancel_requested: false
|
|
1984
|
+
}, {
|
|
1985
|
+
status: "PAUSE_REQUESTED",
|
|
1986
|
+
message: "暂停请求已写入 detached chat run 控制文件。",
|
|
1987
|
+
lastMessage: "暂停请求已写入 detached chat run 控制文件。"
|
|
1988
|
+
}) || getBossChatRunTool({ args });
|
|
1989
|
+
}
|
|
1618
1990
|
return getBossChatRunTool({ args });
|
|
1619
1991
|
}
|
|
1620
1992
|
}
|
|
@@ -1665,6 +2037,18 @@ export function resumeBossChatRunTool({ args = {} } = {}) {
|
|
|
1665
2037
|
if (persisted) {
|
|
1666
2038
|
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1667
2039
|
const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
|
|
2040
|
+
if (!TERMINAL_STATUSES.has(reconciledStatus)) {
|
|
2041
|
+
return patchPersistedChatControl(runId, {
|
|
2042
|
+
pause_requested: false,
|
|
2043
|
+
pause_requested_at: null,
|
|
2044
|
+
pause_requested_by: null,
|
|
2045
|
+
cancel_requested: false
|
|
2046
|
+
}, {
|
|
2047
|
+
status: "RESUME_REQUESTED",
|
|
2048
|
+
message: "恢复请求已写入 detached chat run 控制文件。",
|
|
2049
|
+
lastMessage: "恢复请求已写入 detached chat run 控制文件。"
|
|
2050
|
+
}) || getBossChatRunTool({ args });
|
|
2051
|
+
}
|
|
1668
2052
|
return {
|
|
1669
2053
|
status: "FAILED",
|
|
1670
2054
|
error: {
|
|
@@ -1743,6 +2127,16 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
|
|
|
1743
2127
|
chrome: null
|
|
1744
2128
|
};
|
|
1745
2129
|
}
|
|
2130
|
+
return patchPersistedChatControl(runId, {
|
|
2131
|
+
pause_requested: true,
|
|
2132
|
+
pause_requested_at: new Date().toISOString(),
|
|
2133
|
+
pause_requested_by: "cancel_boss_chat_run",
|
|
2134
|
+
cancel_requested: true
|
|
2135
|
+
}, {
|
|
2136
|
+
status: "CANCEL_REQUESTED",
|
|
2137
|
+
message: "取消请求已写入 detached chat run 控制文件。",
|
|
2138
|
+
lastMessage: "取消请求已写入 detached chat run 控制文件。"
|
|
2139
|
+
}) || getBossChatRunTool({ args });
|
|
1746
2140
|
}
|
|
1747
2141
|
return getBossChatRunTool({ args });
|
|
1748
2142
|
}
|