@reconcrap/boss-recommend-mcp 2.1.14 → 2.1.15
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 -5
- package/package.json +1 -1
- package/skills/boss-chat/README.md +2 -2
- package/skills/boss-chat/SKILL.md +7 -7
- package/skills/boss-recruit-pipeline/SKILL.md +23 -1
- package/src/chat-mcp.js +70 -73
- package/src/domains/chat/detail.js +79 -47
- package/src/domains/chat/run-service.js +400 -158
- package/src/domains/recruit/constants.js +65 -0
- package/src/domains/recruit/instruction-parser.js +362 -86
- package/src/domains/recruit/run-service.js +281 -10
- package/src/domains/recruit/search.js +2076 -298
- package/src/index.js +18 -12
- package/src/recommend-mcp.js +77 -8
- package/src/recruit-mcp.js +228 -3
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ boss-recommend-mcp schedule-status --schedule-id <schedule_id>
|
|
|
176
176
|
- 不会对每位候选人重复确认
|
|
177
177
|
- 推荐页详情处理完成后,会强制关闭详情页并确认已关闭
|
|
178
178
|
- 简历提取优先使用 Network 响应;没有可解析 Network CV 时,回退到完整滚动截图序列再交给多模态模型判断
|
|
179
|
-
- recommend / search / chat 正式运行默认全部使用 `screening-config.json` 配置的 LLM 筛选;deterministic/local scorer 只保留给明确测试场景,必须显式传 `debug_test_mode=true` 且 `screening_mode=deterministic` 或 `use_llm=false`。
|
|
179
|
+
- recommend / search / 带 `criteria` 的 chat 正式运行默认全部使用 `screening-config.json` 配置的 LLM 筛选;chat 的 `criteria` 留空时进入收集简历模式,不需要 LLM 配置。deterministic/local scorer 只保留给明确测试场景,必须显式传 `debug_test_mode=true` 且 `screening_mode=deterministic` 或 `use_llm=false`。
|
|
180
180
|
- `detail_limit=0`、`no_filter`、`filter_enabled=false`、后置动作 dry-run、chat 求简历 dry-run 等调试路径不会在正式 live run 默认启用;需要测试时必须显式传 `debug_test_mode=true`。
|
|
181
181
|
- 提供显式运维自愈工具:只在手动调用 `run_recommend_self_heal` 时运行,不会接入正常 run / doctor / preflight 自动链路
|
|
182
182
|
- 运行前会自动做依赖体检(Node.js、Python、Pillow、`chrome-remote-interface`、`ws`),缺失时会在 `doctor` 与流水线失败诊断中明确提示
|
|
@@ -433,7 +433,7 @@ node src/cli.js chat prepare-run --slow-live --port 9222
|
|
|
433
433
|
|
|
434
434
|
说明:
|
|
435
435
|
|
|
436
|
-
- `
|
|
436
|
+
- `start_from` / `target_count` 为必填;`criteria` 可选,留空时 chat run 进入收集简历模式:不触发 LLM 筛选,针对没有在线/附件简历的人选发送求简历消息并点击求简历按钮
|
|
437
437
|
- `greeting_text` 可选(兼容 `greetingText`)
|
|
438
438
|
- `profile` 可选,默认 `default`
|
|
439
439
|
- `job` 与 `port` 继承 recommend run 已选岗位和调试端口
|
|
@@ -468,15 +468,15 @@ node src/cli.js chat prepare-run --slow-live --port 9222
|
|
|
468
468
|
chat-only 交互建议:
|
|
469
469
|
|
|
470
470
|
- 先调用一次 `list_boss_chat_jobs` 或 `prepare_boss_chat_run`(可不带参数),服务会先导航到 `https://www.zhipin.com/web/chat/index` 并返回 `NEED_INPUT`,其中包含岗位 `job_options` 与待补字段。
|
|
471
|
-
- 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count
|
|
471
|
+
- 然后基于 `job_options` 让用户选择 `job`,并补齐 `start_from`、`target_count` 后调用 `start_boss_chat_run` 启动任务;若要筛选再求简历,提供 `criteria`,若只想收集缺失简历则留空。
|
|
472
472
|
- `greeting_text` 可选;未传时使用 `screening-config.json.greetingMessage`,若未配置则使用默认招呼语(`Hi同学,能麻烦发下简历吗?`)。
|
|
473
473
|
- `target_count` 支持正整数、`all`、`-1`;若用户给出 `全部候选人` / `所有候选人`,会自动按不限(扫到底)处理。
|
|
474
474
|
|
|
475
475
|
Trae-CN / 长对话防循环建议:
|
|
476
476
|
|
|
477
|
-
- 固定流程:`boss_chat_health_check` -> `list_boss_chat_jobs(空参可)` / `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count
|
|
477
|
+
- 固定流程:`boss_chat_health_check` -> `list_boss_chat_jobs(空参可)` / `prepare_boss_chat_run(空参可)` -> 一次性补齐 `job/start_from/target_count`,筛选任务再带 `criteria` -> `start_boss_chat_run`。
|
|
478
478
|
- chat-only 场景严禁调用 `list_recommend_jobs`、`run_recommend` 或 `start_recommend_pipeline_run`。
|
|
479
|
-
- `start_boss_chat_run` 的工具 schema 已把 `job/start_from/target_count
|
|
479
|
+
- `start_boss_chat_run` 的工具 schema 已把 `job/start_from/target_count` 标记为必填;`criteria` 留空会进入收集简历模式。不要用它获取岗位列表。
|
|
480
480
|
- 若 `pending_questions` / UI 选项里出现“扫到底(必须传 `target_count="all"`)”,下一次工具调用请直接照抄 `"target_count": "all"`,不要只保留“扫到底”这层自然语言语义。
|
|
481
481
|
- `start_boss_chat_run` 返回 `ACCEPTED` 后直接结束当前回合,不要自动轮询。
|
|
482
482
|
- 缺参或校验失败时,一次性列出全部缺失/错误项,避免重复同一句提示触发宿主“陷入循环”保护。
|
package/package.json
CHANGED
|
@@ -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 list_boss_chat_jobs once (empty params allowed), or prepare_boss_chat_run once, to fetch job_options and missing fields.
|
|
20
|
-
3) Ask for these required fields in one shot: job, start_from (unread/all), target_count,
|
|
20
|
+
3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, rest_level (low/medium/high). Ask for criteria only when the user wants screening; leave it blank for collect-CV runs.
|
|
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
|
|
|
@@ -26,7 +26,7 @@ Anti-loop rules:
|
|
|
26
26
|
- On validation errors, list all missing/invalid fields once.
|
|
27
27
|
- Do not call list_recommend_jobs for chat-only tasks; it is recommend-page only and will switch the browser to /web/chat/recommend.
|
|
28
28
|
- Do not call run_recommend or start_recommend_pipeline_run for chat-only tasks; use start_boss_chat_run.
|
|
29
|
-
- 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/
|
|
29
|
+
- 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. Include criteria for screening; leave criteria blank to collect CVs from candidates without an online/attachment CV.
|
|
30
30
|
- Do not call start_boss_chat_run repeatedly in one turn.
|
|
31
31
|
- Do not call get_boss_chat_run unless user explicitly asks for progress.
|
|
32
32
|
- Do not choose rest_level for the user. Pass the user's choice as `human_behavior.restLevel`.
|
|
@@ -29,11 +29,11 @@ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server.
|
|
|
29
29
|
- `job`
|
|
30
30
|
- `start_from`: `unread|all`
|
|
31
31
|
- `target_count`
|
|
32
|
-
- `criteria`
|
|
33
32
|
- `rest_level`: `low|medium|high`
|
|
34
33
|
|
|
35
34
|
可选:
|
|
36
35
|
|
|
36
|
+
- `criteria`(留空时进入收集简历模式:不触发 LLM 筛选,只向没有在线/附件简历的人选求简历)
|
|
37
37
|
- `profile`(默认 `default`)
|
|
38
38
|
- `greeting_text`(兼容 `greetingText`,可选自定义首条打招呼消息)
|
|
39
39
|
- `port`
|
|
@@ -62,7 +62,7 @@ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server.
|
|
|
62
62
|
|
|
63
63
|
## Hard Rules
|
|
64
64
|
|
|
65
|
-
- LLM 配置必须复用 `boss-recommend-mcp` 的 `screening-config.json`;不要再向用户单独要 `baseUrl/apiKey/model
|
|
65
|
+
- 有筛选 `criteria` 时,LLM 配置必须复用 `boss-recommend-mcp` 的 `screening-config.json`;不要再向用户单独要 `baseUrl/apiKey/model`。`criteria` 留空的收集简历模式不需要 LLM 配置。
|
|
66
66
|
- 路由护栏(强制):
|
|
67
67
|
- 只在用户明确是 chat-only 任务时使用本 skill。
|
|
68
68
|
- 只要用户提到推荐页、先找人后沟通、或需要推荐筛选阶段,禁止直接调用 `start_boss_chat_run`;必须先交给 `boss-recommend-pipeline` 完成推荐页任务。
|
|
@@ -70,7 +70,7 @@ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server.
|
|
|
70
70
|
- 启动或准备 chat run 时,若本机默认 `127.0.0.1:9222` Chrome DevTools 端口不可连,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/index`。
|
|
71
71
|
- 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
|
|
72
72
|
- 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
|
|
73
|
-
- `job` / `start_from`
|
|
73
|
+
- `job` / `start_from` 缺一不可;`criteria` 可留空,留空时表示收集缺失简历而不是筛选。缺参时只补缺口。
|
|
74
74
|
- `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
|
|
75
75
|
- chat-only 岗位列表只能通过 `list_boss_chat_jobs` 或 `prepare_boss_chat_run` 获取;严禁调用 `list_recommend_jobs`,因为它会切到推荐页。
|
|
76
76
|
- chat-only 启动只能调用 `start_boss_chat_run`;严禁调用 `run_recommend` 或 `start_recommend_pipeline_run`。
|
|
@@ -80,10 +80,10 @@ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server.
|
|
|
80
80
|
- 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
|
|
81
81
|
- `greeting_text` 是可选项,不能因为缺少它阻塞启动或追加必填追问。
|
|
82
82
|
- 若工具或提问选项里出现“扫到底(必须传 `target_count=\"all\"`)”之类字样,下一次工具调用时必须直接照抄这个字面量,不要只保留“扫到底”语义。
|
|
83
|
-
- 禁止 agent 自行补全 `job/start_from
|
|
83
|
+
- 禁止 agent 自行补全 `job/start_from` 并直接执行,必须由用户明确给出或确认。`criteria` 只有在用户要筛选时才必须明确给出;用户要收集简历时可留空。
|
|
84
84
|
- chat-only 启动流程必须先进入聊天页并拉取岗位列表,再让用户从列表中选择 `job`。
|
|
85
85
|
- 必须先用空参调用 `list_boss_chat_jobs` 或 `prepare_boss_chat_run` 获取 `job_options`;不要用 `start_boss_chat_run` 做预备调用。
|
|
86
|
-
- `start_boss_chat_run` 只能用于真正启动,必须一次性传齐 `job` / `start_from` / `target_count`
|
|
86
|
+
- `start_boss_chat_run` 只能用于真正启动,必须一次性传齐 `job` / `start_from` / `target_count`;`criteria` 留空会进入收集简历模式。
|
|
87
87
|
- 若 `start_boss_chat_run` 返回 `NEED_INPUT` 且 `missing_fields` 包含 `target_count`,说明你没有把用户选择写入工具参数;下一次调用必须照 `next_call_example` 原样补上 `"target_count": "all"` 或正整数,不要重复空调用。
|
|
88
88
|
- 默认不自动轮询;只有用户要求查进度时才调用 `get_boss_chat_run`。
|
|
89
89
|
- `start_boss_chat_run` 返回 `ACCEPTED` 后,默认立即结束当前回合,不得主动连续调用 `get_boss_chat_run`。
|
|
@@ -95,12 +95,12 @@ Trae/Trae-CN split-server config exposes these under the `boss-chat` MCP server.
|
|
|
95
95
|
|
|
96
96
|
- 若用户先运行了 recommend 流水线,并在手动状态检查时确认 recommend 已完成,且用户目标是“立即进入聊天沟通”:
|
|
97
97
|
- 先调用 `prepare_boss_chat_run` 获取聊天页岗位列表与缺参。
|
|
98
|
-
- 显式确认 `job/start_from/target_count
|
|
98
|
+
- 显式确认 `job/start_from/target_count` 后再调用 `start_boss_chat_run`;若要继续筛选再求简历,还要确认 `criteria`。
|
|
99
99
|
- 不要查找或依赖 `follow_up.chat`;该自动衔接路径属于 legacy-only 行为。
|
|
100
100
|
|
|
101
101
|
## Response Style
|
|
102
102
|
|
|
103
103
|
- 用结构化中文。
|
|
104
104
|
- 首轮建议先调用一次 `list_boss_chat_jobs`(可空参)或 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
|
|
105
|
-
- 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria
|
|
105
|
+
- 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`rest_level`;只有用户要筛选时才追问 `criteria`。
|
|
106
106
|
- 若健康检查失败,明确提示共享配置文件 `screening-config.json` 不可用。
|
|
@@ -20,6 +20,8 @@ Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP serv
|
|
|
20
20
|
- 继续:`resume_recruit_pipeline_run`
|
|
21
21
|
- 取消:`cancel_recruit_pipeline_run`
|
|
22
22
|
|
|
23
|
+
If the visible tool surface only offers `boss-recommend/*` for a search/recruit task, stop and report a tool-surface/config error. Do not call `boss-recommend/list_recommend_jobs`, `boss-recommend/run_recommend`, or `boss-recommend/start_recommend_pipeline_run` as a fallback for search.
|
|
24
|
+
|
|
23
25
|
## Hard Rules
|
|
24
26
|
|
|
25
27
|
- 只在用户明确说搜索页、search、recruit、招聘搜索、`/web/chat/search` 时使用本 skill。
|
|
@@ -31,9 +33,14 @@ Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP serv
|
|
|
31
33
|
- 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
|
|
32
34
|
- 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
|
|
33
35
|
- 若用户未提供岗位,必须先询问岗位。搜索页岗位选择在关键词输入框旁边;不要猜测默认岗位。
|
|
36
|
+
- 搜索页任务不要调用 `list_recommend_jobs` 获取岗位;推荐页岗位列表和搜索页岗位选择不是同一个工具面。用户已经给出岗位时直接传 `overrides.job`。
|
|
34
37
|
- 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件等参数,必须逐项传入或确认。
|
|
35
38
|
- 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
|
|
36
39
|
- 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
|
|
40
|
+
- 启动前展示一次包含岗位、关键词、城市、学历、学校标签、是否过滤已看、criteria、目标人数、后置动作和休息强度的总确认;用户确认后,`confirmation` 只需要 `{ "final_confirmed": true }`。
|
|
41
|
+
- 不要让工具重写用户的 `criteria`。用户给出 `筛选条件` / `筛选标准` / `硬条件` 时,逐字写入 `overrides.criteria`;不要传系统简化版。
|
|
42
|
+
- 用户说学校类型“不限”时,在 `overrides.school_tag` 显式传 `"不限"` 或在 `overrides.schools` 传 `[]`;不要因为 criteria 里出现 `985/211/双一流` 就把它们当作搜索页学校过滤器。
|
|
43
|
+
- 用户说只看未查看“不限”时,在 `overrides.recent_not_view` 显式传 `"不限"` 或在 `overrides.filter_recent_viewed` 传 `false`。
|
|
37
44
|
|
|
38
45
|
## Required Inputs
|
|
39
46
|
|
|
@@ -51,10 +58,25 @@ Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP serv
|
|
|
51
58
|
- `recent_not_view`
|
|
52
59
|
- `port`
|
|
53
60
|
|
|
61
|
+
## Confirmation Flow
|
|
62
|
+
|
|
63
|
+
1. 先从用户原始消息里提取参数;缺什么只问什么。不要先调用 MCP 让工具猜缺参。
|
|
64
|
+
2. `criteria` 必须来自用户明确提供的筛选条件,或来自你向用户追问后得到的完整自然语言标准。禁止把岗位、关键词、学历等字段拼成 criteria。
|
|
65
|
+
3. 如果用户一开始已经给齐 `job`、`keyword`、`criteria`、`target_count`、`rest_level`,并且可选筛选项也已明确或确认不限,直接展示一次总确认。
|
|
66
|
+
4. 用户确认后调用 `boss-recruit/start_recruit_pipeline_run` 或 `boss-recruit/run_recruit_pipeline`,传:
|
|
67
|
+
- `confirmation: { "final_confirmed": true }`
|
|
68
|
+
- 所有规范化字段放在 `overrides`
|
|
69
|
+
- 完整 criteria 放在 `overrides.criteria`
|
|
70
|
+
- 本次休息强度放在 `human_behavior.restLevel`
|
|
71
|
+
5. 工具返回 `NEED_INPUT` 时,按 `pending_questions` 只补具体缺口;不要接受或转述工具生成的简化 criteria。
|
|
72
|
+
|
|
54
73
|
启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
|
|
55
74
|
|
|
56
75
|
```json
|
|
57
|
-
{
|
|
76
|
+
{
|
|
77
|
+
"confirmation": { "final_confirmed": true },
|
|
78
|
+
"human_behavior": { "restLevel": "medium" }
|
|
79
|
+
}
|
|
58
80
|
```
|
|
59
81
|
|
|
60
82
|
## Response Style
|
package/src/chat-mcp.js
CHANGED
|
@@ -66,12 +66,11 @@ const RUN_MODE_ASYNC = "async";
|
|
|
66
66
|
const DETACHED_WORKER_SCRIPT = fileURLToPath(new URL("./detached-worker.js", import.meta.url));
|
|
67
67
|
const DETACHED_WORKER_POLL_MS = 1000;
|
|
68
68
|
|
|
69
|
-
const CHAT_REQUIRED_FIELDS = Object.freeze([
|
|
70
|
-
"job",
|
|
71
|
-
"start_from",
|
|
72
|
-
"target_count"
|
|
73
|
-
|
|
74
|
-
]);
|
|
69
|
+
const CHAT_REQUIRED_FIELDS = Object.freeze([
|
|
70
|
+
"job",
|
|
71
|
+
"start_from",
|
|
72
|
+
"target_count"
|
|
73
|
+
]);
|
|
75
74
|
|
|
76
75
|
const TERMINAL_STATUSES = new Set([
|
|
77
76
|
RUN_STATUS_COMPLETED,
|
|
@@ -1097,14 +1096,13 @@ function buildChatNextCallExample(args, missingFields, normalized) {
|
|
|
1097
1096
|
return Object.keys(example).length ? example : null;
|
|
1098
1097
|
}
|
|
1099
1098
|
|
|
1100
|
-
function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
|
|
1101
|
-
const missing = [];
|
|
1102
|
-
if (!normalized.job) missing.push("job");
|
|
1103
|
-
if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
|
|
1104
|
-
if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1099
|
+
function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
|
|
1100
|
+
const missing = [];
|
|
1101
|
+
if (!normalized.job) missing.push("job");
|
|
1102
|
+
if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
|
|
1103
|
+
if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
|
|
1104
|
+
return missing;
|
|
1105
|
+
}
|
|
1108
1106
|
|
|
1109
1107
|
function buildTargetCountDiagnostics(args, missingFields, normalized) {
|
|
1110
1108
|
if (!missingFields.includes("target_count")) return {};
|
|
@@ -1168,17 +1166,10 @@ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions
|
|
|
1168
1166
|
parse_error: normalized.target.parseError || null
|
|
1169
1167
|
};
|
|
1170
1168
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
value: normalized.criteria || null
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
return {
|
|
1179
|
-
field,
|
|
1180
|
-
question: `请提供 ${field}。`,
|
|
1181
|
-
value: null
|
|
1169
|
+
return {
|
|
1170
|
+
field,
|
|
1171
|
+
question: `请提供 ${field}。`,
|
|
1172
|
+
value: null
|
|
1182
1173
|
};
|
|
1183
1174
|
});
|
|
1184
1175
|
}
|
|
@@ -1186,19 +1177,21 @@ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions
|
|
|
1186
1177
|
async function buildNeedInputResponse({ args, missingFields, normalized }) {
|
|
1187
1178
|
const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
|
|
1188
1179
|
return {
|
|
1189
|
-
status: "NEED_INPUT",
|
|
1190
|
-
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
1191
|
-
missing_fields: missingFields,
|
|
1192
|
-
|
|
1180
|
+
status: "NEED_INPUT",
|
|
1181
|
+
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
1182
|
+
missing_fields: missingFields,
|
|
1183
|
+
criteria_optional: true,
|
|
1184
|
+
empty_criteria_mode: "collect_cv",
|
|
1185
|
+
...diagnostics,
|
|
1193
1186
|
pending_questions: buildPendingChatQuestions({ args, missingFields, normalized }),
|
|
1194
1187
|
job_options: [],
|
|
1195
|
-
error: {
|
|
1196
|
-
code: "MISSING_REQUIRED_FIELDS",
|
|
1197
|
-
message: "缺少必要字段。请补齐 job、start_from、target_count
|
|
1198
|
-
retryable: true
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1188
|
+
error: {
|
|
1189
|
+
code: "MISSING_REQUIRED_FIELDS",
|
|
1190
|
+
message: "缺少必要字段。请补齐 job、start_from、target_count 后再启动 Boss chat CDP-only run。criteria 可留空;留空时会进入收集简历模式。",
|
|
1191
|
+
retryable: true
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1202
1195
|
|
|
1203
1196
|
function shouldRequestChatResume(args = {}, context = {}) {
|
|
1204
1197
|
const action = normalizeText(args.post_action || args.action).toLowerCase();
|
|
@@ -1232,38 +1225,40 @@ function isDebugTestMode(args = {}) {
|
|
|
1232
1225
|
return args.debug_test_mode === true || args.allow_debug_test_mode === true;
|
|
1233
1226
|
}
|
|
1234
1227
|
|
|
1235
|
-
function normalizeScreeningModeArg(args = {}) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
return
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (
|
|
1247
|
-
if (args.
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1228
|
+
function normalizeScreeningModeArg(args = {}, normalized = normalizeChatStartInput(args)) {
|
|
1229
|
+
if (!normalized.criteria) return "collect_cv";
|
|
1230
|
+
const raw = normalizeText(args.screening_mode || args.screeningMode || "");
|
|
1231
|
+
if (args.use_llm === false) return "deterministic";
|
|
1232
|
+
return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
|
|
1233
|
+
? "deterministic"
|
|
1234
|
+
: "llm";
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function collectChatDebugTestOptions(args = {}) {
|
|
1238
|
+
const reasons = [];
|
|
1239
|
+
if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
|
|
1240
|
+
if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
|
|
1241
|
+
if (args.dry_run === true || args.dry_run_request_cv === true) reasons.push("dry_run_request_cv");
|
|
1242
|
+
return reasons;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function shouldUseChatLlm(args = {}, normalized = normalizeChatStartInput(args)) {
|
|
1246
|
+
return normalizeScreeningModeArg(args, normalized) === "llm";
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function getRunOptions(args, normalized, session, { workspaceRoot = "", configResolution = null } = {}) {
|
|
1256
1250
|
const slowLive = args.slow_live === true;
|
|
1257
1251
|
const isAllTarget = normalized.publicTargetCount === "all";
|
|
1258
1252
|
const processedLimit = parsePositiveInteger(
|
|
1259
1253
|
args.max_candidates,
|
|
1260
1254
|
isAllTarget ? CHAT_ALL_MAX_CANDIDATES : CHAT_ALL_MAX_CANDIDATES
|
|
1261
|
-
);
|
|
1262
|
-
const shouldRequestResume = shouldRequestChatResume(args);
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1255
|
+
);
|
|
1256
|
+
const shouldRequestResume = shouldRequestChatResume(args);
|
|
1257
|
+
const screeningMode = normalizeScreeningModeArg(args, normalized);
|
|
1258
|
+
const useLlm = shouldUseChatLlm(args, normalized);
|
|
1259
|
+
const resolvedConfig = configResolution || (useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false });
|
|
1260
|
+
const humanBehavior = resolveHumanBehaviorForRun(args, resolvedConfig?.config || {});
|
|
1261
|
+
return {
|
|
1267
1262
|
client: session.client,
|
|
1268
1263
|
targetUrl: CHAT_TARGET_URL,
|
|
1269
1264
|
job: normalized.job,
|
|
@@ -1302,7 +1297,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
|
|
|
1302
1297
|
llmImageDetail: normalizeText(
|
|
1303
1298
|
args.llm_image_detail || resolvedConfig.config?.llmImageDetail || resolvedConfig.config?.imageDetail
|
|
1304
1299
|
) || "low",
|
|
1305
|
-
screeningMode
|
|
1300
|
+
screeningMode,
|
|
1306
1301
|
listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
|
|
1307
1302
|
listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
|
|
1308
1303
|
listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
|
|
@@ -1373,7 +1368,7 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "", runId =
|
|
|
1373
1368
|
}
|
|
1374
1369
|
|
|
1375
1370
|
const shouldRequestResume = shouldRequestChatResume(args);
|
|
1376
|
-
const useLlm = shouldUseChatLlm(args);
|
|
1371
|
+
const useLlm = shouldUseChatLlm(args, normalized);
|
|
1377
1372
|
const debugTestOptions = collectChatDebugTestOptions(args);
|
|
1378
1373
|
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1379
1374
|
return {
|
|
@@ -1538,9 +1533,11 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
1538
1533
|
status: missingFields.length ? "NEED_INPUT" : "READY",
|
|
1539
1534
|
stage: "chat_run_setup",
|
|
1540
1535
|
page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
|
|
1541
|
-
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
1542
|
-
missing_fields: missingFields,
|
|
1543
|
-
|
|
1536
|
+
required_fields: CHAT_REQUIRED_FIELDS.slice(),
|
|
1537
|
+
missing_fields: missingFields,
|
|
1538
|
+
criteria_optional: true,
|
|
1539
|
+
empty_criteria_mode: "collect_cv",
|
|
1540
|
+
job_options: jobOptions,
|
|
1544
1541
|
selected_job: selectedJob,
|
|
1545
1542
|
selected_job_label: jobs?.selected_label || selectedJob?.label || "",
|
|
1546
1543
|
job_options_source: jobs?.source || "",
|
|
@@ -1552,10 +1549,10 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
|
|
|
1552
1549
|
jobOptions
|
|
1553
1550
|
}),
|
|
1554
1551
|
...diagnostics,
|
|
1555
|
-
...(nextCallExample ? { next_call_example: nextCallExample } : {}),
|
|
1556
|
-
message: missingFields.length
|
|
1557
|
-
? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count
|
|
1558
|
-
: "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening.",
|
|
1552
|
+
...(nextCallExample ? { next_call_example: nextCallExample } : {}),
|
|
1553
|
+
message: missingFields.length
|
|
1554
|
+
? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count。criteria 可留空;留空会进入收集简历模式。"
|
|
1555
|
+
: "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening or collect CVs.",
|
|
1559
1556
|
runtime_evaluate_used: false,
|
|
1560
1557
|
method_summary: methodSummary(session.methodLog || []),
|
|
1561
1558
|
method_log: session.methodLog || [],
|
|
@@ -1723,7 +1720,7 @@ export async function startBossChatDetachedRunTool({ workspaceRoot = "", args =
|
|
|
1723
1720
|
});
|
|
1724
1721
|
}
|
|
1725
1722
|
|
|
1726
|
-
const useLlm = shouldUseChatLlm(args);
|
|
1723
|
+
const useLlm = shouldUseChatLlm(args, normalized);
|
|
1727
1724
|
const debugTestOptions = collectChatDebugTestOptions(args);
|
|
1728
1725
|
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1729
1726
|
return {
|
|
@@ -51,7 +51,8 @@ import {
|
|
|
51
51
|
makeForbiddenChatResumeNavigationError
|
|
52
52
|
} from "./page-guard.js";
|
|
53
53
|
|
|
54
|
-
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
54
|
+
export const CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE = "CHAT_UNSAFE_ONLINE_RESUME_LINK";
|
|
55
|
+
export const CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE = "CHAT_ONLINE_RESUME_MODAL_NOT_OPEN";
|
|
55
56
|
|
|
56
57
|
const CHAT_CONVERSATION_CONTROL_SCOPE_SELECTORS = Object.freeze([
|
|
57
58
|
".conversation-main",
|
|
@@ -128,10 +129,15 @@ export function makeUnsafeChatOnlineResumeLinkError(target = {}, buttonHTML = ""
|
|
|
128
129
|
return error;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
132
|
-
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
133
|
-
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
134
|
-
}
|
|
132
|
+
export function isUnsafeChatOnlineResumeLinkError(error) {
|
|
133
|
+
return error?.code === CHAT_UNSAFE_ONLINE_RESUME_LINK_CODE
|
|
134
|
+
|| /CHAT_UNSAFE_ONLINE_RESUME_LINK/i.test(String(error?.message || error || ""));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isChatOnlineResumeModalOpenFailureError(error) {
|
|
138
|
+
return error?.code === CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE
|
|
139
|
+
|| /Chat online resume modal did not open/i.test(String(error?.message || error || ""));
|
|
140
|
+
}
|
|
135
141
|
|
|
136
142
|
export function createChatProfileNetworkRecorder(client) {
|
|
137
143
|
const events = [];
|
|
@@ -242,9 +248,9 @@ function chatCandidateIdFromAttributes(attributes = {}) {
|
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
246
|
-
if (!activeCandidate?.node_id) return activeCandidate;
|
|
247
|
-
let attributes = {};
|
|
251
|
+
async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
252
|
+
if (!activeCandidate?.node_id) return activeCandidate;
|
|
253
|
+
let attributes = {};
|
|
248
254
|
let outerHTML = "";
|
|
249
255
|
try {
|
|
250
256
|
[attributes, outerHTML] = await Promise.all([
|
|
@@ -258,12 +264,25 @@ async function hydrateActiveChatCandidate(client, activeCandidate = null) {
|
|
|
258
264
|
candidate_id: chatCandidateIdFromAttributes(attributes) || null,
|
|
259
265
|
label: normalizeDetailText(htmlToText(outerHTML)),
|
|
260
266
|
outer_html_length: outerHTML.length
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export async function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function readChatActiveCandidateState(client) {
|
|
271
|
+
const rootState = await getChatRoots(client);
|
|
272
|
+
const activeCandidate = await queryFirstAcrossChatRoots(
|
|
273
|
+
client,
|
|
274
|
+
rootState.roots,
|
|
275
|
+
CHAT_ACTIVE_CANDIDATE_SELECTORS
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
roots: rootState.roots,
|
|
279
|
+
active_candidate: await hydrateActiveChatCandidate(client, activeCandidate)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function waitForChatOnlineResumeButton(client, {
|
|
284
|
+
timeoutMs = 12000,
|
|
285
|
+
intervalMs = 250,
|
|
267
286
|
expectedCandidateId = ""
|
|
268
287
|
} = {}) {
|
|
269
288
|
const started = Date.now();
|
|
@@ -982,10 +1001,12 @@ export async function openChatOnlineResume(client, {
|
|
|
982
1001
|
}
|
|
983
1002
|
}
|
|
984
1003
|
|
|
985
|
-
const error = new Error("Chat online resume modal did not open");
|
|
986
|
-
error.
|
|
987
|
-
|
|
988
|
-
|
|
1004
|
+
const error = new Error("Chat online resume modal did not open");
|
|
1005
|
+
error.code = CHAT_ONLINE_RESUME_MODAL_NOT_OPEN_CODE;
|
|
1006
|
+
error.retryable = true;
|
|
1007
|
+
error.attempts = attempts;
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
989
1010
|
|
|
990
1011
|
export async function readChatConversationReadyState(client) {
|
|
991
1012
|
const rootState = await getChatRoots(client);
|
|
@@ -1173,20 +1194,21 @@ export async function sendChatMessage(client, expectedText = "", {
|
|
|
1173
1194
|
};
|
|
1174
1195
|
}
|
|
1175
1196
|
|
|
1176
|
-
export async function clickChatAskResume(client, {
|
|
1177
|
-
timeoutMs = 8000,
|
|
1178
|
-
settleMs = 700
|
|
1179
|
-
|
|
1197
|
+
export async function clickChatAskResume(client, {
|
|
1198
|
+
timeoutMs = 8000,
|
|
1199
|
+
settleMs = 700,
|
|
1200
|
+
skipWhenAttachmentResumeAvailable = true
|
|
1201
|
+
} = {}) {
|
|
1180
1202
|
const started = Date.now();
|
|
1181
1203
|
let lastState = null;
|
|
1182
1204
|
let lastDisabledAskResume = null;
|
|
1183
1205
|
while (Date.now() - started <= timeoutMs) {
|
|
1184
1206
|
const state = await readChatConversationReadyState(client);
|
|
1185
1207
|
lastState = state;
|
|
1186
|
-
if (state.attachment_resume_enabled) {
|
|
1187
|
-
return {
|
|
1188
|
-
ok: true,
|
|
1189
|
-
already_requested: true,
|
|
1208
|
+
if (skipWhenAttachmentResumeAvailable && state.attachment_resume_enabled) {
|
|
1209
|
+
return {
|
|
1210
|
+
ok: true,
|
|
1211
|
+
already_requested: true,
|
|
1190
1212
|
attachment_resume_available: true,
|
|
1191
1213
|
control: state.attachment_resume
|
|
1192
1214
|
};
|
|
@@ -1368,23 +1390,32 @@ export async function waitForChatResumeRequestMessage(client, {
|
|
|
1368
1390
|
};
|
|
1369
1391
|
}
|
|
1370
1392
|
|
|
1371
|
-
export async function requestChatResumeForPassedCandidate(client, {
|
|
1372
|
-
greetingText = "Hi同学,能麻烦发下简历吗?",
|
|
1373
|
-
maxAttempts = 3,
|
|
1374
|
-
askResumeTimeoutMs = 8000,
|
|
1375
|
-
dryRun = false
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1393
|
+
export async function requestChatResumeForPassedCandidate(client, {
|
|
1394
|
+
greetingText = "Hi同学,能麻烦发下简历吗?",
|
|
1395
|
+
maxAttempts = 3,
|
|
1396
|
+
askResumeTimeoutMs = 8000,
|
|
1397
|
+
dryRun = false,
|
|
1398
|
+
skipWhenAttachmentResumeAvailable = true
|
|
1399
|
+
} = {}) {
|
|
1400
|
+
const effectiveGreetingText = normalizeDetailText(greetingText) || "Hi同学,能麻烦发下简历吗?";
|
|
1401
|
+
const initialState = await readChatConversationReadyState(client);
|
|
1402
|
+
if (skipWhenAttachmentResumeAvailable && initialState.attachment_resume_enabled) {
|
|
1403
|
+
return {
|
|
1404
|
+
requested: false,
|
|
1405
|
+
skipped: true,
|
|
1406
|
+
reason: "attachment_resume_already_available",
|
|
1407
|
+
initial_state: initialState
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
if (initialState.already_requested_resume) {
|
|
1411
|
+
return {
|
|
1412
|
+
requested: false,
|
|
1413
|
+
skipped: true,
|
|
1414
|
+
reason: "resume_request_already_pending",
|
|
1415
|
+
initial_state: initialState
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (dryRun) {
|
|
1388
1419
|
return {
|
|
1389
1420
|
requested: false,
|
|
1390
1421
|
skipped: false,
|
|
@@ -1416,10 +1447,11 @@ export async function requestChatResumeForPassedCandidate(client, {
|
|
|
1416
1447
|
|
|
1417
1448
|
const attempts = [];
|
|
1418
1449
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1419
|
-
const before = await getChatResumeRequestMessageState(client);
|
|
1420
|
-
const askResult = await clickChatAskResume(client, {
|
|
1421
|
-
timeoutMs: askResumeTimeoutMs
|
|
1422
|
-
|
|
1450
|
+
const before = await getChatResumeRequestMessageState(client);
|
|
1451
|
+
const askResult = await clickChatAskResume(client, {
|
|
1452
|
+
timeoutMs: askResumeTimeoutMs,
|
|
1453
|
+
skipWhenAttachmentResumeAvailable
|
|
1454
|
+
});
|
|
1423
1455
|
let confirmResult = {
|
|
1424
1456
|
confirmed: false,
|
|
1425
1457
|
assumed_requested: Boolean(askResult.already_requested),
|