@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +5 -2
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/config/screening-config.example.json +33 -33
  4. package/package.json +2 -1
  5. package/scripts/install-macos.sh +280 -280
  6. package/scripts/postinstall.cjs +44 -44
  7. package/skills/boss-chat/README.md +42 -42
  8. package/skills/boss-chat/SKILL.md +106 -106
  9. package/skills/boss-recommend-pipeline/README.md +13 -13
  10. package/skills/boss-recommend-pipeline/SKILL.md +219 -214
  11. package/skills/boss-recruit-pipeline/README.md +19 -19
  12. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  13. package/src/chat-mcp.js +127 -127
  14. package/src/chat-runtime-config.js +775 -775
  15. package/src/cli.js +573 -573
  16. package/src/core/boss-cards/index.js +199 -199
  17. package/src/core/browser/index.js +2416 -2385
  18. package/src/core/capture/index.js +1201 -1201
  19. package/src/core/cv-acquisition/index.js +238 -238
  20. package/src/core/cv-capture-target/index.js +299 -299
  21. package/src/core/greet-quota/index.js +71 -71
  22. package/src/core/infinite-list/index.js +1315 -1306
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +44 -27
  25. package/src/core/run/timing.js +33 -33
  26. package/src/core/screening/index.js +2135 -2135
  27. package/src/core/self-heal/index.js +973 -973
  28. package/src/core/self-heal/viewport.js +564 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/cards.js +137 -137
  31. package/src/domains/chat/constants.js +9 -9
  32. package/src/domains/chat/detail.js +113 -113
  33. package/src/domains/chat/index.js +7 -7
  34. package/src/domains/chat/jobs.js +620 -620
  35. package/src/domains/chat/page-guard.js +122 -122
  36. package/src/domains/chat/roots.js +56 -56
  37. package/src/domains/chat/run-service.js +571 -571
  38. package/src/domains/common/account-rights-panel.js +314 -314
  39. package/src/domains/common/recovery-settle.js +159 -159
  40. package/src/domains/recommend/actions.js +472 -472
  41. package/src/domains/recommend/cards.js +243 -243
  42. package/src/domains/recommend/colleague-contact.js +333 -333
  43. package/src/domains/recommend/constants.js +228 -159
  44. package/src/domains/recommend/detail.js +655 -646
  45. package/src/domains/recommend/filters.js +748 -377
  46. package/src/domains/recommend/index.js +4 -3
  47. package/src/domains/recommend/jobs.js +542 -542
  48. package/src/domains/recommend/location.js +736 -0
  49. package/src/domains/recommend/refresh.js +575 -351
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +1616 -878
  52. package/src/domains/recommend/scopes.js +246 -246
  53. package/src/domains/recruit/actions.js +277 -277
  54. package/src/domains/recruit/cards.js +74 -74
  55. package/src/domains/recruit/constants.js +236 -236
  56. package/src/domains/recruit/detail.js +588 -588
  57. package/src/domains/recruit/index.js +9 -9
  58. package/src/domains/recruit/instruction-parser.js +866 -866
  59. package/src/domains/recruit/refresh.js +45 -45
  60. package/src/domains/recruit/roots.js +68 -68
  61. package/src/domains/recruit/run-service.js +1620 -1620
  62. package/src/domains/recruit/search.js +3229 -3229
  63. package/src/index.js +121 -4
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +1174 -914
  66. package/src/recommend-scheduler.js +535 -469
  67. package/src/recruit-mcp.js +2121 -2121
@@ -1,18 +1,19 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
- import {
5
- assertNoForbiddenCdpCalls,
6
- bringPageToFront,
7
- connectToChromeTargetOrOpen,
8
- createBossLoginRequiredError,
9
- detectBossLoginState,
10
- enableDomains,
11
- getMainFrameUrl,
12
- isBossLoginUrl,
13
- waitForMainFrameUrl,
14
- sleep
15
- } from "./core/browser/index.js";
4
+ import {
5
+ assertNoForbiddenCdpCalls,
6
+ bringPageToFront,
7
+ connectToChromeTargetOrOpen,
8
+ createBossLoginRequiredError,
9
+ detectBossLoginState,
10
+ enableDomains,
11
+ FORBIDDEN_CDP_METHODS,
12
+ getMainFrameUrl,
13
+ isBossLoginUrl,
14
+ waitForMainFrameUrl,
15
+ sleep
16
+ } from "./core/browser/index.js";
16
17
  import {
17
18
  RUN_STATUS_CANCELING,
18
19
  RUN_STATUS_CANCELED,
@@ -44,21 +45,21 @@ import {
44
45
  parseRecommendInstruction
45
46
  } from "./parser.js";
46
47
  import { getRunsDir } from "./run-state.js";
47
- import {
48
- resolveBossConfiguredOutputDir,
49
- resolveHumanBehaviorForRun,
50
- resolveBossScreeningConfig
51
- } from "./chat-runtime-config.js";
48
+ import {
49
+ resolveBossConfiguredOutputDir,
50
+ resolveHumanBehaviorForRun,
51
+ resolveBossScreeningConfig
52
+ } from "./chat-runtime-config.js";
52
53
  import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
53
54
 
54
55
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
55
- const DEFAULT_RECOMMEND_PORT = 9222;
56
- const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
57
- const STATUS_METHOD_LOG_TAIL_LIMIT = 25;
58
- const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
59
- const RUN_MODE_ASYNC = "async";
60
- const REST_LEVEL_OPTIONS = ["low", "medium", "high"];
61
- const REST_LEVEL_SET = new Set(REST_LEVEL_OPTIONS);
56
+ const DEFAULT_RECOMMEND_PORT = 9222;
57
+ const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
58
+ const STATUS_METHOD_LOG_TAIL_LIMIT = 25;
59
+ const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
60
+ const RUN_MODE_ASYNC = "async";
61
+ const REST_LEVEL_OPTIONS = ["low", "medium", "high"];
62
+ const REST_LEVEL_SET = new Set(REST_LEVEL_OPTIONS);
62
63
 
63
64
  const TERMINAL_STATUSES = new Set([
64
65
  RUN_STATUS_COMPLETED,
@@ -76,188 +77,188 @@ let recommendRunService = createRecommendRunService({
76
77
  });
77
78
  const recommendRunMeta = new Map();
78
79
 
79
- function normalizeText(value) {
80
- return String(value || "").replace(/\s+/g, " ").trim();
81
- }
82
-
83
- function collectRecommendRouteText(args = {}) {
84
- return normalizeText([
85
- args.instruction,
86
- args.criteria,
87
- args.target_url_includes,
88
- args.page_scope,
89
- args.confirmation?.page_value,
90
- args.confirmation?.job_value,
91
- args.overrides?.criteria,
92
- args.overrides?.page_scope,
93
- args.overrides?.job,
94
- args.overrides?.target_url_includes
95
- ].filter((value) => value !== undefined && value !== null).join(" "));
96
- }
97
-
98
- function findRouteSignals(text, patterns = []) {
99
- const signals = [];
100
- for (const { label, pattern } of patterns) {
101
- if (pattern.test(text)) signals.push(label);
102
- }
103
- return signals;
104
- }
105
-
106
- function detectExplicitNonRecommendScope(args = {}) {
107
- const fields = [
108
- ["page_scope", args.page_scope],
109
- ["confirmation.page_value", args.confirmation?.page_value],
110
- ["overrides.page_scope", args.overrides?.page_scope],
111
- ["target_url_includes", args.target_url_includes],
112
- ["overrides.target_url_includes", args.overrides?.target_url_includes]
113
- ];
114
- for (const [field, value] of fields) {
115
- const normalized = normalizeText(value).toLowerCase();
116
- if (!normalized) continue;
117
- if (/\/web\/chat\/index|chat\/index/.test(normalized) || ["chat", "chat-page", "boss-chat"].includes(normalized)) {
118
- return {
119
- domain: "chat",
120
- signals: [`${field}:chat`],
121
- text: normalized
122
- };
123
- }
124
- if (/\/web\/chat\/search|chat\/search/.test(normalized) || ["search", "search-page", "recruit", "recruit-page", "boss-recruit"].includes(normalized)) {
125
- return {
126
- domain: "search",
127
- signals: [`${field}:search`],
128
- text: normalized
129
- };
130
- }
131
- }
132
- return null;
133
- }
134
-
135
- function detectRecruitShapedRecommendArgs(args = {}) {
136
- const checks = [
137
- ["keyword", args.keyword],
138
- ["search_keyword", args.search_keyword],
139
- ["city", args.city],
140
- ["confirmation.keyword_value", args.confirmation?.keyword_value],
141
- ["confirmation.keyword_confirmed", args.confirmation?.keyword_confirmed],
142
- ["confirmation.search_params_confirmed", args.confirmation?.search_params_confirmed],
143
- ["overrides.keyword", args.overrides?.keyword],
144
- ["overrides.search_keyword", args.overrides?.search_keyword],
145
- ["overrides.city", args.overrides?.city],
146
- ["overrides.filter_recent_viewed", args.overrides?.filter_recent_viewed],
147
- ["overrides.schools", args.overrides?.schools],
148
- ["overrides.school_tags", args.overrides?.school_tags]
149
- ];
150
- const signals = [];
151
- for (const [field, value] of checks) {
152
- if (value === undefined || value === null) continue;
153
- if (Array.isArray(value) && value.length === 0) continue;
154
- if (typeof value === "string" && !normalizeText(value)) continue;
155
- signals.push(field);
156
- }
157
- if (!signals.length) return null;
158
- return {
159
- domain: "search",
160
- signals: signals.map((field) => `${field}:recruit_arg`),
161
- text: signals.join(" ")
162
- };
163
- }
164
-
165
- function detectNonRecommendRoute(args = {}) {
166
- const explicitRoute = detectExplicitNonRecommendScope(args);
167
- if (explicitRoute) return explicitRoute;
168
- const recruitArgRoute = detectRecruitShapedRecommendArgs(args);
169
- if (recruitArgRoute) return recruitArgRoute;
170
-
171
- const text = collectRecommendRouteText(args);
172
- if (!text) return null;
173
- const chatSignals = findRouteSignals(text, [
174
- { label: "chat-only", pattern: /\bchat[-\s]?only\b/i },
175
- { label: "boss-chat", pattern: /\bboss[-\s]?chat\b/i },
176
- { label: "chat page", pattern: /\bchat\s+page\b/i },
177
- { label: "chat/index", pattern: /(?:\/web\/chat\/index|chat\/index)/i },
178
- { label: "聊天页", pattern: /聊天页/ },
179
- { label: "聊天列表", pattern: /聊天列表/ },
180
- { label: "未读", pattern: /未读/ },
181
- { label: "全部聊天", pattern: /全部聊天|所有聊天/ },
182
- { label: "求简历", pattern: /求简历|索要简历|要简历|在线简历/ }
183
- ]);
184
- if (chatSignals.length) {
185
- return {
186
- domain: "chat",
187
- signals: chatSignals,
188
- text
189
- };
190
- }
191
- const searchSignals = findRouteSignals(text, [
192
- { label: "search-only", pattern: /\bsearch[-\s]?only\b/i },
193
- { label: "search page", pattern: /\bsearch\s+page\b/i },
194
- { label: "search keyword", pattern: /搜索关键词|关键词|keyword/i },
195
- { label: "recruit pipeline", pattern: /\brecruit\s+pipeline\b/i },
196
- { label: "chat/search", pattern: /(?:\/web\/chat\/search|chat\/search)/i },
197
- { label: "搜索页", pattern: /搜索页/ },
198
- { label: "招聘搜索", pattern: /招聘搜索/ }
199
- ]);
200
- if (searchSignals.length) {
201
- return {
202
- domain: "search",
203
- signals: searchSignals,
204
- text
205
- };
206
- }
207
- return null;
208
- }
209
-
210
- function buildWrongRecommendRouteResponse(route) {
211
- if (route?.domain === "chat") {
212
- return {
213
- status: "FAILED",
214
- route_guard: true,
215
- error: {
216
- code: "WRONG_BOSS_TOOL_FOR_CHAT",
217
- message: "This request is explicitly chat-only/chat-page work. In Trae-CN split toolsets, call boss-chat/boss_chat_health_check, then boss-chat/list_boss_chat_jobs or boss-chat/prepare_boss_chat_run, then boss-chat/start_boss_chat_run. Do not call boss-recommend tools.",
218
- retryable: false
219
- },
220
- detected_domain: "chat",
221
- detected_signals: route.signals || [],
222
- wrong_server: "boss-recommend",
223
- recommended_server: "boss-chat",
224
- recommended_tool_sequence: [
225
- "boss-chat/boss_chat_health_check",
226
- "boss-chat/list_boss_chat_jobs",
227
- "boss-chat/prepare_boss_chat_run",
228
- "boss-chat/start_boss_chat_run"
229
- ]
230
- };
231
- }
232
- if (route?.domain === "search") {
233
- return {
234
- status: "FAILED",
235
- route_guard: true,
236
- error: {
237
- code: "WRONG_BOSS_TOOL_FOR_SEARCH",
238
- message: "This request is explicitly search/recruit-page work. In Trae-CN split toolsets, call boss-recruit/run_recruit_pipeline or boss-recruit/start_recruit_pipeline_run. Do not call boss-recommend/run_recommend or boss-recommend/start_recommend_pipeline_run.",
239
- retryable: false
240
- },
241
- detected_domain: "search",
242
- detected_signals: route.signals || [],
243
- wrong_server: "boss-recommend",
244
- recommended_server: "boss-recruit",
245
- recommended_tool_sequence: [
246
- "boss-recruit/run_recruit_pipeline",
247
- "boss-recruit/start_recruit_pipeline_run"
248
- ]
249
- };
250
- }
251
- return null;
252
- }
253
-
254
- function guardRecommendRoute(args = {}) {
255
- return buildWrongRecommendRouteResponse(detectNonRecommendRoute(args));
256
- }
257
-
258
- function parsePositiveInteger(raw, fallback) {
259
- const parsed = Number.parseInt(String(raw || ""), 10);
260
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
80
+ function normalizeText(value) {
81
+ return String(value || "").replace(/\s+/g, " ").trim();
82
+ }
83
+
84
+ function collectRecommendRouteText(args = {}) {
85
+ return normalizeText([
86
+ args.instruction,
87
+ args.criteria,
88
+ args.target_url_includes,
89
+ args.page_scope,
90
+ args.confirmation?.page_value,
91
+ args.confirmation?.job_value,
92
+ args.overrides?.criteria,
93
+ args.overrides?.page_scope,
94
+ args.overrides?.job,
95
+ args.overrides?.target_url_includes
96
+ ].filter((value) => value !== undefined && value !== null).join(" "));
97
+ }
98
+
99
+ function findRouteSignals(text, patterns = []) {
100
+ const signals = [];
101
+ for (const { label, pattern } of patterns) {
102
+ if (pattern.test(text)) signals.push(label);
103
+ }
104
+ return signals;
105
+ }
106
+
107
+ function detectExplicitNonRecommendScope(args = {}) {
108
+ const fields = [
109
+ ["page_scope", args.page_scope],
110
+ ["confirmation.page_value", args.confirmation?.page_value],
111
+ ["overrides.page_scope", args.overrides?.page_scope],
112
+ ["target_url_includes", args.target_url_includes],
113
+ ["overrides.target_url_includes", args.overrides?.target_url_includes]
114
+ ];
115
+ for (const [field, value] of fields) {
116
+ const normalized = normalizeText(value).toLowerCase();
117
+ if (!normalized) continue;
118
+ if (/\/web\/chat\/index|chat\/index/.test(normalized) || ["chat", "chat-page", "boss-chat"].includes(normalized)) {
119
+ return {
120
+ domain: "chat",
121
+ signals: [`${field}:chat`],
122
+ text: normalized
123
+ };
124
+ }
125
+ if (/\/web\/chat\/search|chat\/search/.test(normalized) || ["search", "search-page", "recruit", "recruit-page", "boss-recruit"].includes(normalized)) {
126
+ return {
127
+ domain: "search",
128
+ signals: [`${field}:search`],
129
+ text: normalized
130
+ };
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function detectRecruitShapedRecommendArgs(args = {}) {
137
+ const checks = [
138
+ ["keyword", args.keyword],
139
+ ["search_keyword", args.search_keyword],
140
+ ["city", args.city],
141
+ ["confirmation.keyword_value", args.confirmation?.keyword_value],
142
+ ["confirmation.keyword_confirmed", args.confirmation?.keyword_confirmed],
143
+ ["confirmation.search_params_confirmed", args.confirmation?.search_params_confirmed],
144
+ ["overrides.keyword", args.overrides?.keyword],
145
+ ["overrides.search_keyword", args.overrides?.search_keyword],
146
+ ["overrides.city", args.overrides?.city],
147
+ ["overrides.filter_recent_viewed", args.overrides?.filter_recent_viewed],
148
+ ["overrides.schools", args.overrides?.schools],
149
+ ["overrides.school_tags", args.overrides?.school_tags]
150
+ ];
151
+ const signals = [];
152
+ for (const [field, value] of checks) {
153
+ if (value === undefined || value === null) continue;
154
+ if (Array.isArray(value) && value.length === 0) continue;
155
+ if (typeof value === "string" && !normalizeText(value)) continue;
156
+ signals.push(field);
157
+ }
158
+ if (!signals.length) return null;
159
+ return {
160
+ domain: "search",
161
+ signals: signals.map((field) => `${field}:recruit_arg`),
162
+ text: signals.join(" ")
163
+ };
164
+ }
165
+
166
+ function detectNonRecommendRoute(args = {}) {
167
+ const explicitRoute = detectExplicitNonRecommendScope(args);
168
+ if (explicitRoute) return explicitRoute;
169
+ const recruitArgRoute = detectRecruitShapedRecommendArgs(args);
170
+ if (recruitArgRoute) return recruitArgRoute;
171
+
172
+ const text = collectRecommendRouteText(args);
173
+ if (!text) return null;
174
+ const chatSignals = findRouteSignals(text, [
175
+ { label: "chat-only", pattern: /\bchat[-\s]?only\b/i },
176
+ { label: "boss-chat", pattern: /\bboss[-\s]?chat\b/i },
177
+ { label: "chat page", pattern: /\bchat\s+page\b/i },
178
+ { label: "chat/index", pattern: /(?:\/web\/chat\/index|chat\/index)/i },
179
+ { label: "聊天页", pattern: /聊天页/ },
180
+ { label: "聊天列表", pattern: /聊天列表/ },
181
+ { label: "未读", pattern: /未读/ },
182
+ { label: "全部聊天", pattern: /全部聊天|所有聊天/ },
183
+ { label: "求简历", pattern: /求简历|索要简历|要简历|在线简历/ }
184
+ ]);
185
+ if (chatSignals.length) {
186
+ return {
187
+ domain: "chat",
188
+ signals: chatSignals,
189
+ text
190
+ };
191
+ }
192
+ const searchSignals = findRouteSignals(text, [
193
+ { label: "search-only", pattern: /\bsearch[-\s]?only\b/i },
194
+ { label: "search page", pattern: /\bsearch\s+page\b/i },
195
+ { label: "search keyword", pattern: /搜索关键词|关键词|keyword/i },
196
+ { label: "recruit pipeline", pattern: /\brecruit\s+pipeline\b/i },
197
+ { label: "chat/search", pattern: /(?:\/web\/chat\/search|chat\/search)/i },
198
+ { label: "搜索页", pattern: /搜索页/ },
199
+ { label: "招聘搜索", pattern: /招聘搜索/ }
200
+ ]);
201
+ if (searchSignals.length) {
202
+ return {
203
+ domain: "search",
204
+ signals: searchSignals,
205
+ text
206
+ };
207
+ }
208
+ return null;
209
+ }
210
+
211
+ function buildWrongRecommendRouteResponse(route) {
212
+ if (route?.domain === "chat") {
213
+ return {
214
+ status: "FAILED",
215
+ route_guard: true,
216
+ error: {
217
+ code: "WRONG_BOSS_TOOL_FOR_CHAT",
218
+ message: "This request is explicitly chat-only/chat-page work. In Trae-CN split toolsets, call boss-chat/boss_chat_health_check, then boss-chat/list_boss_chat_jobs or boss-chat/prepare_boss_chat_run, then boss-chat/start_boss_chat_run. Do not call boss-recommend tools.",
219
+ retryable: false
220
+ },
221
+ detected_domain: "chat",
222
+ detected_signals: route.signals || [],
223
+ wrong_server: "boss-recommend",
224
+ recommended_server: "boss-chat",
225
+ recommended_tool_sequence: [
226
+ "boss-chat/boss_chat_health_check",
227
+ "boss-chat/list_boss_chat_jobs",
228
+ "boss-chat/prepare_boss_chat_run",
229
+ "boss-chat/start_boss_chat_run"
230
+ ]
231
+ };
232
+ }
233
+ if (route?.domain === "search") {
234
+ return {
235
+ status: "FAILED",
236
+ route_guard: true,
237
+ error: {
238
+ code: "WRONG_BOSS_TOOL_FOR_SEARCH",
239
+ message: "This request is explicitly search/recruit-page work. In Trae-CN split toolsets, call boss-recruit/run_recruit_pipeline or boss-recruit/start_recruit_pipeline_run. Do not call boss-recommend/run_recommend or boss-recommend/start_recommend_pipeline_run.",
240
+ retryable: false
241
+ },
242
+ detected_domain: "search",
243
+ detected_signals: route.signals || [],
244
+ wrong_server: "boss-recommend",
245
+ recommended_server: "boss-recruit",
246
+ recommended_tool_sequence: [
247
+ "boss-recruit/run_recruit_pipeline",
248
+ "boss-recruit/start_recruit_pipeline_run"
249
+ ]
250
+ };
251
+ }
252
+ return null;
253
+ }
254
+
255
+ function guardRecommendRoute(args = {}) {
256
+ return buildWrongRecommendRouteResponse(detectNonRecommendRoute(args));
257
+ }
258
+
259
+ function parsePositiveInteger(raw, fallback) {
260
+ const parsed = Number.parseInt(String(raw || ""), 10);
261
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
261
262
  }
262
263
 
263
264
  function parseNonNegativeInteger(raw, fallback) {
@@ -277,13 +278,22 @@ function normalizeScreeningModeArg(args = {}) {
277
278
  : "llm";
278
279
  }
279
280
 
280
- function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
281
- const reasons = [];
281
+ function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
282
+ const reasons = [];
282
283
  if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
283
284
  if (args.allow_card_only_screening === true) reasons.push("allow_card_only_screening");
284
285
  if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
285
286
  if (args.no_filter === true) reasons.push("no_filter");
286
- if (args.filter_enabled === false) reasons.push("filter_enabled=false");
287
+ if (args.filter_enabled === false) reasons.push("filter_enabled=false");
288
+ if (args.debug_force_list_end_after_processed != null) {
289
+ reasons.push("debug_force_list_end_after_processed");
290
+ }
291
+ if (args.debug_force_context_recovery_after_processed != null) {
292
+ reasons.push("debug_force_context_recovery_after_processed");
293
+ }
294
+ if (args.debug_force_cdp_reconnect_after_processed != null) {
295
+ reasons.push("debug_force_cdp_reconnect_after_processed");
296
+ }
287
297
  if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
288
298
  if (args.execute_post_action === false && normalized.postAction && normalized.postAction !== "none") {
289
299
  reasons.push("execute_post_action=false");
@@ -304,22 +314,40 @@ function resolveRecommendDetailLimit(args = {}, normalized = {}) {
304
314
  }
305
315
 
306
316
  function methodSummary(methodLog = []) {
307
- const summary = {};
308
- for (const entry of methodLog || []) {
309
- summary[entry.method] = (summary[entry.method] || 0) + 1;
310
- }
317
+ const summary = {};
318
+ for (const entry of methodLog || []) {
319
+ const method = compactCdpMethodName(entry?.method || entry || "");
320
+ if (!method) continue;
321
+ summary[method] = (summary[method] || 0) + 1;
322
+ }
311
323
  return summary;
312
324
  }
313
325
 
326
+ function compactCdpMethodName(value) {
327
+ const method = normalizeText(value);
328
+ return /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*(?::retry_after_reconnect)?$/.test(method)
329
+ ? method
330
+ : "";
331
+ }
332
+
333
+ function compactCdpMethodTimestamp(value) {
334
+ const timestamp = normalizeText(value);
335
+ const parsed = Date.parse(timestamp);
336
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : "";
337
+ }
338
+
314
339
  function compactMethodLogForStatus(methodLog = []) {
315
340
  if (!Array.isArray(methodLog)) return [];
316
- return methodLog.slice(-STATUS_METHOD_LOG_TAIL_LIMIT).map((entry) => ({
317
- method: normalizeText(entry?.method || entry || ""),
318
- at: normalizeText(entry?.at || "")
319
- }));
341
+ return methodLog
342
+ .map((entry) => ({
343
+ method: compactCdpMethodName(entry?.method || entry || ""),
344
+ at: compactCdpMethodTimestamp(entry?.at || "")
345
+ }))
346
+ .filter((entry) => Boolean(entry.method))
347
+ .slice(-STATUS_METHOD_LOG_TAIL_LIMIT);
320
348
  }
321
-
322
- function clonePlain(value, fallback = null) {
349
+
350
+ function clonePlain(value, fallback = null) {
323
351
  try {
324
352
  return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
325
353
  } catch {
@@ -331,10 +359,130 @@ function plainRecord(value) {
331
359
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
332
360
  }
333
361
 
334
- function nonEmptyRecord(value) {
335
- const record = plainRecord(value);
336
- return Object.keys(record).length ? record : null;
337
- }
362
+ function nonEmptyRecord(value) {
363
+ const record = plainRecord(value);
364
+ return Object.keys(record).length ? record : null;
365
+ }
366
+
367
+ function compactMethodSummaryForStatus(value = {}) {
368
+ const compact = {};
369
+ for (const [rawMethod, rawCount] of Object.entries(plainRecord(value))) {
370
+ const method = compactCdpMethodName(rawMethod);
371
+ const count = Number(rawCount);
372
+ if (!method || !Number.isInteger(count) || count < 1) continue;
373
+ compact[method] = count;
374
+ }
375
+ return compact;
376
+ }
377
+
378
+ function compactChromeEvidenceUrl(value) {
379
+ const text = normalizeText(value);
380
+ if (!text) return null;
381
+ try {
382
+ const parsed = new URL(text);
383
+ parsed.username = "";
384
+ parsed.password = "";
385
+ parsed.search = "";
386
+ parsed.hash = "";
387
+ return parsed.toString();
388
+ } catch {
389
+ return text.split(/[?#]/, 1)[0] || null;
390
+ }
391
+ }
392
+
393
+ function compactChromeAutoLaunchEvidence(value = {}) {
394
+ const source = plainRecord(value);
395
+ if (!Object.keys(source).length) return null;
396
+ const compact = {};
397
+ const booleanKeys = [
398
+ "launched",
399
+ "reused",
400
+ "guard_checked",
401
+ "required_flags_ok",
402
+ "replaced",
403
+ "target_created",
404
+ "fallback_target",
405
+ "fallback_any_page"
406
+ ];
407
+ for (const key of booleanKeys) {
408
+ if (typeof source[key] === "boolean") compact[key] = source[key];
409
+ }
410
+ for (const key of ["port", "target_count", "command_line_args_count"]) {
411
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
412
+ }
413
+ for (const key of ["close_method", "command_line_source"]) {
414
+ const text = normalizeText(source[key]);
415
+ if (text) compact[key] = text;
416
+ }
417
+ return Object.keys(compact).length ? compact : null;
418
+ }
419
+
420
+ function compactChromeEvidence(value = {}) {
421
+ const source = plainRecord(value);
422
+ if (!Object.keys(source).length) return null;
423
+ const compact = {};
424
+ const host = normalizeText(source.host);
425
+ const targetId = normalizeText(source.target_id);
426
+ const targetUrl = compactChromeEvidenceUrl(source.target_url);
427
+ if (host) compact.host = host;
428
+ if (Number.isInteger(source.port) && source.port > 0) compact.port = source.port;
429
+ if (targetUrl) compact.target_url = targetUrl;
430
+ if (targetId) compact.target_id = targetId;
431
+ const autoLaunch = compactChromeAutoLaunchEvidence(source.auto_launch);
432
+ if (autoLaunch) compact.auto_launch = autoLaunch;
433
+ return Object.keys(compact).length ? compact : null;
434
+ }
435
+
436
+ function methodEvidenceFlags(methodSummaryValue = {}) {
437
+ const methods = Object.keys(methodSummaryValue);
438
+ return {
439
+ runtime_evaluate_used: methods.some((method) => method.split(".", 1)[0] === "Runtime"),
440
+ script_injection_used: methods.some((method) => (
441
+ FORBIDDEN_CDP_METHODS.has(method.replace(/:retry_after_reconnect$/, ""))
442
+ ))
443
+ };
444
+ }
445
+
446
+ function buildPersistedCdpEvidence({ meta = {}, persisted = {} } = {}) {
447
+ const hasLiveMethodLog = Array.isArray(meta.methodLog);
448
+ const persistedMethodLog = compactMethodLogForStatus(persisted.method_log);
449
+ const methodLog = hasLiveMethodLog
450
+ ? compactMethodLogForStatus(meta.methodLog)
451
+ : persistedMethodLog;
452
+ const persistedSummary = compactMethodSummaryForStatus(persisted.method_summary);
453
+ const summary = hasLiveMethodLog
454
+ ? methodSummary(meta.methodLog)
455
+ : Object.keys(persistedSummary).length
456
+ ? persistedSummary
457
+ : methodSummary(methodLog);
458
+ const liveTotal = hasLiveMethodLog ? meta.methodLog.length : null;
459
+ const persistedTotal = Number(persisted.method_log_total);
460
+ const methodLogTotal = Number.isInteger(liveTotal)
461
+ ? liveTotal
462
+ : Number.isInteger(persistedTotal) && persistedTotal >= methodLog.length
463
+ ? persistedTotal
464
+ : methodLog.length;
465
+ const inferredFlags = methodEvidenceFlags(summary);
466
+ return {
467
+ runtime_evaluate_used: hasLiveMethodLog
468
+ ? inferredFlags.runtime_evaluate_used
469
+ : persisted.runtime_evaluate_used === true || inferredFlags.runtime_evaluate_used,
470
+ script_injection_used: hasLiveMethodLog
471
+ ? inferredFlags.script_injection_used
472
+ : persisted.script_injection_used === true || inferredFlags.script_injection_used,
473
+ method_summary: summary,
474
+ method_log: methodLog,
475
+ method_log_total: methodLogTotal,
476
+ chrome: compactChromeEvidence(meta.chrome || persisted.chrome)
477
+ };
478
+ }
479
+
480
+ function attachPersistedCdpEvidence(payload, persisted = {}) {
481
+ return {
482
+ ...payload,
483
+ ...buildPersistedCdpEvidence({ persisted })
484
+ };
485
+ }
338
486
 
339
487
  function normalizeRunId(runId) {
340
488
  const normalized = normalizeText(runId);
@@ -382,10 +530,12 @@ function readJsonFile(filePath) {
382
530
 
383
531
  function recommendSearchParamsForCsv(searchParams = {}) {
384
532
  return {
385
- school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
386
- degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
387
- gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
388
- recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限"
533
+ school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
534
+ degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
535
+ gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
536
+ recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限",
537
+ current_city_only: Object.prototype.hasOwnProperty.call(searchParams, "current_city_only") ? searchParams.current_city_only : false,
538
+ activity_level: Object.prototype.hasOwnProperty.call(searchParams, "activity_level") ? searchParams.activity_level : "不限"
389
539
  };
390
540
  }
391
541
 
@@ -424,10 +574,12 @@ function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
424
574
  function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
425
575
  const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
426
576
  const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
427
- school_tag: overrides.school_tag ?? confirmation.school_tag_value,
428
- degree: overrides.degree ?? confirmation.degree_value,
429
- gender: overrides.gender ?? confirmation.gender_value,
430
- recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value
577
+ school_tag: overrides.school_tag ?? confirmation.school_tag_value,
578
+ degree: overrides.degree ?? confirmation.degree_value,
579
+ gender: overrides.gender ?? confirmation.gender_value,
580
+ recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value,
581
+ current_city_only: overrides.current_city_only ?? false,
582
+ activity_level: overrides.activity_level ?? "不限"
431
583
  });
432
584
  const parsedScreenParams = meta.parsed?.screenParams || {};
433
585
  const screenParams = {
@@ -486,7 +638,7 @@ function secondsBetween(startedAt, endedAt) {
486
638
  return Math.max(1, Math.round((endMs - startMs) / 1000));
487
639
  }
488
640
 
489
- function normalizeLegacyProgress(progress = {}, summary = null) {
641
+ function normalizeLegacyProgress(progress = {}, summary = null) {
490
642
  const processed = Number.isInteger(progress.processed)
491
643
  ? progress.processed
492
644
  : Number.isInteger(summary?.processed)
@@ -502,155 +654,254 @@ function normalizeLegacyProgress(progress = {}, summary = null) {
502
654
  : Number.isInteger(summary?.passed)
503
655
  ? summary.passed
504
656
  : 0;
505
- return {
506
- ...progress,
657
+ const normalized = {
658
+ ...progress,
507
659
  processed,
508
660
  inspected: processed,
509
661
  screened,
510
662
  passed,
511
663
  skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
512
- greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
513
- post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
664
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
665
+ post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
514
666
  };
667
+ if (Object.prototype.hasOwnProperty.call(normalized, "last_list_read_stale_diagnostic")) {
668
+ normalized.last_list_read_stale_diagnostic = compactListReadStaleDiagnosticForStatus(
669
+ normalized.last_list_read_stale_diagnostic
670
+ );
671
+ }
672
+ if (Object.prototype.hasOwnProperty.call(normalized, "list_read_stale_diagnostics")) {
673
+ normalized.list_read_stale_diagnostics = compactListReadStaleDiagnosticsForStatus(
674
+ normalized.list_read_stale_diagnostics
675
+ );
676
+ }
677
+ return normalized;
515
678
  }
516
-
517
- const STATUS_COUNT_KEYS = [
518
- "processed",
519
- "screened",
520
- "detail_opened",
521
- "passed",
522
- "skipped",
523
- "llm_screened",
524
- "greet_count",
525
- "post_action_clicked",
526
- "image_capture_failed",
527
- "detail_open_failed",
528
- "transient_recovered",
529
- "colleague_contact_checked",
530
- "recent_colleague_contact_skipped",
531
- "colleague_contact_panel_missing",
532
- "context_recoveries",
533
- "human_rest_count",
534
- "human_rest_ms",
535
- "card_count",
536
- "refresh_rounds"
537
- ];
538
-
539
- function compactPositiveInteger(value, fallback = null) {
540
- return Number.isInteger(value) && value >= 0 ? value : fallback;
541
- }
542
-
679
+
680
+ const STATUS_COUNT_KEYS = [
681
+ "processed",
682
+ "screened",
683
+ "detail_opened",
684
+ "passed",
685
+ "skipped",
686
+ "llm_screened",
687
+ "greet_count",
688
+ "post_action_clicked",
689
+ "image_capture_failed",
690
+ "detail_open_failed",
691
+ "transient_recovered",
692
+ "colleague_contact_checked",
693
+ "recent_colleague_contact_skipped",
694
+ "colleague_contact_panel_missing",
695
+ "context_recoveries",
696
+ "human_rest_count",
697
+ "human_rest_ms",
698
+ "card_count",
699
+ "refresh_rounds"
700
+ ];
701
+
702
+ function compactPositiveInteger(value, fallback = null) {
703
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
704
+ }
705
+
543
706
  function compactSmallRecord(value, fallback = null) {
544
- const record = plainRecord(value);
545
- if (!Object.keys(record).length) return fallback;
707
+ const record = plainRecord(value);
708
+ if (!Object.keys(record).length) return fallback;
546
709
  return clonePlain(record, fallback);
547
710
  }
548
711
 
549
- function compactRecommendSummaryForStatus(summary) {
550
- if (!summary || typeof summary !== "object" || Array.isArray(summary)) return null;
551
- const compact = {
552
- domain: normalizeText(summary.domain || "recommend") || "recommend"
553
- };
554
- for (const key of STATUS_COUNT_KEYS) {
555
- if (Number.isInteger(summary[key])) compact[key] = summary[key];
556
- }
557
- if (summary.target_url) compact.target_url = normalizeText(summary.target_url);
558
- if (summary.list_end_reason) compact.list_end_reason = normalizeText(summary.list_end_reason);
559
- if (Array.isArray(summary.results)) {
560
- compact.results_count = summary.results.length;
561
- } else if (Number.isInteger(summary.results_count)) {
562
- compact.results_count = summary.results_count;
563
- }
564
- if (Array.isArray(summary.refresh_attempts)) {
565
- compact.refresh_attempt_count = summary.refresh_attempts.length;
566
- }
567
- if (Array.isArray(summary.context_recoveries)) {
568
- compact.context_recovery_count = summary.context_recoveries.length;
569
- } else if (Number.isInteger(summary.context_recoveries)) {
570
- compact.context_recoveries = summary.context_recoveries;
712
+ function compactListReadStaleDiagnosticForStatus(value) {
713
+ const source = plainRecord(value);
714
+ if (!Object.keys(source).length) return null;
715
+ const compact = {};
716
+ for (const key of ["code", "message", "phase", "cdp_method", "cdp_at", "recovery_mode"]) {
717
+ const text = normalizeText(source[key] || "");
718
+ if (text) compact[key] = text.slice(0, key === "message" ? 500 : 200);
571
719
  }
572
- if (summary.job_selection) compact.job_selection = compactSmallRecord(summary.job_selection);
573
- if (summary.page_scope) compact.page_scope = compactSmallRecord(summary.page_scope);
574
- if (summary.filter) compact.filter = compactSmallRecord(summary.filter);
575
- if (summary.candidate_list) {
576
- const candidateList = plainRecord(summary.candidate_list);
577
- compact.candidate_list = {
578
- total_count: compactPositiveInteger(candidateList.total_count ?? candidateList.card_count, null),
579
- visible_count: compactPositiveInteger(candidateList.visible_count, null),
580
- list_end_reason: normalizeText(candidateList.list_end_reason || "")
581
- };
720
+ for (const key of ["cdp_node_id", "cdp_backend_node_id", "attempt"]) {
721
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
582
722
  }
583
- if (summary.viewport_health?.stats) {
584
- compact.viewport_health = {
585
- stats: clonePlain(summary.viewport_health.stats, {})
586
- };
723
+ for (const key of ["exhausted", "recovery_applied", "recovered"]) {
724
+ if (typeof source[key] === "boolean") compact[key] = source[key];
587
725
  }
588
- if (summary.human_behavior) {
589
- compact.human_behavior = {
590
- enabled: summary.human_behavior.enabled === true,
591
- profile: normalizeText(summary.human_behavior.profile || ""),
592
- restLevel: normalizeText(summary.human_behavior.restLevel || summary.human_behavior.rest_level || "")
593
- };
726
+ for (const key of ["at", "recovery_applied_at", "recovered_at"]) {
727
+ const text = normalizeText(source[key] || "");
728
+ if (text && Number.isFinite(Date.parse(text))) compact[key] = new Date(text).toISOString();
594
729
  }
595
- if (summary.human_rest) {
596
- const humanRest = plainRecord(summary.human_rest);
597
- compact.human_rest = {
598
- enabled: humanRest.enabled === true,
599
- restLevel: normalizeText(humanRest.restLevel || humanRest.rest_level || ""),
600
- rest_count: compactPositiveInteger(humanRest.rest_count ?? humanRest.count, null),
601
- total_pause_ms: compactPositiveInteger(humanRest.total_pause_ms ?? humanRest.pause_ms, null)
602
- };
730
+ if (Array.isArray(source.cdp_param_keys)) {
731
+ compact.cdp_param_keys = source.cdp_param_keys
732
+ .map((key) => normalizeText(key))
733
+ .filter((key) => /^[A-Za-z][A-Za-z0-9_]*$/.test(key))
734
+ .slice(0, 20);
603
735
  }
604
- return compact;
736
+ return Object.keys(compact).length ? compact : null;
605
737
  }
606
738
 
607
- function compactRecommendCheckpointForStatus(checkpoint) {
608
- if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return {};
739
+ function compactListReadStaleDiagnosticsForStatus(value) {
740
+ if (!Array.isArray(value)) return [];
741
+ return value
742
+ .map((item) => compactListReadStaleDiagnosticForStatus(item))
743
+ .filter(Boolean)
744
+ .slice(-12);
745
+ }
746
+
747
+ function compactListReadStaleCheckpointEvent(value) {
748
+ const source = plainRecord(value);
749
+ if (!Object.keys(source).length) return null;
609
750
  const compact = {};
610
- if (checkpoint.updatedAt || checkpoint.updated_at) {
611
- compact.updatedAt = normalizeText(checkpoint.updatedAt || checkpoint.updated_at);
751
+ const diagnostic = compactListReadStaleDiagnosticForStatus(source.diagnostic || source.trigger);
752
+ if (diagnostic) compact.diagnostic = diagnostic;
753
+ const recentDiagnostics = compactListReadStaleDiagnosticsForStatus(source.recent_diagnostics);
754
+ if (recentDiagnostics.length) compact.recent_diagnostics = recentDiagnostics;
755
+ for (const key of ["recovery_count", "recovery_applied_count"]) {
756
+ if (Number.isInteger(source[key]) && source[key] >= 0) compact[key] = source[key];
612
757
  }
613
- if (Array.isArray(checkpoint.results)) {
614
- compact.results_count = checkpoint.results.length;
615
- } else if (Number.isInteger(checkpoint.results_count)) {
616
- compact.results_count = checkpoint.results_count;
758
+ if (source.recovery_error) {
759
+ const recoveryError = compactListReadStaleDiagnosticForStatus(source.recovery_error);
760
+ if (recoveryError) compact.recovery_error = recoveryError;
617
761
  }
618
- for (const key of STATUS_COUNT_KEYS) {
619
- if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
762
+ if (source.candidate_list) {
763
+ const candidateList = plainRecord(source.candidate_list);
764
+ compact.candidate_list = {};
765
+ for (const key of ["seen_count", "queued_count", "processed_count", "read_error_count", "scroll_count"]) {
766
+ if (Number.isInteger(candidateList[key]) && candidateList[key] >= 0) {
767
+ compact.candidate_list[key] = candidateList[key];
768
+ }
769
+ }
620
770
  }
621
- return compact;
771
+ return Object.keys(compact).length ? compact : null;
622
772
  }
623
-
624
- function compactRecommendResultForStatus(result) {
625
- if (!result || typeof result !== "object" || Array.isArray(result)) return result || null;
626
- const compact = {
627
- ...result
628
- };
629
- if (Array.isArray(result.results)) {
630
- compact.results_count = result.results.length;
631
- compact.results_available = result.results.length > 0;
632
- } else if (Number.isInteger(result.results_count)) {
633
- compact.results_count = result.results_count;
634
- compact.results_available = result.results_available === true || result.results_count > 0;
773
+
774
+ function compactRecommendSummaryForStatus(summary) {
775
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) return null;
776
+ const compact = {
777
+ domain: normalizeText(summary.domain || "recommend") || "recommend"
778
+ };
779
+ for (const key of STATUS_COUNT_KEYS) {
780
+ if (Number.isInteger(summary[key])) compact[key] = summary[key];
781
+ }
782
+ if (summary.target_url) compact.target_url = normalizeText(summary.target_url);
783
+ if (summary.list_end_reason) compact.list_end_reason = normalizeText(summary.list_end_reason);
784
+ if (Array.isArray(summary.results)) {
785
+ compact.results_count = summary.results.length;
786
+ } else if (Number.isInteger(summary.results_count)) {
787
+ compact.results_count = summary.results_count;
788
+ }
789
+ if (Array.isArray(summary.refresh_attempts)) {
790
+ compact.refresh_attempt_count = summary.refresh_attempts.length;
791
+ }
792
+ if (Array.isArray(summary.context_recoveries)) {
793
+ compact.context_recovery_count = summary.context_recoveries.length;
794
+ } else if (Number.isInteger(summary.context_recoveries)) {
795
+ compact.context_recoveries = summary.context_recoveries;
796
+ }
797
+ if (summary.job_selection) compact.job_selection = compactSmallRecord(summary.job_selection);
798
+ if (summary.page_scope) compact.page_scope = compactSmallRecord(summary.page_scope);
799
+ if (summary.filter) compact.filter = compactSmallRecord(summary.filter);
800
+ if (summary.candidate_list) {
801
+ const candidateList = plainRecord(summary.candidate_list);
802
+ compact.candidate_list = {
803
+ total_count: compactPositiveInteger(candidateList.total_count ?? candidateList.card_count, null),
804
+ visible_count: compactPositiveInteger(candidateList.visible_count, null),
805
+ list_end_reason: normalizeText(candidateList.list_end_reason || "")
806
+ };
807
+ }
808
+ if (summary.viewport_health?.stats) {
809
+ compact.viewport_health = {
810
+ stats: clonePlain(summary.viewport_health.stats, {})
811
+ };
812
+ }
813
+ if (summary.human_behavior) {
814
+ compact.human_behavior = {
815
+ enabled: summary.human_behavior.enabled === true,
816
+ profile: normalizeText(summary.human_behavior.profile || ""),
817
+ restLevel: normalizeText(summary.human_behavior.restLevel || summary.human_behavior.rest_level || "")
818
+ };
819
+ }
820
+ if (summary.human_rest) {
821
+ const humanRest = plainRecord(summary.human_rest);
822
+ compact.human_rest = {
823
+ enabled: humanRest.enabled === true,
824
+ restLevel: normalizeText(humanRest.restLevel || humanRest.rest_level || ""),
825
+ rest_count: compactPositiveInteger(humanRest.rest_count ?? humanRest.count, null),
826
+ total_pause_ms: compactPositiveInteger(humanRest.total_pause_ms ?? humanRest.pause_ms, null)
827
+ };
828
+ }
829
+ for (const key of [
830
+ "list_read_stale_recovery_attempts",
831
+ "list_read_stale_recovery_applied",
832
+ "list_read_stale_recoveries"
833
+ ]) {
834
+ if (Number.isInteger(summary[key]) && summary[key] >= 0) compact[key] = summary[key];
635
835
  }
636
- delete compact.results;
836
+ if (summary.last_list_read_recovery_mode) {
837
+ compact.last_list_read_recovery_mode = normalizeText(summary.last_list_read_recovery_mode).slice(0, 100);
838
+ }
839
+ const lastStaleDiagnostic = compactListReadStaleDiagnosticForStatus(
840
+ summary.last_list_read_stale_diagnostic
841
+ );
842
+ if (lastStaleDiagnostic) compact.last_list_read_stale_diagnostic = lastStaleDiagnostic;
843
+ const staleDiagnostics = compactListReadStaleDiagnosticsForStatus(summary.list_read_stale_diagnostics);
844
+ if (staleDiagnostics.length) compact.list_read_stale_diagnostics = staleDiagnostics;
637
845
  return compact;
638
846
  }
639
-
640
- export function compactRecommendRunForStatus(run) {
641
- if (!run || typeof run !== "object" || Array.isArray(run)) return run || null;
642
- const compact = {
643
- ...run
644
- };
645
- if (compact.result) compact.result = compactRecommendResultForStatus(compact.result);
646
- if (compact.summary) compact.summary = compactRecommendSummaryForStatus(compact.summary);
647
- if (compact.checkpoint) compact.checkpoint = compactRecommendCheckpointForStatus(compact.checkpoint);
847
+
848
+ function compactRecommendCheckpointForStatus(checkpoint) {
849
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return {};
850
+ const compact = {};
851
+ if (checkpoint.updatedAt || checkpoint.updated_at) {
852
+ compact.updatedAt = normalizeText(checkpoint.updatedAt || checkpoint.updated_at);
853
+ }
854
+ if (Array.isArray(checkpoint.results)) {
855
+ compact.results_count = checkpoint.results.length;
856
+ } else if (Number.isInteger(checkpoint.results_count)) {
857
+ compact.results_count = checkpoint.results_count;
858
+ }
859
+ for (const key of STATUS_COUNT_KEYS) {
860
+ if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
861
+ }
862
+ for (const key of [
863
+ "list_read_stale_recovery",
864
+ "list_read_stale_recovery_applied",
865
+ "list_read_stale_recovered",
866
+ "list_read_stale_recovery_exhausted",
867
+ "list_read_stale_recovery_failed"
868
+ ]) {
869
+ const event = compactListReadStaleCheckpointEvent(checkpoint[key]);
870
+ if (event) compact[key] = event;
871
+ }
648
872
  return compact;
649
873
  }
650
-
651
- function completionReason(status) {
652
- if (status === RUN_STATUS_COMPLETED) return "completed";
653
- if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
874
+
875
+ function compactRecommendResultForStatus(result) {
876
+ if (!result || typeof result !== "object" || Array.isArray(result)) return result || null;
877
+ const compact = {
878
+ ...result
879
+ };
880
+ if (Array.isArray(result.results)) {
881
+ compact.results_count = result.results.length;
882
+ compact.results_available = result.results.length > 0;
883
+ } else if (Number.isInteger(result.results_count)) {
884
+ compact.results_count = result.results_count;
885
+ compact.results_available = result.results_available === true || result.results_count > 0;
886
+ }
887
+ delete compact.results;
888
+ return compact;
889
+ }
890
+
891
+ export function compactRecommendRunForStatus(run) {
892
+ if (!run || typeof run !== "object" || Array.isArray(run)) return run || null;
893
+ const compact = {
894
+ ...run
895
+ };
896
+ if (compact.result) compact.result = compactRecommendResultForStatus(compact.result);
897
+ if (compact.summary) compact.summary = compactRecommendSummaryForStatus(compact.summary);
898
+ if (compact.checkpoint) compact.checkpoint = compactRecommendCheckpointForStatus(compact.checkpoint);
899
+ return compact;
900
+ }
901
+
902
+ function completionReason(status) {
903
+ if (status === RUN_STATUS_COMPLETED) return "completed";
904
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
654
905
  if (status === RUN_STATUS_FAILED) return "failed";
655
906
  if (status === RUN_STATUS_PAUSED) return "paused";
656
907
  return null;
@@ -663,9 +914,9 @@ function normalizeErrorText(error = {}) {
663
914
  ].join(" "));
664
915
  }
665
916
 
666
- function classifyRecommendRecovery(error = {}) {
667
- const text = normalizeErrorText(error);
668
- if (!text) return null;
917
+ function classifyRecommendRecovery(error = {}) {
918
+ const text = normalizeErrorText(error);
919
+ if (!text) return null;
669
920
  if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
670
921
  if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
671
922
  return "transient_stale_dom";
@@ -676,13 +927,13 @@ function classifyRecommendRecovery(error = {}) {
676
927
  if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
677
928
  return "transient_network_or_llm";
678
929
  }
679
- return null;
680
- }
681
-
682
- function isCancelShutdownError(error = {}) {
683
- const text = normalizeErrorText(error);
684
- return /socket hang up|ECONNREFUSED|ECONNRESET|WebSocket is not open|Target closed|Session closed|Connection closed|RUN_PROCESS_EXITED|DETACHED_WORKER|RUN_WORKER/i.test(text);
685
- }
930
+ return null;
931
+ }
932
+
933
+ function isCancelShutdownError(error = {}) {
934
+ const text = normalizeErrorText(error);
935
+ return /socket hang up|ECONNREFUSED|ECONNRESET|WebSocket is not open|Target closed|Session closed|Connection closed|RUN_PROCESS_EXITED|DETACHED_WORKER|RUN_WORKER/i.test(text);
936
+ }
686
937
 
687
938
  function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
688
939
  const error = snapshot?.error || snapshot?.result?.error || null;
@@ -828,14 +1079,14 @@ function buildLegacyRecommendResult(snapshot) {
828
1079
  effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
829
1080
  },
830
1081
  search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
831
- screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
832
- target_count_semantics: TARGET_COUNT_SEMANTICS,
833
- error: snapshot.error || null,
834
- recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
835
- results_count: resultRows.length,
836
- results_available: resultRows.length > 0
837
- };
838
- }
1082
+ screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
1083
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
1084
+ error: snapshot.error || null,
1085
+ recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
1086
+ results_count: resultRows.length,
1087
+ results_available: resultRows.length > 0
1088
+ };
1089
+ }
839
1090
 
840
1091
  function normalizeRunSnapshot(snapshot) {
841
1092
  if (!snapshot) return null;
@@ -858,12 +1109,12 @@ function normalizeRunSnapshot(snapshot) {
858
1109
  follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
859
1110
  target_count_semantics: TARGET_COUNT_SEMANTICS
860
1111
  };
861
- return {
862
- ...snapshot,
863
- checkpoint: compactRecommendCheckpointForStatus(snapshot.checkpoint),
864
- summary: compactRecommendSummaryForStatus(summary),
865
- progress,
866
- run_id: snapshot.runId,
1112
+ return {
1113
+ ...snapshot,
1114
+ checkpoint: compactRecommendCheckpointForStatus(snapshot.checkpoint),
1115
+ summary: compactRecommendSummaryForStatus(summary),
1116
+ progress,
1117
+ run_id: snapshot.runId,
867
1118
  mode: RUN_MODE_ASYNC,
868
1119
  state: snapshot.status,
869
1120
  stage: snapshot.phase,
@@ -900,31 +1151,31 @@ function normalizeRunSnapshot(snapshot) {
900
1151
  };
901
1152
  }
902
1153
 
903
- function mergePersistedControlRequest(normalized, existing) {
904
- const control = {
905
- ...(normalized?.control || {})
906
- };
907
- const existingControl = plainRecord(existing?.control);
908
- if (!normalized) return control;
909
- if (TERMINAL_STATUSES.has(normalized.state)) {
910
- if (
911
- normalized.state === RUN_STATUS_FAILED
912
- && existingControl.cancel_requested === true
913
- && isCancelShutdownError(normalized.error || normalized.result?.error || "")
914
- ) {
915
- return {
916
- ...control,
917
- pause_requested: true,
918
- pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
919
- pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
920
- cancel_requested: true
921
- };
922
- }
923
- return control;
924
- }
925
- if (existingControl.cancel_requested === true) {
926
- return {
927
- ...control,
1154
+ function mergePersistedControlRequest(normalized, existing) {
1155
+ const control = {
1156
+ ...(normalized?.control || {})
1157
+ };
1158
+ const existingControl = plainRecord(existing?.control);
1159
+ if (!normalized) return control;
1160
+ if (TERMINAL_STATUSES.has(normalized.state)) {
1161
+ if (
1162
+ normalized.state === RUN_STATUS_FAILED
1163
+ && existingControl.cancel_requested === true
1164
+ && isCancelShutdownError(normalized.error || normalized.result?.error || "")
1165
+ ) {
1166
+ return {
1167
+ ...control,
1168
+ pause_requested: true,
1169
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
1170
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
1171
+ cancel_requested: true
1172
+ };
1173
+ }
1174
+ return control;
1175
+ }
1176
+ if (existingControl.cancel_requested === true) {
1177
+ return {
1178
+ ...control,
928
1179
  pause_requested: true,
929
1180
  pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
930
1181
  pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
@@ -947,82 +1198,86 @@ function mergePersistedControlRequest(normalized, existing) {
947
1198
  pause_requested_by: null,
948
1199
  cancel_requested: false
949
1200
  };
950
- }
951
- return control;
952
- }
953
-
954
- function cancelErrorFromShutdown(shutdownError = null) {
955
- return {
956
- code: "PIPELINE_CANCELED",
957
- message: "流水线已取消。",
958
- retryable: true,
959
- shutdown_error: shutdownError || undefined
960
- };
961
- }
962
-
963
- function coerceCanceledTerminalSnapshot(normalized, existing) {
964
- const existingControl = plainRecord(existing?.control);
965
- const shutdownError = normalized?.error || normalized?.result?.error || null;
966
- const shouldWrapCanceledShutdown = (
967
- normalized
968
- && (
969
- (
970
- normalized.state === RUN_STATUS_FAILED
971
- && existingControl.cancel_requested === true
972
- )
973
- || normalized.state === RUN_STATUS_CANCELED
974
- )
975
- && isCancelShutdownError(shutdownError || "")
976
- );
977
- if (
978
- !shouldWrapCanceledShutdown
979
- ) {
980
- return normalized;
981
- }
982
- const canceledError = cancelErrorFromShutdown(shutdownError);
983
- return {
984
- ...normalized,
985
- state: RUN_STATUS_CANCELED,
986
- status: RUN_STATUS_CANCELED,
987
- last_message: "流水线已取消;取消收尾时浏览器连接已关闭。",
988
- control: {
989
- pause_requested: false,
990
- pause_requested_at: null,
991
- pause_requested_by: null,
992
- cancel_requested: false
993
- },
994
- error: canceledError,
995
- result: normalized.result ? {
996
- ...normalized.result,
997
- status: "CANCELED",
998
- completion_reason: "canceled_by_user",
999
- error: canceledError
1000
- } : {
1001
- status: "CANCELED",
1002
- completion_reason: "canceled_by_user",
1003
- error: canceledError,
1004
- run_id: normalized.run_id,
1005
- processed_count: normalized.progress?.processed || 0,
1006
- screened_count: normalized.progress?.screened || normalized.progress?.processed || 0,
1007
- passed_count: normalized.progress?.passed || 0
1008
- }
1009
- };
1010
- }
1011
-
1012
- function persistRecommendRunSnapshot(snapshot, {
1013
- persistActiveCheckpoint = false
1014
- } = {}) {
1015
- let normalized = normalizeRunSnapshot(snapshot);
1016
- if (!normalized?.run_id) return normalized;
1017
- const artifacts = getRecommendRunArtifacts(normalized.run_id);
1018
- if (!artifacts) return normalized;
1019
- const existing = readJsonFile(artifacts.run_state_path);
1201
+ }
1202
+ return control;
1203
+ }
1204
+
1205
+ function cancelErrorFromShutdown(shutdownError = null) {
1206
+ return {
1207
+ code: "PIPELINE_CANCELED",
1208
+ message: "流水线已取消。",
1209
+ retryable: true,
1210
+ shutdown_error: shutdownError || undefined
1211
+ };
1212
+ }
1213
+
1214
+ function coerceCanceledTerminalSnapshot(normalized, existing) {
1215
+ const existingControl = plainRecord(existing?.control);
1216
+ const shutdownError = normalized?.error || normalized?.result?.error || null;
1217
+ const shouldWrapCanceledShutdown = (
1218
+ normalized
1219
+ && (
1220
+ (
1221
+ normalized.state === RUN_STATUS_FAILED
1222
+ && existingControl.cancel_requested === true
1223
+ )
1224
+ || normalized.state === RUN_STATUS_CANCELED
1225
+ )
1226
+ && isCancelShutdownError(shutdownError || "")
1227
+ );
1228
+ if (
1229
+ !shouldWrapCanceledShutdown
1230
+ ) {
1231
+ return normalized;
1232
+ }
1233
+ const canceledError = cancelErrorFromShutdown(shutdownError);
1234
+ return {
1235
+ ...normalized,
1236
+ state: RUN_STATUS_CANCELED,
1237
+ status: RUN_STATUS_CANCELED,
1238
+ last_message: "流水线已取消;取消收尾时浏览器连接已关闭。",
1239
+ control: {
1240
+ pause_requested: false,
1241
+ pause_requested_at: null,
1242
+ pause_requested_by: null,
1243
+ cancel_requested: false
1244
+ },
1245
+ error: canceledError,
1246
+ result: normalized.result ? {
1247
+ ...normalized.result,
1248
+ status: "CANCELED",
1249
+ completion_reason: "canceled_by_user",
1250
+ error: canceledError
1251
+ } : {
1252
+ status: "CANCELED",
1253
+ completion_reason: "canceled_by_user",
1254
+ error: canceledError,
1255
+ run_id: normalized.run_id,
1256
+ processed_count: normalized.progress?.processed || 0,
1257
+ screened_count: normalized.progress?.screened || normalized.progress?.processed || 0,
1258
+ passed_count: normalized.progress?.passed || 0
1259
+ }
1260
+ };
1261
+ }
1262
+
1263
+ function persistRecommendRunSnapshot(snapshot, {
1264
+ persistActiveCheckpoint = false
1265
+ } = {}) {
1266
+ let normalized = normalizeRunSnapshot(snapshot);
1267
+ if (!normalized?.run_id) return normalized;
1268
+ const artifacts = getRecommendRunArtifacts(normalized.run_id);
1269
+ if (!artifacts) return normalized;
1270
+ const existing = readJsonFile(artifacts.run_state_path);
1020
1271
  normalized.control = mergePersistedControlRequest(normalized, existing);
1021
1272
  normalized = coerceCanceledTerminalSnapshot(normalized, existing);
1022
1273
  if (persistActiveCheckpoint) {
1023
1274
  persistRecommendCheckpointSnapshot(snapshot);
1024
1275
  }
1025
- const payload = {
1276
+ const cdpEvidence = buildPersistedCdpEvidence({
1277
+ meta: getRecommendRunMeta(normalized.run_id),
1278
+ persisted: existing || {}
1279
+ });
1280
+ const payload = {
1026
1281
  run_id: normalized.run_id,
1027
1282
  mode: normalized.mode,
1028
1283
  state: normalized.state,
@@ -1038,65 +1293,67 @@ function persistRecommendRunSnapshot(snapshot, {
1038
1293
  context: normalized.context,
1039
1294
  control: normalized.control,
1040
1295
  resume: normalized.resume,
1041
- error: normalized.error,
1042
- recovery: normalized.recovery,
1043
- result: normalized.result,
1044
- summary: normalized.summary,
1045
- artifacts: normalized.artifacts
1046
- };
1047
- writeJsonAtomic(artifacts.run_state_path, payload);
1048
- return normalized;
1049
- }
1050
-
1051
- function patchPersistedRecommendRunControl(runId, controlPatch = {}, {
1052
- message = ""
1053
- } = {}) {
1054
- const artifacts = getRecommendRunArtifacts(runId);
1055
- if (!artifacts) return null;
1056
- const current = readJsonFile(artifacts.run_state_path);
1057
- const state = normalizeText(current?.state || current?.status || "");
1058
- if (!current || TERMINAL_STATUSES.has(state)) return null;
1059
- const now = new Date().toISOString();
1060
- const patched = {
1061
- ...current,
1062
- updated_at: now,
1063
- heartbeat_at: current.heartbeat_at || now,
1064
- last_message: message || current.last_message || "",
1065
- control: {
1066
- ...(current.control || {}),
1067
- ...controlPatch
1068
- }
1296
+ error: normalized.error,
1297
+ recovery: normalized.recovery,
1298
+ result: normalized.result,
1299
+ summary: normalized.summary,
1300
+ checkpoint: normalized.checkpoint,
1301
+ artifacts: normalized.artifacts,
1302
+ ...cdpEvidence
1069
1303
  };
1070
- writeJsonAtomic(artifacts.run_state_path, patched);
1071
- return patched;
1072
- }
1073
-
1074
- function reconcilePersistedRecommendRunIfNeeded(persisted) {
1075
- if (!persisted || typeof persisted !== "object") return persisted;
1076
- const persistedState = normalizeText(persisted.state || persisted.status);
1077
- if (TERMINAL_STATUSES.has(persistedState)) return persisted;
1304
+ writeJsonAtomic(artifacts.run_state_path, payload);
1305
+ return normalized;
1306
+ }
1307
+
1308
+ function patchPersistedRecommendRunControl(runId, controlPatch = {}, {
1309
+ message = ""
1310
+ } = {}) {
1311
+ const artifacts = getRecommendRunArtifacts(runId);
1312
+ if (!artifacts) return null;
1313
+ const current = readJsonFile(artifacts.run_state_path);
1314
+ const state = normalizeText(current?.state || current?.status || "");
1315
+ if (!current || TERMINAL_STATUSES.has(state)) return null;
1316
+ const now = new Date().toISOString();
1317
+ const patched = {
1318
+ ...current,
1319
+ updated_at: now,
1320
+ heartbeat_at: current.heartbeat_at || now,
1321
+ last_message: message || current.last_message || "",
1322
+ control: {
1323
+ ...(current.control || {}),
1324
+ ...controlPatch
1325
+ }
1326
+ };
1327
+ writeJsonAtomic(artifacts.run_state_path, patched);
1328
+ return patched;
1329
+ }
1330
+
1331
+ function reconcilePersistedRecommendRunIfNeeded(persisted) {
1332
+ if (!persisted || typeof persisted !== "object") return persisted;
1333
+ const persistedState = normalizeText(persisted.state || persisted.status);
1334
+ if (TERMINAL_STATUSES.has(persistedState)) return persisted;
1078
1335
  if (isProcessAlive(persisted.pid)) return persisted;
1079
1336
 
1080
- const runId = normalizeRunId(persisted.run_id || persisted.runId);
1081
- const artifacts = getRecommendRunArtifacts(runId);
1082
- const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
1083
- const now = new Date().toISOString();
1084
- const cancelRequested = persisted.control?.cancel_requested === true;
1085
- const processExitedError = {
1086
- code: "RUN_PROCESS_EXITED",
1087
- message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"})。`,
1088
- retryable: true
1089
- };
1090
- const error = cancelRequested
1091
- ? cancelErrorFromShutdown(processExitedError)
1092
- : {
1093
- ...processExitedError,
1094
- message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`
1095
- };
1096
- return persistRecommendRunSnapshot({
1337
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
1338
+ const artifacts = getRecommendRunArtifacts(runId);
1339
+ const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
1340
+ const now = new Date().toISOString();
1341
+ const cancelRequested = persisted.control?.cancel_requested === true;
1342
+ const processExitedError = {
1343
+ code: "RUN_PROCESS_EXITED",
1344
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"})。`,
1345
+ retryable: true
1346
+ };
1347
+ const error = cancelRequested
1348
+ ? cancelErrorFromShutdown(processExitedError)
1349
+ : {
1350
+ ...processExitedError,
1351
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`
1352
+ };
1353
+ return persistRecommendRunSnapshot({
1097
1354
  runId,
1098
1355
  name: persisted.name || runId,
1099
- status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
1356
+ status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
1100
1357
  phase: persisted.stage || persisted.phase || "recommend:orphaned",
1101
1358
  progress: persisted.progress || {},
1102
1359
  context: persisted.context || {},
@@ -1122,11 +1379,10 @@ function attachMethodEvidence(payload, runId) {
1122
1379
  assertNoForbiddenCdpCalls(methodLog);
1123
1380
  return {
1124
1381
  ...payload,
1125
- runtime_evaluate_used: false,
1126
- method_summary: methodSummary(methodLog),
1127
- method_log: compactMethodLogForStatus(methodLog),
1128
- method_log_total: Array.isArray(methodLog) ? methodLog.length : 0,
1129
- chrome: meta.chrome || null
1382
+ ...buildPersistedCdpEvidence({
1383
+ meta,
1384
+ persisted: readRecommendRunState(runId) || {}
1385
+ })
1130
1386
  };
1131
1387
  }
1132
1388
 
@@ -1178,17 +1434,17 @@ async function readRecommendJobOptionsFromSession(session) {
1178
1434
  };
1179
1435
  }
1180
1436
 
1181
- export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
1182
- const routeGuard = guardRecommendRoute(args);
1183
- if (routeGuard) {
1184
- return {
1185
- ...routeGuard,
1186
- stage: "recommend_job_list",
1187
- message: "list_recommend_jobs is recommend-page only. For chat-only job lists, call list_boss_chat_jobs or prepare_boss_chat_run."
1188
- };
1189
- }
1190
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
1191
- const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
1437
+ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
1438
+ const routeGuard = guardRecommendRoute(args);
1439
+ if (routeGuard) {
1440
+ return {
1441
+ ...routeGuard,
1442
+ stage: "recommend_job_list",
1443
+ message: "list_recommend_jobs is recommend-page only. For chat-only job lists, call list_boss_chat_jobs or prepare_boss_chat_run."
1444
+ };
1445
+ }
1446
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1447
+ const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
1192
1448
  const port = parsePositiveInteger(
1193
1449
  args.port,
1194
1450
  configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
@@ -1462,146 +1718,146 @@ async function connectRecommendChromeSession({
1462
1718
  };
1463
1719
  }
1464
1720
 
1465
- function parseRecommendPipelineRequest(args = {}) {
1466
- return parseRecommendInstruction({
1467
- instruction: args.instruction,
1721
+ function parseRecommendPipelineRequest(args = {}) {
1722
+ return parseRecommendInstruction({
1723
+ instruction: args.instruction,
1468
1724
  confirmation: args.confirmation,
1469
1725
  overrides: args.overrides
1470
- });
1471
- }
1472
-
1473
- function readOwn(source, keys = []) {
1474
- if (!source || typeof source !== "object" || Array.isArray(source)) return undefined;
1475
- for (const key of keys) {
1476
- if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
1477
- }
1478
- return undefined;
1479
- }
1480
-
1481
- function getExplicitRestLevel(args = {}) {
1482
- const behavior = readOwn(args, ["human_behavior", "humanBehavior"]);
1483
- const raw = readOwn(behavior, ["restLevel", "rest_level"]);
1484
- const normalized = normalizeText(raw).toLowerCase();
1485
- return {
1486
- raw: raw ?? null,
1487
- restLevel: REST_LEVEL_SET.has(normalized) ? normalized : null,
1488
- valid: REST_LEVEL_SET.has(normalized),
1489
- missing: raw === undefined || raw === null || normalizeText(raw) === ""
1490
- };
1491
- }
1492
-
1493
- function buildReviewScreenParams(parsed) {
1494
- return {
1495
- ...(parsed.screenParams || {}),
1496
- criteria: parsed.screenParams?.criteria || null,
1497
- criteria_normalized: parsed.criteria_normalized || null,
1498
- target_count: parsed.screenParams?.target_count ?? parsed.proposed_target_count ?? null,
1499
- post_action: parsed.screenParams?.post_action || parsed.proposed_post_action || null,
1500
- max_greet_count: parsed.screenParams?.max_greet_count ?? parsed.proposed_max_greet_count ?? null
1501
- };
1502
- }
1503
-
1504
- function buildReviewPageScope(parsed) {
1505
- return parsed.page_scope || parsed.proposed_page_scope || "recommend";
1506
- }
1507
-
1508
- function buildReviewJob(args = {}) {
1509
- return normalizeText(args.confirmation?.job_value || args.overrides?.job || "") || null;
1510
- }
1511
-
1512
- function buildScheduleReview(args = {}) {
1513
- const scheduleRunAt = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
1514
- const scheduleDelayMinutes = args.schedule_delay_minutes ?? args.scheduleDelayMinutes;
1515
- const scheduleDelaySeconds = args.schedule_delay_seconds ?? args.scheduleDelaySeconds;
1516
- if (!scheduleRunAt && scheduleDelayMinutes === undefined && scheduleDelaySeconds === undefined) return null;
1517
- return {
1518
- schedule_run_at: scheduleRunAt || null,
1519
- schedule_delay_minutes: scheduleDelayMinutes ?? null,
1520
- schedule_delay_seconds: scheduleDelaySeconds ?? null
1521
- };
1522
- }
1523
-
1524
- function buildRequiredConfirmations(parsed, args = {}) {
1525
- const required = [];
1526
- if (parsed.needs_page_confirmation) required.push("page_scope");
1726
+ });
1727
+ }
1728
+
1729
+ function readOwn(source, keys = []) {
1730
+ if (!source || typeof source !== "object" || Array.isArray(source)) return undefined;
1731
+ for (const key of keys) {
1732
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
1733
+ }
1734
+ return undefined;
1735
+ }
1736
+
1737
+ function getExplicitRestLevel(args = {}) {
1738
+ const behavior = readOwn(args, ["human_behavior", "humanBehavior"]);
1739
+ const raw = readOwn(behavior, ["restLevel", "rest_level"]);
1740
+ const normalized = normalizeText(raw).toLowerCase();
1741
+ return {
1742
+ raw: raw ?? null,
1743
+ restLevel: REST_LEVEL_SET.has(normalized) ? normalized : null,
1744
+ valid: REST_LEVEL_SET.has(normalized),
1745
+ missing: raw === undefined || raw === null || normalizeText(raw) === ""
1746
+ };
1747
+ }
1748
+
1749
+ function buildReviewScreenParams(parsed) {
1750
+ return {
1751
+ ...(parsed.screenParams || {}),
1752
+ criteria: parsed.screenParams?.criteria || null,
1753
+ criteria_normalized: parsed.criteria_normalized || null,
1754
+ target_count: parsed.screenParams?.target_count ?? parsed.proposed_target_count ?? null,
1755
+ post_action: parsed.screenParams?.post_action || parsed.proposed_post_action || null,
1756
+ max_greet_count: parsed.screenParams?.max_greet_count ?? parsed.proposed_max_greet_count ?? null
1757
+ };
1758
+ }
1759
+
1760
+ function buildReviewPageScope(parsed) {
1761
+ return parsed.page_scope || parsed.proposed_page_scope || "recommend";
1762
+ }
1763
+
1764
+ function buildReviewJob(args = {}) {
1765
+ return normalizeText(args.confirmation?.job_value || args.overrides?.job || "") || null;
1766
+ }
1767
+
1768
+ function buildScheduleReview(args = {}) {
1769
+ const scheduleRunAt = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
1770
+ const scheduleDelayMinutes = args.schedule_delay_minutes ?? args.scheduleDelayMinutes;
1771
+ const scheduleDelaySeconds = args.schedule_delay_seconds ?? args.scheduleDelaySeconds;
1772
+ if (!scheduleRunAt && scheduleDelayMinutes === undefined && scheduleDelaySeconds === undefined) return null;
1773
+ return {
1774
+ schedule_run_at: scheduleRunAt || null,
1775
+ schedule_delay_minutes: scheduleDelayMinutes ?? null,
1776
+ schedule_delay_seconds: scheduleDelaySeconds ?? null
1777
+ };
1778
+ }
1779
+
1780
+ function buildRequiredConfirmations(parsed, args = {}) {
1781
+ const required = [];
1782
+ if (parsed.needs_page_confirmation) required.push("page_scope");
1527
1783
  if (parsed.needs_filters_confirmation) required.push("filters");
1528
1784
  if (parsed.needs_school_tag_confirmation) required.push("school_tag");
1529
1785
  if (parsed.needs_degree_confirmation) required.push("degree");
1530
1786
  if (parsed.needs_gender_confirmation) required.push("gender");
1531
1787
  if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
1532
- if (parsed.needs_criteria_confirmation) required.push("criteria");
1533
- if (parsed.needs_target_count_confirmation) required.push("target_count");
1534
- if (parsed.needs_post_action_confirmation) required.push("post_action");
1535
- if (parsed.needs_skip_recent_colleague_contacted_confirmation) required.push("skip_recent_colleague_contacted");
1536
- if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
1537
-
1538
- const confirmation = args.confirmation || {};
1539
- const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
1540
- if (!jobValue) required.push("job");
1541
- const restLevel = getExplicitRestLevel(args);
1542
- if (!restLevel.valid) required.push("rest_level");
1543
- const blocksFinalReview = required.some((field) => field !== "rest_level");
1544
- if (confirmation.final_confirmed !== true && !blocksFinalReview) required.push("final_review");
1545
- return Array.from(new Set(required));
1546
- }
1788
+ if (parsed.needs_criteria_confirmation) required.push("criteria");
1789
+ if (parsed.needs_target_count_confirmation) required.push("target_count");
1790
+ if (parsed.needs_post_action_confirmation) required.push("post_action");
1791
+ if (parsed.needs_skip_recent_colleague_contacted_confirmation) required.push("skip_recent_colleague_contacted");
1792
+ if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
1793
+
1794
+ const confirmation = args.confirmation || {};
1795
+ const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
1796
+ if (!jobValue) required.push("job");
1797
+ const restLevel = getExplicitRestLevel(args);
1798
+ if (!restLevel.valid) required.push("rest_level");
1799
+ const blocksFinalReview = required.some((field) => field !== "rest_level");
1800
+ if (confirmation.final_confirmed !== true && !blocksFinalReview) required.push("final_review");
1801
+ return Array.from(new Set(required));
1802
+ }
1547
1803
 
1548
- function buildJobPendingQuestion(args = {}) {
1549
- const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1804
+ function buildJobPendingQuestion(args = {}) {
1805
+ const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1550
1806
  return {
1551
1807
  field: "job",
1552
1808
  question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
1553
1809
  value: value || null
1554
- };
1555
- }
1556
-
1557
- function buildRestLevelPendingQuestion(args = {}) {
1558
- const restLevel = getExplicitRestLevel(args);
1559
- return {
1560
- field: "rest_level",
1561
- question: restLevel.missing
1562
- ? "请确认本次运行休息强度 rest_level。"
1563
- : "rest_level 只能是 low / medium / high,请重新确认本次运行休息强度。",
1564
- value: restLevel.restLevel || restLevel.raw || null,
1565
- options: REST_LEVEL_OPTIONS.map((value) => ({
1566
- label: value,
1567
- value
1568
- }))
1569
- };
1570
- }
1571
-
1572
- function buildSuspiciousFieldsQuestion(parsed) {
1573
- return {
1574
- field: "suspicious_fields",
1575
- question: "检测到需要修正或明确确认的异常字段,请先修正后再启动。",
1576
- value: parsed.suspicious_fields || []
1577
- };
1578
- }
1579
-
1580
- function buildFinalReviewQuestion(parsed, args = {}) {
1581
- const restLevel = getExplicitRestLevel(args);
1582
- return {
1583
- field: "final_review",
1584
- question: "请最终确认本次推荐页筛选参数无误;确认后设置 final_confirmed=true 即可启动或创建定时任务。",
1585
- value: {
1586
- page_scope: buildReviewPageScope(parsed),
1587
- job: buildReviewJob(args),
1588
- search_params: parsed.searchParams,
1589
- screen_params: buildReviewScreenParams(parsed),
1590
- human_behavior: {
1591
- restLevel: restLevel.restLevel || null
1592
- },
1593
- schedule: buildScheduleReview(args)
1594
- }
1595
- };
1596
- }
1597
-
1598
- function buildNeedInputResponse(parsed, args = {}) {
1599
- return {
1600
- status: "NEED_INPUT",
1601
- missing_fields: parsed.missing_fields,
1602
- required_confirmations: buildRequiredConfirmations(parsed, args),
1603
- search_params: parsed.searchParams,
1604
- screen_params: parsed.screenParams,
1810
+ };
1811
+ }
1812
+
1813
+ function buildRestLevelPendingQuestion(args = {}) {
1814
+ const restLevel = getExplicitRestLevel(args);
1815
+ return {
1816
+ field: "rest_level",
1817
+ question: restLevel.missing
1818
+ ? "请确认本次运行休息强度 rest_level。"
1819
+ : "rest_level 只能是 low / medium / high,请重新确认本次运行休息强度。",
1820
+ value: restLevel.restLevel || restLevel.raw || null,
1821
+ options: REST_LEVEL_OPTIONS.map((value) => ({
1822
+ label: value,
1823
+ value
1824
+ }))
1825
+ };
1826
+ }
1827
+
1828
+ function buildSuspiciousFieldsQuestion(parsed) {
1829
+ return {
1830
+ field: "suspicious_fields",
1831
+ question: "检测到需要修正或明确确认的异常字段,请先修正后再启动。",
1832
+ value: parsed.suspicious_fields || []
1833
+ };
1834
+ }
1835
+
1836
+ function buildFinalReviewQuestion(parsed, args = {}) {
1837
+ const restLevel = getExplicitRestLevel(args);
1838
+ return {
1839
+ field: "final_review",
1840
+ question: "请最终确认本次推荐页筛选参数无误;确认后设置 final_confirmed=true 即可启动或创建定时任务。",
1841
+ value: {
1842
+ page_scope: buildReviewPageScope(parsed),
1843
+ job: buildReviewJob(args),
1844
+ search_params: parsed.searchParams,
1845
+ screen_params: buildReviewScreenParams(parsed),
1846
+ human_behavior: {
1847
+ restLevel: restLevel.restLevel || null
1848
+ },
1849
+ schedule: buildScheduleReview(args)
1850
+ }
1851
+ };
1852
+ }
1853
+
1854
+ function buildNeedInputResponse(parsed, args = {}) {
1855
+ return {
1856
+ status: "NEED_INPUT",
1857
+ missing_fields: parsed.missing_fields,
1858
+ required_confirmations: buildRequiredConfirmations(parsed, args),
1859
+ search_params: parsed.searchParams,
1860
+ screen_params: parsed.screenParams,
1605
1861
  pending_questions: parsed.pending_questions,
1606
1862
  review: parsed.review,
1607
1863
  error: {
@@ -1612,36 +1868,36 @@ function buildNeedInputResponse(parsed, args = {}) {
1612
1868
  };
1613
1869
  }
1614
1870
 
1615
- function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
1616
- const pending = [...(parsed.pending_questions || [])];
1617
- if (requiredConfirmations.includes("suspicious_fields") && !pending.some((item) => item.field === "suspicious_fields")) {
1618
- pending.push(buildSuspiciousFieldsQuestion(parsed));
1619
- }
1620
- if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
1621
- pending.push(buildJobPendingQuestion(args));
1622
- }
1623
- if (requiredConfirmations.includes("rest_level") && !pending.some((item) => item.field === "rest_level")) {
1624
- pending.push(buildRestLevelPendingQuestion(args));
1625
- }
1626
- if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
1627
- pending.push(buildFinalReviewQuestion(parsed, args));
1628
- }
1629
- return {
1630
- status: "NEED_CONFIRMATION",
1631
- required_confirmations: requiredConfirmations,
1632
- page_scope: buildReviewPageScope(parsed),
1633
- search_params: parsed.searchParams,
1634
- screen_params: buildReviewScreenParams(parsed),
1635
- pending_questions: pending,
1636
- review: {
1637
- ...(parsed.review || {}),
1871
+ function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
1872
+ const pending = [...(parsed.pending_questions || [])];
1873
+ if (requiredConfirmations.includes("suspicious_fields") && !pending.some((item) => item.field === "suspicious_fields")) {
1874
+ pending.push(buildSuspiciousFieldsQuestion(parsed));
1875
+ }
1876
+ if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
1877
+ pending.push(buildJobPendingQuestion(args));
1878
+ }
1879
+ if (requiredConfirmations.includes("rest_level") && !pending.some((item) => item.field === "rest_level")) {
1880
+ pending.push(buildRestLevelPendingQuestion(args));
1881
+ }
1882
+ if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
1883
+ pending.push(buildFinalReviewQuestion(parsed, args));
1884
+ }
1885
+ return {
1886
+ status: "NEED_CONFIRMATION",
1887
+ required_confirmations: requiredConfirmations,
1888
+ page_scope: buildReviewPageScope(parsed),
1889
+ search_params: parsed.searchParams,
1890
+ screen_params: buildReviewScreenParams(parsed),
1891
+ pending_questions: pending,
1892
+ review: {
1893
+ ...(parsed.review || {}),
1638
1894
  required_confirmations: requiredConfirmations
1639
1895
  }
1640
1896
  };
1641
1897
  }
1642
-
1643
- function evaluateRecommendPipelineGate(parsed, args = {}) {
1644
- if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed, args);
1898
+
1899
+ function evaluateRecommendPipelineGate(parsed, args = {}) {
1900
+ if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed, args);
1645
1901
  const requiredConfirmations = buildRequiredConfirmations(parsed, args);
1646
1902
  if (requiredConfirmations.length) {
1647
1903
  return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
@@ -1679,7 +1935,13 @@ function buildRecommendFilter(parsed, args = {}) {
1679
1935
  return { enabled: false };
1680
1936
  }
1681
1937
 
1682
- const groups = [];
1938
+ const groups = [{
1939
+ group: "activity",
1940
+ labels: [parsed.searchParams?.activity_level || "不限"],
1941
+ selectAllLabels: false,
1942
+ allowUnlimited: true,
1943
+ verifySticky: true
1944
+ }];
1683
1945
  const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
1684
1946
  if (recentNotView.length) {
1685
1947
  groups.push({
@@ -1716,8 +1978,12 @@ function buildRecommendFilter(parsed, args = {}) {
1716
1978
  });
1717
1979
  }
1718
1980
 
1719
- return groups.length ? { filterGroups: groups } : { enabled: false };
1720
- }
1981
+ return {
1982
+ enabled: true,
1983
+ currentCityOnly: parsed.searchParams?.current_city_only === true,
1984
+ filterGroups: groups
1985
+ };
1986
+ }
1721
1987
 
1722
1988
  function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
1723
1989
  const confirmation = args.confirmation || {};
@@ -1740,24 +2006,24 @@ function normalizeRecommendStartInput(args = {}, parsed, configResolution = null
1740
2006
  targetCount,
1741
2007
  job: normalizeText(confirmation.job_value || overrides.job || ""),
1742
2008
  pageScope: parsed.page_scope || "recommend",
1743
- filter: buildRecommendFilter(parsed, args),
1744
- postAction: parsed.screenParams?.post_action || "none",
1745
- maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
1746
- ? parsed.screenParams.max_greet_count
1747
- : null,
1748
- skipRecentColleagueContacted: parsed.screenParams?.skip_recent_colleague_contacted !== false,
1749
- colleagueContactWindowDays: 14,
1750
- screeningMode: normalizeScreeningModeArg(args)
1751
- };
1752
- }
2009
+ filter: buildRecommendFilter(parsed, args),
2010
+ postAction: parsed.screenParams?.post_action || "none",
2011
+ maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
2012
+ ? parsed.screenParams.max_greet_count
2013
+ : null,
2014
+ skipRecentColleagueContacted: parsed.screenParams?.skip_recent_colleague_contacted !== false,
2015
+ colleagueContactWindowDays: 14,
2016
+ screeningMode: normalizeScreeningModeArg(args)
2017
+ };
2018
+ }
1753
2019
 
1754
- function getRunOptions(args, parsed, normalized, session, configResolution = null) {
1755
- const slowLive = args.slow_live === true;
1756
- const executePostAction = args.dry_run_post_action === true
1757
- ? false
1758
- : args.execute_post_action !== false;
1759
- const humanBehavior = resolveHumanBehaviorForRun(args, configResolution?.config || {});
1760
- return {
2020
+ function getRunOptions(args, parsed, normalized, session, configResolution = null) {
2021
+ const slowLive = args.slow_live === true;
2022
+ const executePostAction = args.dry_run_post_action === true
2023
+ ? false
2024
+ : args.execute_post_action !== false;
2025
+ const humanBehavior = resolveHumanBehaviorForRun(args, configResolution?.config || {});
2026
+ return {
1761
2027
  client: session.client,
1762
2028
  targetUrl: RECOMMEND_TARGET_URL,
1763
2029
  criteria: normalized.criteria,
@@ -1787,8 +2053,8 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1787
2053
  executePostAction,
1788
2054
  actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
1789
2055
  actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
1790
- actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
1791
- screeningMode: normalized.screeningMode,
2056
+ actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
2057
+ screeningMode: normalized.screeningMode,
1792
2058
  llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
1793
2059
  ...configResolution.config
1794
2060
  } : null,
@@ -1800,23 +2066,36 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1800
2066
  args.llm_image_limit,
1801
2067
  parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
1802
2068
  ),
1803
- llmImageDetail: normalizeText(
1804
- args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
1805
- ) || "low",
1806
- imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1807
- humanRestEnabled: humanBehavior.restEnabled,
2069
+ llmImageDetail: normalizeText(
2070
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
2071
+ ) || "low",
2072
+ imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
2073
+ humanRestEnabled: humanBehavior.restEnabled,
1808
2074
  humanBehavior,
1809
2075
  skipRecentColleagueContacted: normalized.skipRecentColleagueContacted,
1810
2076
  colleagueContactWindowDays: normalized.colleagueContactWindowDays,
2077
+ debugTestMode: isDebugTestMode(args),
2078
+ debugForceListEndAfterProcessed: parsePositiveInteger(
2079
+ args.debug_force_list_end_after_processed,
2080
+ null
2081
+ ),
2082
+ debugForceContextRecoveryAfterProcessed: parsePositiveInteger(
2083
+ args.debug_force_context_recovery_after_processed,
2084
+ null
2085
+ ),
2086
+ debugForceCdpReconnectAfterProcessed: parsePositiveInteger(
2087
+ args.debug_force_cdp_reconnect_after_processed,
2088
+ null
2089
+ ),
1811
2090
  name: "mcp-recommend-pipeline-run",
1812
- parsed
1813
- };
1814
- }
2091
+ parsed
2092
+ };
2093
+ }
1815
2094
 
1816
- function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1817
- const routeGuard = guardRecommendRoute(args);
1818
- if (routeGuard) return { response: routeGuard };
1819
- const parsed = parseRecommendPipelineRequest(args);
2095
+ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
2096
+ const routeGuard = guardRecommendRoute(args);
2097
+ if (routeGuard) return { response: routeGuard };
2098
+ const parsed = parseRecommendPipelineRequest(args);
1820
2099
  const gate = evaluateRecommendPipelineGate(parsed, args);
1821
2100
  if (gate) return { response: gate };
1822
2101
  const configResolution = resolveBossScreeningConfig(workspaceRoot);
@@ -2002,72 +2281,72 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
2002
2281
  };
2003
2282
  }
2004
2283
 
2005
- export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
2006
- const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
2007
- if (prepared.response) {
2008
- return {
2009
- ...prepared.response,
2010
- cron_ready: false
2011
- };
2012
- }
2013
- const { parsed, normalized } = prepared;
2014
- const nextAction = {
2015
- immediate_run: {
2016
- recommended_next_tool: "start_recommend_pipeline_run",
2017
- alternate_next_tool: "run_recommend",
2018
- same_arguments: true,
2019
- native_mcp_required: true
2020
- },
2021
- scheduled_run: {
2022
- recommended_next_tool: "schedule_recommend_pipeline_run",
2023
- same_arguments: true,
2024
- requires_schedule_field: true
2025
- },
2026
- do_not_call_prepare_again: true,
2027
- do_not_use_cli_fallback_when_mcp_tools_available: true,
2028
- do_not_use_terminal_or_shell: true,
2029
- forbidden_fallbacks: [
2030
- "run_command",
2031
- "terminal",
2032
- "shell",
2033
- "CLI detached fallback",
2034
- "manual JSON-RPC over shell",
2035
- "npx --stdio"
2036
- ]
2037
- };
2038
- const agentGuidance = {
2039
- host_has_native_mcp: true,
2040
- native_mcp_required_after_prepare: true,
2041
- proof: "This prepare_recommend_pipeline_run response was returned by the MCP server, so the host can call MCP tools from this server.",
2042
- immediate_run: {
2043
- tool: nextAction.immediate_run.recommended_next_tool,
2044
- alternate_tool: nextAction.immediate_run.alternate_next_tool,
2045
- same_arguments: true
2046
- },
2047
- scheduled_run: {
2048
- tool: nextAction.scheduled_run.recommended_next_tool,
2049
- same_arguments: true
2050
- },
2051
- trae_cn: {
2052
- next_step: "Call boss-recommend/start_recommend_pipeline_run or boss-recommend/run_recommend through the native MCP tool interface.",
2053
- never_use_terminal_fallback_after_prepare: true
2054
- },
2055
- forbidden_when_mcp_tools_are_available: nextAction.forbidden_fallbacks
2056
- };
2057
- return {
2058
- status: "READY",
2059
- cron_ready: true,
2060
- prepared_only: true,
2061
- run_started: false,
2062
- recommended_next_tool: nextAction.immediate_run.recommended_next_tool,
2063
- alternate_next_tool: nextAction.immediate_run.alternate_next_tool,
2064
- next_action: nextAction,
2065
- agent_guidance: agentGuidance,
2066
- message: "READY only means the payload passed validation; prepare_recommend_pipeline_run did not start a run. This response proves the MCP server is available. To start now, call the native MCP tool start_recommend_pipeline_run or run_recommend with the same arguments. Do not call prepare_recommend_pipeline_run again, and do not use terminal, shell, CLI detached fallback, or manual JSON-RPC when MCP tools are available.",
2067
- review: parsed.review,
2068
- post_action: {
2069
- requested: normalized.postAction,
2070
- execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
2284
+ export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
2285
+ const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
2286
+ if (prepared.response) {
2287
+ return {
2288
+ ...prepared.response,
2289
+ cron_ready: false
2290
+ };
2291
+ }
2292
+ const { parsed, normalized } = prepared;
2293
+ const nextAction = {
2294
+ immediate_run: {
2295
+ recommended_next_tool: "start_recommend_pipeline_run",
2296
+ alternate_next_tool: "run_recommend",
2297
+ same_arguments: true,
2298
+ native_mcp_required: true
2299
+ },
2300
+ scheduled_run: {
2301
+ recommended_next_tool: "schedule_recommend_pipeline_run",
2302
+ same_arguments: true,
2303
+ requires_schedule_field: true
2304
+ },
2305
+ do_not_call_prepare_again: true,
2306
+ do_not_use_cli_fallback_when_mcp_tools_available: true,
2307
+ do_not_use_terminal_or_shell: true,
2308
+ forbidden_fallbacks: [
2309
+ "run_command",
2310
+ "terminal",
2311
+ "shell",
2312
+ "CLI detached fallback",
2313
+ "manual JSON-RPC over shell",
2314
+ "npx --stdio"
2315
+ ]
2316
+ };
2317
+ const agentGuidance = {
2318
+ host_has_native_mcp: true,
2319
+ native_mcp_required_after_prepare: true,
2320
+ proof: "This prepare_recommend_pipeline_run response was returned by the MCP server, so the host can call MCP tools from this server.",
2321
+ immediate_run: {
2322
+ tool: nextAction.immediate_run.recommended_next_tool,
2323
+ alternate_tool: nextAction.immediate_run.alternate_next_tool,
2324
+ same_arguments: true
2325
+ },
2326
+ scheduled_run: {
2327
+ tool: nextAction.scheduled_run.recommended_next_tool,
2328
+ same_arguments: true
2329
+ },
2330
+ trae_cn: {
2331
+ next_step: "Call boss-recommend/start_recommend_pipeline_run or boss-recommend/run_recommend through the native MCP tool interface.",
2332
+ never_use_terminal_fallback_after_prepare: true
2333
+ },
2334
+ forbidden_when_mcp_tools_are_available: nextAction.forbidden_fallbacks
2335
+ };
2336
+ return {
2337
+ status: "READY",
2338
+ cron_ready: true,
2339
+ prepared_only: true,
2340
+ run_started: false,
2341
+ recommended_next_tool: nextAction.immediate_run.recommended_next_tool,
2342
+ alternate_next_tool: nextAction.immediate_run.alternate_next_tool,
2343
+ next_action: nextAction,
2344
+ agent_guidance: agentGuidance,
2345
+ message: "READY only means the payload passed validation; prepare_recommend_pipeline_run did not start a run. This response proves the MCP server is available. To start now, call the native MCP tool start_recommend_pipeline_run or run_recommend with the same arguments. Do not call prepare_recommend_pipeline_run again, and do not use terminal, shell, CLI detached fallback, or manual JSON-RPC when MCP tools are available.",
2346
+ review: parsed.review,
2347
+ post_action: {
2348
+ requested: normalized.postAction,
2349
+ execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
2071
2350
  max_greet_count: normalized.maxGreetCount
2072
2351
  },
2073
2352
  target_count_semantics: TARGET_COUNT_SEMANTICS
@@ -2093,29 +2372,26 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
2093
2372
  };
2094
2373
  }
2095
2374
  try {
2096
- const run = recommendRunService.getRecommendRun(runId);
2097
- const normalizedRun = persistRecommendRunSnapshot(run);
2098
- return attachMethodEvidence({
2099
- status: "RUN_STATUS",
2100
- run: compactRecommendRunForStatus(normalizedRun)
2101
- }, runId);
2102
- } catch {
2375
+ const run = recommendRunService.getRecommendRun(runId);
2376
+ const normalizedRun = persistRecommendRunSnapshot(run);
2377
+ return attachMethodEvidence({
2378
+ status: "RUN_STATUS",
2379
+ run: compactRecommendRunForStatus(normalizedRun)
2380
+ }, runId);
2381
+ } catch {
2103
2382
  const persisted = readRecommendRunState(runId);
2104
2383
  if (persisted) {
2105
2384
  const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2106
- return {
2385
+ const persistedEvidence = readRecommendRunState(runId) || persisted;
2386
+ return attachPersistedCdpEvidence({
2107
2387
  status: "RUN_STATUS",
2108
2388
  run: reconciled,
2109
- persistence: {
2110
- source: "disk",
2111
- active_control_available: false,
2112
- stale_process_reconciled: reconciled?.state !== persisted.state
2113
- },
2114
- runtime_evaluate_used: false,
2115
- method_summary: {},
2116
- method_log: [],
2117
- chrome: null
2118
- };
2389
+ persistence: {
2390
+ source: "disk",
2391
+ active_control_available: false,
2392
+ stale_process_reconciled: reconciled?.state !== persisted.state
2393
+ }
2394
+ }, persistedEvidence);
2119
2395
  }
2120
2396
  return {
2121
2397
  status: "FAILED",
@@ -2133,40 +2409,36 @@ export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
2133
2409
  try {
2134
2410
  const before = recommendRunService.getRecommendRun(runId);
2135
2411
  if (TERMINAL_STATUSES.has(before.status)) {
2136
- const normalizedBefore = persistRecommendRunSnapshot(before);
2137
- return attachMethodEvidence({
2138
- status: "PAUSE_IGNORED",
2139
- run: compactRecommendRunForStatus(normalizedBefore),
2140
- message: "目标任务已结束,无需暂停。"
2141
- }, runId);
2412
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2413
+ return attachMethodEvidence({
2414
+ status: "PAUSE_IGNORED",
2415
+ run: compactRecommendRunForStatus(normalizedBefore),
2416
+ message: "目标任务已结束,无需暂停。"
2417
+ }, runId);
2142
2418
  }
2143
2419
  if (before.status === RUN_STATUS_PAUSED) {
2144
- const normalizedBefore = persistRecommendRunSnapshot(before);
2145
- return attachMethodEvidence({
2146
- status: "PAUSE_IGNORED",
2147
- run: compactRecommendRunForStatus(normalizedBefore),
2148
- message: "目标任务已经处于 paused 状态。"
2149
- }, runId);
2420
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2421
+ return attachMethodEvidence({
2422
+ status: "PAUSE_IGNORED",
2423
+ run: compactRecommendRunForStatus(normalizedBefore),
2424
+ message: "目标任务已经处于 paused 状态。"
2425
+ }, runId);
2150
2426
  }
2151
2427
  const run = recommendRunService.pauseRecommendRun(runId);
2152
- const normalizedRun = persistRecommendRunSnapshot(run);
2153
- return attachMethodEvidence({
2154
- status: "PAUSE_REQUESTED",
2155
- run: compactRecommendRunForStatus(normalizedRun),
2156
- message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2157
- }, runId);
2428
+ const normalizedRun = persistRecommendRunSnapshot(run);
2429
+ return attachMethodEvidence({
2430
+ status: "PAUSE_REQUESTED",
2431
+ run: compactRecommendRunForStatus(normalizedRun),
2432
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2433
+ }, runId);
2158
2434
  } catch {
2159
- const persisted = readRecommendRunState(runId);
2160
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2161
- return {
2435
+ const persisted = readRecommendRunState(runId);
2436
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2437
+ return attachPersistedCdpEvidence({
2162
2438
  status: "PAUSE_IGNORED",
2163
2439
  run: compactRecommendRunForStatus(persisted),
2164
- message: "目标任务已结束,无需暂停。",
2165
- runtime_evaluate_used: false,
2166
- method_summary: {},
2167
- method_log: [],
2168
- chrome: null
2169
- };
2440
+ message: "目标任务已结束,无需暂停。"
2441
+ }, persisted);
2170
2442
  }
2171
2443
  return getRecommendPipelineRunTool({ args });
2172
2444
  }
@@ -2177,28 +2449,28 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2177
2449
  try {
2178
2450
  const before = recommendRunService.getRecommendRun(runId);
2179
2451
  if (TERMINAL_STATUSES.has(before.status)) {
2180
- const normalizedBefore = persistRecommendRunSnapshot(before);
2181
- return attachMethodEvidence({
2182
- status: "FAILED",
2452
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2453
+ return attachMethodEvidence({
2454
+ status: "FAILED",
2183
2455
  error: {
2184
2456
  code: "RUN_ALREADY_TERMINATED",
2185
2457
  message: "目标任务已结束,无法继续。",
2186
2458
  retryable: false
2187
2459
  },
2188
- run: compactRecommendRunForStatus(normalizedBefore)
2189
- }, runId);
2460
+ run: compactRecommendRunForStatus(normalizedBefore)
2461
+ }, runId);
2190
2462
  }
2191
2463
  if (before.status !== RUN_STATUS_PAUSED) {
2192
- const normalizedBefore = persistRecommendRunSnapshot(before);
2193
- return attachMethodEvidence({
2194
- status: "FAILED",
2464
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2465
+ return attachMethodEvidence({
2466
+ status: "FAILED",
2195
2467
  error: {
2196
2468
  code: "RUN_NOT_PAUSED",
2197
2469
  message: "仅 paused 状态的 run 才能继续。",
2198
2470
  retryable: true
2199
2471
  },
2200
- run: compactRecommendRunForStatus(normalizedBefore)
2201
- }, runId);
2472
+ run: compactRecommendRunForStatus(normalizedBefore)
2473
+ }, runId);
2202
2474
  }
2203
2475
  const run = recommendRunService.resumeRecommendRun(runId);
2204
2476
  const meta = getRecommendRunMeta(runId);
@@ -2206,35 +2478,31 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2206
2478
  meta.resumeCount = (meta.resumeCount || 0) + 1;
2207
2479
  meta.lastResumedAt = new Date().toISOString();
2208
2480
  }
2209
- const normalizedRun = persistRecommendRunSnapshot(run);
2210
- return attachMethodEvidence({
2211
- status: "RESUME_REQUESTED",
2212
- run: compactRecommendRunForStatus(normalizedRun),
2213
- poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
2214
- message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2481
+ const normalizedRun = persistRecommendRunSnapshot(run);
2482
+ return attachMethodEvidence({
2483
+ status: "RESUME_REQUESTED",
2484
+ run: compactRecommendRunForStatus(normalizedRun),
2485
+ poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
2486
+ message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2215
2487
  }, runId);
2216
2488
  } catch {
2217
- const persisted = readRecommendRunState(runId);
2218
- if (persisted) {
2219
- return {
2220
- status: "FAILED",
2221
- error: {
2489
+ const persisted = readRecommendRunState(runId);
2490
+ if (persisted) {
2491
+ return attachPersistedCdpEvidence({
2492
+ status: "FAILED",
2493
+ error: {
2222
2494
  code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
2223
2495
  message: TERMINAL_STATUSES.has(persisted.state)
2224
2496
  ? "目标任务已结束,无法继续。"
2225
2497
  : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
2226
2498
  retryable: !TERMINAL_STATUSES.has(persisted.state)
2227
2499
  },
2228
- run: compactRecommendRunForStatus(persisted),
2500
+ run: compactRecommendRunForStatus(persisted),
2229
2501
  persistence: {
2230
2502
  source: "disk",
2231
- active_control_available: false
2232
- },
2233
- runtime_evaluate_used: false,
2234
- method_summary: {},
2235
- method_log: [],
2236
- chrome: null
2237
- };
2503
+ active_control_available: false
2504
+ }
2505
+ }, persisted);
2238
2506
  }
2239
2507
  return getRecommendPipelineRunTool({ args });
2240
2508
  }
@@ -2245,61 +2513,53 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2245
2513
  try {
2246
2514
  const before = recommendRunService.getRecommendRun(runId);
2247
2515
  if (TERMINAL_STATUSES.has(before.status)) {
2248
- const normalizedBefore = persistRecommendRunSnapshot(before);
2249
- return attachMethodEvidence({
2250
- status: "CANCEL_IGNORED",
2251
- run: compactRecommendRunForStatus(normalizedBefore),
2252
- message: "目标任务已结束,无需取消。"
2253
- }, runId);
2516
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2517
+ return attachMethodEvidence({
2518
+ status: "CANCEL_IGNORED",
2519
+ run: compactRecommendRunForStatus(normalizedBefore),
2520
+ message: "目标任务已结束,无需取消。"
2521
+ }, runId);
2254
2522
  }
2255
2523
  const run = recommendRunService.cancelRecommendRun(runId);
2256
- const normalizedRun = persistRecommendRunSnapshot(run);
2257
- return attachMethodEvidence({
2258
- status: "CANCEL_REQUESTED",
2259
- run: compactRecommendRunForStatus(normalizedRun),
2260
- message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2261
- }, runId);
2524
+ const normalizedRun = persistRecommendRunSnapshot(run);
2525
+ return attachMethodEvidence({
2526
+ status: "CANCEL_REQUESTED",
2527
+ run: compactRecommendRunForStatus(normalizedRun),
2528
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2529
+ }, runId);
2262
2530
  } catch {
2263
- const persisted = readRecommendRunState(runId);
2531
+ const persisted = readRecommendRunState(runId);
2264
2532
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2265
- return {
2533
+ return attachPersistedCdpEvidence({
2266
2534
  status: "CANCEL_IGNORED",
2267
2535
  run: compactRecommendRunForStatus(persisted),
2268
- message: "目标任务已结束,无需取消。",
2269
- runtime_evaluate_used: false,
2270
- method_summary: {},
2271
- method_log: [],
2272
- chrome: null
2273
- };
2274
- }
2275
- const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
2276
- const patched = patchPersistedRecommendRunControl(runId, {
2277
- pause_requested: true,
2278
- pause_requested_at: new Date().toISOString(),
2279
- pause_requested_by: "cancel_recommend_pipeline_run",
2280
- cancel_requested: true
2281
- }, {
2282
- message: cancelMessage
2536
+ message: "目标任务已结束,无需取消。"
2537
+ }, persisted);
2538
+ }
2539
+ const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
2540
+ const patched = patchPersistedRecommendRunControl(runId, {
2541
+ pause_requested: true,
2542
+ pause_requested_at: new Date().toISOString(),
2543
+ pause_requested_by: "cancel_recommend_pipeline_run",
2544
+ cancel_requested: true
2545
+ }, {
2546
+ message: cancelMessage
2283
2547
  });
2284
2548
  if (patched) {
2285
- return {
2549
+ return attachPersistedCdpEvidence({
2286
2550
  status: "CANCEL_REQUESTED",
2287
2551
  run: compactRecommendRunForStatus(patched),
2288
- message: cancelMessage,
2289
- persistence: {
2290
- source: "disk",
2552
+ message: cancelMessage,
2553
+ persistence: {
2554
+ source: "disk",
2291
2555
  active_control_available: false,
2292
2556
  detached_control_requested: true
2293
- },
2294
- runtime_evaluate_used: false,
2295
- method_summary: {},
2296
- method_log: [],
2297
- chrome: null
2298
- };
2299
- }
2300
- return getRecommendPipelineRunTool({ args });
2301
- }
2302
- }
2557
+ }
2558
+ }, patched);
2559
+ }
2560
+ return getRecommendPipelineRunTool({ args });
2561
+ }
2562
+ }
2303
2563
 
2304
2564
  export function getRecommendMcpHealthSnapshot(runId) {
2305
2565
  const meta = getRecommendRunMeta(runId);