@reconcrap/boss-recommend-mcp 2.0.57 → 2.1.1

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
@@ -20,6 +20,7 @@ Boss 推荐 / 搜索 / 聊天筛选 MCP(stdio)服务。
20
20
  MCP 工具:
21
21
 
22
22
  - `list_recommend_jobs`(只读读取推荐页岗位下拉框,返回可直接用于 cron/一次性任务的 `job_names`)
23
+ - `prepare_recommend_pipeline_run`(只校验完整 payload 是否已可用于 cron/一次性任务;不启动筛选,返回 `READY + cron_ready=true` 后才应创建 cron)
23
24
  - `start_recommend_pipeline_run`(异步启动;同样先经过前置门禁,通过后返回 run_id)
24
25
  - `get_recommend_pipeline_run`(轮询 run_id 状态)
25
26
  - `cancel_recommend_pipeline_run`(取消运行中任务)
@@ -53,10 +54,19 @@ boss-recommend-mcp list-jobs --slow-live --port 9222
53
54
 
54
55
  返回的 `job_names` 可直接作为后续 `start_recommend_pipeline_run` 的 `confirmation.job_value` / `overrides.job`。
55
56
 
57
+ Cron / 一次性定时任务设置建议先在设置阶段完成 Chrome/登录/岗位发现与全部确认:
58
+
59
+ ```bash
60
+ 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
61
+ ```
62
+
63
+ 只有输出 `status: "READY"` 且 `cron_ready: true` 后,才把同一份 payload 写入 cron 并在到点时调用 `start_recommend_pipeline_run` / `boss-recommend-mcp run --detached`。如果设置阶段返回 `BOSS_LOGIN_REQUIRED`、`NEED_INPUT` 或 `NEED_CONFIRMATION`,先让用户登录或补齐确认,不要创建 cron。
64
+
56
65
  状态机:
57
66
 
58
67
  - `NEED_INPUT`
59
68
  - `NEED_CONFIRMATION`
69
+ - `READY`(仅准备工具)
60
70
  - `COMPLETED`
61
71
  - `FAILED`
62
72
 
@@ -259,13 +269,16 @@ config/screening-config.example.json
259
269
  - `debugPort`:未显式传 `port` 时,recommend / search / chat CDP-only MCP run 和健康检查默认连接这个 Chrome 调试端口。
260
270
  - `outputDir`:recommend / search / chat 完成后的最终 CSV 与 report JSON 会写入这里;run state / checkpoint 仍保留在各自状态目录,方便 pause/resume/cancel。
261
271
  - `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。
262
- - `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests" }`。用于 recommend / search / chat 的可靠性实验,支持:
272
+ - `humanBehavior`:默认 `{ "enabled": true, "profile": "paced_with_rests", "restLevel": "low" }`。用于 recommend / search / chat 的可靠性实验,支持:
263
273
  - `profile: "baseline"`:关闭人类节奏,保持确定性行为。
264
274
  - `profile: "paced"`:启用 CDP-only Bezier 鼠标移动、较大按钮的安全 inset 点击点、分块 `Input.insertText`、列表 wheel/settle jitter,以及小的动作前后读秒。
265
275
  - `profile: "paced_with_rests"`:在 `paced` 基础上启用候选人短休和批次休息。
276
+ - `restLevel: "low"`:保持旧版休息策略不变,候选人短休 8% 概率暂停 3-7 秒,批次休息约每 25-32 人暂停 15-30 秒。
277
+ - `restLevel: "medium"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 30 分钟。
278
+ - `restLevel: "high"`:随机分散短/长休息,平均目标约每 5 小时或 700 位候选人累计休息 1 小时。
266
279
  - `humanRestEnabled`:兼容旧配置。设为 `true` 时等价于 `humanBehavior.profile="paced_with_rests"`;设为 `false` 时不会关闭当前默认节奏。如需关闭,请显式设置 `humanBehavior.enabled=false` 或 `humanBehavior.profile="baseline"`。
267
280
  - recommend / search / chat 图片简历 fallback 与主列表滚动都会在启用 `listScrollJitter` 时使用 coverage-safe scroll jitter:每次 wheel delta 在安全范围内变化,并保留截图重叠、重复检测、bottom-marker / stop-boundary 逻辑,实际 delta 和 settle 时间会写入 artifact metadata。
268
- - chat/recommend/search run 也兼容显式参数 `safe_pacing` 与 `batch_rest_enabled`:run 参数优先于配置文件。
281
+ - chat/recommend/search run 也兼容显式参数 `safe_pacing`、`batch_rest_enabled` 与 `human_behavior.restLevel`:run 参数优先于配置文件。AI harness/skill 启动每次 run 前必须让用户明确选择 `low/medium/high`,再把选择写入 `human_behavior.restLevel`。
269
282
 
270
283
  ## 常用命令
271
284
 
@@ -371,6 +384,7 @@ Trae-CN / 长对话防循环建议:
371
384
  说明:
372
385
 
373
386
  - `start_recommend_pipeline_run` 为异步入口,但不会跳过同步确认流程。
387
+ - `prepare_recommend_pipeline_run` / `boss-recommend-mcp prepare-run` 用于 cron 设置阶段;它不启动筛选,只确认 payload 已经不需要到点后再问用户。
374
388
  - 定时心跳默认 120 秒一次;`updated_at` 仍会在阶段或进度变化时刷新。
375
389
  - 每个 run 会持久化到 `~/.boss-recommend-mcp/runs/<run_id>.json`(可通过 `BOSS_RECOMMEND_HOME` 覆盖)。
376
390
  - screen 阶段会持久化 checkpoint:`~/.boss-recommend-mcp/runs/<run_id>.checkpoint.json`。
@@ -16,6 +16,7 @@
16
16
  "humanBehavior": {
17
17
  "enabled": true,
18
18
  "profile": "paced_with_rests",
19
+ "restLevel": "low",
19
20
  "clickMovement": true,
20
21
  "textEntry": true,
21
22
  "listScrollJitter": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "2.0.57",
3
+ "version": "2.1.1",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -17,7 +17,7 @@ Please run a Boss chat-only task (do not switch to recommend flow).
17
17
  Execution order:
18
18
  1) Call boss_chat_health_check.
19
19
  2) Call prepare_boss_chat_run once (empty params allowed) to fetch job_options and missing fields.
20
- 3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria.
20
+ 3) Ask for these required fields in one shot: job, start_from (unread/all), target_count, criteria, rest_level (low/medium/high).
21
21
  4) After user reply, call start_boss_chat_run exactly once to start the run.
22
22
  5) If ACCEPTED, reply only with run_id and "task started"; no auto polling.
23
23
 
@@ -27,6 +27,7 @@ Anti-loop rules:
27
27
  - Do not use start_boss_chat_run for preflight. It is only for the final start call and must include job/start_from/target_count/criteria.
28
28
  - Do not call start_boss_chat_run repeatedly in one turn.
29
29
  - Do not call get_boss_chat_run unless user explicitly asks for progress.
30
+ - Do not choose rest_level for the user. Pass the user's choice as `human_behavior.restLevel`.
30
31
 
31
32
  target_count mapping:
32
33
  - Positive integer means explicit cap (for example 20).
@@ -27,6 +27,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
27
27
  - `start_from`: `unread|all`
28
28
  - `target_count`
29
29
  - `criteria`
30
+ - `rest_level`: `low|medium|high`
30
31
 
31
32
  可选:
32
33
 
@@ -38,6 +39,12 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
38
39
  - `safe_pacing`
39
40
  - `batch_rest_enabled`
40
41
 
42
+ 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
43
+
44
+ ```json
45
+ { "human_behavior": { "restLevel": "medium" } }
46
+ ```
47
+
41
48
  `greeting_text` 默认规则:
42
49
 
43
50
  - 本次显式传入 `greeting_text`:使用本次值
@@ -62,6 +69,7 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
62
69
  - 若本机找不到 Chrome,可提示用户设置 `BOSS_MCP_CHROME_PATH` 或 `BOSS_RECOMMEND_CHROME_PATH`;非本机 debug host 不自动启动。
63
70
  - `job` / `start_from` / `criteria` 缺一不可;缺参时只补缺口。
64
71
  - `target_count` 在 chat-only 启动前也是必填项,不能默认省略。
72
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
65
73
  - 当用户说“全部候选人/所有候选人”时,必须按“扫到底(unlimited)”处理,不要再追问正整数。
66
74
  - 参数名必须写 `target_count`(不要写“目标数量”等中文键名)。
67
75
  - 当用户选择“扫到底/全部候选人/所有候选人”时,调用参数优先写:`"target_count": "all"`;`-1` 只作为兼容输入和内部 CLI 表示。
@@ -89,5 +97,5 @@ description: "Use when users want Boss chat-page screening/outreach via the bund
89
97
 
90
98
  - 用结构化中文。
91
99
  - 首轮建议先调用一次 `prepare_boss_chat_run`(可空参)获取 `job_options` 与 `pending_questions`。
92
- - 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`。
100
+ - 缺参时必须逐项确认:`job`(来自岗位列表)、`start_from`(`unread|all`)、`target_count`、`criteria`、`rest_level`。
93
101
  - 若健康检查失败,明确提示共享配置文件 `screening-config.json` 不可用。
@@ -7,6 +7,7 @@
7
7
  - 先确认推荐页 filters
8
8
  - 再确认筛选 criteria
9
9
  - 再确认本次运行统一动作 `favorite` 或 `greet`
10
+ - 每次运行都要让用户明确选择休息强度 `low` / `medium` / `high`,并传入 `human_behavior.restLevel`
10
11
  - 只确认一次 `post_action`,不要逐个候选人反复确认
11
12
  - 运行中临时中断请使用 `pause_recommend_pipeline_run`(按 run_id),不要靠自然语言“暂停/继续”指令
12
13
  - 继续执行请使用 `resume_recommend_pipeline_run`;状态查询默认按用户指令触发,不自动轮询
@@ -20,7 +20,8 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
20
20
  - **确认不可代填(强制)**
21
21
  - 禁止 agent 自行“设置合理参数”并代替用户确认。
22
22
  - 禁止在用户未明确回复前,把任意 `*_confirmed` 字段设为 `true`。
23
- - 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job`。
23
+ - 禁止在用户未明确回复前,自行填充 `page_scope/school_tag/degree/gender/recent_not_view/criteria/target_count/post_action/max_greet_count/job/rest_level`。
24
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
24
25
  - 若工具返回 `pending_questions`,必须逐项向用户提问并等待用户回复;不得跳过提问直接执行。
25
26
 
26
27
  - **岗位确认时机**
@@ -54,6 +55,7 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
54
55
  - `target_count`(可空)
55
56
  - `post_action`:`favorite|greet|none`
56
57
  - `max_greet_count`(仅当 `post_action=greet`)
58
+ - `rest_level`:`low|medium|high`
57
59
 
58
60
  ### Stage B (页面就绪后)
59
61
 
@@ -82,18 +84,24 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
82
84
  - `gender`: `不限/男/女`
83
85
  - `recent_not_view`: `不限/近14天没有`
84
86
  - `post_action`: `favorite/greet/none`
87
+ - `rest_level`: `low/medium/high`
85
88
 
86
89
  ## Tool Usage
87
90
 
88
91
  - 岗位发现工具:`list_recommend_jobs`
89
- - 用途:当用户需要为 cron / 一次性自动任务提前填写完整参数时,先用它读取推荐页岗位下拉框的全部可用岗位名。
92
+ - 用途:当用户需要为 cron / 一次性自动任务提前填写完整参数时,先用它读取推荐页岗位下拉框的全部可用岗位名;默认会复用/自动打开本机 9222 Chrome 并导航到推荐页。
90
93
  - 输出:优先把 `job_names` 里的值作为后续 `overrides.job` / `confirmation.job_value`。
91
- - 限制:只读岗位列表,不启动筛选任务;仍然必须在正式 `start_recommend_pipeline_run` 前完成岗位与最终确认。
94
+ - 限制:只读岗位列表,不启动筛选任务;若返回 `BOSS_LOGIN_REQUIRED`,必须让用户在自动打开的 Chrome 完成登录后重试,本次 cron 不得创建。
95
+ - Cron 准备工具:`prepare_recommend_pipeline_run`
96
+ - 用途:只校验参数是否已可用于 cron / 一次性任务,不启动筛选任务。
97
+ - 要求:只有返回 `status=READY` 且 `cron_ready=true` 后,才允许创建 cron。
98
+ - 若返回 `NEED_INPUT` / `NEED_CONFIRMATION` / `FAILED`:继续补齐 `pending_questions` 或修复登录/页面/config;不得先创建 cron。
92
99
  - 主工具:`start_recommend_pipeline_run`
93
100
  - 必填:`instruction`
94
101
  - 关键输入:
95
102
  - `confirmation`:`page_confirmed/page_value/filters_confirmed/school_tag_confirmed.../job_confirmed/job_value/final_confirmed`
96
103
  - `overrides`:`page_scope/school_tag/degree/gender/recent_not_view/criteria/job/target_count/post_action/max_greet_count`
104
+ - `human_behavior`:必须包含本次用户确认的 `restLevel`(例如 `{ "restLevel": "medium" }`)
97
105
  - 不要传 `follow_up.chat`;该路径属于 legacy-only 行为
98
106
 
99
107
  最小策略:
@@ -103,6 +111,25 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
103
111
  - 拿到 `ACCEPTED + run_id` 后默认停止本轮,不自动轮询。
104
112
  - 在拿到 `ACCEPTED + run_id` 之前,禁止以“我已帮你确认参数”为由越过必填确认阶段。
105
113
 
114
+ ## Cron / Scheduled Run Policy
115
+
116
+ 创建 cron 时必须在设置 cron 的当下完成全部交互:
117
+
118
+ 1. 锁定用户原始 instruction,不改写、不摘要,criteria 放入 `overrides.criteria` 时必须逐字保留。
119
+ 2. 收集 Stage A 全部筛选项、`target_count`、`post_action`、`max_greet_count`(如需要)和本次 `rest_level`。
120
+ 3. 调用 `list_recommend_jobs`;若 Chrome 未打开,工具会尝试自动打开本机 9222 Chrome 并进入推荐页。若返回 `BOSS_LOGIN_REQUIRED` 或页面不可用,停止 cron 创建,让用户登录/修复后重试。
121
+ 4. 用 `job_names` 中的精确岗位名完成岗位确认,并完成最终总确认,写入 `job_confirmed=true` 与 `final_confirmed=true`。
122
+ 5. 调用 `prepare_recommend_pipeline_run` 传入将来要执行的完整 payload;只有 `READY + cron_ready=true` 才能创建 cron。
123
+ 6. cron 到点时只调用 `start_recommend_pipeline_run`,并传入准备阶段验证过的同一份 payload。到点后若 Chrome/登录异常,应作为运行失败处理,不得再向用户追问参数。
124
+
125
+ Shell-only OpenClaw/QClaw cron 设置同样先运行:
126
+
127
+ ```powershell
128
+ npx -y @reconcrap/boss-recommend-mcp@latest prepare-run --instruction-file .\boss-recommend-instruction.txt --overrides-file .\boss-recommend-overrides.json --confirmation-file .\boss-recommend-confirmation.json --slow-live --port 9222
129
+ ```
130
+
131
+ 仅当上述命令输出 `READY` 且 `cron_ready=true` 后,才把对应的 detached `run` 命令写入 cron。
132
+
106
133
  ## Async Run Policy
107
134
 
108
135
  - 用户未明确要求“持续跟进”时,不自动 `sleep + get_recommend_pipeline_run`。
@@ -146,13 +173,14 @@ description: "Use when users want Boss recommend-page filtering/screening via bo
146
173
  推荐做法:
147
174
 
148
175
  1. 将锁定的用户原文写入 instruction 文件,将已确认参数写入 `overrides` 与 `confirmation` JSON 文件。
149
- 2. detached CLI 启动,让父命令返回启动证据,子进程继续持有 CDP 会话:
176
+ 2. 先用 `prepare-run` 校验完整 payload cron-ready;未返回 `READY + cron_ready=true` 不得创建 cron 或启动 run。
177
+ 3. 用 detached CLI 启动,让父命令返回启动证据,子进程继续持有 CDP 会话:
150
178
 
151
179
  ```powershell
152
180
  npx -y @reconcrap/boss-recommend-mcp@latest run --detached --instruction-file .\boss-recommend-instruction.txt --overrides-file .\boss-recommend-overrides.json --confirmation-file .\boss-recommend-confirmation.json --slow-live --port 9222
153
181
  ```
154
182
 
155
- 3. 若返回 `ACCEPTED + run_id`,即任务已启动;记录 `run_id/stdout_path/stderr_path`。若返回 `NEED_INPUT` 或 `NEED_CONFIRMATION`,只补 `pending_questions`,不要重写已锁定的用户原文。
183
+ 4. 若返回 `ACCEPTED + run_id`,即任务已启动;记录 `run_id/stdout_path/stderr_path`。若返回 `NEED_INPUT` 或 `NEED_CONFIRMATION`,说明 cron 设置阶段漏掉了准备门禁,应回到 `prepare-run` 补齐,不能在到点后继续问用户确认。
156
184
 
157
185
  兼容路径:
158
186
 
@@ -15,3 +15,5 @@ This skill intentionally replaces legacy `boss-recruit-mcp` skill installs. It r
15
15
  - `cancel_recruit_pipeline_run`
16
16
 
17
17
  Do not call the old `@reconcrap/boss-recruit-mcp` package from this skill.
18
+
19
+ Each run must ask the user to choose `rest_level` (`low` / `medium` / `high`) and pass the answer as `human_behavior.restLevel`; do not pick a default for the user.
@@ -32,6 +32,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
32
32
  - 若用户提供城市、学历、学校、关键词、过滤已看、人选目标数、筛选条件、post action、max greet 等参数,必须逐项传入或确认。
33
33
  - `post_action=greet` 时必须确认 `max_greet_count`;不要默认等于 `target_count`。
34
34
  - 搜索页和推荐页一样支持多选筛选条件;不要把多选降级成单选。
35
+ - 每次 run 必须明确询问用户本次休息强度 `rest_level`:`low`(旧策略)/ `medium`(约 5 小时或 700 人累计休息 30 分钟)/ `high`(约 5 小时或 700 人累计休息 1 小时);不得默认使用配置文件里的值替用户决定。
35
36
 
36
37
  ## Required Inputs
37
38
 
@@ -39,6 +40,7 @@ description: "Use when users want Boss search/recruit-page screening via the uni
39
40
  - `keyword` 或用户明确的搜索意图
40
41
  - `criteria`
41
42
  - `target_count`
43
+ - `rest_level`: `low|medium|high`
42
44
 
43
45
  常用可选项:
44
46
 
@@ -50,6 +52,12 @@ description: "Use when users want Boss search/recruit-page screening via the uni
50
52
  - `max_greet_count`
51
53
  - `port`
52
54
 
55
+ 启动工具时,把用户确认的休息强度写入 `human_behavior.restLevel`,例如:
56
+
57
+ ```json
58
+ { "human_behavior": { "restLevel": "medium" } }
59
+ ```
60
+
53
61
  ## Response Style
54
62
 
55
63
  - 用结构化中文确认参数。
package/src/cli.js CHANGED
@@ -23,10 +23,11 @@ import {
23
23
  prepareBossChatRunTool,
24
24
  resumeBossChatRunTool
25
25
  } from "./chat-mcp.js";
26
- import {
27
- listRecommendJobsTool,
28
- startRecommendPipelineRunTool
29
- } from "./recommend-mcp.js";
26
+ import {
27
+ listRecommendJobsTool,
28
+ prepareRecommendPipelineRunTool,
29
+ startRecommendPipelineRunTool
30
+ } from "./recommend-mcp.js";
30
31
  import {
31
32
  getBossScreenConfigResolution,
32
33
  resolveBossChatRuntimeLayout as resolveCdpBossChatRuntimeLayout,
@@ -53,7 +54,7 @@ const defaultMcpServerName = "boss-recommend";
53
54
  const defaultMcpCommand = "npx";
54
55
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
55
56
  const recommendMcpBinaryName = "boss-recommend-mcp";
56
- const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
57
+ const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
57
58
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
58
59
  const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
59
60
  const installConfigDefaults = Object.freeze({
@@ -63,12 +64,13 @@ const installConfigDefaults = Object.freeze({
63
64
  llmMaxRetries: 3,
64
65
  llmImageLimit: 8,
65
66
  llmImageDetail: "low",
66
- humanRestEnabled: true,
67
- humanBehavior: {
68
- enabled: true,
69
- profile: "paced_with_rests"
70
- }
71
- });
67
+ humanRestEnabled: true,
68
+ humanBehavior: {
69
+ enabled: true,
70
+ profile: "paced_with_rests",
71
+ restLevel: "low"
72
+ }
73
+ });
72
74
  const bossChatRuntimeChildDirs = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
73
75
  const bossChatCliUnsupportedStartCode = "CHAT_CLI_ASYNC_UNSUPPORTED_CDP_ONLY";
74
76
  const calibrateUnsupportedCode = "CALIBRATE_UNSUPPORTED_CDP_ONLY";
@@ -613,14 +615,20 @@ function parseBossChatTargetCountOption(raw) {
613
615
  return parsed ?? text;
614
616
  }
615
617
 
616
- function parseBooleanOption(raw, fallback = undefined) {
617
- if (raw === undefined || raw === null || raw === "") return fallback;
618
- if (raw === true) return true;
619
- const normalized = String(raw).trim().toLowerCase();
620
- if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
621
- if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
622
- return fallback;
623
- }
618
+ function parseBooleanOption(raw, fallback = undefined) {
619
+ if (raw === undefined || raw === null || raw === "") return fallback;
620
+ if (raw === true) return true;
621
+ const normalized = String(raw).trim().toLowerCase();
622
+ if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
623
+ if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
624
+ return fallback;
625
+ }
626
+
627
+ function parseRestLevelOption(raw) {
628
+ if (raw === undefined || raw === null || raw === "") return undefined;
629
+ const normalized = String(raw).trim().toLowerCase();
630
+ return ["low", "medium", "high"].includes(normalized) ? normalized : undefined;
631
+ }
624
632
 
625
633
  function normalizePageScope(value) {
626
634
  const normalized = String(value || "").trim().toLowerCase();
@@ -1271,20 +1279,33 @@ async function resolveCliConfigTarget(options = {}) {
1271
1279
  };
1272
1280
  }
1273
1281
 
1274
- function applyMissingInstallConfigDefaults(config = {}) {
1275
- const nextConfig = { ...config };
1276
- const patchedKeys = [];
1277
- for (const [key, defaultValue] of Object.entries(installConfigDefaults)) {
1282
+ function applyMissingInstallConfigDefaults(config = {}) {
1283
+ const nextConfig = { ...config };
1284
+ const patchedKeys = [];
1285
+ for (const [key, defaultValue] of Object.entries(installConfigDefaults)) {
1278
1286
  if (!Object.prototype.hasOwnProperty.call(nextConfig, key)) {
1279
1287
  nextConfig[key] = defaultValue;
1280
- patchedKeys.push(key);
1281
- }
1282
- }
1283
- return {
1284
- nextConfig,
1285
- patchedKeys
1286
- };
1287
- }
1288
+ patchedKeys.push(key);
1289
+ }
1290
+ }
1291
+ if (
1292
+ nextConfig.humanBehavior
1293
+ && typeof nextConfig.humanBehavior === "object"
1294
+ && !Array.isArray(nextConfig.humanBehavior)
1295
+ && !Object.prototype.hasOwnProperty.call(nextConfig.humanBehavior, "restLevel")
1296
+ && !Object.prototype.hasOwnProperty.call(nextConfig.humanBehavior, "rest_level")
1297
+ ) {
1298
+ nextConfig.humanBehavior = {
1299
+ ...nextConfig.humanBehavior,
1300
+ restLevel: "low"
1301
+ };
1302
+ patchedKeys.push("humanBehavior.restLevel");
1303
+ }
1304
+ return {
1305
+ nextConfig,
1306
+ patchedKeys
1307
+ };
1308
+ }
1288
1309
 
1289
1310
  async function ensureUserConfig(options = {}) {
1290
1311
  const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
@@ -2601,10 +2622,11 @@ function printHelp() {
2601
2622
  console.log("boss-recommend-mcp");
2602
2623
  console.log("");
2603
2624
  console.log("Usage:");
2604
- console.log(" boss-recommend-mcp Start the MCP server");
2605
- console.log(" boss-recommend-mcp start Start the MCP server");
2606
- console.log(" boss-recommend-mcp run Start a CDP-only recommend run through the shared run service");
2607
- console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
2625
+ console.log(" boss-recommend-mcp Start the MCP server");
2626
+ console.log(" boss-recommend-mcp start Start the MCP server");
2627
+ console.log(" boss-recommend-mcp prepare-run Validate a cron-ready recommend run payload without starting screening");
2628
+ console.log(" boss-recommend-mcp run Start a CDP-only recommend run through the shared run service");
2629
+ console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
2608
2630
  console.log(" boss-recommend-mcp chat <subcommand> Run CDP-only boss-chat health/prepare/status commands");
2609
2631
  console.log(" boss-recommend-mcp install Install/migrate skills and MCP configs; replaces legacy Boss MCP routes (supports --agent trae-cn/openclaw/qclaw/...)");
2610
2632
  console.log(" boss-recommend-mcp install-skill Install bundled Codex skills (recommend/recruit/chat)");
@@ -2616,10 +2638,11 @@ function printHelp() {
2616
2638
  console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
2617
2639
  console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
2618
2640
  console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
2619
- console.log("");
2620
- console.log("Run command:");
2621
- console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" --overrides-file overrides.json --confirmation-file confirmation.json");
2622
- console.log(" boss-recommend-mcp run --detached --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
2641
+ console.log("");
2642
+ console.log("Run command:");
2643
+ console.log(" boss-recommend-mcp prepare-run --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
2644
+ console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" --overrides-file overrides.json --confirmation-file confirmation.json");
2645
+ console.log(" boss-recommend-mcp run --detached --instruction \"...\" --overrides-file overrides.json --confirmation-file confirmation.json");
2623
2646
  console.log(" boss-recommend-mcp list-jobs --slow-live --port 9222");
2624
2647
  console.log(" boss-recommend-mcp chat prepare-run --slow-live --port 9222 # CDP-only preflight; start runs through MCP start_boss_chat_run");
2625
2648
  console.log(" boss-recommend-mcp config set --base-url <url> --api-key <key> --model <model> [--thinking-level off|low|medium|high|current] [--greeting-message <text>] [--openai-organization <id>] [--openai-project <id>]");
@@ -2713,27 +2736,37 @@ async function installAll(options = {}) {
2713
2736
  }
2714
2737
  }
2715
2738
 
2716
- async function runPipelineOnce(options = {}) {
2717
- const instruction = getRunInstruction(options);
2718
- const confirmation = getRunConfirmation(options);
2719
- const overrides = getRunOverrides(options);
2720
- const followUp = getRunFollowUp(options);
2721
- const workspaceRoot = getWorkspaceRoot(options);
2722
- const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
2723
-
2724
- const args = {
2725
- instruction,
2726
- confirmation: confirmation ?? undefined,
2727
- overrides: overrides ?? undefined,
2728
- follow_up: followUp ?? undefined,
2739
+ function buildRecommendRunCliInput(options = {}) {
2740
+ const instruction = getRunInstruction(options);
2741
+ const confirmation = getRunConfirmation(options);
2742
+ const overrides = getRunOverrides(options);
2743
+ const followUp = getRunFollowUp(options);
2744
+ const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
2745
+
2746
+ const args = {
2747
+ instruction,
2748
+ confirmation: confirmation ?? undefined,
2749
+ overrides: overrides ?? undefined,
2750
+ follow_up: followUp ?? undefined,
2729
2751
  host: typeof options.host === "string" && options.host.trim() ? options.host.trim() : undefined,
2730
2752
  port,
2731
2753
  target_url_includes: typeof options["target-url-includes"] === "string" && options["target-url-includes"].trim()
2732
2754
  ? options["target-url-includes"].trim()
2733
2755
  : undefined,
2734
2756
  allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2735
- slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2736
- };
2757
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2758
+ };
2759
+ const restLevel = parseRestLevelOption(
2760
+ options["rest-level"]
2761
+ ?? options.rest_level
2762
+ ?? options["human-behavior-rest-level"]
2763
+ ?? options.human_behavior_rest_level
2764
+ );
2765
+ if (restLevel) {
2766
+ args.human_behavior = {
2767
+ restLevel
2768
+ };
2769
+ }
2737
2770
 
2738
2771
  const optionalPassthrough = [
2739
2772
  "detail_limit",
@@ -2766,15 +2799,46 @@ async function runPipelineOnce(options = {}) {
2766
2799
  "llm_image_limit",
2767
2800
  "llm_image_detail"
2768
2801
  ];
2769
- for (const key of optionalPassthrough) {
2770
- const kebab = key.replace(/_/g, "-");
2771
- if (options[key] !== undefined) args[key] = options[key];
2772
- else if (options[kebab] !== undefined) args[key] = options[kebab];
2773
- }
2774
-
2775
- const result = await startRecommendPipelineRunTool({
2776
- workspaceRoot,
2777
- args
2802
+ for (const key of optionalPassthrough) {
2803
+ const kebab = key.replace(/_/g, "-");
2804
+ if (options[key] !== undefined) args[key] = options[key];
2805
+ else if (options[kebab] !== undefined) args[key] = options[kebab];
2806
+ }
2807
+
2808
+ return {
2809
+ args,
2810
+ port
2811
+ };
2812
+ }
2813
+
2814
+ async function preparePipelineOnce(options = {}) {
2815
+ const workspaceRoot = getWorkspaceRoot(options);
2816
+ const { args, port } = buildRecommendRunCliInput(options);
2817
+ const result = prepareRecommendPipelineRunTool({
2818
+ workspaceRoot,
2819
+ args
2820
+ });
2821
+ printJson({
2822
+ ...result,
2823
+ cli: {
2824
+ command: "prepare-run",
2825
+ cdp_only: true,
2826
+ shared_run_service: true,
2827
+ workspace_root: workspaceRoot,
2828
+ port
2829
+ }
2830
+ });
2831
+ if (result.status !== "READY" || result.cron_ready !== true) {
2832
+ process.exitCode = 1;
2833
+ }
2834
+ }
2835
+
2836
+ async function runPipelineOnce(options = {}) {
2837
+ const workspaceRoot = getWorkspaceRoot(options);
2838
+ const { args, port } = buildRecommendRunCliInput(options);
2839
+ const result = await startRecommendPipelineRunTool({
2840
+ workspaceRoot,
2841
+ args
2778
2842
  });
2779
2843
  printJson({
2780
2844
  ...result,
@@ -2810,16 +2874,22 @@ async function listRecommendJobsCli(options = {}) {
2810
2874
  }));
2811
2875
  }
2812
2876
 
2813
- function buildBossChatCliInput(options = {}) {
2814
- const greetingTextRaw =
2815
- options["greeting-text"]
2816
- ?? options.greeting_text
2817
- ?? options.greetingText
2818
- ?? options.greeting;
2819
- const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2820
- const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2821
- const host = String(options.host || "").trim();
2822
- return {
2877
+ function buildBossChatCliInput(options = {}) {
2878
+ const greetingTextRaw =
2879
+ options["greeting-text"]
2880
+ ?? options.greeting_text
2881
+ ?? options.greetingText
2882
+ ?? options.greeting;
2883
+ const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2884
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2885
+ const host = String(options.host || "").trim();
2886
+ const restLevel = parseRestLevelOption(
2887
+ options["rest-level"]
2888
+ ?? options.rest_level
2889
+ ?? options["human-behavior-rest-level"]
2890
+ ?? options.human_behavior_rest_level
2891
+ );
2892
+ return {
2823
2893
  profile: typeof options.profile === "string" ? options.profile.trim() : undefined,
2824
2894
  job: typeof options.job === "string" ? options.job.trim() : undefined,
2825
2895
  start_from: String(options["start-from"] || options.start_from || "").trim().toLowerCase() || undefined,
@@ -2837,13 +2907,18 @@ function buildBossChatCliInput(options = {}) {
2837
2907
  dry_run: options["dry-run"] === true || options.dryRun === true,
2838
2908
  no_state: options["no-state"] === true || options.noState === true,
2839
2909
  human_behavior_enabled: parseBooleanOption(options["human-behavior-enabled"] ?? options.human_behavior_enabled),
2840
- human_behavior_profile: typeof (options["human-behavior-profile"] ?? options.human_behavior_profile) === "string"
2841
- ? (options["human-behavior-profile"] ?? options.human_behavior_profile).trim()
2842
- : undefined,
2843
- safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
2844
- batch_rest_enabled: parseBooleanOption(options["batch-rest"] ?? options.batch_rest_enabled)
2845
- };
2846
- }
2910
+ human_behavior_profile: typeof (options["human-behavior-profile"] ?? options.human_behavior_profile) === "string"
2911
+ ? (options["human-behavior-profile"] ?? options.human_behavior_profile).trim()
2912
+ : undefined,
2913
+ human_behavior: restLevel
2914
+ ? {
2915
+ restLevel
2916
+ }
2917
+ : undefined,
2918
+ safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
2919
+ batch_rest_enabled: parseBooleanOption(options["batch-rest"] ?? options.batch_rest_enabled)
2920
+ };
2921
+ }
2847
2922
 
2848
2923
  function getBossChatCliRunTarget(options = {}) {
2849
2924
  return {
@@ -2932,11 +3007,11 @@ export async function runCli(argv = process.argv) {
2932
3007
  case "mcp":
2933
3008
  startServer();
2934
3009
  break;
2935
- case "run":
2936
- try {
2937
- if (
2938
- (options.detached === true || options.background === true)
2939
- && process.env[detachedRecommendRunChildEnv] !== "1"
3010
+ case "run":
3011
+ try {
3012
+ if (
3013
+ (options.detached === true || options.background === true)
3014
+ && process.env[detachedRecommendRunChildEnv] !== "1"
2940
3015
  ) {
2941
3016
  await runPipelineDetached(argv.slice(3), options);
2942
3017
  } else {
@@ -2951,9 +3026,26 @@ export async function runCli(argv = process.argv) {
2951
3026
  retryable: false
2952
3027
  }
2953
3028
  });
2954
- process.exitCode = 1;
2955
- }
2956
- break;
3029
+ process.exitCode = 1;
3030
+ }
3031
+ break;
3032
+ case "prepare-run":
3033
+ case "prepare":
3034
+ try {
3035
+ await preparePipelineOnce(options);
3036
+ } catch (error) {
3037
+ printJson({
3038
+ status: "FAILED",
3039
+ cron_ready: false,
3040
+ error: {
3041
+ code: "INVALID_CLI_INPUT",
3042
+ message: error.message || "Invalid CLI input",
3043
+ retryable: false
3044
+ }
3045
+ });
3046
+ process.exitCode = 1;
3047
+ }
3048
+ break;
2957
3049
  case "list-jobs":
2958
3050
  case "jobs":
2959
3051
  case "recommend-jobs":
@@ -3111,10 +3203,11 @@ export const __testables = {
3111
3203
  installSkill,
3112
3204
  isInstalledPackageRoot,
3113
3205
  mergeMcpServerConfigFile,
3114
- resolveBossChatRuntimeLayout: resolveCdpBossChatRuntimeLayout,
3115
- runBossChatCliCommand,
3116
- runPipelineOnce
3117
- };
3206
+ resolveBossChatRuntimeLayout: resolveCdpBossChatRuntimeLayout,
3207
+ runBossChatCliCommand,
3208
+ preparePipelineOnce,
3209
+ runPipelineOnce
3210
+ };
3118
3211
 
3119
3212
  if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
3120
3213
  await runCli(process.argv);
@@ -86,6 +86,46 @@ const HUMAN_BEHAVIOR_PROFILE_ALIASES = Object.freeze({
86
86
  rests: "paced_with_rests",
87
87
  rest: "paced_with_rests"
88
88
  });
89
+ const DEFAULT_HUMAN_REST_LEVEL = "low";
90
+ const HUMAN_REST_LEVEL_ALIASES = Object.freeze({
91
+ default: "low",
92
+ light: "low",
93
+ normal: "medium",
94
+ med: "medium",
95
+ heavy: "high"
96
+ });
97
+ const HUMAN_REST_LEVEL_PROFILES = Object.freeze({
98
+ medium: Object.freeze({
99
+ targetRestMs: 30 * 60 * 1000,
100
+ targetCandidateCount: 700,
101
+ targetWindowMs: 5 * 60 * 60 * 1000,
102
+ intervalMin: 4,
103
+ intervalMax: 16,
104
+ longRestProbability: 0.22,
105
+ shortRestMinMs: 8000,
106
+ shortRestMaxMs: 45000,
107
+ longRestMinMs: 60000,
108
+ longRestMaxMs: 180000,
109
+ minDebtToRestMs: 8000,
110
+ forceDebtMs: 90000,
111
+ maxOverspendMs: 15000
112
+ }),
113
+ high: Object.freeze({
114
+ targetRestMs: 60 * 60 * 1000,
115
+ targetCandidateCount: 700,
116
+ targetWindowMs: 5 * 60 * 60 * 1000,
117
+ intervalMin: 3,
118
+ intervalMax: 12,
119
+ longRestProbability: 0.28,
120
+ shortRestMinMs: 12000,
121
+ shortRestMaxMs: 75000,
122
+ longRestMinMs: 90000,
123
+ longRestMaxMs: 300000,
124
+ minDebtToRestMs: 12000,
125
+ forceDebtMs: 150000,
126
+ maxOverspendMs: 25000
127
+ })
128
+ });
89
129
 
90
130
  function clampNumber(value, min, max) {
91
131
  const number = Number(value);
@@ -152,6 +192,14 @@ export function normalizeHumanBehaviorProfile(raw, fallback = "baseline") {
152
192
  : fallback;
153
193
  }
154
194
 
195
+ export function normalizeHumanRestLevel(raw, fallback = DEFAULT_HUMAN_REST_LEVEL) {
196
+ const normalized = String(raw || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
197
+ const level = HUMAN_REST_LEVEL_ALIASES[normalized] || normalized;
198
+ return level === "low" || level === "medium" || level === "high"
199
+ ? level
200
+ : fallback;
201
+ }
202
+
155
203
  export function normalizeHumanBehaviorOptions(raw = null, {
156
204
  legacyEnabled = false,
157
205
  safePacing = null,
@@ -231,6 +279,10 @@ export function normalizeHumanBehaviorOptions(raw = null, {
231
279
  readFirstOption(rawObject, ["batchRest", "batch_rest", "batchRestEnabled", "batch_rest_enabled"]),
232
280
  profileDefaults.batchRest
233
281
  );
282
+ const restLevel = normalizeHumanRestLevel(
283
+ readFirstOption(rawObject, ["restLevel", "rest_level"]),
284
+ DEFAULT_HUMAN_REST_LEVEL
285
+ );
234
286
  if (batchRestFlag !== null) {
235
287
  batchRest = batchRestFlag;
236
288
  if (batchRestFlag === true && readFirstOption(rawObject, ["shortRest", "short_rest", "randomRest", "random_rest"]) === undefined) {
@@ -248,6 +300,7 @@ export function normalizeHumanBehaviorOptions(raw = null, {
248
300
  shortRest: enabled && shortRest === true,
249
301
  batchRest: enabled && batchRest === true,
250
302
  actionCooldown: enabled && actionCooldown === true,
303
+ restLevel,
251
304
  restEnabled: enabled && (shortRest === true || batchRest === true)
252
305
  };
253
306
  }
@@ -404,6 +457,8 @@ export function createHumanRestController({
404
457
  shortRestEnabled = true,
405
458
  batchRestEnabled = true,
406
459
  random = Math.random,
460
+ nowFn = Date.now,
461
+ restLevel = DEFAULT_HUMAN_REST_LEVEL,
407
462
  shortRestProbability = 0.08,
408
463
  shortRestMinMs = 3000,
409
464
  shortRestMaxMs = 7000,
@@ -413,12 +468,26 @@ export function createHumanRestController({
413
468
  batchRestMaxMs = 30000
414
469
  } = {}) {
415
470
  const nextRandom = normalizeRandom(random);
471
+ const readNow = typeof nowFn === "function" ? nowFn : Date.now;
472
+ const normalizedRestLevel = normalizeHumanRestLevel(restLevel);
473
+ const budgetProfile = (shortRestEnabled !== false || batchRestEnabled !== false)
474
+ ? HUMAN_REST_LEVEL_PROFILES[normalizedRestLevel] || null
475
+ : null;
476
+ const nextBudgetRestInterval = () => budgetProfile
477
+ ? randomIntegerBetween(nextRandom, budgetProfile.intervalMin, budgetProfile.intervalMax)
478
+ : 0;
416
479
  const state = {
417
480
  enabled: enabled === true,
481
+ rest_level: normalizedRestLevel,
418
482
  short_rest_enabled: enabled === true && shortRestEnabled !== false,
419
483
  batch_rest_enabled: enabled === true && batchRestEnabled !== false,
420
484
  rest_counter: 0,
421
485
  rest_threshold: Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1))),
486
+ processed_count: 0,
487
+ candidates_since_last_rest: 0,
488
+ candidates_until_next_rest: nextBudgetRestInterval(),
489
+ active_elapsed_ms: 0,
490
+ last_active_at_ms: Number(readNow()) || 0,
422
491
  rest_count: 0,
423
492
  total_rest_ms: 0
424
493
  };
@@ -427,6 +496,67 @@ export function createHumanRestController({
427
496
  state.rest_threshold = Math.max(1, Math.floor(Number(batchThresholdBase) || 25) + Math.floor(nextRandom() * Math.max(1, Number(batchThresholdJitter) || 1)));
428
497
  }
429
498
 
499
+ function updateActiveElapsed() {
500
+ const now = Number(readNow()) || 0;
501
+ if (state.last_active_at_ms >= 0 && now >= state.last_active_at_ms) {
502
+ state.active_elapsed_ms += now - state.last_active_at_ms;
503
+ }
504
+ state.last_active_at_ms = now;
505
+ return now;
506
+ }
507
+
508
+ function getBudgetTargetMs() {
509
+ if (!budgetProfile) return 0;
510
+ const candidateTarget = state.processed_count * (budgetProfile.targetRestMs / budgetProfile.targetCandidateCount);
511
+ const elapsedTarget = state.active_elapsed_ms * (budgetProfile.targetRestMs / budgetProfile.targetWindowMs);
512
+ return Math.max(candidateTarget, elapsedTarget);
513
+ }
514
+
515
+ function chooseBudgetRestPause(debtMs) {
516
+ const longRest = nextRandom() < budgetProfile.longRestProbability;
517
+ const minMs = longRest ? budgetProfile.longRestMinMs : budgetProfile.shortRestMinMs;
518
+ const maxMs = longRest ? budgetProfile.longRestMaxMs : budgetProfile.shortRestMaxMs;
519
+ const scaleMin = longRest ? 0.75 : 0.38;
520
+ const scaleMax = longRest ? 1.1 : 0.78;
521
+ const desiredMs = debtMs * randomBetween(nextRandom, scaleMin, scaleMax);
522
+ const randomizedMs = randomBetween(nextRandom, minMs, maxMs);
523
+ const blendedMs = Math.max(minMs, Math.min(maxMs, (desiredMs + randomizedMs) / 2));
524
+ const maxAllowedMs = Math.max(minMs, debtMs + budgetProfile.maxOverspendMs);
525
+ return {
526
+ pauseMs: Math.round(Math.min(blendedMs, maxAllowedMs)),
527
+ restSize: longRest ? "long" : "short"
528
+ };
529
+ }
530
+
531
+ async function takeBudgetBreakIfNeeded(sleeper) {
532
+ state.processed_count += 1;
533
+ state.candidates_since_last_rest += 1;
534
+ state.candidates_until_next_rest -= 1;
535
+ const debtMs = getBudgetTargetMs() - state.total_rest_ms;
536
+ const intervalDue = state.candidates_until_next_rest <= 0;
537
+ const forceDue = debtMs >= budgetProfile.forceDebtMs;
538
+ if (!intervalDue && !forceDue) {
539
+ return null;
540
+ }
541
+ if (debtMs < budgetProfile.minDebtToRestMs) {
542
+ if (intervalDue) state.candidates_until_next_rest = nextBudgetRestInterval();
543
+ return null;
544
+ }
545
+ const { pauseMs, restSize } = chooseBudgetRestPause(debtMs);
546
+ await sleeper(pauseMs);
547
+ const event = {
548
+ kind: "random_rest",
549
+ rest_level: normalizedRestLevel,
550
+ rest_size: restSize,
551
+ pause_ms: pauseMs,
552
+ processed_since_last_rest: state.candidates_since_last_rest,
553
+ rest_budget_debt_ms: Math.round(Math.max(0, debtMs))
554
+ };
555
+ state.candidates_since_last_rest = 0;
556
+ state.candidates_until_next_rest = nextBudgetRestInterval();
557
+ return event;
558
+ }
559
+
430
560
  async function takeBreakIfNeeded({ sleepFn = sleep } = {}) {
431
561
  if (!state.enabled) {
432
562
  return {
@@ -438,18 +568,44 @@ export function createHumanRestController({
438
568
  };
439
569
  }
440
570
  const sleeper = typeof sleepFn === "function" ? sleepFn : sleep;
571
+ updateActiveElapsed();
572
+ if (budgetProfile) {
573
+ const budgetEvent = await takeBudgetBreakIfNeeded(sleeper);
574
+ const pauseMs = budgetEvent?.pause_ms || 0;
575
+ if (pauseMs > 0) {
576
+ state.rest_count += 1;
577
+ state.total_rest_ms += pauseMs;
578
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
579
+ }
580
+ return {
581
+ enabled: true,
582
+ rested: Boolean(budgetEvent),
583
+ pause_ms: pauseMs,
584
+ rest_level: normalizedRestLevel,
585
+ rest_counter: state.rest_counter,
586
+ rest_threshold: state.rest_threshold,
587
+ processed_count: state.processed_count,
588
+ candidates_until_next_rest: state.candidates_until_next_rest,
589
+ active_elapsed_ms: state.active_elapsed_ms,
590
+ rest_count: state.rest_count,
591
+ total_rest_ms: state.total_rest_ms,
592
+ events: budgetEvent ? [budgetEvent] : []
593
+ };
594
+ }
441
595
  state.rest_counter += 1;
596
+ state.processed_count += 1;
442
597
  const events = [];
443
598
  if (state.short_rest_enabled && nextRandom() < Math.max(0, Number(shortRestProbability) || 0)) {
444
599
  const pauseMs = Math.round(randomBetween(nextRandom, shortRestMinMs, shortRestMaxMs));
445
600
  await sleeper(pauseMs);
446
- events.push({ kind: "random_rest", pause_ms: pauseMs });
601
+ events.push({ kind: "random_rest", rest_level: normalizedRestLevel, pause_ms: pauseMs });
447
602
  }
448
603
  if (state.batch_rest_enabled && state.rest_counter >= state.rest_threshold) {
449
604
  const pauseMs = Math.round(randomBetween(nextRandom, batchRestMinMs, batchRestMaxMs));
450
605
  await sleeper(pauseMs);
451
606
  events.push({
452
607
  kind: "batch_rest",
608
+ rest_level: normalizedRestLevel,
453
609
  pause_ms: pauseMs,
454
610
  processed_since_last_batch_rest: state.rest_counter
455
611
  });
@@ -460,13 +616,17 @@ export function createHumanRestController({
460
616
  if (pauseMs > 0) {
461
617
  state.rest_count += events.length;
462
618
  state.total_rest_ms += pauseMs;
619
+ state.last_active_at_ms = Number(readNow()) || state.last_active_at_ms;
463
620
  }
464
621
  return {
465
622
  enabled: true,
466
623
  rested: events.length > 0,
467
624
  pause_ms: pauseMs,
625
+ rest_level: normalizedRestLevel,
468
626
  rest_counter: state.rest_counter,
469
627
  rest_threshold: state.rest_threshold,
628
+ processed_count: state.processed_count,
629
+ active_elapsed_ms: state.active_elapsed_ms,
470
630
  rest_count: state.rest_count,
471
631
  total_rest_ms: state.total_rest_ms,
472
632
  events
@@ -743,11 +743,12 @@ export async function runChatWorkflow({
743
743
  safeClickPointEnabled: effectiveHumanBehavior.clickMovement,
744
744
  actionCooldownEnabled: effectiveHumanBehavior.actionCooldown
745
745
  });
746
- const humanRestController = createHumanRestController({
747
- enabled: effectiveHumanRestEnabled,
748
- shortRestEnabled: effectiveHumanBehavior.shortRest,
749
- batchRestEnabled: effectiveHumanBehavior.batchRest
750
- });
746
+ const humanRestController = createHumanRestController({
747
+ enabled: effectiveHumanRestEnabled,
748
+ shortRestEnabled: effectiveHumanBehavior.shortRest,
749
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
750
+ restLevel: effectiveHumanBehavior.restLevel
751
+ });
751
752
  const normalizedDetailSource = normalizeDetailSource(detailSource);
752
753
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
753
754
  const useLlmScreening = normalizedScreeningMode !== "deterministic";
@@ -966,11 +967,12 @@ export async function runChatWorkflow({
966
967
  context_recoveries: contextRecoveryAttempts,
967
968
  list_end_reason: listEndReason,
968
969
  viewport_checks: viewportGuard.getStats().checks,
969
- viewport_recoveries: viewportGuard.getStats().recoveries,
970
- human_behavior_enabled: effectiveHumanBehavior.enabled,
971
- human_behavior_profile: effectiveHumanBehavior.profile,
972
- human_rest_enabled: effectiveHumanRestEnabled,
973
- human_rest_count: humanRestController.getState().rest_count,
970
+ viewport_recoveries: viewportGuard.getStats().recoveries,
971
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
972
+ human_behavior_profile: effectiveHumanBehavior.profile,
973
+ human_rest_level: effectiveHumanBehavior.restLevel,
974
+ human_rest_enabled: effectiveHumanRestEnabled,
975
+ human_rest_count: humanRestController.getState().rest_count,
974
976
  human_rest_ms: humanRestController.getState().total_rest_ms,
975
977
  last_human_event: lastHumanEvent
976
978
  });
@@ -1771,11 +1773,12 @@ export async function runChatWorkflow({
1771
1773
  context_recoveries: contextRecoveryAttempts,
1772
1774
  list_end_reason: listEndReason || null,
1773
1775
  viewport_checks: viewportGuard.getStats().checks,
1774
- viewport_recoveries: viewportGuard.getStats().recoveries,
1775
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1776
- human_behavior_profile: effectiveHumanBehavior.profile,
1777
- human_rest_enabled: effectiveHumanRestEnabled,
1778
- human_rest_count: humanRestController.getState().rest_count,
1776
+ viewport_recoveries: viewportGuard.getStats().recoveries,
1777
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1778
+ human_behavior_profile: effectiveHumanBehavior.profile,
1779
+ human_rest_level: effectiveHumanBehavior.restLevel,
1780
+ human_rest_enabled: effectiveHumanRestEnabled,
1781
+ human_rest_count: humanRestController.getState().rest_count,
1779
1782
  human_rest_ms: humanRestController.getState().total_rest_ms,
1780
1783
  last_human_event: lastHumanEvent,
1781
1784
  last_candidate_id: screeningCandidate.id || null,
@@ -1815,10 +1818,11 @@ export async function runChatWorkflow({
1815
1818
  compactResult.human_rest = restResult;
1816
1819
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1817
1820
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1818
- runControl.updateProgress({
1819
- human_rest_enabled: effectiveHumanRestEnabled,
1820
- human_rest_count: humanRestController.getState().rest_count,
1821
- human_rest_ms: humanRestController.getState().total_rest_ms,
1821
+ runControl.updateProgress({
1822
+ human_rest_enabled: effectiveHumanRestEnabled,
1823
+ human_rest_level: effectiveHumanBehavior.restLevel,
1824
+ human_rest_count: humanRestController.getState().rest_count,
1825
+ human_rest_ms: humanRestController.getState().total_rest_ms,
1822
1826
  human_rest_last: restResult,
1823
1827
  context_recoveries: contextRecoveryAttempts,
1824
1828
  last_human_event: lastHumanEvent
@@ -1958,12 +1962,13 @@ export function createChatRunService({
1958
1962
  list_settle_ms: listSettleMs,
1959
1963
  list_fallback_point: listFallbackPoint,
1960
1964
  online_resume_button_timeout_ms: onlineResumeButtonTimeoutMs,
1961
- image_output_dir: imageOutputDir || "",
1962
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1963
- human_behavior_profile: effectiveHumanBehavior.profile,
1964
- human_behavior: effectiveHumanBehavior,
1965
- human_rest_enabled: effectiveHumanRestEnabled
1966
- },
1965
+ image_output_dir: imageOutputDir || "",
1966
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1967
+ human_behavior_profile: effectiveHumanBehavior.profile,
1968
+ human_behavior: effectiveHumanBehavior,
1969
+ human_rest_level: effectiveHumanBehavior.restLevel,
1970
+ human_rest_enabled: effectiveHumanRestEnabled
1971
+ },
1967
1972
  progress: {
1968
1973
  card_count: 0,
1969
1974
  target_count: targetPassCount || (processUntilListEnd ? "all" : processedLimit),
@@ -1978,10 +1983,11 @@ export function createChatRunService({
1978
1983
  requested: 0,
1979
1984
  request_satisfied: 0,
1980
1985
  request_skipped: 0,
1981
- context_recoveries: 0,
1982
- human_behavior_enabled: effectiveHumanBehavior.enabled,
1983
- human_behavior_profile: effectiveHumanBehavior.profile,
1984
- human_rest_enabled: effectiveHumanRestEnabled,
1986
+ context_recoveries: 0,
1987
+ human_behavior_enabled: effectiveHumanBehavior.enabled,
1988
+ human_behavior_profile: effectiveHumanBehavior.profile,
1989
+ human_rest_level: effectiveHumanBehavior.restLevel,
1990
+ human_rest_enabled: effectiveHumanRestEnabled,
1985
1991
  human_rest_count: 0,
1986
1992
  human_rest_ms: 0,
1987
1993
  last_human_event: null
@@ -690,7 +690,8 @@ export async function runRecommendWorkflow({
690
690
  const humanRestController = createHumanRestController({
691
691
  enabled: effectiveHumanRestEnabled,
692
692
  shortRestEnabled: effectiveHumanBehavior.shortRest,
693
- batchRestEnabled: effectiveHumanBehavior.batchRest
693
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
694
+ restLevel: effectiveHumanBehavior.restLevel
694
695
  });
695
696
  const normalizedFilter = normalizeFilter(filter);
696
697
  const normalizedPostAction = normalizeRecommendPostAction(postAction) || "none";
@@ -790,6 +791,7 @@ export async function runRecommendWorkflow({
790
791
  viewport_recoveries: viewportGuard.getStats().recoveries,
791
792
  human_behavior_enabled: effectiveHumanBehavior.enabled,
792
793
  human_behavior_profile: effectiveHumanBehavior.profile,
794
+ human_rest_level: effectiveHumanBehavior.restLevel,
793
795
  human_rest_enabled: effectiveHumanRestEnabled,
794
796
  human_rest_count: humanRestState.rest_count,
795
797
  human_rest_ms: humanRestState.total_rest_ms,
@@ -1509,6 +1511,7 @@ export async function runRecommendWorkflow({
1509
1511
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1510
1512
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1511
1513
  updateRecommendProgress({
1514
+ human_rest_level: effectiveHumanBehavior.restLevel,
1512
1515
  human_rest_last: restResult
1513
1516
  });
1514
1517
  }
@@ -1651,6 +1654,7 @@ export function createRecommendRunService({
1651
1654
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1652
1655
  human_behavior_profile: effectiveHumanBehavior.profile,
1653
1656
  human_behavior: effectiveHumanBehavior,
1657
+ human_rest_level: effectiveHumanBehavior.restLevel,
1654
1658
  human_rest_enabled: effectiveHumanRestEnabled
1655
1659
  },
1656
1660
  progress: {
@@ -1670,6 +1674,7 @@ export function createRecommendRunService({
1670
1674
  context_recoveries: 0,
1671
1675
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1672
1676
  human_behavior_profile: effectiveHumanBehavior.profile,
1677
+ human_rest_level: effectiveHumanBehavior.restLevel,
1673
1678
  human_rest_enabled: effectiveHumanRestEnabled,
1674
1679
  human_rest_count: 0,
1675
1680
  human_rest_ms: 0,
@@ -395,7 +395,8 @@ export async function runRecruitWorkflow({
395
395
  const humanRestController = createHumanRestController({
396
396
  enabled: effectiveHumanRestEnabled,
397
397
  shortRestEnabled: effectiveHumanBehavior.shortRest,
398
- batchRestEnabled: effectiveHumanBehavior.batchRest
398
+ batchRestEnabled: effectiveHumanBehavior.batchRest,
399
+ restLevel: effectiveHumanBehavior.restLevel
399
400
  });
400
401
  const normalizedSearchParams = normalizeSearchParams(searchParams);
401
402
  const normalizedScreeningMode = normalizeScreeningMode(screeningMode);
@@ -485,6 +486,7 @@ export async function runRecruitWorkflow({
485
486
  viewport_recoveries: viewportGuard.getStats().recoveries,
486
487
  human_behavior_enabled: effectiveHumanBehavior.enabled,
487
488
  human_behavior_profile: effectiveHumanBehavior.profile,
489
+ human_rest_level: effectiveHumanBehavior.restLevel,
488
490
  human_rest_enabled: effectiveHumanRestEnabled,
489
491
  human_rest_count: humanRestState.rest_count,
490
492
  human_rest_ms: humanRestState.total_rest_ms,
@@ -1101,6 +1103,7 @@ export async function runRecruitWorkflow({
1101
1103
  addTiming(compactResult.timings, "human_rest_ms", restElapsed);
1102
1104
  compactResult.timings.total_ms = Date.now() - candidateStarted;
1103
1105
  updateRecruitProgress({
1106
+ human_rest_level: effectiveHumanBehavior.restLevel,
1104
1107
  human_rest_last: restResult
1105
1108
  });
1106
1109
  }
@@ -1223,6 +1226,7 @@ export function createRecruitRunService({
1223
1226
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1224
1227
  human_behavior_profile: effectiveHumanBehavior.profile,
1225
1228
  human_behavior: effectiveHumanBehavior,
1229
+ human_rest_level: effectiveHumanBehavior.restLevel,
1226
1230
  human_rest_enabled: effectiveHumanRestEnabled
1227
1231
  },
1228
1232
  progress: {
@@ -1239,6 +1243,7 @@ export function createRecruitRunService({
1239
1243
  context_recoveries: 0,
1240
1244
  human_behavior_enabled: effectiveHumanBehavior.enabled,
1241
1245
  human_behavior_profile: effectiveHumanBehavior.profile,
1246
+ human_rest_level: effectiveHumanBehavior.restLevel,
1242
1247
  human_rest_enabled: effectiveHumanRestEnabled,
1243
1248
  human_rest_count: 0,
1244
1249
  human_rest_ms: 0,
package/src/index.js CHANGED
@@ -87,8 +87,9 @@ import {
87
87
  } from "./run-state.js";
88
88
 
89
89
  const require = createRequire(import.meta.url);
90
- const { version: SERVER_VERSION } = require("../package.json");
91
-
90
+ const { version: SERVER_VERSION } = require("../package.json");
91
+
92
+ const TOOL_PREPARE_RUN = "prepare_recommend_pipeline_run";
92
93
  const TOOL_START_RUN = "start_recommend_pipeline_run";
93
94
  const TOOL_GET_RUN = "get_recommend_pipeline_run";
94
95
  const TOOL_CANCEL_RUN = "cancel_recommend_pipeline_run";
@@ -308,7 +309,17 @@ function createHumanBehaviorInputSchema(description = "可选,启用可靠性
308
309
  listScrollJitter: { type: "boolean" },
309
310
  shortRest: { type: "boolean" },
310
311
  batchRest: { type: "boolean" },
311
- actionCooldown: { type: "boolean" }
312
+ actionCooldown: { type: "boolean" },
313
+ restLevel: {
314
+ type: "string",
315
+ enum: ["low", "medium", "high"],
316
+ description: "本次 run 的休息强度:low 保持旧策略;medium 约 5 小时/700 人累计休息 30 分钟;high 约 5 小时/700 人累计休息 1 小时"
317
+ },
318
+ rest_level: {
319
+ type: "string",
320
+ enum: ["low", "medium", "high"],
321
+ description: "兼容字段;优先使用 restLevel"
322
+ }
312
323
  },
313
324
  additionalProperties: false,
314
325
  description
@@ -1079,10 +1090,15 @@ function createToolsSchema() {
1079
1090
  description: "CDP-only 读取 Boss 推荐页岗位下拉框,返回所有可用岗位完整名称,方便 cron/一次性任务提前填写 job 参数。不会启动筛选任务。",
1080
1091
  inputSchema: createListRecommendJobsInputSchema()
1081
1092
  },
1093
+ {
1094
+ name: TOOL_PREPARE_RUN,
1095
+ description: "只校验 Boss 推荐页流水线参数是否已可用于 cron/一次性任务;不会启动筛选任务。只有返回 READY/cron_ready=true 后才应创建定时任务。",
1096
+ inputSchema: createRunInputSchema()
1097
+ },
1082
1098
  {
1083
1099
  name: TOOL_START_RUN,
1084
1100
  description: "异步启动 Boss 推荐页流水线(含同步门禁预检);只有在前置确认与页面就绪通过后才返回 run_id。",
1085
- inputSchema: createRunInputSchema()
1101
+ inputSchema: createRunInputSchema()
1086
1102
  },
1087
1103
  {
1088
1104
  name: TOOL_GET_RUN,
@@ -2608,6 +2624,8 @@ async function handleRequest(message, workspaceRoot) {
2608
2624
  let payload;
2609
2625
  if (toolName === TOOL_LIST_RECOMMEND_JOBS) {
2610
2626
  payload = await listRecommendJobsTool({ workspaceRoot, args });
2627
+ } else if (toolName === TOOL_PREPARE_RUN) {
2628
+ payload = prepareRecommendPipelineRunTool({ workspaceRoot, args });
2611
2629
  } else if (toolName === TOOL_START_RUN) {
2612
2630
  payload = await handleStartRunTool({ workspaceRoot, args });
2613
2631
  } else if (toolName === TOOL_GET_RUN) {
@@ -1464,15 +1464,21 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
1464
1464
  };
1465
1465
  }
1466
1466
 
1467
- export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
1468
- const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
1469
- if (prepared.response) return prepared.response;
1470
- const { parsed, normalized } = prepared;
1471
- return {
1472
- status: "READY",
1473
- review: parsed.review,
1474
- post_action: {
1475
- requested: normalized.postAction,
1467
+ export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
1468
+ const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
1469
+ if (prepared.response) {
1470
+ return {
1471
+ ...prepared.response,
1472
+ cron_ready: false
1473
+ };
1474
+ }
1475
+ const { parsed, normalized } = prepared;
1476
+ return {
1477
+ status: "READY",
1478
+ cron_ready: true,
1479
+ review: parsed.review,
1480
+ post_action: {
1481
+ requested: normalized.postAction,
1476
1482
  execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
1477
1483
  max_greet_count: normalized.maxGreetCount
1478
1484
  },
@@ -665,7 +665,17 @@ function createHumanBehaviorInputSchema(description = "可选,search/recruit
665
665
  listScrollJitter: { type: "boolean" },
666
666
  shortRest: { type: "boolean" },
667
667
  batchRest: { type: "boolean" },
668
- actionCooldown: { type: "boolean" }
668
+ actionCooldown: { type: "boolean" },
669
+ restLevel: {
670
+ type: "string",
671
+ enum: ["low", "medium", "high"],
672
+ description: "本次 run 的休息强度:low 保持旧策略;medium 约 5 小时/700 人累计休息 30 分钟;high 约 5 小时/700 人累计休息 1 小时"
673
+ },
674
+ rest_level: {
675
+ type: "string",
676
+ enum: ["low", "medium", "high"],
677
+ description: "兼容字段;优先使用 restLevel"
678
+ }
669
679
  },
670
680
  additionalProperties: false,
671
681
  description