@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,89 +1,89 @@
1
- ---
2
- name: "boss-recruit-pipeline"
3
- description: "Use when users want Boss search/recruit-page screening via the unified boss-recommend-mcp package. Replaces the legacy boss-recruit-mcp skill."
4
- ---
5
-
6
- # Boss Recruit Pipeline Skill
7
-
8
- ## Goal
9
-
10
- 当用户要在 Boss 搜索页 / 招聘搜索页筛人时,必须走 `@reconcrap/boss-recommend-mcp` 2.x 内置的 recruit/search MCP 工具,不要安装或调用旧的 `@reconcrap/boss-recruit-mcp`。
11
-
12
- ## Tool Routing
13
-
14
- Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP server. Search/recruit tasks should call `boss-recruit/<tool>` when the host shows server-qualified tool names.
15
-
16
- - 同步启动:`run_recruit_pipeline`
17
- - 异步启动:`start_recruit_pipeline_run`
18
- - 查询进度:`get_recruit_pipeline_run`
19
- - 暂停:`pause_recruit_pipeline_run`
20
- - 继续:`resume_recruit_pipeline_run`
21
- - 取消:`cancel_recruit_pipeline_run`
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
-
25
- ## Hard Rules
26
-
27
- - 只在用户明确说搜索页、search、recruit、招聘搜索、`/web/chat/search` 时使用本 skill。
28
- - 如果用户说推荐页、recommend、`/web/chat/recommend`,必须交给 `boss-recommend-pipeline`。
29
- - 如果用户说聊天页、未读、全部聊天、求简历,必须交给 `boss-chat`。
30
- - 禁止调用旧包:`@reconcrap/boss-recruit-mcp`、`boss-recruit-mcp`、旧本地 recruit repo、旧 vendor 脚本。
31
- - 浏览器自动化必须走 CDP-only 2.x MCP 工具;不得要求用户启用 legacy page-JS 或 `Runtime.evaluate` 路径。
32
- - 启动 search/recruit run 时,若本机默认 `127.0.0.1:9222` Chrome DevTools 端口不可连,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/search`。
33
- - 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
34
- - 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
35
- - 若用户未提供岗位,必须先询问岗位。搜索页岗位选择在关键词输入框旁边;不要猜测默认岗位。
36
- - 搜索页任务不要调用 `list_recommend_jobs` 获取岗位;推荐页岗位列表和搜索页岗位选择不是同一个工具面。用户已经给出岗位时直接传 `overrides.job`。
37
- - 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件等参数,必须逐项传入或确认。
38
- - 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
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`。
44
- - “只看未查看/过滤已看”只控制 Boss 的“过滤近14天查看”筛选;“是否过滤近期同事触达”是单独输入,写入 `overrides.filter_recent_colleague_contacted` 或兼容字段 `overrides.skip_recent_colleague_contacted`。用户说近期同事触达“不限/不过滤”时传 `false`;用户说过滤近期同事触达/跳过同事已联系时传 `true`。
45
-
46
- ## Required Inputs
47
-
48
- - `job`
49
- - `keyword` 或用户明确的搜索意图
50
- - `criteria`
51
- - `target_count`
52
- - `rest_level`: `low|medium|high`
53
-
54
- 常用可选项:
55
-
56
- - `city`
57
- - `degree`
58
- - `school_tag`
59
- - `recent_not_view`
60
- - `filter_recent_colleague_contacted`
61
- - `port`
62
-
63
- ## Confirmation Flow
64
-
65
- 1. 先从用户原始消息里提取参数;缺什么只问什么。不要先调用 MCP 让工具猜缺参。
66
- 2. `criteria` 必须来自用户明确提供的筛选条件,或来自你向用户追问后得到的完整自然语言标准。禁止把岗位、关键词、学历等字段拼成 criteria。
67
- 3. 如果用户一开始已经给齐 `job`、`keyword`、`criteria`、`target_count`、`rest_level`,并且可选筛选项也已明确或确认不限,直接展示一次总确认。
68
- 4. 用户确认后调用 `boss-recruit/start_recruit_pipeline_run` 或 `boss-recruit/run_recruit_pipeline`,传:
69
- - `confirmation: { "final_confirmed": true }`
70
- - 所有规范化字段放在 `overrides`
71
- - 完整 criteria 放在 `overrides.criteria`
72
- - 本次休息强度放在 `human_behavior.restLevel`
73
- 5. 工具返回 `NEED_INPUT` 时,按 `pending_questions` 只补具体缺口;不要接受或转述工具生成的简化 criteria。
74
-
75
- 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
76
-
77
- ```json
78
- {
79
- "confirmation": { "final_confirmed": true },
80
- "human_behavior": { "restLevel": "medium" }
81
- }
82
- ```
83
-
84
- ## Response Style
85
-
86
- - 用结构化中文确认参数。
87
- - 缺参时只补缺口,不要改写用户的筛选条件。
88
- - 拿到 `ACCEPTED + run_id` 后默认停止本轮,不主动高频轮询。
89
- - 查询、暂停、恢复、取消时必须复用同一个 `run_id`。
1
+ ---
2
+ name: "boss-recruit-pipeline"
3
+ description: "Use when users want Boss search/recruit-page screening via the unified boss-recommend-mcp package. Replaces the legacy boss-recruit-mcp skill."
4
+ ---
5
+
6
+ # Boss Recruit Pipeline Skill
7
+
8
+ ## Goal
9
+
10
+ 当用户要在 Boss 搜索页 / 招聘搜索页筛人时,必须走 `@reconcrap/boss-recommend-mcp` 2.x 内置的 recruit/search MCP 工具,不要安装或调用旧的 `@reconcrap/boss-recruit-mcp`。
11
+
12
+ ## Tool Routing
13
+
14
+ Trae/Trae-CN split-server config exposes these under the `boss-recruit` MCP server. Search/recruit tasks should call `boss-recruit/<tool>` when the host shows server-qualified tool names.
15
+
16
+ - 同步启动:`run_recruit_pipeline`
17
+ - 异步启动:`start_recruit_pipeline_run`
18
+ - 查询进度:`get_recruit_pipeline_run`
19
+ - 暂停:`pause_recruit_pipeline_run`
20
+ - 继续:`resume_recruit_pipeline_run`
21
+ - 取消:`cancel_recruit_pipeline_run`
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
+
25
+ ## Hard Rules
26
+
27
+ - 只在用户明确说搜索页、search、recruit、招聘搜索、`/web/chat/search` 时使用本 skill。
28
+ - 如果用户说推荐页、recommend、`/web/chat/recommend`,必须交给 `boss-recommend-pipeline`。
29
+ - 如果用户说聊天页、未读、全部聊天、求简历,必须交给 `boss-chat`。
30
+ - 禁止调用旧包:`@reconcrap/boss-recruit-mcp`、`boss-recruit-mcp`、旧本地 recruit repo、旧 vendor 脚本。
31
+ - 浏览器自动化必须走 CDP-only 2.x MCP 工具;不得要求用户启用 legacy page-JS 或 `Runtime.evaluate` 路径。
32
+ - 启动 search/recruit run 时,若本机默认 `127.0.0.1:9222` Chrome DevTools 端口不可连,工具会自动打开 Chrome 并导航到 `https://www.zhipin.com/web/chat/search`。
33
+ - 只有工具返回 `BOSS_LOGIN_REQUIRED` / `requires_login=true` 时,才要求用户在自动打开的 Chrome 窗口人工登录 Boss 后重试;不要把“没开 9222 Chrome”当作缺参。
34
+ - 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
35
+ - 若用户未提供岗位,必须先询问岗位。搜索页岗位选择在关键词输入框旁边;不要猜测默认岗位。
36
+ - 搜索页任务不要调用 `list_recommend_jobs` 获取岗位;推荐页岗位列表和搜索页岗位选择不是同一个工具面。用户已经给出岗位时直接传 `overrides.job`。
37
+ - 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件等参数,必须逐项传入或确认。
38
+ - 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
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`。
44
+ - “只看未查看/过滤已看”只控制 Boss 的“过滤近14天查看”筛选;“是否过滤近期同事触达”是单独输入,写入 `overrides.filter_recent_colleague_contacted` 或兼容字段 `overrides.skip_recent_colleague_contacted`。用户说近期同事触达“不限/不过滤”时传 `false`;用户说过滤近期同事触达/跳过同事已联系时传 `true`。
45
+
46
+ ## Required Inputs
47
+
48
+ - `job`
49
+ - `keyword` 或用户明确的搜索意图
50
+ - `criteria`
51
+ - `target_count`
52
+ - `rest_level`: `low|medium|high`
53
+
54
+ 常用可选项:
55
+
56
+ - `city`
57
+ - `degree`
58
+ - `school_tag`
59
+ - `recent_not_view`
60
+ - `filter_recent_colleague_contacted`
61
+ - `port`
62
+
63
+ ## Confirmation Flow
64
+
65
+ 1. 先从用户原始消息里提取参数;缺什么只问什么。不要先调用 MCP 让工具猜缺参。
66
+ 2. `criteria` 必须来自用户明确提供的筛选条件,或来自你向用户追问后得到的完整自然语言标准。禁止把岗位、关键词、学历等字段拼成 criteria。
67
+ 3. 如果用户一开始已经给齐 `job`、`keyword`、`criteria`、`target_count`、`rest_level`,并且可选筛选项也已明确或确认不限,直接展示一次总确认。
68
+ 4. 用户确认后调用 `boss-recruit/start_recruit_pipeline_run` 或 `boss-recruit/run_recruit_pipeline`,传:
69
+ - `confirmation: { "final_confirmed": true }`
70
+ - 所有规范化字段放在 `overrides`
71
+ - 完整 criteria 放在 `overrides.criteria`
72
+ - 本次休息强度放在 `human_behavior.restLevel`
73
+ 5. 工具返回 `NEED_INPUT` 时,按 `pending_questions` 只补具体缺口;不要接受或转述工具生成的简化 criteria。
74
+
75
+ 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
76
+
77
+ ```json
78
+ {
79
+ "confirmation": { "final_confirmed": true },
80
+ "human_behavior": { "restLevel": "medium" }
81
+ }
82
+ ```
83
+
84
+ ## Response Style
85
+
86
+ - 用结构化中文确认参数。
87
+ - 缺参时只补缺口,不要改写用户的筛选条件。
88
+ - 拿到 `ACCEPTED + run_id` 后默认停止本轮,不主动高频轮询。
89
+ - 查询、暂停、恢复、取消时必须复用同一个 `run_id`。
package/src/chat-mcp.js CHANGED
@@ -66,11 +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
- ]);
69
+ const CHAT_REQUIRED_FIELDS = Object.freeze([
70
+ "job",
71
+ "start_from",
72
+ "target_count"
73
+ ]);
74
74
 
75
75
  const TERMINAL_STATUSES = new Set([
76
76
  RUN_STATUS_COMPLETED,
@@ -756,8 +756,8 @@ function buildLegacyChatResult(snapshot) {
756
756
  };
757
757
  }
758
758
 
759
- function normalizeRunSnapshot(snapshot) {
760
- if (!snapshot) return null;
759
+ function normalizeRunSnapshot(snapshot) {
760
+ if (!snapshot) return null;
761
761
  const meta = getChatRunMeta(snapshot.runId);
762
762
  const artifacts = getChatRunArtifacts(snapshot.runId);
763
763
  const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
@@ -812,61 +812,61 @@ function normalizeRunSnapshot(snapshot) {
812
812
  },
813
813
  result: legacyResult,
814
814
  artifacts
815
- };
816
- }
817
-
818
- function plainRecord(value) {
819
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
820
- }
821
-
822
- function mergePersistedChatControlRequest(normalized, existing) {
823
- const control = {
824
- ...(normalized?.control || {})
825
- };
826
- if (!normalized) return control;
827
- const existingControl = plainRecord(existing?.control);
828
- if (TERMINAL_STATUSES.has(normalized.state)) return control;
829
- if (existingControl.cancel_requested === true) {
830
- return {
831
- ...control,
832
- pause_requested: true,
833
- pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
834
- pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_boss_chat_run",
835
- cancel_requested: true
836
- };
837
- }
838
- if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
839
- return {
840
- ...control,
841
- pause_requested: true,
842
- pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
843
- pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_boss_chat_run"
844
- };
845
- }
846
- if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
847
- return {
848
- ...control,
849
- pause_requested: false,
850
- pause_requested_at: null,
851
- pause_requested_by: null,
852
- cancel_requested: false
853
- };
854
- }
855
- return control;
856
- }
857
-
858
- function persistChatRunSnapshot(snapshot, {
859
- persistActiveCheckpoint = false
860
- } = {}) {
861
- const normalized = normalizeRunSnapshot(snapshot);
862
- if (!normalized?.run_id) return normalized;
863
- const artifacts = getChatRunArtifacts(normalized.run_id);
864
- if (!artifacts) return normalized;
865
- const existing = readJsonFile(artifacts.run_state_path);
866
- normalized.control = mergePersistedChatControlRequest(normalized, existing);
867
- if (persistActiveCheckpoint) {
868
- persistChatCheckpointSnapshot(normalized);
869
- }
815
+ };
816
+ }
817
+
818
+ function plainRecord(value) {
819
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
820
+ }
821
+
822
+ function mergePersistedChatControlRequest(normalized, existing) {
823
+ const control = {
824
+ ...(normalized?.control || {})
825
+ };
826
+ if (!normalized) return control;
827
+ const existingControl = plainRecord(existing?.control);
828
+ if (TERMINAL_STATUSES.has(normalized.state)) return control;
829
+ if (existingControl.cancel_requested === true) {
830
+ return {
831
+ ...control,
832
+ pause_requested: true,
833
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
834
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_boss_chat_run",
835
+ cancel_requested: true
836
+ };
837
+ }
838
+ if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
839
+ return {
840
+ ...control,
841
+ pause_requested: true,
842
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
843
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_boss_chat_run"
844
+ };
845
+ }
846
+ if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
847
+ return {
848
+ ...control,
849
+ pause_requested: false,
850
+ pause_requested_at: null,
851
+ pause_requested_by: null,
852
+ cancel_requested: false
853
+ };
854
+ }
855
+ return control;
856
+ }
857
+
858
+ function persistChatRunSnapshot(snapshot, {
859
+ persistActiveCheckpoint = false
860
+ } = {}) {
861
+ const normalized = normalizeRunSnapshot(snapshot);
862
+ if (!normalized?.run_id) return normalized;
863
+ const artifacts = getChatRunArtifacts(normalized.run_id);
864
+ if (!artifacts) return normalized;
865
+ const existing = readJsonFile(artifacts.run_state_path);
866
+ normalized.control = mergePersistedChatControlRequest(normalized, existing);
867
+ if (persistActiveCheckpoint) {
868
+ persistChatCheckpointSnapshot(normalized);
869
+ }
870
870
  const payload = {
871
871
  run_id: normalized.run_id,
872
872
  mode: normalized.mode,
@@ -1138,13 +1138,13 @@ function buildChatNextCallExample(args, missingFields, normalized) {
1138
1138
  return Object.keys(example).length ? example : null;
1139
1139
  }
1140
1140
 
1141
- function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
1142
- const missing = [];
1143
- if (!normalized.job) missing.push("job");
1144
- if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
1145
- if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
1146
- return missing;
1147
- }
1141
+ function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
1142
+ const missing = [];
1143
+ if (!normalized.job) missing.push("job");
1144
+ if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
1145
+ if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
1146
+ return missing;
1147
+ }
1148
1148
 
1149
1149
  function buildTargetCountDiagnostics(args, missingFields, normalized) {
1150
1150
  if (!missingFields.includes("target_count")) return {};
@@ -1208,10 +1208,10 @@ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions
1208
1208
  parse_error: normalized.target.parseError || null
1209
1209
  };
1210
1210
  }
1211
- return {
1212
- field,
1213
- question: `请提供 ${field}。`,
1214
- value: null
1211
+ return {
1212
+ field,
1213
+ question: `请提供 ${field}。`,
1214
+ value: null
1215
1215
  };
1216
1216
  });
1217
1217
  }
@@ -1219,21 +1219,21 @@ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions
1219
1219
  async function buildNeedInputResponse({ args, missingFields, normalized }) {
1220
1220
  const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
1221
1221
  return {
1222
- status: "NEED_INPUT",
1223
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
1224
- missing_fields: missingFields,
1225
- criteria_optional: true,
1226
- empty_criteria_mode: "collect_cv",
1227
- ...diagnostics,
1222
+ status: "NEED_INPUT",
1223
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
1224
+ missing_fields: missingFields,
1225
+ criteria_optional: true,
1226
+ empty_criteria_mode: "collect_cv",
1227
+ ...diagnostics,
1228
1228
  pending_questions: buildPendingChatQuestions({ args, missingFields, normalized }),
1229
1229
  job_options: [],
1230
- error: {
1231
- code: "MISSING_REQUIRED_FIELDS",
1232
- message: "缺少必要字段。请补齐 job、start_from、target_count 后再启动 Boss chat CDP-only run。criteria 可留空;留空时会进入收集简历模式。",
1233
- retryable: true
1234
- }
1235
- };
1236
- }
1230
+ error: {
1231
+ code: "MISSING_REQUIRED_FIELDS",
1232
+ message: "缺少必要字段。请补齐 job、start_from、target_count 后再启动 Boss chat CDP-only run。criteria 可留空;留空时会进入收集简历模式。",
1233
+ retryable: true
1234
+ }
1235
+ };
1236
+ }
1237
1237
 
1238
1238
  function shouldRequestChatResume(args = {}, context = {}) {
1239
1239
  const action = normalizeText(args.post_action || args.action).toLowerCase();
@@ -1267,40 +1267,40 @@ function isDebugTestMode(args = {}) {
1267
1267
  return args.debug_test_mode === true || args.allow_debug_test_mode === true;
1268
1268
  }
1269
1269
 
1270
- function normalizeScreeningModeArg(args = {}, normalized = normalizeChatStartInput(args)) {
1271
- if (!normalized.criteria) return "collect_cv";
1272
- const raw = normalizeText(args.screening_mode || args.screeningMode || "");
1273
- if (args.use_llm === false) return "deterministic";
1274
- return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
1275
- ? "deterministic"
1276
- : "llm";
1277
- }
1278
-
1279
- function collectChatDebugTestOptions(args = {}) {
1280
- const reasons = [];
1281
- if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
1282
- if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
1283
- if (args.dry_run === true || args.dry_run_request_cv === true) reasons.push("dry_run_request_cv");
1284
- return reasons;
1285
- }
1286
-
1287
- function shouldUseChatLlm(args = {}, normalized = normalizeChatStartInput(args)) {
1288
- return normalizeScreeningModeArg(args, normalized) === "llm";
1289
- }
1290
-
1291
- function getRunOptions(args, normalized, session, { workspaceRoot = "", configResolution = null } = {}) {
1270
+ function normalizeScreeningModeArg(args = {}, normalized = normalizeChatStartInput(args)) {
1271
+ if (!normalized.criteria) return "collect_cv";
1272
+ const raw = normalizeText(args.screening_mode || args.screeningMode || "");
1273
+ if (args.use_llm === false) return "deterministic";
1274
+ return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
1275
+ ? "deterministic"
1276
+ : "llm";
1277
+ }
1278
+
1279
+ function collectChatDebugTestOptions(args = {}) {
1280
+ const reasons = [];
1281
+ if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
1282
+ if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
1283
+ if (args.dry_run === true || args.dry_run_request_cv === true) reasons.push("dry_run_request_cv");
1284
+ return reasons;
1285
+ }
1286
+
1287
+ function shouldUseChatLlm(args = {}, normalized = normalizeChatStartInput(args)) {
1288
+ return normalizeScreeningModeArg(args, normalized) === "llm";
1289
+ }
1290
+
1291
+ function getRunOptions(args, normalized, session, { workspaceRoot = "", configResolution = null } = {}) {
1292
1292
  const slowLive = args.slow_live === true;
1293
1293
  const isAllTarget = normalized.publicTargetCount === "all";
1294
1294
  const processedLimit = parsePositiveInteger(
1295
1295
  args.max_candidates,
1296
1296
  isAllTarget ? CHAT_ALL_MAX_CANDIDATES : CHAT_ALL_MAX_CANDIDATES
1297
- );
1298
- const shouldRequestResume = shouldRequestChatResume(args);
1299
- const screeningMode = normalizeScreeningModeArg(args, normalized);
1300
- const useLlm = shouldUseChatLlm(args, normalized);
1301
- const resolvedConfig = configResolution || (useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false });
1302
- const humanBehavior = resolveHumanBehaviorForRun(args, resolvedConfig?.config || {});
1303
- return {
1297
+ );
1298
+ const shouldRequestResume = shouldRequestChatResume(args);
1299
+ const screeningMode = normalizeScreeningModeArg(args, normalized);
1300
+ const useLlm = shouldUseChatLlm(args, normalized);
1301
+ const resolvedConfig = configResolution || (useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false });
1302
+ const humanBehavior = resolveHumanBehaviorForRun(args, resolvedConfig?.config || {});
1303
+ return {
1304
1304
  client: session.client,
1305
1305
  targetUrl: CHAT_TARGET_URL,
1306
1306
  job: normalized.job,
@@ -1339,7 +1339,7 @@ function getRunOptions(args, normalized, session, { workspaceRoot = "", configRe
1339
1339
  llmImageDetail: normalizeText(
1340
1340
  args.llm_image_detail || resolvedConfig.config?.llmImageDetail || resolvedConfig.config?.imageDetail
1341
1341
  ) || "low",
1342
- screeningMode,
1342
+ screeningMode,
1343
1343
  listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
1344
1344
  listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
1345
1345
  listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
@@ -1410,7 +1410,7 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "", runId =
1410
1410
  }
1411
1411
 
1412
1412
  const shouldRequestResume = shouldRequestChatResume(args);
1413
- const useLlm = shouldUseChatLlm(args, normalized);
1413
+ const useLlm = shouldUseChatLlm(args, normalized);
1414
1414
  const debugTestOptions = collectChatDebugTestOptions(args);
1415
1415
  if (debugTestOptions.length && !isDebugTestMode(args)) {
1416
1416
  return {
@@ -1575,11 +1575,11 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
1575
1575
  status: missingFields.length ? "NEED_INPUT" : "READY",
1576
1576
  stage: "chat_run_setup",
1577
1577
  page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1578
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
1579
- missing_fields: missingFields,
1580
- criteria_optional: true,
1581
- empty_criteria_mode: "collect_cv",
1582
- job_options: jobOptions,
1578
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
1579
+ missing_fields: missingFields,
1580
+ criteria_optional: true,
1581
+ empty_criteria_mode: "collect_cv",
1582
+ job_options: jobOptions,
1583
1583
  selected_job: selectedJob,
1584
1584
  selected_job_label: jobs?.selected_label || selectedJob?.label || "",
1585
1585
  job_options_source: jobs?.source || "",
@@ -1591,10 +1591,10 @@ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } =
1591
1591
  jobOptions
1592
1592
  }),
1593
1593
  ...diagnostics,
1594
- ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
1595
- message: missingFields.length
1596
- ? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count。criteria 可留空;留空会进入收集简历模式。"
1597
- : "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening or collect CVs.",
1594
+ ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
1595
+ message: missingFields.length
1596
+ ? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count。criteria 可留空;留空会进入收集简历模式。"
1597
+ : "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening or collect CVs.",
1598
1598
  runtime_evaluate_used: false,
1599
1599
  method_summary: methodSummary(session.methodLog || []),
1600
1600
  method_log: session.methodLog || [],
@@ -1762,7 +1762,7 @@ export async function startBossChatDetachedRunTool({ workspaceRoot = "", args =
1762
1762
  });
1763
1763
  }
1764
1764
 
1765
- const useLlm = shouldUseChatLlm(args, normalized);
1765
+ const useLlm = shouldUseChatLlm(args, normalized);
1766
1766
  const debugTestOptions = collectChatDebugTestOptions(args);
1767
1767
  if (debugTestOptions.length && !isDebugTestMode(args)) {
1768
1768
  return {