@reconcrap/boss-recommend-mcp 2.1.3 → 2.1.4

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 CHANGED
@@ -116,9 +116,9 @@ MCP 工具:
116
116
  boss-recommend-mcp list-jobs --slow-live --port 9222
117
117
  ```
118
118
 
119
- 返回的 `job_names` 可直接作为后续 `start_recommend_pipeline_run` 的 `confirmation.job_value` / `overrides.job`。
119
+ 返回的 `job_names` 可直接作为后续 `start_recommend_pipeline_run` 的 `overrides.job`;旧版 `confirmation.job_value` 仍兼容。
120
120
 
121
- Cron / 一次性定时任务设置建议先在设置阶段完成 Chrome/登录/岗位发现与全部确认:
121
+ Cron / 一次性定时任务设置建议先在设置阶段完成 Chrome/登录/岗位发现与一次总确认;确认文件推荐只包含 `{ "final_confirmed": true }`:
122
122
 
123
123
  ```bash
124
124
  boss-recommend-mcp prepare-run --instruction-file boss-recommend-instruction.txt --overrides-file boss-recommend-overrides.json --confirmation-file boss-recommend-confirmation.json --slow-live --port 9222
@@ -158,18 +158,16 @@ boss-recommend-mcp schedule-status --schedule-id <schedule_id>
158
158
  - 学校标签支持多选语义:如“985、211”会同时勾选这两项
159
159
  - 学校标签对“混合输入”按容错处理:如“985、211、qs100”会忽略无效项 `qs100`,保留并应用有效项;仅当全部无效或用户明确“不限”时才回落到“不限”
160
160
  - 学历支持单选与多选语义:如“本科及以上”会展开为 `本科/硕士/博士`;如“大专、本科”只勾选这两项
161
- - 执行前会逐项确认筛选参数:学校标签 / 学历 / 性别 / 是否过滤近14天已看
162
- - 页面就绪(已登录且在 recommend 页)后,会先提取岗位栏全部岗位并要求用户确认本次岗位;确认后先点击岗位,再执行 search/screen
163
- - 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
161
+ - 执行前会先补齐筛选值、岗位、后置动作和休息强度,然后只做一次总确认
162
+ - 页面就绪(已登录且在 recommend 页)后,会先提取岗位栏全部岗位,使用精确岗位名填入 run payload,再进入总确认
163
+ - 在真正开始 search/screen 或创建 cron 前,总确认需包含岗位、全部筛选参数、criteriatarget_countpost_action、max_greet_count、restLevel 和定时信息(如适用)
164
164
  - npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
165
165
  - 2.x installer 会迁移已存在的 legacy Boss MCP 配置:把 `boss-recommend` 指向统一 `@reconcrap/boss-recommend-mcp`,并从同一个 `mcp.json` 中移除旧 `boss-recruit-mcp` / standalone `boss-chat` / 本地 legacy Boss 路径;写入前会生成 `.boss-mcp-migration-*.bak` 备份
166
166
  - 2.x installer 会刷新外部 agent skills:`boss-recommend-pipeline`、`boss-recruit-pipeline`、`boss-chat` 都来自当前包,旧 recruit/chat skill 会被覆盖为统一 MCP 路由
167
167
  - npm / npx 安装后会自动初始化 `screening-config.json` 模板(优先写入 workspace 的 `config/`,不可写时回退到用户目录)
168
168
  - npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs`、`~/.boss-recommend-mcp/boss-chat` 及其 `logs/runs/profiles/reports/artifacts/state`
169
- - `post_action` 必须在每次完整运行开始时确认一次
170
- - `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
171
- - 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
172
- - 若检测到 `max_greet_count` 可能由 agent 自动默认(例如与 `target_count` 相同且原始指令未明确),会强制再次向用户确认
169
+ - `post_action`、`target_count` 和 `max_greet_count`(当 `post_action=greet`)通过同一次总确认锁定
170
+ - 新流程中 `confirmation.final_confirmed=true` 是总确认;旧版逐字段 `*_confirmed` JSON 仍兼容但不是推荐写法
173
171
  - 一旦确认 `post_action`,本次运行内所有通过人选都统一按该动作执行
174
172
  - 若达到 `max_greet_count` 但流程仍需继续,后续通过人选会自动改为收藏
175
173
  - 不会对每位候选人重复确认
@@ -492,27 +490,10 @@ Trae-CN / 长对话防循环建议:
492
490
  {
493
491
  "instruction": "推荐页筛选211女生,近14天没有,有 AI Agent 经验,符合标准的直接沟通",
494
492
  "confirmation": {
495
- "filters_confirmed": true,
496
- "school_tag_confirmed": true,
497
- "school_tag_value": ["985", "211"],
498
- "degree_confirmed": true,
499
- "degree_value": ["本科", "硕士", "博士"],
500
- "gender_confirmed": true,
501
- "gender_value": "女",
502
- "recent_not_view_confirmed": true,
503
- "recent_not_view_value": "近14天没有",
504
- "criteria_confirmed": true,
505
- "target_count_confirmed": true,
506
- "target_count_value": 20,
507
- "post_action_confirmed": true,
508
- "post_action_value": "greet",
509
- "final_confirmed": true,
510
- "job_confirmed": true,
511
- "job_value": "算法工程师(视频/图像模型方向) _ 杭州",
512
- "max_greet_count_confirmed": true,
513
- "max_greet_count_value": 10
493
+ "final_confirmed": true
514
494
  },
515
495
  "overrides": {
496
+ "page_scope": "recommend",
516
497
  "school_tag": ["985", "211"],
517
498
  "degree": ["本科", "硕士", "博士"],
518
499
  "gender": "女",
@@ -523,18 +504,14 @@ Trae-CN / 长对话防循环建议:
523
504
  "post_action": "greet",
524
505
  "max_greet_count": 10
525
506
  },
526
- "follow_up": {
527
- "chat": {
528
- "criteria": "继续在聊天页处理有 AI Agent 或 MCP 项目经验的人选",
529
- "start_from": "unread",
530
- "target_count": 20,
531
- "profile": "default",
532
- "safe_pacing": true
533
- }
507
+ "human_behavior": {
508
+ "restLevel": "medium"
534
509
  }
535
510
  }
536
511
  ```
537
512
 
513
+ `confirmation.final_confirmed=true` 表示用户已经看过并确认总览。旧版 `page_confirmed`、`school_tag_confirmed`、`job_confirmed` 等逐字段布尔值仍兼容,但新流程不需要主动生成它们。
514
+
538
515
  ## 测试
539
516
 
540
517
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: "boss-recommend-pipeline"
3
- description: "Use when users want Boss recommend-page filtering/screening via boss-recommend-mcp. Confirm required params first, then run in two-stage confirmation with strict recommend routing."
3
+ description: "Use when users want Boss recommend-page filtering/screening via boss-recommend-mcp. Gather required params, show one consolidated review, then run or schedule through the recommend MCP tools."
4
4
  ---
5
5
 
6
6
  # Boss Recommend Pipeline Skill
7
7
 
8
8
  ## Goal
9
9
 
10
- 当用户要在 Boss 推荐页筛人时,必须走 `start_recommend_pipeline_run`,并按“两阶段确认 -> 页面就绪 -> 岗位确认 -> 最终确认 -> 执行”的顺序完成。2.0 CDP-only 路径不再支持 legacy recommend -> chat 自动衔接;若用户要聊天页筛选或求简历,必须在推荐页任务完成后显式改用 `boss-chat` 工具。
10
+ 当用户要在 Boss 推荐页筛人时,必须走 `start_recommend_pipeline_run`;若是稍后/cron 启动,必须走 `schedule_recommend_pipeline_run`。先补齐缺失值并读取岗位列表,然后展示一次包含岗位、筛选项、criteria、目标人数、后置动作、最大招呼数、休息强度和定时信息的总确认;用户确认后设置 `final_confirmed=true` 即可启动或创建定时任务。2.0 CDP-only 路径不再支持 legacy recommend -> chat 自动衔接;若用户要聊天页筛选或求简历,必须在推荐页任务完成后显式改用 `boss-chat` 工具。
11
11
 
12
12
  ## Hard Rules (Must Follow)
13
13
 
@@ -19,10 +19,11 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
19
19
 
20
20
  - **确认不可代填(强制)**
21
21
  - 禁止 agent 自行“设置合理参数”并代替用户确认。
22
- - 禁止在用户未明确回复前,把任意 `*_confirmed` 字段设为 `true`。
22
+ - 禁止在用户未明确回复前,把 `final_confirmed=true`。
23
+ - 旧版 `*_confirmed` 字段仍兼容,但新流程不要逐项设置;把规范化后的值写入 `overrides`,总确认后只需要 `confirmation.final_confirmed=true`。
23
24
  - 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job/rest_level`。
24
25
  - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
25
- - 若工具返回 `pending_questions`,必须逐项向用户提问并等待用户回复;不得跳过提问直接执行。
26
+ - 若工具返回 `pending_questions`,只追问这些缺口;若只返回 `final_review`,不要再拆成逐字段确认。
26
27
 
27
28
  - **岗位确认时机**
28
29
  - 页面未就绪前,禁止询问 `job`。
@@ -30,7 +31,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
30
31
 
31
32
  - **参数确认**
32
33
  - `criteria` 必须是用户开放式自然语言;禁止“严格/宽松执行”等预设替代。
33
- - `post_action=greet` 时,必须确认 `max_greet_count`;禁止自动默认为 `target_count`。
34
+ - `post_action=greet` 时,`max_greet_count` 必须出现在总确认里;禁止未告知用户就自动默认为 `target_count`。
34
35
  - 正式执行前必须 `final_confirmed=true`。
35
36
  - 真实筛选禁止传 `detail_limit: 0`;recommend 默认必须打开候选人详情/CV。只有用户明确要求“卡片-only 调试”时,才允许同时传 `detail_limit: 0` 和 `allow_card_only_screening: true`。
36
37
 
@@ -40,29 +41,29 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
40
41
  - 最终执行前逐字回显将提交的 `instruction`;若与锁定值不一致,先修正再执行。
41
42
  - 禁止在中途把用户意图拆成“recommend 已默认确认 + chat 单独执行”两条链路。
42
43
 
43
- ## Two-Stage Confirmation
44
+ ## Single Review Confirmation
44
45
 
45
- ### Stage A (页面就绪前,禁止问岗位)
46
-
47
- 必须确认:
46
+ 先收集这些值:
48
47
 
49
48
  - `page_scope`:`recommend|latest|featured`
50
- - `school_tag`(多选)
51
- - `degree`(多选)
52
- - `gender`
53
- - `recent_not_view`
49
+ - `school_tag`、`degree`、`gender`、`recent_not_view`
54
50
  - `criteria`(开放文本)
55
51
  - `target_count`(可空)
56
52
  - `post_action`:`favorite|greet|none`
57
53
  - `max_greet_count`(仅当 `post_action=greet`)
58
54
  - `rest_level`:`low|medium|high`
55
+ - `job`(来自 `list_recommend_jobs` / `job_options` 的精确岗位名)
56
+ - cron/定时任务的启动时间(如适用)
59
57
 
60
- ### Stage B (页面就绪后)
58
+ 然后只做一次总确认。用户回复“确认 / 全部确认 / 无需调整”后,下一次工具调用写入:
61
59
 
62
- 必须确认:
60
+ ```json
61
+ {
62
+ "confirmation": { "final_confirmed": true }
63
+ }
64
+ ```
63
65
 
64
- - `job`(来自 `job_options`,必须全量展示)
65
- - `final_review`(岗位 + 全参数总确认)
66
+ 已确认值放在 `overrides` `human_behavior.restLevel`。不要因为工具返回 `final_review` 就再问页面、学历、学校、性别、14天、criteria、目标人数、动作、最大招呼数等逐项确认。
66
67
 
67
68
  ## Chat Handoff
68
69
 
@@ -105,7 +106,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
105
106
  - 主工具:`start_recommend_pipeline_run`
106
107
  - 必填:`instruction`
107
108
  - 关键输入:
108
- - `confirmation`:`page_confirmed/page_value/filters_confirmed/school_tag_confirmed.../job_confirmed/job_value/final_confirmed`
109
+ - `confirmation`:新流程只需要 `{ "final_confirmed": true }`;旧版 `page_confirmed/page_value/.../job_confirmed/job_value` 仍兼容但不要主动制造逐项确认。
109
110
  - `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
110
111
  - `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
111
112
  - 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
@@ -122,9 +123,9 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
122
123
  创建 cron 时必须在设置 cron 的当下完成全部交互:
123
124
 
124
125
  1. 锁定用户原始 instruction,不改写、不摘要,criteria 放入 `overrides.criteria` 时必须逐字保留。
125
- 2. 收集 Stage A 全部筛选项、`target_count`、`post_action`、`max_greet_count`(如需要)和本次 `rest_level`。
126
+ 2. 收集全部筛选项、`target_count`、`post_action`、`max_greet_count`(如需要)和本次 `rest_level`。
126
127
  3. 调用 `list_recommend_jobs`;若 Chrome 未打开,工具会尝试自动打开本机 9222 Chrome 并进入推荐页。若返回 `BOSS_LOGIN_REQUIRED` 或页面不可用,停止 cron 创建,让用户登录/修复后重试。
127
- 4. 用 `job_names` 中的精确岗位名完成岗位确认,并完成最终总确认,写入 `job_confirmed=true` `final_confirmed=true`。
128
+ 4. 用 `job_names` 中的精确岗位名填入 `overrides.job`,展示包含启动时间的最终总确认;用户确认后写入 `final_confirmed=true`。
128
129
  5. 调用 `prepare_recommend_pipeline_run` 传入将来要执行的完整 payload;只有 `READY + cron_ready=true` 才能继续。
129
130
  6. 立即调用 `schedule_recommend_pipeline_run`,传入同一份完整 payload 和 `schedule_delay_minutes` / `schedule_run_at`。禁止让 OpenClaw 自己写 `/tmp/*.log` shell cron、自然语言提醒、或未来对话回调来代替此工具。
130
131
  7. 只有拿到 `SCHEDULED + schedule_id` 后才告诉用户定时任务已创建。回复必须包含 `schedule_id`,而不是只说“10 分钟后会启动”。
@@ -192,7 +193,7 @@ npx -y @reconcrap/boss-recommend-mcp@latest schedule-status --schedule-id <sched
192
193
 
193
194
  推荐做法:
194
195
 
195
- 1. 将锁定的用户原文写入 instruction 文件,将已确认参数写入 `overrides` 与 `confirmation` JSON 文件。
196
+ 1. 将锁定的用户原文写入 instruction 文件,将已确认参数写入 `overrides` JSON;`confirmation` JSON 只需要 `{ "final_confirmed": true }`。
196
197
  2. 先用 `prepare-run` 校验完整 payload 已 cron-ready;未返回 `READY + cron_ready=true` 不得创建定时任务或启动 run。
197
198
  3. 若用户要“现在启动”,用 detached CLI 启动,让父命令返回启动证据,子进程继续持有 CDP 会话:
198
199
 
@@ -220,8 +221,8 @@ npx -y @reconcrap/boss-recommend-mcp@latest run --detached --instruction-file .\
220
221
  ## Response Style
221
222
 
222
223
  - 用结构化中文。
223
- - 第一轮确认卡片不出现 `job`。
224
- - 仅在 `job_options` 出现后给岗位确认卡片,且岗位选项必须全量。
224
+ - 未读取岗位列表前不要要求用户最终确认。
225
+ - 仅在 `job_options` 出现后选择精确岗位;最终确认卡片必须包含岗位和全部参数。
225
226
  - 封闭式问题必须带完整标签选项;开放式问题(如 `criteria`)保持自由输入。
226
227
  - 页面就绪失败提示必须包含 `debug_port`、recommend URL、以及登录 URL(若未登录):
227
228
  - `https://www.zhipin.com/web/chat/recommend`
package/src/index.js CHANGED
@@ -520,6 +520,7 @@ function createRunInputSchema() {
520
520
  },
521
521
  confirmation: {
522
522
  type: "object",
523
+ description: "推荐页确认状态。新流程推荐只在用户看过总览后传 final_confirmed=true;逐字段 *_confirmed 为兼容旧调用保留。",
523
524
  properties: {
524
525
  page_confirmed: { type: "boolean" },
525
526
  page_value: {
@@ -584,7 +585,10 @@ function createRunInputSchema() {
584
585
  type: "string",
585
586
  enum: ["favorite", "greet", "none"]
586
587
  },
587
- final_confirmed: { type: "boolean" },
588
+ final_confirmed: {
589
+ type: "boolean",
590
+ description: "用户已确认包含岗位、筛选项、criteria、目标、动作、最大招呼数和 restLevel 的总览。"
591
+ },
588
592
  job_confirmed: { type: "boolean" },
589
593
  job_value: { type: "string" },
590
594
  max_greet_count_confirmed: { type: "boolean" },
@@ -714,8 +718,8 @@ function createRunInputSchema() {
714
718
  type: "boolean",
715
719
  description: "可选,VPN/慢页面 live 测试模式,放宽等待时间"
716
720
  },
717
- human_behavior: createHumanBehaviorInputSchema("可选,recommend 可靠性实验用节奏配置;默认 paced_with_rests/on"),
718
- humanBehavior: createHumanBehaviorInputSchema("兼容字段;优先使用 human_behavior"),
721
+ human_behavior: createHumanBehaviorInputSchema("recommend 运行必须显式包含本次用户确认的 restLevel: low|medium|high;其他节奏配置可选"),
722
+ humanBehavior: createHumanBehaviorInputSchema("兼容字段;优先使用 human_behavior;recommend 运行同样必须显式包含 restLevel"),
719
723
  human_behavior_enabled: {
720
724
  type: "boolean",
721
725
  description: "兼容字段;true 等同启用 paced 默认配置,false 等同 baseline"
package/src/parser.js CHANGED
@@ -546,7 +546,7 @@ function buildCriteria({ instruction, rawInstruction, overrideCriteria }) {
546
546
  };
547
547
  }
548
548
 
549
- function resolvePostAction({ instruction, confirmation, overrides }) {
549
+ function resolvePostAction({ instruction, confirmation, overrides, finalConfirmed = false }) {
550
550
  const confirmed = confirmation?.post_action_confirmed === true;
551
551
  const confirmationValue = normalizePostAction(confirmation?.post_action_value);
552
552
  const overrideValue = normalizePostAction(overrides?.post_action);
@@ -557,40 +557,34 @@ function resolvePostAction({ instruction, confirmation, overrides }) {
557
557
  ? "greet"
558
558
  : /什么也不做|不做任何操作|不操作|仅筛选|只筛选/.test(instruction)
559
559
  ? "none"
560
- : null;
560
+ : null;
561
561
  const proposed = overrideValue || confirmationValue || instructionValue || null;
562
562
 
563
- if (confirmed && confirmationValue) {
564
- return {
565
- post_action: confirmationValue,
566
- proposed_post_action: confirmationValue,
567
- needs_post_action_confirmation: false
568
- };
569
- }
570
-
571
563
  return {
572
- post_action: confirmed ? confirmationValue || proposed : null,
564
+ post_action: (confirmed || finalConfirmed) && proposed ? proposed : null,
573
565
  proposed_post_action: proposed,
574
- needs_post_action_confirmation: true
566
+ needs_post_action_confirmation: !proposed
575
567
  };
576
568
  }
577
569
 
578
- function resolveTargetCount({ instruction, confirmation, overrides }) {
570
+ function resolveTargetCount({ instruction, confirmation, overrides, finalConfirmed = false }) {
579
571
  const confirmed = confirmation?.target_count_confirmed === true;
580
572
  const overrideValue = parsePositiveIntegerValue(overrides?.target_count);
581
573
  const confirmationValue = parsePositiveIntegerValue(confirmation?.target_count_value);
582
574
  const instructionValue = extractTargetCount(instruction);
583
575
  const proposed = overrideValue || confirmationValue || instructionValue || null;
584
- const resolved = overrideValue || (confirmed ? confirmationValue : null);
576
+ const resolved = (confirmed || finalConfirmed)
577
+ ? (overrideValue || confirmationValue || instructionValue || null)
578
+ : null;
585
579
 
586
580
  return {
587
581
  target_count: resolved,
588
582
  proposed_target_count: proposed,
589
- needs_target_count_confirmation: !confirmed
583
+ needs_target_count_confirmation: false
590
584
  };
591
585
  }
592
586
 
593
- function resolveMaxGreetCount({ instruction, confirmation, overrides, postActionResolution }) {
587
+ function resolveMaxGreetCount({ instruction, confirmation, overrides, postActionResolution, finalConfirmed = false }) {
594
588
  const actionHint = postActionResolution.post_action || postActionResolution.proposed_post_action;
595
589
  if (actionHint !== "greet") {
596
590
  return {
@@ -612,9 +606,12 @@ function resolveMaxGreetCount({ instruction, confirmation, overrides, postAction
612
606
  || null
613
607
  );
614
608
  const proposed = confirmationValue || overrideValue || instructionValue || null;
615
- const resolved = confirmed ? (confirmationValue || overrideValue || null) : null;
609
+ const resolved = (confirmed || finalConfirmed)
610
+ ? (confirmationValue || overrideValue || instructionValue || null)
611
+ : null;
616
612
  const suspiciousAutoFill = Boolean(
617
613
  !confirmed
614
+ && !finalConfirmed
618
615
  && Number.isInteger(proposed)
619
616
  && proposed > 0
620
617
  && !Number.isInteger(instructionValue)
@@ -622,32 +619,31 @@ function resolveMaxGreetCount({ instruction, confirmation, overrides, postAction
622
619
  && targetCountHint > 0
623
620
  && proposed === targetCountHint
624
621
  );
625
- const needsConfirmation = (
626
- !(Number.isInteger(resolved) && resolved > 0)
627
- );
622
+ const hasProposedValue = Number.isInteger(proposed) && proposed > 0;
623
+ const needsConfirmation = !hasProposedValue;
628
624
 
629
625
  return {
630
- max_greet_count: needsConfirmation ? null : resolved,
626
+ max_greet_count: (confirmed || finalConfirmed) && hasProposedValue ? resolved : null,
631
627
  proposed_max_greet_count: proposed,
632
628
  needs_max_greet_count_confirmation: needsConfirmation,
633
629
  suspicious_auto_fill: suspiciousAutoFill
634
630
  };
635
631
  }
636
632
 
637
- function resolvePageScope({ instruction, confirmation, overrides }) {
633
+ function resolvePageScope({ instruction, confirmation, overrides, finalConfirmed = false }) {
638
634
  const confirmed = confirmation?.page_confirmed === true;
639
635
  const confirmationValue = normalizePageScope(confirmation?.page_value);
640
636
  const overrideValue = normalizePageScope(overrides?.page_scope);
641
637
  const instructionValue = extractPageScope(instruction);
642
638
  const proposed = overrideValue || confirmationValue || instructionValue || "recommend";
643
639
  return {
644
- page_scope: confirmed && confirmationValue ? confirmationValue : null,
640
+ page_scope: (confirmed && confirmationValue) || finalConfirmed ? proposed : null,
645
641
  proposed_page_scope: proposed,
646
- needs_page_confirmation: !(confirmed && Boolean(confirmationValue))
642
+ needs_page_confirmation: !proposed
647
643
  };
648
644
  }
649
645
 
650
- function collectSuspiciousFields({ invalidOverrideSchoolTags, maxGreetCountResolution }) {
646
+ function collectSuspiciousFields({ invalidOverrideSchoolTags }) {
651
647
  const suspicious = [];
652
648
  if (Array.isArray(invalidOverrideSchoolTags) && invalidOverrideSchoolTags.length > 0) {
653
649
  suspicious.push({
@@ -656,19 +652,13 @@ function collectSuspiciousFields({ invalidOverrideSchoolTags, maxGreetCountResol
656
652
  reason: `已忽略无效学校标签:${invalidOverrideSchoolTags.join(" / ")};仅保留可识别选项。`
657
653
  });
658
654
  }
659
- if (maxGreetCountResolution?.suspicious_auto_fill) {
660
- suspicious.push({
661
- field: "max_greet_count",
662
- value: maxGreetCountResolution.proposed_max_greet_count,
663
- reason: "检测到 max_greet_count 与 target_count 相同且原始指令未明确该值,可能是自动填充,需用户再次确认。"
664
- });
665
- }
666
655
  return suspicious;
667
656
  }
668
657
 
669
658
  export function parseRecommendInstruction({ instruction, confirmation, overrides }) {
670
659
  const rawInstruction = String(instruction || "");
671
660
  const text = normalizeText(rawInstruction);
661
+ const finalConfirmed = confirmation?.final_confirmed === true;
672
662
  const detectedSchoolTags = extractSchoolTags(text);
673
663
  const detectedDegrees = extractDegrees(text);
674
664
  const schoolTagAudit = auditSchoolTagSelections(overrides?.school_tag);
@@ -692,7 +682,7 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
692
682
  || extractJobSelectionHint(rawInstruction)
693
683
  || ""
694
684
  );
695
- const pageScopeResolution = resolvePageScope({ instruction: text, confirmation, overrides });
685
+ const pageScopeResolution = resolvePageScope({ instruction: text, confirmation, overrides, finalConfirmed });
696
686
 
697
687
  const inferredSchoolTag = detectedSchoolTags.length > 0
698
688
  ? sortSchoolTagSelections(detectedSchoolTags)
@@ -717,15 +707,16 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
717
707
  post_action: null,
718
708
  max_greet_count: null
719
709
  };
720
- const targetCountResolution = resolveTargetCount({ instruction: text, confirmation, overrides });
710
+ const targetCountResolution = resolveTargetCount({ instruction: text, confirmation, overrides, finalConfirmed });
721
711
  screenParams.target_count = targetCountResolution.target_count;
722
- const postActionResolution = resolvePostAction({ instruction: text, confirmation, overrides });
712
+ const postActionResolution = resolvePostAction({ instruction: text, confirmation, overrides, finalConfirmed });
723
713
  screenParams.post_action = postActionResolution.post_action;
724
714
  const maxGreetCountResolution = resolveMaxGreetCount({
725
715
  instruction: text,
726
716
  confirmation,
727
717
  overrides,
728
- postActionResolution
718
+ postActionResolution,
719
+ finalConfirmed
729
720
  });
730
721
  screenParams.max_greet_count = maxGreetCountResolution.max_greet_count;
731
722
 
@@ -735,37 +726,23 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
735
726
  }
736
727
 
737
728
  const suspicious_fields = collectSuspiciousFields({
738
- invalidOverrideSchoolTags: schoolTagAudit.invalid,
739
- maxGreetCountResolution
729
+ invalidOverrideSchoolTags: schoolTagAudit.invalid
740
730
  });
741
- const hasConfirmedSchoolTagValue = Array.isArray(confirmationSchoolTag) && confirmationSchoolTag.length > 0;
742
- const hasConfirmedDegreeValue = Array.isArray(confirmationDegrees) && confirmationDegrees.length > 0;
743
- const hasConfirmedGenderValue = Boolean(confirmationGender);
744
- const hasConfirmedRecentNotViewValue = Boolean(confirmationRecentNotView);
745
- const needs_school_tag_confirmation = (
746
- confirmation?.school_tag_confirmed !== true
747
- || !hasConfirmedSchoolTagValue
748
- );
749
- const needs_degree_confirmation = (
750
- confirmation?.degree_confirmed !== true
751
- || !hasConfirmedDegreeValue
752
- );
753
- const needs_gender_confirmation = (
754
- confirmation?.gender_confirmed !== true
755
- || !hasConfirmedGenderValue
756
- );
757
- const needs_recent_not_view_confirmation = (
758
- confirmation?.recent_not_view_confirmed !== true
759
- || !hasConfirmedRecentNotViewValue
760
- );
731
+ const hasResolvedSchoolTagValue = Array.isArray(searchParams.school_tag) && searchParams.school_tag.length > 0;
732
+ const hasResolvedDegreeValue = Array.isArray(searchParams.degree) && searchParams.degree.length > 0;
733
+ const hasResolvedGenderValue = Boolean(searchParams.gender);
734
+ const hasResolvedRecentNotViewValue = Boolean(searchParams.recent_not_view);
735
+ const needs_school_tag_confirmation = !hasResolvedSchoolTagValue;
736
+ const needs_degree_confirmation = !hasResolvedDegreeValue;
737
+ const needs_gender_confirmation = !hasResolvedGenderValue;
738
+ const needs_recent_not_view_confirmation = !hasResolvedRecentNotViewValue;
761
739
  const needs_filters_confirmation = (
762
- confirmation?.filters_confirmed !== true
763
- || needs_school_tag_confirmation
740
+ needs_school_tag_confirmation
764
741
  || needs_degree_confirmation
765
742
  || needs_gender_confirmation
766
743
  || needs_recent_not_view_confirmation
767
744
  );
768
- const needs_criteria_confirmation = confirmation?.criteria_confirmed !== true;
745
+ const needs_criteria_confirmation = !screenParams.criteria;
769
746
  const needs_target_count_confirmation = targetCountResolution.needs_target_count_confirmation;
770
747
  const needs_post_action_confirmation = postActionResolution.needs_post_action_confirmation;
771
748
  const needs_max_greet_count_confirmation = maxGreetCountResolution.needs_max_greet_count_confirmation;
@@ -52,10 +52,12 @@ import {
52
52
  import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
53
53
 
54
54
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
55
- const DEFAULT_RECOMMEND_PORT = 9222;
56
- const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
57
- const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
58
- const RUN_MODE_ASYNC = "async";
55
+ const DEFAULT_RECOMMEND_PORT = 9222;
56
+ const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
57
+ const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
58
+ const RUN_MODE_ASYNC = "async";
59
+ const REST_LEVEL_OPTIONS = ["low", "medium", "high"];
60
+ const REST_LEVEL_SET = new Set(REST_LEVEL_OPTIONS);
59
61
 
60
62
  const TERMINAL_STATUSES = new Set([
61
63
  RUN_STATUS_COMPLETED,
@@ -1019,17 +1021,68 @@ async function connectRecommendChromeSession({
1019
1021
  };
1020
1022
  }
1021
1023
 
1022
- function parseRecommendPipelineRequest(args = {}) {
1023
- return parseRecommendInstruction({
1024
- instruction: args.instruction,
1024
+ function parseRecommendPipelineRequest(args = {}) {
1025
+ return parseRecommendInstruction({
1026
+ instruction: args.instruction,
1025
1027
  confirmation: args.confirmation,
1026
1028
  overrides: args.overrides
1027
- });
1028
- }
1029
-
1030
- function buildRequiredConfirmations(parsed, args = {}) {
1031
- const required = [];
1032
- if (parsed.needs_page_confirmation) required.push("page_scope");
1029
+ });
1030
+ }
1031
+
1032
+ function readOwn(source, keys = []) {
1033
+ if (!source || typeof source !== "object" || Array.isArray(source)) return undefined;
1034
+ for (const key of keys) {
1035
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
1036
+ }
1037
+ return undefined;
1038
+ }
1039
+
1040
+ function getExplicitRestLevel(args = {}) {
1041
+ const behavior = readOwn(args, ["human_behavior", "humanBehavior"]);
1042
+ const raw = readOwn(behavior, ["restLevel", "rest_level"]);
1043
+ const normalized = normalizeText(raw).toLowerCase();
1044
+ return {
1045
+ raw: raw ?? null,
1046
+ restLevel: REST_LEVEL_SET.has(normalized) ? normalized : null,
1047
+ valid: REST_LEVEL_SET.has(normalized),
1048
+ missing: raw === undefined || raw === null || normalizeText(raw) === ""
1049
+ };
1050
+ }
1051
+
1052
+ function buildReviewScreenParams(parsed) {
1053
+ return {
1054
+ ...(parsed.screenParams || {}),
1055
+ criteria: parsed.screenParams?.criteria || null,
1056
+ criteria_normalized: parsed.criteria_normalized || null,
1057
+ target_count: parsed.screenParams?.target_count ?? parsed.proposed_target_count ?? null,
1058
+ post_action: parsed.screenParams?.post_action || parsed.proposed_post_action || null,
1059
+ max_greet_count: parsed.screenParams?.max_greet_count ?? parsed.proposed_max_greet_count ?? null
1060
+ };
1061
+ }
1062
+
1063
+ function buildReviewPageScope(parsed) {
1064
+ return parsed.page_scope || parsed.proposed_page_scope || "recommend";
1065
+ }
1066
+
1067
+ function buildReviewJob(args = {}) {
1068
+ return normalizeText(args.confirmation?.job_value || args.overrides?.job || "") || null;
1069
+ }
1070
+
1071
+ function buildScheduleReview(args = {}) {
1072
+ const scheduleRunAt = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
1073
+ const scheduleDelayMinutes = args.schedule_delay_minutes ?? args.scheduleDelayMinutes;
1074
+ const scheduleDelaySeconds = args.schedule_delay_seconds ?? args.scheduleDelaySeconds;
1075
+ if (!scheduleRunAt && scheduleDelayMinutes === undefined && scheduleDelaySeconds === undefined) return null;
1076
+ return {
1077
+ schedule_run_at: scheduleRunAt || null,
1078
+ schedule_delay_minutes: scheduleDelayMinutes ?? null,
1079
+ schedule_delay_seconds: scheduleDelaySeconds ?? null
1080
+ };
1081
+ }
1082
+
1083
+ function buildRequiredConfirmations(parsed, args = {}) {
1084
+ const required = [];
1085
+ if (parsed.needs_page_confirmation) required.push("page_scope");
1033
1086
  if (parsed.needs_filters_confirmation) required.push("filters");
1034
1087
  if (parsed.needs_school_tag_confirmation) required.push("school_tag");
1035
1088
  if (parsed.needs_degree_confirmation) required.push("degree");
@@ -1037,45 +1090,77 @@ function buildRequiredConfirmations(parsed, args = {}) {
1037
1090
  if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
1038
1091
  if (parsed.needs_criteria_confirmation) required.push("criteria");
1039
1092
  if (parsed.needs_target_count_confirmation) required.push("target_count");
1040
- if (parsed.needs_post_action_confirmation) required.push("post_action");
1041
- if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
1042
- if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
1043
-
1044
- const confirmation = args.confirmation || {};
1045
- const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
1046
- if (confirmation.job_confirmed !== true || !jobValue) required.push("job");
1047
- if (confirmation.final_confirmed !== true) required.push("final_review");
1048
- return Array.from(new Set(required));
1049
- }
1093
+ if (parsed.needs_post_action_confirmation) required.push("post_action");
1094
+ if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
1095
+ if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
1096
+
1097
+ const confirmation = args.confirmation || {};
1098
+ const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
1099
+ if (!jobValue) required.push("job");
1100
+ const restLevel = getExplicitRestLevel(args);
1101
+ if (!restLevel.valid) required.push("rest_level");
1102
+ const blocksFinalReview = required.some((field) => field !== "rest_level");
1103
+ if (confirmation.final_confirmed !== true && !blocksFinalReview) required.push("final_review");
1104
+ return Array.from(new Set(required));
1105
+ }
1050
1106
 
1051
- function buildJobPendingQuestion(args = {}) {
1052
- const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1107
+ function buildJobPendingQuestion(args = {}) {
1108
+ const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1053
1109
  return {
1054
1110
  field: "job",
1055
1111
  question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
1056
1112
  value: value || null
1057
- };
1058
- }
1059
-
1060
- function buildFinalReviewQuestion(parsed) {
1061
- return {
1062
- field: "final_review",
1063
- question: "请最终确认本次推荐页筛选参数无误,并明确 final_confirmed=true 后再启动。",
1064
- value: {
1065
- page_scope: parsed.page_scope,
1066
- search_params: parsed.searchParams,
1067
- screen_params: parsed.screenParams
1068
- }
1069
- };
1070
- }
1071
-
1072
- function buildNeedInputResponse(parsed) {
1073
- return {
1074
- status: "NEED_INPUT",
1075
- missing_fields: parsed.missing_fields,
1076
- required_confirmations: buildRequiredConfirmations(parsed),
1077
- search_params: parsed.searchParams,
1078
- screen_params: parsed.screenParams,
1113
+ };
1114
+ }
1115
+
1116
+ function buildRestLevelPendingQuestion(args = {}) {
1117
+ const restLevel = getExplicitRestLevel(args);
1118
+ return {
1119
+ field: "rest_level",
1120
+ question: restLevel.missing
1121
+ ? "请确认本次运行休息强度 rest_level。"
1122
+ : "rest_level 只能是 low / medium / high,请重新确认本次运行休息强度。",
1123
+ value: restLevel.restLevel || restLevel.raw || null,
1124
+ options: REST_LEVEL_OPTIONS.map((value) => ({
1125
+ label: value,
1126
+ value
1127
+ }))
1128
+ };
1129
+ }
1130
+
1131
+ function buildSuspiciousFieldsQuestion(parsed) {
1132
+ return {
1133
+ field: "suspicious_fields",
1134
+ question: "检测到需要修正或明确确认的异常字段,请先修正后再启动。",
1135
+ value: parsed.suspicious_fields || []
1136
+ };
1137
+ }
1138
+
1139
+ function buildFinalReviewQuestion(parsed, args = {}) {
1140
+ const restLevel = getExplicitRestLevel(args);
1141
+ return {
1142
+ field: "final_review",
1143
+ question: "请最终确认本次推荐页筛选参数无误;确认后设置 final_confirmed=true 即可启动或创建定时任务。",
1144
+ value: {
1145
+ page_scope: buildReviewPageScope(parsed),
1146
+ job: buildReviewJob(args),
1147
+ search_params: parsed.searchParams,
1148
+ screen_params: buildReviewScreenParams(parsed),
1149
+ human_behavior: {
1150
+ restLevel: restLevel.restLevel || null
1151
+ },
1152
+ schedule: buildScheduleReview(args)
1153
+ }
1154
+ };
1155
+ }
1156
+
1157
+ function buildNeedInputResponse(parsed, args = {}) {
1158
+ return {
1159
+ status: "NEED_INPUT",
1160
+ missing_fields: parsed.missing_fields,
1161
+ required_confirmations: buildRequiredConfirmations(parsed, args),
1162
+ search_params: parsed.searchParams,
1163
+ screen_params: parsed.screenParams,
1079
1164
  pending_questions: parsed.pending_questions,
1080
1165
  review: parsed.review,
1081
1166
  error: {
@@ -1086,30 +1171,36 @@ function buildNeedInputResponse(parsed) {
1086
1171
  };
1087
1172
  }
1088
1173
 
1089
- function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
1090
- const pending = [...(parsed.pending_questions || [])];
1091
- if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
1092
- pending.push(buildJobPendingQuestion(args));
1093
- }
1094
- if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
1095
- pending.push(buildFinalReviewQuestion(parsed));
1096
- }
1097
- return {
1098
- status: "NEED_CONFIRMATION",
1099
- required_confirmations: requiredConfirmations,
1100
- page_scope: parsed.page_scope,
1101
- search_params: parsed.searchParams,
1102
- screen_params: parsed.screenParams,
1103
- pending_questions: pending,
1104
- review: {
1105
- ...(parsed.review || {}),
1174
+ function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
1175
+ const pending = [...(parsed.pending_questions || [])];
1176
+ if (requiredConfirmations.includes("suspicious_fields") && !pending.some((item) => item.field === "suspicious_fields")) {
1177
+ pending.push(buildSuspiciousFieldsQuestion(parsed));
1178
+ }
1179
+ if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
1180
+ pending.push(buildJobPendingQuestion(args));
1181
+ }
1182
+ if (requiredConfirmations.includes("rest_level") && !pending.some((item) => item.field === "rest_level")) {
1183
+ pending.push(buildRestLevelPendingQuestion(args));
1184
+ }
1185
+ if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
1186
+ pending.push(buildFinalReviewQuestion(parsed, args));
1187
+ }
1188
+ return {
1189
+ status: "NEED_CONFIRMATION",
1190
+ required_confirmations: requiredConfirmations,
1191
+ page_scope: buildReviewPageScope(parsed),
1192
+ search_params: parsed.searchParams,
1193
+ screen_params: buildReviewScreenParams(parsed),
1194
+ pending_questions: pending,
1195
+ review: {
1196
+ ...(parsed.review || {}),
1106
1197
  required_confirmations: requiredConfirmations
1107
1198
  }
1108
1199
  };
1109
1200
  }
1110
-
1111
- function evaluateRecommendPipelineGate(parsed, args = {}) {
1112
- if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed);
1201
+
1202
+ function evaluateRecommendPipelineGate(parsed, args = {}) {
1203
+ if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed, args);
1113
1204
  const requiredConfirmations = buildRequiredConfirmations(parsed, args);
1114
1205
  if (requiredConfirmations.length) {
1115
1206
  return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);