@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22

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 +8 -8
  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 +2419 -2415
  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 +1326 -1326
  23. package/src/core/reporting/legacy-csv.js +334 -332
  24. package/src/core/run/index.js +32 -32
  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 +650 -650
  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 +504 -361
  50. package/src/domains/recommend/roots.js +80 -80
  51. package/src/domains/recommend/run-service.js +987 -854
  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 +13 -0
  64. package/src/parser.js +376 -8
  65. package/src/recommend-mcp.js +929 -915
  66. package/src/recommend-scheduler.js +496 -496
  67. package/src/recruit-mcp.js +2121 -2121
@@ -44,21 +44,21 @@ import {
44
44
  parseRecommendInstruction
45
45
  } from "./parser.js";
46
46
  import { getRunsDir } from "./run-state.js";
47
- import {
48
- resolveBossConfiguredOutputDir,
49
- resolveHumanBehaviorForRun,
50
- resolveBossScreeningConfig
51
- } from "./chat-runtime-config.js";
47
+ import {
48
+ resolveBossConfiguredOutputDir,
49
+ resolveHumanBehaviorForRun,
50
+ resolveBossScreeningConfig
51
+ } from "./chat-runtime-config.js";
52
52
  import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
53
53
 
54
54
  const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
55
- const DEFAULT_RECOMMEND_PORT = 9222;
56
- const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
57
- const 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);
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);
62
62
 
63
63
  const TERMINAL_STATUSES = new Set([
64
64
  RUN_STATUS_COMPLETED,
@@ -76,188 +76,188 @@ let recommendRunService = createRecommendRunService({
76
76
  });
77
77
  const recommendRunMeta = new Map();
78
78
 
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;
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;
261
261
  }
262
262
 
263
263
  function parseNonNegativeInteger(raw, fallback) {
@@ -303,23 +303,23 @@ function resolveRecommendDetailLimit(args = {}, normalized = {}) {
303
303
  return requested;
304
304
  }
305
305
 
306
- function methodSummary(methodLog = []) {
306
+ function methodSummary(methodLog = []) {
307
307
  const summary = {};
308
308
  for (const entry of methodLog || []) {
309
309
  summary[entry.method] = (summary[entry.method] || 0) + 1;
310
310
  }
311
- return summary;
312
- }
313
-
314
- function compactMethodLogForStatus(methodLog = []) {
315
- 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
- }));
320
- }
321
-
322
- function clonePlain(value, fallback = null) {
311
+ return summary;
312
+ }
313
+
314
+ function compactMethodLogForStatus(methodLog = []) {
315
+ 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
+ }));
320
+ }
321
+
322
+ function clonePlain(value, fallback = null) {
323
323
  try {
324
324
  return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
325
325
  } catch {
@@ -382,10 +382,12 @@ function readJsonFile(filePath) {
382
382
 
383
383
  function recommendSearchParamsForCsv(searchParams = {}) {
384
384
  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 : "不限"
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 : "不限",
389
+ current_city_only: Object.prototype.hasOwnProperty.call(searchParams, "current_city_only") ? searchParams.current_city_only : false,
390
+ activity_level: Object.prototype.hasOwnProperty.call(searchParams, "activity_level") ? searchParams.activity_level : "不限"
389
391
  };
390
392
  }
391
393
 
@@ -424,10 +426,12 @@ function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
424
426
  function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
425
427
  const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
426
428
  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
429
+ school_tag: overrides.school_tag ?? confirmation.school_tag_value,
430
+ degree: overrides.degree ?? confirmation.degree_value,
431
+ gender: overrides.gender ?? confirmation.gender_value,
432
+ recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value,
433
+ current_city_only: overrides.current_city_only ?? false,
434
+ activity_level: overrides.activity_level ?? "不限"
431
435
  });
432
436
  const parsedScreenParams = meta.parsed?.screenParams || {};
433
437
  const screenParams = {
@@ -486,7 +490,7 @@ function secondsBetween(startedAt, endedAt) {
486
490
  return Math.max(1, Math.round((endMs - startMs) / 1000));
487
491
  }
488
492
 
489
- function normalizeLegacyProgress(progress = {}, summary = null) {
493
+ function normalizeLegacyProgress(progress = {}, summary = null) {
490
494
  const processed = Number.isInteger(progress.processed)
491
495
  ? progress.processed
492
496
  : Number.isInteger(summary?.processed)
@@ -511,146 +515,146 @@ function normalizeLegacyProgress(progress = {}, summary = null) {
511
515
  skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
512
516
  greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
513
517
  post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
514
- };
515
- }
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
-
543
- function compactSmallRecord(value, fallback = null) {
544
- const record = plainRecord(value);
545
- if (!Object.keys(record).length) return fallback;
546
- return clonePlain(record, fallback);
547
- }
548
-
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;
571
- }
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
- };
582
- }
583
- if (summary.viewport_health?.stats) {
584
- compact.viewport_health = {
585
- stats: clonePlain(summary.viewport_health.stats, {})
586
- };
587
- }
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
- };
594
- }
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
- };
603
- }
604
- return compact;
605
- }
606
-
607
- function compactRecommendCheckpointForStatus(checkpoint) {
608
- if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return {};
609
- const compact = {};
610
- if (checkpoint.updatedAt || checkpoint.updated_at) {
611
- compact.updatedAt = normalizeText(checkpoint.updatedAt || checkpoint.updated_at);
612
- }
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;
617
- }
618
- for (const key of STATUS_COUNT_KEYS) {
619
- if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
620
- }
621
- return compact;
622
- }
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;
635
- }
636
- delete compact.results;
637
- return compact;
638
- }
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);
648
- return compact;
649
- }
650
-
651
- function completionReason(status) {
652
- if (status === RUN_STATUS_COMPLETED) return "completed";
653
- if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
518
+ };
519
+ }
520
+
521
+ const STATUS_COUNT_KEYS = [
522
+ "processed",
523
+ "screened",
524
+ "detail_opened",
525
+ "passed",
526
+ "skipped",
527
+ "llm_screened",
528
+ "greet_count",
529
+ "post_action_clicked",
530
+ "image_capture_failed",
531
+ "detail_open_failed",
532
+ "transient_recovered",
533
+ "colleague_contact_checked",
534
+ "recent_colleague_contact_skipped",
535
+ "colleague_contact_panel_missing",
536
+ "context_recoveries",
537
+ "human_rest_count",
538
+ "human_rest_ms",
539
+ "card_count",
540
+ "refresh_rounds"
541
+ ];
542
+
543
+ function compactPositiveInteger(value, fallback = null) {
544
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
545
+ }
546
+
547
+ function compactSmallRecord(value, fallback = null) {
548
+ const record = plainRecord(value);
549
+ if (!Object.keys(record).length) return fallback;
550
+ return clonePlain(record, fallback);
551
+ }
552
+
553
+ function compactRecommendSummaryForStatus(summary) {
554
+ if (!summary || typeof summary !== "object" || Array.isArray(summary)) return null;
555
+ const compact = {
556
+ domain: normalizeText(summary.domain || "recommend") || "recommend"
557
+ };
558
+ for (const key of STATUS_COUNT_KEYS) {
559
+ if (Number.isInteger(summary[key])) compact[key] = summary[key];
560
+ }
561
+ if (summary.target_url) compact.target_url = normalizeText(summary.target_url);
562
+ if (summary.list_end_reason) compact.list_end_reason = normalizeText(summary.list_end_reason);
563
+ if (Array.isArray(summary.results)) {
564
+ compact.results_count = summary.results.length;
565
+ } else if (Number.isInteger(summary.results_count)) {
566
+ compact.results_count = summary.results_count;
567
+ }
568
+ if (Array.isArray(summary.refresh_attempts)) {
569
+ compact.refresh_attempt_count = summary.refresh_attempts.length;
570
+ }
571
+ if (Array.isArray(summary.context_recoveries)) {
572
+ compact.context_recovery_count = summary.context_recoveries.length;
573
+ } else if (Number.isInteger(summary.context_recoveries)) {
574
+ compact.context_recoveries = summary.context_recoveries;
575
+ }
576
+ if (summary.job_selection) compact.job_selection = compactSmallRecord(summary.job_selection);
577
+ if (summary.page_scope) compact.page_scope = compactSmallRecord(summary.page_scope);
578
+ if (summary.filter) compact.filter = compactSmallRecord(summary.filter);
579
+ if (summary.candidate_list) {
580
+ const candidateList = plainRecord(summary.candidate_list);
581
+ compact.candidate_list = {
582
+ total_count: compactPositiveInteger(candidateList.total_count ?? candidateList.card_count, null),
583
+ visible_count: compactPositiveInteger(candidateList.visible_count, null),
584
+ list_end_reason: normalizeText(candidateList.list_end_reason || "")
585
+ };
586
+ }
587
+ if (summary.viewport_health?.stats) {
588
+ compact.viewport_health = {
589
+ stats: clonePlain(summary.viewport_health.stats, {})
590
+ };
591
+ }
592
+ if (summary.human_behavior) {
593
+ compact.human_behavior = {
594
+ enabled: summary.human_behavior.enabled === true,
595
+ profile: normalizeText(summary.human_behavior.profile || ""),
596
+ restLevel: normalizeText(summary.human_behavior.restLevel || summary.human_behavior.rest_level || "")
597
+ };
598
+ }
599
+ if (summary.human_rest) {
600
+ const humanRest = plainRecord(summary.human_rest);
601
+ compact.human_rest = {
602
+ enabled: humanRest.enabled === true,
603
+ restLevel: normalizeText(humanRest.restLevel || humanRest.rest_level || ""),
604
+ rest_count: compactPositiveInteger(humanRest.rest_count ?? humanRest.count, null),
605
+ total_pause_ms: compactPositiveInteger(humanRest.total_pause_ms ?? humanRest.pause_ms, null)
606
+ };
607
+ }
608
+ return compact;
609
+ }
610
+
611
+ function compactRecommendCheckpointForStatus(checkpoint) {
612
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return {};
613
+ const compact = {};
614
+ if (checkpoint.updatedAt || checkpoint.updated_at) {
615
+ compact.updatedAt = normalizeText(checkpoint.updatedAt || checkpoint.updated_at);
616
+ }
617
+ if (Array.isArray(checkpoint.results)) {
618
+ compact.results_count = checkpoint.results.length;
619
+ } else if (Number.isInteger(checkpoint.results_count)) {
620
+ compact.results_count = checkpoint.results_count;
621
+ }
622
+ for (const key of STATUS_COUNT_KEYS) {
623
+ if (Number.isInteger(checkpoint[key])) compact[key] = checkpoint[key];
624
+ }
625
+ return compact;
626
+ }
627
+
628
+ function compactRecommendResultForStatus(result) {
629
+ if (!result || typeof result !== "object" || Array.isArray(result)) return result || null;
630
+ const compact = {
631
+ ...result
632
+ };
633
+ if (Array.isArray(result.results)) {
634
+ compact.results_count = result.results.length;
635
+ compact.results_available = result.results.length > 0;
636
+ } else if (Number.isInteger(result.results_count)) {
637
+ compact.results_count = result.results_count;
638
+ compact.results_available = result.results_available === true || result.results_count > 0;
639
+ }
640
+ delete compact.results;
641
+ return compact;
642
+ }
643
+
644
+ export function compactRecommendRunForStatus(run) {
645
+ if (!run || typeof run !== "object" || Array.isArray(run)) return run || null;
646
+ const compact = {
647
+ ...run
648
+ };
649
+ if (compact.result) compact.result = compactRecommendResultForStatus(compact.result);
650
+ if (compact.summary) compact.summary = compactRecommendSummaryForStatus(compact.summary);
651
+ if (compact.checkpoint) compact.checkpoint = compactRecommendCheckpointForStatus(compact.checkpoint);
652
+ return compact;
653
+ }
654
+
655
+ function completionReason(status) {
656
+ if (status === RUN_STATUS_COMPLETED) return "completed";
657
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
654
658
  if (status === RUN_STATUS_FAILED) return "failed";
655
659
  if (status === RUN_STATUS_PAUSED) return "paused";
656
660
  return null;
@@ -663,9 +667,9 @@ function normalizeErrorText(error = {}) {
663
667
  ].join(" "));
664
668
  }
665
669
 
666
- function classifyRecommendRecovery(error = {}) {
667
- const text = normalizeErrorText(error);
668
- if (!text) return null;
670
+ function classifyRecommendRecovery(error = {}) {
671
+ const text = normalizeErrorText(error);
672
+ if (!text) return null;
669
673
  if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
670
674
  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
675
  return "transient_stale_dom";
@@ -676,13 +680,13 @@ function classifyRecommendRecovery(error = {}) {
676
680
  if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
677
681
  return "transient_network_or_llm";
678
682
  }
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
- }
683
+ return null;
684
+ }
685
+
686
+ function isCancelShutdownError(error = {}) {
687
+ const text = normalizeErrorText(error);
688
+ 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);
689
+ }
686
690
 
687
691
  function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
688
692
  const error = snapshot?.error || snapshot?.result?.error || null;
@@ -828,14 +832,14 @@ function buildLegacyRecommendResult(snapshot) {
828
832
  effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
829
833
  },
830
834
  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
- }
835
+ screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
836
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
837
+ error: snapshot.error || null,
838
+ recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
839
+ results_count: resultRows.length,
840
+ results_available: resultRows.length > 0
841
+ };
842
+ }
839
843
 
840
844
  function normalizeRunSnapshot(snapshot) {
841
845
  if (!snapshot) return null;
@@ -858,12 +862,12 @@ function normalizeRunSnapshot(snapshot) {
858
862
  follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
859
863
  target_count_semantics: TARGET_COUNT_SEMANTICS
860
864
  };
861
- return {
862
- ...snapshot,
863
- checkpoint: compactRecommendCheckpointForStatus(snapshot.checkpoint),
864
- summary: compactRecommendSummaryForStatus(summary),
865
- progress,
866
- run_id: snapshot.runId,
865
+ return {
866
+ ...snapshot,
867
+ checkpoint: compactRecommendCheckpointForStatus(snapshot.checkpoint),
868
+ summary: compactRecommendSummaryForStatus(summary),
869
+ progress,
870
+ run_id: snapshot.runId,
867
871
  mode: RUN_MODE_ASYNC,
868
872
  state: snapshot.status,
869
873
  stage: snapshot.phase,
@@ -900,31 +904,31 @@ function normalizeRunSnapshot(snapshot) {
900
904
  };
901
905
  }
902
906
 
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,
907
+ function mergePersistedControlRequest(normalized, existing) {
908
+ const control = {
909
+ ...(normalized?.control || {})
910
+ };
911
+ const existingControl = plainRecord(existing?.control);
912
+ if (!normalized) return control;
913
+ if (TERMINAL_STATUSES.has(normalized.state)) {
914
+ if (
915
+ normalized.state === RUN_STATUS_FAILED
916
+ && existingControl.cancel_requested === true
917
+ && isCancelShutdownError(normalized.error || normalized.result?.error || "")
918
+ ) {
919
+ return {
920
+ ...control,
921
+ pause_requested: true,
922
+ pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
923
+ pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
924
+ cancel_requested: true
925
+ };
926
+ }
927
+ return control;
928
+ }
929
+ if (existingControl.cancel_requested === true) {
930
+ return {
931
+ ...control,
928
932
  pause_requested: true,
929
933
  pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
930
934
  pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
@@ -947,81 +951,81 @@ function mergePersistedControlRequest(normalized, existing) {
947
951
  pause_requested_by: null,
948
952
  cancel_requested: false
949
953
  };
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);
1020
- normalized.control = mergePersistedControlRequest(normalized, existing);
1021
- normalized = coerceCanceledTerminalSnapshot(normalized, existing);
1022
- if (persistActiveCheckpoint) {
1023
- persistRecommendCheckpointSnapshot(snapshot);
1024
- }
954
+ }
955
+ return control;
956
+ }
957
+
958
+ function cancelErrorFromShutdown(shutdownError = null) {
959
+ return {
960
+ code: "PIPELINE_CANCELED",
961
+ message: "流水线已取消。",
962
+ retryable: true,
963
+ shutdown_error: shutdownError || undefined
964
+ };
965
+ }
966
+
967
+ function coerceCanceledTerminalSnapshot(normalized, existing) {
968
+ const existingControl = plainRecord(existing?.control);
969
+ const shutdownError = normalized?.error || normalized?.result?.error || null;
970
+ const shouldWrapCanceledShutdown = (
971
+ normalized
972
+ && (
973
+ (
974
+ normalized.state === RUN_STATUS_FAILED
975
+ && existingControl.cancel_requested === true
976
+ )
977
+ || normalized.state === RUN_STATUS_CANCELED
978
+ )
979
+ && isCancelShutdownError(shutdownError || "")
980
+ );
981
+ if (
982
+ !shouldWrapCanceledShutdown
983
+ ) {
984
+ return normalized;
985
+ }
986
+ const canceledError = cancelErrorFromShutdown(shutdownError);
987
+ return {
988
+ ...normalized,
989
+ state: RUN_STATUS_CANCELED,
990
+ status: RUN_STATUS_CANCELED,
991
+ last_message: "流水线已取消;取消收尾时浏览器连接已关闭。",
992
+ control: {
993
+ pause_requested: false,
994
+ pause_requested_at: null,
995
+ pause_requested_by: null,
996
+ cancel_requested: false
997
+ },
998
+ error: canceledError,
999
+ result: normalized.result ? {
1000
+ ...normalized.result,
1001
+ status: "CANCELED",
1002
+ completion_reason: "canceled_by_user",
1003
+ error: canceledError
1004
+ } : {
1005
+ status: "CANCELED",
1006
+ completion_reason: "canceled_by_user",
1007
+ error: canceledError,
1008
+ run_id: normalized.run_id,
1009
+ processed_count: normalized.progress?.processed || 0,
1010
+ screened_count: normalized.progress?.screened || normalized.progress?.processed || 0,
1011
+ passed_count: normalized.progress?.passed || 0
1012
+ }
1013
+ };
1014
+ }
1015
+
1016
+ function persistRecommendRunSnapshot(snapshot, {
1017
+ persistActiveCheckpoint = false
1018
+ } = {}) {
1019
+ let normalized = normalizeRunSnapshot(snapshot);
1020
+ if (!normalized?.run_id) return normalized;
1021
+ const artifacts = getRecommendRunArtifacts(normalized.run_id);
1022
+ if (!artifacts) return normalized;
1023
+ const existing = readJsonFile(artifacts.run_state_path);
1024
+ normalized.control = mergePersistedControlRequest(normalized, existing);
1025
+ normalized = coerceCanceledTerminalSnapshot(normalized, existing);
1026
+ if (persistActiveCheckpoint) {
1027
+ persistRecommendCheckpointSnapshot(snapshot);
1028
+ }
1025
1029
  const payload = {
1026
1030
  run_id: normalized.run_id,
1027
1031
  mode: normalized.mode,
@@ -1044,59 +1048,59 @@ function persistRecommendRunSnapshot(snapshot, {
1044
1048
  summary: normalized.summary,
1045
1049
  artifacts: normalized.artifacts
1046
1050
  };
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
- }
1069
- };
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;
1051
+ writeJsonAtomic(artifacts.run_state_path, payload);
1052
+ return normalized;
1053
+ }
1054
+
1055
+ function patchPersistedRecommendRunControl(runId, controlPatch = {}, {
1056
+ message = ""
1057
+ } = {}) {
1058
+ const artifacts = getRecommendRunArtifacts(runId);
1059
+ if (!artifacts) return null;
1060
+ const current = readJsonFile(artifacts.run_state_path);
1061
+ const state = normalizeText(current?.state || current?.status || "");
1062
+ if (!current || TERMINAL_STATUSES.has(state)) return null;
1063
+ const now = new Date().toISOString();
1064
+ const patched = {
1065
+ ...current,
1066
+ updated_at: now,
1067
+ heartbeat_at: current.heartbeat_at || now,
1068
+ last_message: message || current.last_message || "",
1069
+ control: {
1070
+ ...(current.control || {}),
1071
+ ...controlPatch
1072
+ }
1073
+ };
1074
+ writeJsonAtomic(artifacts.run_state_path, patched);
1075
+ return patched;
1076
+ }
1077
+
1078
+ function reconcilePersistedRecommendRunIfNeeded(persisted) {
1079
+ if (!persisted || typeof persisted !== "object") return persisted;
1080
+ const persistedState = normalizeText(persisted.state || persisted.status);
1081
+ if (TERMINAL_STATUSES.has(persistedState)) return persisted;
1078
1082
  if (isProcessAlive(persisted.pid)) return persisted;
1079
1083
 
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({
1084
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
1085
+ const artifacts = getRecommendRunArtifacts(runId);
1086
+ const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
1087
+ const now = new Date().toISOString();
1088
+ const cancelRequested = persisted.control?.cancel_requested === true;
1089
+ const processExitedError = {
1090
+ code: "RUN_PROCESS_EXITED",
1091
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"})。`,
1092
+ retryable: true
1093
+ };
1094
+ const error = cancelRequested
1095
+ ? cancelErrorFromShutdown(processExitedError)
1096
+ : {
1097
+ ...processExitedError,
1098
+ message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`
1099
+ };
1100
+ return persistRecommendRunSnapshot({
1097
1101
  runId,
1098
1102
  name: persisted.name || runId,
1099
- status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
1103
+ status: cancelRequested ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
1100
1104
  phase: persisted.stage || persisted.phase || "recommend:orphaned",
1101
1105
  progress: persisted.progress || {},
1102
1106
  context: persisted.context || {},
@@ -1116,19 +1120,19 @@ function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
1116
1120
  });
1117
1121
  }
1118
1122
 
1119
- function attachMethodEvidence(payload, runId) {
1120
- const meta = getRecommendRunMeta(runId);
1121
- const methodLog = meta.methodLog || [];
1122
- assertNoForbiddenCdpCalls(methodLog);
1123
- return {
1124
- ...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
1130
- };
1131
- }
1123
+ function attachMethodEvidence(payload, runId) {
1124
+ const meta = getRecommendRunMeta(runId);
1125
+ const methodLog = meta.methodLog || [];
1126
+ assertNoForbiddenCdpCalls(methodLog);
1127
+ return {
1128
+ ...payload,
1129
+ runtime_evaluate_used: false,
1130
+ method_summary: methodSummary(methodLog),
1131
+ method_log: compactMethodLogForStatus(methodLog),
1132
+ method_log_total: Array.isArray(methodLog) ? methodLog.length : 0,
1133
+ chrome: meta.chrome || null
1134
+ };
1135
+ }
1132
1136
 
1133
1137
  function compactRecommendJobListOption(option, index) {
1134
1138
  const label = normalizeText(option?.label);
@@ -1178,17 +1182,17 @@ async function readRecommendJobOptionsFromSession(session) {
1178
1182
  };
1179
1183
  }
1180
1184
 
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;
1185
+ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
1186
+ const routeGuard = guardRecommendRoute(args);
1187
+ if (routeGuard) {
1188
+ return {
1189
+ ...routeGuard,
1190
+ stage: "recommend_job_list",
1191
+ message: "list_recommend_jobs is recommend-page only. For chat-only job lists, call list_boss_chat_jobs or prepare_boss_chat_run."
1192
+ };
1193
+ }
1194
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1195
+ const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
1192
1196
  const port = parsePositiveInteger(
1193
1197
  args.port,
1194
1198
  configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
@@ -1462,146 +1466,146 @@ async function connectRecommendChromeSession({
1462
1466
  };
1463
1467
  }
1464
1468
 
1465
- function parseRecommendPipelineRequest(args = {}) {
1466
- return parseRecommendInstruction({
1467
- instruction: args.instruction,
1469
+ function parseRecommendPipelineRequest(args = {}) {
1470
+ return parseRecommendInstruction({
1471
+ instruction: args.instruction,
1468
1472
  confirmation: args.confirmation,
1469
1473
  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");
1474
+ });
1475
+ }
1476
+
1477
+ function readOwn(source, keys = []) {
1478
+ if (!source || typeof source !== "object" || Array.isArray(source)) return undefined;
1479
+ for (const key of keys) {
1480
+ if (Object.prototype.hasOwnProperty.call(source, key)) return source[key];
1481
+ }
1482
+ return undefined;
1483
+ }
1484
+
1485
+ function getExplicitRestLevel(args = {}) {
1486
+ const behavior = readOwn(args, ["human_behavior", "humanBehavior"]);
1487
+ const raw = readOwn(behavior, ["restLevel", "rest_level"]);
1488
+ const normalized = normalizeText(raw).toLowerCase();
1489
+ return {
1490
+ raw: raw ?? null,
1491
+ restLevel: REST_LEVEL_SET.has(normalized) ? normalized : null,
1492
+ valid: REST_LEVEL_SET.has(normalized),
1493
+ missing: raw === undefined || raw === null || normalizeText(raw) === ""
1494
+ };
1495
+ }
1496
+
1497
+ function buildReviewScreenParams(parsed) {
1498
+ return {
1499
+ ...(parsed.screenParams || {}),
1500
+ criteria: parsed.screenParams?.criteria || null,
1501
+ criteria_normalized: parsed.criteria_normalized || null,
1502
+ target_count: parsed.screenParams?.target_count ?? parsed.proposed_target_count ?? null,
1503
+ post_action: parsed.screenParams?.post_action || parsed.proposed_post_action || null,
1504
+ max_greet_count: parsed.screenParams?.max_greet_count ?? parsed.proposed_max_greet_count ?? null
1505
+ };
1506
+ }
1507
+
1508
+ function buildReviewPageScope(parsed) {
1509
+ return parsed.page_scope || parsed.proposed_page_scope || "recommend";
1510
+ }
1511
+
1512
+ function buildReviewJob(args = {}) {
1513
+ return normalizeText(args.confirmation?.job_value || args.overrides?.job || "") || null;
1514
+ }
1515
+
1516
+ function buildScheduleReview(args = {}) {
1517
+ const scheduleRunAt = normalizeText(args.schedule_run_at || args.scheduleRunAt || args.run_at || args.runAt);
1518
+ const scheduleDelayMinutes = args.schedule_delay_minutes ?? args.scheduleDelayMinutes;
1519
+ const scheduleDelaySeconds = args.schedule_delay_seconds ?? args.scheduleDelaySeconds;
1520
+ if (!scheduleRunAt && scheduleDelayMinutes === undefined && scheduleDelaySeconds === undefined) return null;
1521
+ return {
1522
+ schedule_run_at: scheduleRunAt || null,
1523
+ schedule_delay_minutes: scheduleDelayMinutes ?? null,
1524
+ schedule_delay_seconds: scheduleDelaySeconds ?? null
1525
+ };
1526
+ }
1527
+
1528
+ function buildRequiredConfirmations(parsed, args = {}) {
1529
+ const required = [];
1530
+ if (parsed.needs_page_confirmation) required.push("page_scope");
1527
1531
  if (parsed.needs_filters_confirmation) required.push("filters");
1528
1532
  if (parsed.needs_school_tag_confirmation) required.push("school_tag");
1529
1533
  if (parsed.needs_degree_confirmation) required.push("degree");
1530
1534
  if (parsed.needs_gender_confirmation) required.push("gender");
1531
1535
  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
- }
1536
+ if (parsed.needs_criteria_confirmation) required.push("criteria");
1537
+ if (parsed.needs_target_count_confirmation) required.push("target_count");
1538
+ if (parsed.needs_post_action_confirmation) required.push("post_action");
1539
+ if (parsed.needs_skip_recent_colleague_contacted_confirmation) required.push("skip_recent_colleague_contacted");
1540
+ if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
1547
1541
 
1548
- function buildJobPendingQuestion(args = {}) {
1549
- const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1542
+ const confirmation = args.confirmation || {};
1543
+ const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
1544
+ if (!jobValue) required.push("job");
1545
+ const restLevel = getExplicitRestLevel(args);
1546
+ if (!restLevel.valid) required.push("rest_level");
1547
+ const blocksFinalReview = required.some((field) => field !== "rest_level");
1548
+ if (confirmation.final_confirmed !== true && !blocksFinalReview) required.push("final_review");
1549
+ return Array.from(new Set(required));
1550
+ }
1551
+
1552
+ function buildJobPendingQuestion(args = {}) {
1553
+ const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
1550
1554
  return {
1551
1555
  field: "job",
1552
1556
  question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
1553
1557
  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,
1558
+ };
1559
+ }
1560
+
1561
+ function buildRestLevelPendingQuestion(args = {}) {
1562
+ const restLevel = getExplicitRestLevel(args);
1563
+ return {
1564
+ field: "rest_level",
1565
+ question: restLevel.missing
1566
+ ? "请确认本次运行休息强度 rest_level。"
1567
+ : "rest_level 只能是 low / medium / high,请重新确认本次运行休息强度。",
1568
+ value: restLevel.restLevel || restLevel.raw || null,
1569
+ options: REST_LEVEL_OPTIONS.map((value) => ({
1570
+ label: value,
1571
+ value
1572
+ }))
1573
+ };
1574
+ }
1575
+
1576
+ function buildSuspiciousFieldsQuestion(parsed) {
1577
+ return {
1578
+ field: "suspicious_fields",
1579
+ question: "检测到需要修正或明确确认的异常字段,请先修正后再启动。",
1580
+ value: parsed.suspicious_fields || []
1581
+ };
1582
+ }
1583
+
1584
+ function buildFinalReviewQuestion(parsed, args = {}) {
1585
+ const restLevel = getExplicitRestLevel(args);
1586
+ return {
1587
+ field: "final_review",
1588
+ question: "请最终确认本次推荐页筛选参数无误;确认后设置 final_confirmed=true 即可启动或创建定时任务。",
1589
+ value: {
1590
+ page_scope: buildReviewPageScope(parsed),
1591
+ job: buildReviewJob(args),
1592
+ search_params: parsed.searchParams,
1593
+ screen_params: buildReviewScreenParams(parsed),
1594
+ human_behavior: {
1595
+ restLevel: restLevel.restLevel || null
1596
+ },
1597
+ schedule: buildScheduleReview(args)
1598
+ }
1599
+ };
1600
+ }
1601
+
1602
+ function buildNeedInputResponse(parsed, args = {}) {
1603
+ return {
1604
+ status: "NEED_INPUT",
1605
+ missing_fields: parsed.missing_fields,
1606
+ required_confirmations: buildRequiredConfirmations(parsed, args),
1607
+ search_params: parsed.searchParams,
1608
+ screen_params: parsed.screenParams,
1605
1609
  pending_questions: parsed.pending_questions,
1606
1610
  review: parsed.review,
1607
1611
  error: {
@@ -1612,36 +1616,36 @@ function buildNeedInputResponse(parsed, args = {}) {
1612
1616
  };
1613
1617
  }
1614
1618
 
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 || {}),
1619
+ function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
1620
+ const pending = [...(parsed.pending_questions || [])];
1621
+ if (requiredConfirmations.includes("suspicious_fields") && !pending.some((item) => item.field === "suspicious_fields")) {
1622
+ pending.push(buildSuspiciousFieldsQuestion(parsed));
1623
+ }
1624
+ if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
1625
+ pending.push(buildJobPendingQuestion(args));
1626
+ }
1627
+ if (requiredConfirmations.includes("rest_level") && !pending.some((item) => item.field === "rest_level")) {
1628
+ pending.push(buildRestLevelPendingQuestion(args));
1629
+ }
1630
+ if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
1631
+ pending.push(buildFinalReviewQuestion(parsed, args));
1632
+ }
1633
+ return {
1634
+ status: "NEED_CONFIRMATION",
1635
+ required_confirmations: requiredConfirmations,
1636
+ page_scope: buildReviewPageScope(parsed),
1637
+ search_params: parsed.searchParams,
1638
+ screen_params: buildReviewScreenParams(parsed),
1639
+ pending_questions: pending,
1640
+ review: {
1641
+ ...(parsed.review || {}),
1638
1642
  required_confirmations: requiredConfirmations
1639
1643
  }
1640
1644
  };
1641
1645
  }
1642
-
1643
- function evaluateRecommendPipelineGate(parsed, args = {}) {
1644
- if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed, args);
1646
+
1647
+ function evaluateRecommendPipelineGate(parsed, args = {}) {
1648
+ if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed, args);
1645
1649
  const requiredConfirmations = buildRequiredConfirmations(parsed, args);
1646
1650
  if (requiredConfirmations.length) {
1647
1651
  return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
@@ -1679,7 +1683,13 @@ function buildRecommendFilter(parsed, args = {}) {
1679
1683
  return { enabled: false };
1680
1684
  }
1681
1685
 
1682
- const groups = [];
1686
+ const groups = [{
1687
+ group: "activity",
1688
+ labels: [parsed.searchParams?.activity_level || "不限"],
1689
+ selectAllLabels: false,
1690
+ allowUnlimited: true,
1691
+ verifySticky: true
1692
+ }];
1683
1693
  const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
1684
1694
  if (recentNotView.length) {
1685
1695
  groups.push({
@@ -1716,8 +1726,12 @@ function buildRecommendFilter(parsed, args = {}) {
1716
1726
  });
1717
1727
  }
1718
1728
 
1719
- return groups.length ? { filterGroups: groups } : { enabled: false };
1720
- }
1729
+ return {
1730
+ enabled: true,
1731
+ currentCityOnly: parsed.searchParams?.current_city_only === true,
1732
+ filterGroups: groups
1733
+ };
1734
+ }
1721
1735
 
1722
1736
  function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
1723
1737
  const confirmation = args.confirmation || {};
@@ -1740,24 +1754,24 @@ function normalizeRecommendStartInput(args = {}, parsed, configResolution = null
1740
1754
  targetCount,
1741
1755
  job: normalizeText(confirmation.job_value || overrides.job || ""),
1742
1756
  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
- }
1757
+ filter: buildRecommendFilter(parsed, args),
1758
+ postAction: parsed.screenParams?.post_action || "none",
1759
+ maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
1760
+ ? parsed.screenParams.max_greet_count
1761
+ : null,
1762
+ skipRecentColleagueContacted: parsed.screenParams?.skip_recent_colleague_contacted !== false,
1763
+ colleagueContactWindowDays: 14,
1764
+ screeningMode: normalizeScreeningModeArg(args)
1765
+ };
1766
+ }
1753
1767
 
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 {
1768
+ function getRunOptions(args, parsed, normalized, session, configResolution = null) {
1769
+ const slowLive = args.slow_live === true;
1770
+ const executePostAction = args.dry_run_post_action === true
1771
+ ? false
1772
+ : args.execute_post_action !== false;
1773
+ const humanBehavior = resolveHumanBehaviorForRun(args, configResolution?.config || {});
1774
+ return {
1761
1775
  client: session.client,
1762
1776
  targetUrl: RECOMMEND_TARGET_URL,
1763
1777
  criteria: normalized.criteria,
@@ -1787,8 +1801,8 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1787
1801
  executePostAction,
1788
1802
  actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
1789
1803
  actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
1790
- actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
1791
- screeningMode: normalized.screeningMode,
1804
+ actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
1805
+ screeningMode: normalized.screeningMode,
1792
1806
  llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
1793
1807
  ...configResolution.config
1794
1808
  } : null,
@@ -1800,23 +1814,23 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
1800
1814
  args.llm_image_limit,
1801
1815
  parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
1802
1816
  ),
1803
- llmImageDetail: normalizeText(
1804
- args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
1805
- ) || "low",
1806
- imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1807
- humanRestEnabled: humanBehavior.restEnabled,
1808
- humanBehavior,
1809
- skipRecentColleagueContacted: normalized.skipRecentColleagueContacted,
1810
- colleagueContactWindowDays: normalized.colleagueContactWindowDays,
1811
- name: "mcp-recommend-pipeline-run",
1812
- parsed
1813
- };
1814
- }
1817
+ llmImageDetail: normalizeText(
1818
+ args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
1819
+ ) || "low",
1820
+ imageOutputDir: resolveBossConfiguredOutputDir("", getRunsDir()),
1821
+ humanRestEnabled: humanBehavior.restEnabled,
1822
+ humanBehavior,
1823
+ skipRecentColleagueContacted: normalized.skipRecentColleagueContacted,
1824
+ colleagueContactWindowDays: normalized.colleagueContactWindowDays,
1825
+ name: "mcp-recommend-pipeline-run",
1826
+ parsed
1827
+ };
1828
+ }
1815
1829
 
1816
- function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1817
- const routeGuard = guardRecommendRoute(args);
1818
- if (routeGuard) return { response: routeGuard };
1819
- const parsed = parseRecommendPipelineRequest(args);
1830
+ function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
1831
+ const routeGuard = guardRecommendRoute(args);
1832
+ if (routeGuard) return { response: routeGuard };
1833
+ const parsed = parseRecommendPipelineRequest(args);
1820
1834
  const gate = evaluateRecommendPipelineGate(parsed, args);
1821
1835
  if (gate) return { response: gate };
1822
1836
  const configResolution = resolveBossScreeningConfig(workspaceRoot);
@@ -2002,72 +2016,72 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
2002
2016
  };
2003
2017
  }
2004
2018
 
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,
2019
+ export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
2020
+ const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
2021
+ if (prepared.response) {
2022
+ return {
2023
+ ...prepared.response,
2024
+ cron_ready: false
2025
+ };
2026
+ }
2027
+ const { parsed, normalized } = prepared;
2028
+ const nextAction = {
2029
+ immediate_run: {
2030
+ recommended_next_tool: "start_recommend_pipeline_run",
2031
+ alternate_next_tool: "run_recommend",
2032
+ same_arguments: true,
2033
+ native_mcp_required: true
2034
+ },
2035
+ scheduled_run: {
2036
+ recommended_next_tool: "schedule_recommend_pipeline_run",
2037
+ same_arguments: true,
2038
+ requires_schedule_field: true
2039
+ },
2040
+ do_not_call_prepare_again: true,
2041
+ do_not_use_cli_fallback_when_mcp_tools_available: true,
2042
+ do_not_use_terminal_or_shell: true,
2043
+ forbidden_fallbacks: [
2044
+ "run_command",
2045
+ "terminal",
2046
+ "shell",
2047
+ "CLI detached fallback",
2048
+ "manual JSON-RPC over shell",
2049
+ "npx --stdio"
2050
+ ]
2051
+ };
2052
+ const agentGuidance = {
2053
+ host_has_native_mcp: true,
2054
+ native_mcp_required_after_prepare: true,
2055
+ proof: "This prepare_recommend_pipeline_run response was returned by the MCP server, so the host can call MCP tools from this server.",
2056
+ immediate_run: {
2057
+ tool: nextAction.immediate_run.recommended_next_tool,
2058
+ alternate_tool: nextAction.immediate_run.alternate_next_tool,
2059
+ same_arguments: true
2060
+ },
2061
+ scheduled_run: {
2062
+ tool: nextAction.scheduled_run.recommended_next_tool,
2063
+ same_arguments: true
2064
+ },
2065
+ trae_cn: {
2066
+ next_step: "Call boss-recommend/start_recommend_pipeline_run or boss-recommend/run_recommend through the native MCP tool interface.",
2067
+ never_use_terminal_fallback_after_prepare: true
2068
+ },
2069
+ forbidden_when_mcp_tools_are_available: nextAction.forbidden_fallbacks
2070
+ };
2071
+ return {
2072
+ status: "READY",
2073
+ cron_ready: true,
2074
+ prepared_only: true,
2075
+ run_started: false,
2076
+ recommended_next_tool: nextAction.immediate_run.recommended_next_tool,
2077
+ alternate_next_tool: nextAction.immediate_run.alternate_next_tool,
2078
+ next_action: nextAction,
2079
+ agent_guidance: agentGuidance,
2080
+ 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.",
2081
+ review: parsed.review,
2082
+ post_action: {
2083
+ requested: normalized.postAction,
2084
+ execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
2071
2085
  max_greet_count: normalized.maxGreetCount
2072
2086
  },
2073
2087
  target_count_semantics: TARGET_COUNT_SEMANTICS
@@ -2093,19 +2107,19 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
2093
2107
  };
2094
2108
  }
2095
2109
  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 {
2103
- const persisted = readRecommendRunState(runId);
2104
- if (persisted) {
2105
- const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2106
- return {
2107
- status: "RUN_STATUS",
2108
- run: reconciled,
2110
+ const run = recommendRunService.getRecommendRun(runId);
2111
+ const normalizedRun = persistRecommendRunSnapshot(run);
2112
+ return attachMethodEvidence({
2113
+ status: "RUN_STATUS",
2114
+ run: compactRecommendRunForStatus(normalizedRun)
2115
+ }, runId);
2116
+ } catch {
2117
+ const persisted = readRecommendRunState(runId);
2118
+ if (persisted) {
2119
+ const reconciled = compactRecommendRunForStatus(reconcilePersistedRecommendRunIfNeeded(persisted));
2120
+ return {
2121
+ status: "RUN_STATUS",
2122
+ run: reconciled,
2109
2123
  persistence: {
2110
2124
  source: "disk",
2111
2125
  active_control_available: false,
@@ -2133,36 +2147,36 @@ export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
2133
2147
  try {
2134
2148
  const before = recommendRunService.getRecommendRun(runId);
2135
2149
  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);
2150
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2151
+ return attachMethodEvidence({
2152
+ status: "PAUSE_IGNORED",
2153
+ run: compactRecommendRunForStatus(normalizedBefore),
2154
+ message: "目标任务已结束,无需暂停。"
2155
+ }, runId);
2142
2156
  }
2143
2157
  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);
2158
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2159
+ return attachMethodEvidence({
2160
+ status: "PAUSE_IGNORED",
2161
+ run: compactRecommendRunForStatus(normalizedBefore),
2162
+ message: "目标任务已经处于 paused 状态。"
2163
+ }, runId);
2150
2164
  }
2151
2165
  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);
2166
+ const normalizedRun = persistRecommendRunSnapshot(run);
2167
+ return attachMethodEvidence({
2168
+ status: "PAUSE_REQUESTED",
2169
+ run: compactRecommendRunForStatus(normalizedRun),
2170
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
2171
+ }, runId);
2158
2172
  } catch {
2159
2173
  const persisted = readRecommendRunState(runId);
2160
2174
  if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2161
- return {
2162
- status: "PAUSE_IGNORED",
2163
- run: compactRecommendRunForStatus(persisted),
2164
- message: "目标任务已结束,无需暂停。",
2165
- runtime_evaluate_used: false,
2175
+ return {
2176
+ status: "PAUSE_IGNORED",
2177
+ run: compactRecommendRunForStatus(persisted),
2178
+ message: "目标任务已结束,无需暂停。",
2179
+ runtime_evaluate_used: false,
2166
2180
  method_summary: {},
2167
2181
  method_log: [],
2168
2182
  chrome: null
@@ -2177,28 +2191,28 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2177
2191
  try {
2178
2192
  const before = recommendRunService.getRecommendRun(runId);
2179
2193
  if (TERMINAL_STATUSES.has(before.status)) {
2180
- const normalizedBefore = persistRecommendRunSnapshot(before);
2181
- return attachMethodEvidence({
2182
- status: "FAILED",
2194
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2195
+ return attachMethodEvidence({
2196
+ status: "FAILED",
2183
2197
  error: {
2184
2198
  code: "RUN_ALREADY_TERMINATED",
2185
2199
  message: "目标任务已结束,无法继续。",
2186
2200
  retryable: false
2187
2201
  },
2188
- run: compactRecommendRunForStatus(normalizedBefore)
2189
- }, runId);
2202
+ run: compactRecommendRunForStatus(normalizedBefore)
2203
+ }, runId);
2190
2204
  }
2191
2205
  if (before.status !== RUN_STATUS_PAUSED) {
2192
- const normalizedBefore = persistRecommendRunSnapshot(before);
2193
- return attachMethodEvidence({
2194
- status: "FAILED",
2206
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2207
+ return attachMethodEvidence({
2208
+ status: "FAILED",
2195
2209
  error: {
2196
2210
  code: "RUN_NOT_PAUSED",
2197
2211
  message: "仅 paused 状态的 run 才能继续。",
2198
2212
  retryable: true
2199
2213
  },
2200
- run: compactRecommendRunForStatus(normalizedBefore)
2201
- }, runId);
2214
+ run: compactRecommendRunForStatus(normalizedBefore)
2215
+ }, runId);
2202
2216
  }
2203
2217
  const run = recommendRunService.resumeRecommendRun(runId);
2204
2218
  const meta = getRecommendRunMeta(runId);
@@ -2206,12 +2220,12 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2206
2220
  meta.resumeCount = (meta.resumeCount || 0) + 1;
2207
2221
  meta.lastResumedAt = new Date().toISOString();
2208
2222
  }
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 按需轮询。"
2223
+ const normalizedRun = persistRecommendRunSnapshot(run);
2224
+ return attachMethodEvidence({
2225
+ status: "RESUME_REQUESTED",
2226
+ run: compactRecommendRunForStatus(normalizedRun),
2227
+ poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
2228
+ message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
2215
2229
  }, runId);
2216
2230
  } catch {
2217
2231
  const persisted = readRecommendRunState(runId);
@@ -2225,9 +2239,9 @@ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
2225
2239
  : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
2226
2240
  retryable: !TERMINAL_STATUSES.has(persisted.state)
2227
2241
  },
2228
- run: compactRecommendRunForStatus(persisted),
2229
- persistence: {
2230
- source: "disk",
2242
+ run: compactRecommendRunForStatus(persisted),
2243
+ persistence: {
2244
+ source: "disk",
2231
2245
  active_control_available: false
2232
2246
  },
2233
2247
  runtime_evaluate_used: false,
@@ -2245,61 +2259,61 @@ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
2245
2259
  try {
2246
2260
  const before = recommendRunService.getRecommendRun(runId);
2247
2261
  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);
2262
+ const normalizedBefore = persistRecommendRunSnapshot(before);
2263
+ return attachMethodEvidence({
2264
+ status: "CANCEL_IGNORED",
2265
+ run: compactRecommendRunForStatus(normalizedBefore),
2266
+ message: "目标任务已结束,无需取消。"
2267
+ }, runId);
2254
2268
  }
2255
2269
  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);
2270
+ const normalizedRun = persistRecommendRunSnapshot(run);
2271
+ return attachMethodEvidence({
2272
+ status: "CANCEL_REQUESTED",
2273
+ run: compactRecommendRunForStatus(normalizedRun),
2274
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
2275
+ }, runId);
2262
2276
  } catch {
2263
2277
  const persisted = readRecommendRunState(runId);
2264
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2265
- return {
2266
- status: "CANCEL_IGNORED",
2267
- run: compactRecommendRunForStatus(persisted),
2268
- message: "目标任务已结束,无需取消。",
2269
- runtime_evaluate_used: false,
2278
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
2279
+ return {
2280
+ status: "CANCEL_IGNORED",
2281
+ run: compactRecommendRunForStatus(persisted),
2282
+ message: "目标任务已结束,无需取消。",
2283
+ runtime_evaluate_used: false,
2270
2284
  method_summary: {},
2271
2285
  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
2283
- });
2284
- if (patched) {
2285
- return {
2286
- status: "CANCEL_REQUESTED",
2287
- run: compactRecommendRunForStatus(patched),
2288
- message: cancelMessage,
2289
- persistence: {
2290
- source: "disk",
2291
- active_control_available: false,
2292
- 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
- }
2286
+ chrome: null
2287
+ };
2288
+ }
2289
+ const cancelMessage = "已收到取消请求,将由 detached worker 在下一个安全边界停止。";
2290
+ const patched = patchPersistedRecommendRunControl(runId, {
2291
+ pause_requested: true,
2292
+ pause_requested_at: new Date().toISOString(),
2293
+ pause_requested_by: "cancel_recommend_pipeline_run",
2294
+ cancel_requested: true
2295
+ }, {
2296
+ message: cancelMessage
2297
+ });
2298
+ if (patched) {
2299
+ return {
2300
+ status: "CANCEL_REQUESTED",
2301
+ run: compactRecommendRunForStatus(patched),
2302
+ message: cancelMessage,
2303
+ persistence: {
2304
+ source: "disk",
2305
+ active_control_available: false,
2306
+ detached_control_requested: true
2307
+ },
2308
+ runtime_evaluate_used: false,
2309
+ method_summary: {},
2310
+ method_log: [],
2311
+ chrome: null
2312
+ };
2313
+ }
2314
+ return getRecommendPipelineRunTool({ args });
2315
+ }
2316
+ }
2303
2317
 
2304
2318
  export function getRecommendMcpHealthSnapshot(runId) {
2305
2319
  const meta = getRecommendRunMeta(runId);