@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.0

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 (85) hide show
  1. package/README.md +53 -33
  2. package/package.json +61 -9
  3. package/skills/boss-recommend-pipeline/SKILL.md +4 -0
  4. package/src/chat-mcp.js +1333 -0
  5. package/src/chat-runtime-config.js +559 -0
  6. package/src/cli.js +1095 -196
  7. package/src/core/browser/index.js +378 -0
  8. package/src/core/capture/index.js +298 -0
  9. package/src/core/cv-acquisition/index.js +219 -0
  10. package/src/core/greet-quota/index.js +54 -0
  11. package/src/core/infinite-list/index.js +459 -0
  12. package/src/core/reporting/legacy-csv.js +332 -0
  13. package/src/core/run/index.js +286 -0
  14. package/src/core/screening/index.js +1166 -0
  15. package/src/core/self-heal/index.js +848 -0
  16. package/src/domains/chat/cards.js +129 -0
  17. package/src/domains/chat/constants.js +183 -0
  18. package/src/domains/chat/detail.js +1369 -0
  19. package/src/domains/chat/index.js +7 -0
  20. package/src/domains/chat/jobs.js +334 -0
  21. package/src/domains/chat/page-guard.js +88 -0
  22. package/src/domains/chat/roots.js +56 -0
  23. package/src/domains/chat/run-service.js +1101 -0
  24. package/src/domains/recommend/actions.js +457 -0
  25. package/src/domains/recommend/cards.js +228 -0
  26. package/src/domains/recommend/constants.js +141 -0
  27. package/src/domains/recommend/detail.js +341 -0
  28. package/src/domains/recommend/filters.js +581 -0
  29. package/src/domains/recommend/index.js +10 -0
  30. package/src/domains/recommend/jobs.js +232 -0
  31. package/src/domains/recommend/refresh.js +204 -0
  32. package/src/domains/recommend/roots.js +78 -0
  33. package/src/domains/recommend/run-service.js +903 -0
  34. package/src/domains/recommend/scopes.js +245 -0
  35. package/src/domains/recruit/actions.js +277 -0
  36. package/src/domains/recruit/cards.js +67 -0
  37. package/src/domains/recruit/constants.js +130 -0
  38. package/src/domains/recruit/detail.js +414 -0
  39. package/src/domains/recruit/index.js +9 -0
  40. package/src/domains/recruit/instruction-parser.js +451 -0
  41. package/src/domains/recruit/refresh.js +40 -0
  42. package/src/domains/recruit/roots.js +68 -0
  43. package/src/domains/recruit/run-service.js +580 -0
  44. package/src/domains/recruit/search.js +1149 -0
  45. package/src/index.js +578 -419
  46. package/src/recommend-mcp.js +1257 -0
  47. package/src/recruit-mcp.js +1035 -0
  48. package/src/adapters.js +0 -3079
  49. package/src/boss-chat.js +0 -1037
  50. package/src/pipeline.js +0 -2249
  51. package/src/recommend-healing-config.js +0 -131
  52. package/src/recommend-healing-rules.json +0 -261
  53. package/src/self-heal.js +0 -2237
  54. package/src/test-adapters-runtime.js +0 -628
  55. package/src/test-boss-chat.js +0 -3196
  56. package/src/test-index-async.js +0 -498
  57. package/src/test-parser.js +0 -742
  58. package/src/test-pipeline.js +0 -2703
  59. package/src/test-run-state.js +0 -152
  60. package/src/test-self-heal.js +0 -224
  61. package/vendor/boss-chat-cli/README.md +0 -134
  62. package/vendor/boss-chat-cli/package.json +0 -53
  63. package/vendor/boss-chat-cli/src/app.js +0 -1501
  64. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  65. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  66. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  67. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  68. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  69. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  70. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  71. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  72. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  73. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  74. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  75. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  76. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  77. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  78. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  79. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  80. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  81. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  82. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  83. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  84. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  85. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
@@ -0,0 +1,1257 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import {
5
+ assertNoForbiddenCdpCalls,
6
+ bringPageToFront,
7
+ connectToChromeTarget,
8
+ enableDomains,
9
+ sleep
10
+ } from "./core/browser/index.js";
11
+ import {
12
+ RUN_STATUS_CANCELING,
13
+ RUN_STATUS_CANCELED,
14
+ RUN_STATUS_COMPLETED,
15
+ RUN_STATUS_FAILED,
16
+ RUN_STATUS_PAUSED
17
+ } from "./core/run/index.js";
18
+ import {
19
+ buildLegacyScreenInputRows,
20
+ cloneReportInput,
21
+ writeLegacyScreenCsv
22
+ } from "./core/reporting/legacy-csv.js";
23
+ import {
24
+ buildRecommendSelfHealConfig,
25
+ HEALTH_STATUS,
26
+ resolveRecommendSelfHealRoots,
27
+ runSelfHealCheck
28
+ } from "./core/self-heal/index.js";
29
+ import {
30
+ closeRecommendJobDropdown,
31
+ closeRecommendDetail,
32
+ createRecommendRunService,
33
+ getRecommendRoots,
34
+ listRecommendJobOptions,
35
+ RECOMMEND_TARGET_URL,
36
+ runRecommendWorkflow
37
+ } from "./domains/recommend/index.js";
38
+ import {
39
+ parseRecommendInstruction
40
+ } from "./parser.js";
41
+ import { getRunsDir } from "./run-state.js";
42
+
43
+ const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
44
+ const DEFAULT_RECOMMEND_PORT = 9222;
45
+ const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
46
+ const TARGET_COUNT_SEMANTICS = "target_count means processed recommend candidates, not passed candidates";
47
+ const RUN_MODE_ASYNC = "async";
48
+
49
+ const TERMINAL_STATUSES = new Set([
50
+ RUN_STATUS_COMPLETED,
51
+ RUN_STATUS_FAILED,
52
+ RUN_STATUS_CANCELED
53
+ ]);
54
+
55
+ let recommendWorkflowImpl = runRecommendWorkflow;
56
+ let recommendConnectorImpl = connectRecommendChromeSession;
57
+ let recommendJobReaderImpl = readRecommendJobOptionsFromSession;
58
+ let recommendRunService = createRecommendRunService({
59
+ idPrefix: "mcp_recommend",
60
+ workflow: (...args) => recommendWorkflowImpl(...args)
61
+ });
62
+ const recommendRunMeta = new Map();
63
+
64
+ function normalizeText(value) {
65
+ return String(value || "").replace(/\s+/g, " ").trim();
66
+ }
67
+
68
+ function parsePositiveInteger(raw, fallback) {
69
+ const parsed = Number.parseInt(String(raw || ""), 10);
70
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
71
+ }
72
+
73
+ function parseNonNegativeInteger(raw, fallback) {
74
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
75
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
76
+ }
77
+
78
+ function methodSummary(methodLog = []) {
79
+ const summary = {};
80
+ for (const entry of methodLog || []) {
81
+ summary[entry.method] = (summary[entry.method] || 0) + 1;
82
+ }
83
+ return summary;
84
+ }
85
+
86
+ function clonePlain(value, fallback = null) {
87
+ try {
88
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
89
+ } catch {
90
+ return fallback;
91
+ }
92
+ }
93
+
94
+ function normalizeRunId(runId) {
95
+ const normalized = normalizeText(runId);
96
+ if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
97
+ return normalized;
98
+ }
99
+
100
+ function getRecommendRunArtifacts(runId) {
101
+ const normalized = normalizeRunId(runId);
102
+ if (!normalized) return null;
103
+ const runsDir = getRunsDir();
104
+ return {
105
+ runs_dir: runsDir,
106
+ run_state_path: path.join(runsDir, `${normalized}.json`),
107
+ checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
108
+ output_csv: path.join(runsDir, `${normalized}.results.csv`),
109
+ report_json: path.join(runsDir, `${normalized}.report.json`)
110
+ };
111
+ }
112
+
113
+ function ensureDirectory(dirPath) {
114
+ fs.mkdirSync(dirPath, { recursive: true });
115
+ }
116
+
117
+ function writeJsonAtomic(filePath, payload) {
118
+ ensureDirectory(path.dirname(filePath));
119
+ const tempPath = `${filePath}.tmp`;
120
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
121
+ fs.renameSync(tempPath, filePath);
122
+ }
123
+
124
+ function readJsonFile(filePath) {
125
+ try {
126
+ if (!fs.existsSync(filePath)) return null;
127
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
128
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function recommendSearchParamsForCsv(searchParams = {}) {
135
+ return {
136
+ school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
137
+ degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
138
+ gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
139
+ recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限"
140
+ };
141
+ }
142
+
143
+ function selectedRecommendJobForCsv(meta = {}) {
144
+ const value = normalizeText(
145
+ meta.args?.confirmation?.job_value
146
+ || meta.normalized?.job
147
+ || meta.args?.overrides?.job
148
+ || ""
149
+ );
150
+ return {
151
+ value,
152
+ title: value,
153
+ label: value
154
+ };
155
+ }
156
+
157
+ function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
158
+ const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {});
159
+ const screenParams = meta.parsed?.screenParams || {};
160
+ return buildLegacyScreenInputRows({
161
+ instruction: meta.args?.instruction || "",
162
+ selectedPage: "recommend",
163
+ selectedJob: selectedRecommendJobForCsv(meta),
164
+ userSearchParams: cloneReportInput(searchParams, {}),
165
+ effectiveSearchParams: cloneReportInput(searchParams, {}),
166
+ screenParams: {
167
+ criteria: screenParams.criteria || meta.normalized?.criteria || "",
168
+ target_count: screenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || "",
169
+ post_action: screenParams.post_action || "none",
170
+ max_greet_count: screenParams.max_greet_count ?? ""
171
+ },
172
+ followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || null
173
+ });
174
+ }
175
+
176
+ function writeRecommendLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
177
+ writeLegacyScreenCsv(filePath, {
178
+ inputRows: buildRecommendCsvInputRows(snapshot, meta),
179
+ results: rows
180
+ });
181
+ }
182
+
183
+ function readRecommendRunState(runId) {
184
+ const artifacts = getRecommendRunArtifacts(runId);
185
+ if (!artifacts) return null;
186
+ return readJsonFile(artifacts.run_state_path);
187
+ }
188
+
189
+ function getRecommendRunMeta(runId) {
190
+ return recommendRunMeta.get(runId) || {};
191
+ }
192
+
193
+ function toIsoOrNull(value) {
194
+ const normalized = normalizeText(value);
195
+ return normalized || null;
196
+ }
197
+
198
+ function secondsBetween(startedAt, endedAt) {
199
+ const startMs = Date.parse(startedAt || "");
200
+ const endMs = Date.parse(endedAt || "") || Date.now();
201
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
202
+ return Math.max(1, Math.round((endMs - startMs) / 1000));
203
+ }
204
+
205
+ function normalizeLegacyProgress(progress = {}, summary = null) {
206
+ const processed = Number.isInteger(progress.processed)
207
+ ? progress.processed
208
+ : Number.isInteger(summary?.processed)
209
+ ? summary.processed
210
+ : 0;
211
+ const screened = Number.isInteger(progress.screened)
212
+ ? progress.screened
213
+ : Number.isInteger(summary?.screened)
214
+ ? summary.screened
215
+ : processed;
216
+ const passed = Number.isInteger(progress.passed)
217
+ ? progress.passed
218
+ : Number.isInteger(summary?.passed)
219
+ ? summary.passed
220
+ : 0;
221
+ return {
222
+ ...progress,
223
+ processed,
224
+ inspected: processed,
225
+ screened,
226
+ passed,
227
+ skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
228
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
229
+ post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
230
+ };
231
+ }
232
+
233
+ function completionReason(status) {
234
+ if (status === RUN_STATUS_COMPLETED) return "completed";
235
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
236
+ if (status === RUN_STATUS_FAILED) return "failed";
237
+ if (status === RUN_STATUS_PAUSED) return "paused";
238
+ return null;
239
+ }
240
+
241
+ function ensureRecommendRunArtifacts(snapshot) {
242
+ const artifacts = getRecommendRunArtifacts(snapshot?.runId || snapshot?.run_id);
243
+ if (!artifacts) return null;
244
+
245
+ const meta = getRecommendRunMeta(snapshot?.runId || snapshot?.run_id);
246
+ const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
247
+ ? snapshot.checkpoint
248
+ : {};
249
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
250
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
251
+
252
+ const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
253
+ if (summary) {
254
+ const rows = Array.isArray(summary.results) ? summary.results : [];
255
+ writeRecommendLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
256
+ writeJsonAtomic(artifacts.report_json, {
257
+ run_id: snapshot.runId || snapshot.run_id,
258
+ status: snapshot.status || snapshot.state,
259
+ phase: snapshot.phase || snapshot.stage,
260
+ progress: snapshot.progress || {},
261
+ context: snapshot.context || {},
262
+ checkpoint,
263
+ summary,
264
+ generated_at: new Date().toISOString()
265
+ });
266
+ if (meta) {
267
+ meta.outputCsvPath = artifacts.output_csv;
268
+ meta.reportJsonPath = artifacts.report_json;
269
+ }
270
+ }
271
+
272
+ return artifacts;
273
+ }
274
+
275
+ function buildLegacyRecommendResult(snapshot) {
276
+ if (!snapshot) return null;
277
+ const artifacts = ensureRecommendRunArtifacts(snapshot);
278
+ const meta = getRecommendRunMeta(snapshot.runId);
279
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
280
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
281
+ const targetCount = Number.isInteger(progress.target_count)
282
+ ? progress.target_count
283
+ : Number.isInteger(snapshot.context?.max_candidates)
284
+ ? snapshot.context.max_candidates
285
+ : meta.parsed?.screenParams?.target_count || null;
286
+ return {
287
+ status: snapshot.status === RUN_STATUS_COMPLETED
288
+ ? "COMPLETED"
289
+ : snapshot.status === RUN_STATUS_CANCELED
290
+ ? "CANCELED"
291
+ : snapshot.status === RUN_STATUS_PAUSED
292
+ ? "PAUSED"
293
+ : snapshot.status === RUN_STATUS_FAILED
294
+ ? "FAILED"
295
+ : snapshot.status,
296
+ run_id: snapshot.runId,
297
+ completion_reason: completionReason(snapshot.status),
298
+ requested_count: targetCount,
299
+ processed_count: progress.processed,
300
+ inspected_count: progress.processed,
301
+ screened_count: progress.screened,
302
+ passed_count: progress.passed,
303
+ skipped_count: progress.skipped,
304
+ detail_opened: progress.detail_opened || summary?.detail_opened || 0,
305
+ greet_count: progress.greet_count || 0,
306
+ post_action_clicked: progress.post_action_clicked || summary?.post_action_clicked || 0,
307
+ output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
308
+ report_json: artifacts?.report_json || meta.reportJsonPath || null,
309
+ checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
310
+ started_at: snapshot.startedAt,
311
+ completed_at: snapshot.completedAt || null,
312
+ duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
313
+ selected_job: {
314
+ title: meta.normalized?.job || meta.args?.confirmation?.job_value || meta.args?.overrides?.job || ""
315
+ },
316
+ selected_page_scope: summary?.page_scope || {
317
+ requested_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend",
318
+ effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
319
+ },
320
+ search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
321
+ screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
322
+ target_count_semantics: TARGET_COUNT_SEMANTICS,
323
+ error: snapshot.error || null,
324
+ results: Array.isArray(summary?.results) ? summary.results : []
325
+ };
326
+ }
327
+
328
+ function normalizeRunSnapshot(snapshot) {
329
+ if (!snapshot) return null;
330
+ const meta = getRecommendRunMeta(snapshot.runId);
331
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
332
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
333
+ const legacyResult = (
334
+ TERMINAL_STATUSES.has(snapshot.status)
335
+ || snapshot.status === RUN_STATUS_PAUSED
336
+ ) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
337
+ const oldContext = {
338
+ workspace_root: meta.workspaceRoot || null,
339
+ instruction: meta.args?.instruction || "",
340
+ confirmation: clonePlain(meta.args?.confirmation || {}, {}),
341
+ overrides: clonePlain(meta.args?.overrides || {}, {}),
342
+ follow_up: clonePlain(meta.args?.follow_up || {}, {}),
343
+ target_count_semantics: TARGET_COUNT_SEMANTICS
344
+ };
345
+ return {
346
+ ...snapshot,
347
+ progress,
348
+ run_id: snapshot.runId,
349
+ mode: RUN_MODE_ASYNC,
350
+ state: snapshot.status,
351
+ stage: snapshot.phase,
352
+ started_at: snapshot.startedAt,
353
+ updated_at: snapshot.updatedAt,
354
+ completed_at: toIsoOrNull(snapshot.completedAt),
355
+ heartbeat_at: snapshot.updatedAt,
356
+ pid: process.pid || null,
357
+ last_message: snapshot.error?.message || snapshot.phase || null,
358
+ context: {
359
+ ...(snapshot.context || {}),
360
+ ...oldContext,
361
+ shared_run_context: snapshot.context || {}
362
+ },
363
+ control: {
364
+ pause_requested: snapshot.status === RUN_STATUS_PAUSED,
365
+ pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
366
+ pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_recommend_pipeline_run" : null,
367
+ cancel_requested: snapshot.status === RUN_STATUS_CANCELING
368
+ },
369
+ resume: {
370
+ checkpoint_path: legacyResult?.checkpoint_path || null,
371
+ pause_control_path: getRecommendRunArtifacts(snapshot.runId)?.run_state_path || null,
372
+ output_csv: legacyResult?.output_csv || null,
373
+ resume_count: meta.resumeCount || 0,
374
+ last_resumed_at: meta.lastResumedAt || null,
375
+ last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
376
+ },
377
+ result: legacyResult,
378
+ artifacts: getRecommendRunArtifacts(snapshot.runId)
379
+ };
380
+ }
381
+
382
+ function persistRecommendRunSnapshot(snapshot) {
383
+ const normalized = normalizeRunSnapshot(snapshot);
384
+ if (!normalized?.run_id) return normalized;
385
+ const artifacts = getRecommendRunArtifacts(normalized.run_id);
386
+ if (!artifacts) return normalized;
387
+ const payload = {
388
+ run_id: normalized.run_id,
389
+ mode: normalized.mode,
390
+ state: normalized.state,
391
+ status: normalized.status,
392
+ stage: normalized.stage,
393
+ started_at: normalized.started_at,
394
+ updated_at: normalized.updated_at,
395
+ heartbeat_at: normalized.heartbeat_at,
396
+ completed_at: normalized.completed_at,
397
+ pid: normalized.pid,
398
+ progress: normalized.progress,
399
+ last_message: normalized.last_message,
400
+ context: normalized.context,
401
+ control: normalized.control,
402
+ resume: normalized.resume,
403
+ error: normalized.error,
404
+ result: normalized.result,
405
+ summary: normalized.summary,
406
+ artifacts: normalized.artifacts
407
+ };
408
+ writeJsonAtomic(artifacts.run_state_path, payload);
409
+ return normalized;
410
+ }
411
+
412
+ function attachMethodEvidence(payload, runId) {
413
+ const meta = getRecommendRunMeta(runId);
414
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
415
+ return {
416
+ ...payload,
417
+ runtime_evaluate_used: false,
418
+ method_summary: methodSummary(meta.methodLog || []),
419
+ method_log: meta.methodLog || [],
420
+ chrome: meta.chrome || null
421
+ };
422
+ }
423
+
424
+ function compactRecommendJobListOption(option, index) {
425
+ const label = normalizeText(option?.label);
426
+ const name = normalizeText(option?.label_without_salary || label);
427
+ return {
428
+ index,
429
+ name,
430
+ label,
431
+ label_without_salary: name,
432
+ current: Boolean(option?.current),
433
+ visible: Boolean(option?.visible)
434
+ };
435
+ }
436
+
437
+ async function readRecommendJobOptionsFromSession(session) {
438
+ const client = session?.client;
439
+ if (!client) throw new Error("Recommend Chrome session is missing a CDP client");
440
+ const rootState = await getRecommendRoots(client);
441
+ const frameNodeId = rootState?.iframe?.documentNodeId;
442
+ if (!frameNodeId) throw new Error("recommendFrame iframe document was not found");
443
+
444
+ let options = [];
445
+ try {
446
+ options = await listRecommendJobOptions(client, frameNodeId, {
447
+ openDropdown: true
448
+ });
449
+ } finally {
450
+ await closeRecommendJobDropdown(client).catch(() => {});
451
+ }
452
+
453
+ const compacted = [];
454
+ const seen = new Set();
455
+ for (const option of options) {
456
+ const compact = compactRecommendJobListOption(option, compacted.length);
457
+ if (!compact.name && !compact.label) continue;
458
+ const key = `${compact.name}\n${compact.label}`;
459
+ if (seen.has(key)) continue;
460
+ seen.add(key);
461
+ compacted.push(compact);
462
+ }
463
+
464
+ return {
465
+ source: "recommend_job_dropdown",
466
+ selector: "recommend job selection dropdown",
467
+ job_options: compacted,
468
+ selected_job: compacted.find((option) => option.current) || null
469
+ };
470
+ }
471
+
472
+ export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
473
+ const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
474
+ const port = parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT);
475
+ const targetUrlIncludes = normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL;
476
+ const allowNavigate = args.allow_navigate !== false;
477
+ const slowLive = args.slow_live === true;
478
+ let session;
479
+
480
+ try {
481
+ session = await recommendConnectorImpl({
482
+ host,
483
+ port,
484
+ targetUrlIncludes,
485
+ allowNavigate,
486
+ slowLive
487
+ });
488
+
489
+ const jobs = await recommendJobReaderImpl(session, {
490
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
491
+ args: clonePlain(args, {}),
492
+ normalized: {
493
+ host,
494
+ port,
495
+ targetUrlIncludes,
496
+ allowNavigate,
497
+ slowLive
498
+ }
499
+ });
500
+ const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
501
+ assertNoForbiddenCdpCalls(session.methodLog || []);
502
+ return {
503
+ status: "OK",
504
+ stage: "recommend_job_list",
505
+ cdp_only: true,
506
+ runtime_evaluate_used: false,
507
+ page_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
508
+ job_count: jobOptions.length,
509
+ job_names: jobOptions.map((option) => option.name || option.label).filter(Boolean),
510
+ job_full_labels: jobOptions.map((option) => option.label || option.name).filter(Boolean),
511
+ job_options: jobOptions,
512
+ selected_job: jobs?.selected_job || jobOptions.find((option) => option.current) || null,
513
+ source: jobs?.source || "recommend_job_dropdown",
514
+ selector: jobs?.selector || "",
515
+ message: "已通过 CDP-only 从推荐页岗位下拉框读取可用岗位。Cron/一次性任务里的 job 参数优先使用 job_names 中的完整岗位名。",
516
+ chrome: {
517
+ host,
518
+ port,
519
+ target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
520
+ target_id: session.target?.id || null
521
+ },
522
+ method_summary: methodSummary(session.methodLog || []),
523
+ method_log: session.methodLog || []
524
+ };
525
+ } catch (error) {
526
+ const methodLog = session?.methodLog || [];
527
+ return {
528
+ status: "FAILED",
529
+ stage: "recommend_job_list",
530
+ cdp_only: true,
531
+ runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
532
+ error: {
533
+ code: "RECOMMEND_JOB_LIST_FAILED",
534
+ message: error?.message || "Failed to read recommend job list",
535
+ retryable: true
536
+ },
537
+ chrome: {
538
+ host,
539
+ port,
540
+ target_url: targetUrlIncludes
541
+ },
542
+ method_summary: methodSummary(methodLog),
543
+ method_log: methodLog
544
+ };
545
+ } finally {
546
+ if (session) {
547
+ try {
548
+ await session.close?.();
549
+ } catch {
550
+ // Best-effort cleanup after a read-only helper.
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ function compactHealth(check) {
557
+ if (!check) return null;
558
+ return {
559
+ status: check.status,
560
+ summary: check.summary,
561
+ drift_report: check.drift_report,
562
+ probes: (check.probes || []).map((probe) => ({
563
+ id: probe.id,
564
+ type: probe.type,
565
+ status: probe.status,
566
+ count: probe.count,
567
+ required: probe.required
568
+ }))
569
+ };
570
+ }
571
+
572
+ async function waitForHealthyRecommend(client, config, {
573
+ timeoutMs = 90000,
574
+ intervalMs = 1000
575
+ } = {}) {
576
+ const started = Date.now();
577
+ let lastCheck = null;
578
+ while (Date.now() - started <= timeoutMs) {
579
+ const roots = await resolveRecommendSelfHealRoots(client, config);
580
+ lastCheck = await runSelfHealCheck({
581
+ client,
582
+ domain: "recommend",
583
+ roots: roots.roots,
584
+ selectorProbes: config.selectorProbes,
585
+ accessibilityProbes: config.accessibilityProbes
586
+ });
587
+ if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
588
+ await sleep(intervalMs);
589
+ }
590
+ return lastCheck;
591
+ }
592
+
593
+ function shouldNavigateToRecommend(url) {
594
+ return !String(url || "").includes("/web/chat/recommend");
595
+ }
596
+
597
+ async function connectRecommendChromeSession({
598
+ host = DEFAULT_RECOMMEND_HOST,
599
+ port = DEFAULT_RECOMMEND_PORT,
600
+ targetUrlIncludes = RECOMMEND_TARGET_URL,
601
+ allowNavigate = true,
602
+ slowLive = false
603
+ } = {}) {
604
+ let session;
605
+ try {
606
+ session = await connectToChromeTarget({
607
+ host,
608
+ port,
609
+ targetUrlIncludes
610
+ });
611
+ } catch (error) {
612
+ if (!allowNavigate) throw error;
613
+ session = await connectToChromeTarget({
614
+ host,
615
+ port,
616
+ targetPredicate: (target) => (
617
+ target?.type === "page"
618
+ && String(target?.url || "").includes("zhipin.com/web/chat")
619
+ )
620
+ });
621
+ }
622
+
623
+ const { client, target } = session;
624
+ await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
625
+ if (typeof client?.Network?.setCacheDisabled === "function") {
626
+ await client.Network.setCacheDisabled({ cacheDisabled: true });
627
+ }
628
+ await bringPageToFront(client);
629
+
630
+ const targetUrl = String(target?.url || "");
631
+ let navigation = {
632
+ navigated: false,
633
+ url: targetUrl
634
+ };
635
+ if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
636
+ await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
637
+ const settleMs = slowLive ? 12000 : 5000;
638
+ await sleep(settleMs);
639
+ navigation = {
640
+ navigated: true,
641
+ url: RECOMMEND_TARGET_URL,
642
+ settle_ms: settleMs
643
+ };
644
+ }
645
+
646
+ const selfHealConfig = buildRecommendSelfHealConfig();
647
+ const health = await waitForHealthyRecommend(client, selfHealConfig, {
648
+ timeoutMs: slowLive ? 180000 : 90000,
649
+ intervalMs: slowLive ? 1200 : 800
650
+ });
651
+ if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
652
+ throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
653
+ }
654
+
655
+ return {
656
+ ...session,
657
+ navigation,
658
+ health
659
+ };
660
+ }
661
+
662
+ function parseRecommendPipelineRequest(args = {}) {
663
+ return parseRecommendInstruction({
664
+ instruction: args.instruction,
665
+ confirmation: args.confirmation,
666
+ overrides: args.overrides
667
+ });
668
+ }
669
+
670
+ function buildRequiredConfirmations(parsed, args = {}) {
671
+ const required = [];
672
+ if (parsed.needs_page_confirmation) required.push("page_scope");
673
+ if (parsed.needs_filters_confirmation) required.push("filters");
674
+ if (parsed.needs_school_tag_confirmation) required.push("school_tag");
675
+ if (parsed.needs_degree_confirmation) required.push("degree");
676
+ if (parsed.needs_gender_confirmation) required.push("gender");
677
+ if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
678
+ if (parsed.needs_criteria_confirmation) required.push("criteria");
679
+ if (parsed.needs_target_count_confirmation) required.push("target_count");
680
+ if (parsed.needs_post_action_confirmation) required.push("post_action");
681
+ if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
682
+ if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
683
+
684
+ const confirmation = args.confirmation || {};
685
+ const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
686
+ if (confirmation.job_confirmed !== true || !jobValue) required.push("job");
687
+ if (confirmation.final_confirmed !== true) required.push("final_review");
688
+ return Array.from(new Set(required));
689
+ }
690
+
691
+ function buildJobPendingQuestion(args = {}) {
692
+ const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
693
+ return {
694
+ field: "job",
695
+ question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
696
+ value: value || null
697
+ };
698
+ }
699
+
700
+ function buildFinalReviewQuestion(parsed) {
701
+ return {
702
+ field: "final_review",
703
+ question: "请最终确认本次推荐页筛选参数无误,并明确 final_confirmed=true 后再启动。",
704
+ value: {
705
+ page_scope: parsed.page_scope,
706
+ search_params: parsed.searchParams,
707
+ screen_params: parsed.screenParams
708
+ }
709
+ };
710
+ }
711
+
712
+ function buildNeedInputResponse(parsed) {
713
+ return {
714
+ status: "NEED_INPUT",
715
+ missing_fields: parsed.missing_fields,
716
+ required_confirmations: buildRequiredConfirmations(parsed),
717
+ search_params: parsed.searchParams,
718
+ screen_params: parsed.screenParams,
719
+ pending_questions: parsed.pending_questions,
720
+ review: parsed.review,
721
+ error: {
722
+ code: "MISSING_REQUIRED_FIELDS",
723
+ message: "缺少必要字段。请补齐推荐页 criteria 等必填字段后再启动 CDP-only recommend run。",
724
+ retryable: true
725
+ }
726
+ };
727
+ }
728
+
729
+ function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
730
+ const pending = [...(parsed.pending_questions || [])];
731
+ if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
732
+ pending.push(buildJobPendingQuestion(args));
733
+ }
734
+ if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
735
+ pending.push(buildFinalReviewQuestion(parsed));
736
+ }
737
+ return {
738
+ status: "NEED_CONFIRMATION",
739
+ required_confirmations: requiredConfirmations,
740
+ page_scope: parsed.page_scope,
741
+ search_params: parsed.searchParams,
742
+ screen_params: parsed.screenParams,
743
+ pending_questions: pending,
744
+ review: {
745
+ ...(parsed.review || {}),
746
+ required_confirmations: requiredConfirmations
747
+ }
748
+ };
749
+ }
750
+
751
+ function evaluateRecommendPipelineGate(parsed, args = {}) {
752
+ if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed);
753
+ const requiredConfirmations = buildRequiredConfirmations(parsed, args);
754
+ if (requiredConfirmations.length) {
755
+ return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
756
+ }
757
+
758
+ if (args.follow_up?.chat || args.overrides?.follow_up?.chat) {
759
+ return {
760
+ status: "FAILED",
761
+ error: {
762
+ code: "FOLLOW_UP_CHAT_NOT_CDP_REWRITTEN",
763
+ message: "recommend -> chat follow-up orchestration is legacy-only and intentionally fenced from the CDP-only MCP route. Run recommend first, then use the direct chat MCP route separately, or keep the old chained behavior in the archived legacy lane.",
764
+ retryable: true
765
+ },
766
+ review: parsed.review
767
+ };
768
+ }
769
+
770
+ return null;
771
+ }
772
+
773
+ function toArray(value) {
774
+ if (Array.isArray(value)) return value;
775
+ if (value === undefined || value === null) return [];
776
+ return [value];
777
+ }
778
+
779
+ function withoutUnlimited(values = []) {
780
+ return toArray(values)
781
+ .map((value) => normalizeText(value))
782
+ .filter((value) => value && value !== "不限" && value.toLowerCase() !== "all" && value !== "全部");
783
+ }
784
+
785
+ function buildRecommendFilter(parsed, args = {}) {
786
+ if (args.no_filter === true || args.filter_enabled === false) {
787
+ return { enabled: false };
788
+ }
789
+
790
+ const groups = [];
791
+ const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
792
+ if (recentNotView.length) {
793
+ groups.push({
794
+ group: "recentNotView",
795
+ labels: recentNotView,
796
+ selectAllLabels: true
797
+ });
798
+ }
799
+
800
+ const degree = withoutUnlimited(parsed.searchParams?.degree);
801
+ if (degree.length) {
802
+ groups.push({
803
+ group: "degree",
804
+ labels: degree,
805
+ selectAllLabels: true
806
+ });
807
+ }
808
+
809
+ const gender = withoutUnlimited(parsed.searchParams?.gender);
810
+ if (gender.length) {
811
+ groups.push({
812
+ group: "gender",
813
+ labels: gender,
814
+ selectAllLabels: true
815
+ });
816
+ }
817
+
818
+ const school = withoutUnlimited(parsed.searchParams?.school_tag);
819
+ if (school.length) {
820
+ groups.push({
821
+ group: "school",
822
+ labels: school,
823
+ selectAllLabels: true
824
+ });
825
+ }
826
+
827
+ return groups.length ? { filterGroups: groups } : { enabled: false };
828
+ }
829
+
830
+ function normalizeRecommendStartInput(args = {}, parsed) {
831
+ const confirmation = args.confirmation || {};
832
+ const overrides = args.overrides || {};
833
+ const slowLive = args.slow_live === true;
834
+ const targetCount = parsePositiveInteger(
835
+ args.max_candidates,
836
+ parsed.screenParams?.target_count || parsePositiveInteger(confirmation.target_count_value, 5)
837
+ );
838
+ return {
839
+ host: normalizeText(args.host) || DEFAULT_RECOMMEND_HOST,
840
+ port: parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT),
841
+ targetUrlIncludes: normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL,
842
+ allowNavigate: args.allow_navigate !== false,
843
+ slowLive,
844
+ criteria: parsed.screenParams?.criteria || normalizeText(overrides.criteria),
845
+ targetCount,
846
+ job: normalizeText(confirmation.job_value || overrides.job || ""),
847
+ pageScope: parsed.page_scope || "recommend",
848
+ filter: buildRecommendFilter(parsed, args),
849
+ postAction: parsed.screenParams?.post_action || "none",
850
+ maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
851
+ ? parsed.screenParams.max_greet_count
852
+ : null
853
+ };
854
+ }
855
+
856
+ function getRunOptions(args, parsed, normalized, session) {
857
+ const slowLive = args.slow_live === true;
858
+ const executePostAction = args.dry_run_post_action === true
859
+ ? false
860
+ : args.execute_post_action !== false;
861
+ return {
862
+ client: session.client,
863
+ targetUrl: RECOMMEND_TARGET_URL,
864
+ criteria: normalized.criteria,
865
+ jobLabel: normalized.job,
866
+ pageScope: normalized.pageScope,
867
+ fallbackPageScope: "recommend",
868
+ filter: normalized.filter,
869
+ maxCandidates: normalized.targetCount,
870
+ detailLimit: parseNonNegativeInteger(args.detail_limit, 0),
871
+ closeDetail: true,
872
+ delayMs: parseNonNegativeInteger(args.delay_ms, 0),
873
+ cardTimeoutMs: slowLive ? 180000 : 90000,
874
+ maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
875
+ imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
876
+ cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
877
+ listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
878
+ listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
879
+ listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
880
+ listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
881
+ listFallbackPoint: null,
882
+ refreshOnEnd: args.refresh_on_end !== false,
883
+ maxRefreshRounds: parseNonNegativeInteger(args.max_refresh_rounds, 2),
884
+ refreshButtonSettleMs: parsePositiveInteger(args.refresh_button_settle_ms, slowLive ? 10000 : 8000),
885
+ refreshReloadSettleMs: parsePositiveInteger(args.refresh_reload_settle_ms, slowLive ? 12000 : 8000),
886
+ postAction: normalized.postAction,
887
+ maxGreetCount: normalized.maxGreetCount,
888
+ executePostAction,
889
+ actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
890
+ actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
891
+ actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
892
+ name: "mcp-recommend-pipeline-run",
893
+ parsed
894
+ };
895
+ }
896
+
897
+ async function closeRecommendRunSession(runId) {
898
+ const meta = recommendRunMeta.get(runId);
899
+ if (!meta || meta.closed) return;
900
+ try {
901
+ try {
902
+ if (meta.session?.client) {
903
+ await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
904
+ }
905
+ } catch {
906
+ // Cleanup is best-effort once the run has settled.
907
+ }
908
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
909
+ } finally {
910
+ meta.closed = true;
911
+ try {
912
+ await meta.session?.close?.();
913
+ } catch {
914
+ // Nothing actionable for the caller once the run has settled.
915
+ }
916
+ }
917
+ }
918
+
919
+ async function waitForRecommendRunTerminal(runId) {
920
+ while (true) {
921
+ try {
922
+ const snapshot = recommendRunService.getRecommendRun(runId);
923
+ if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
924
+ } catch {
925
+ return null;
926
+ }
927
+ await sleep(1000);
928
+ }
929
+ }
930
+
931
+ function trackRecommendRun(runId) {
932
+ waitForRecommendRunTerminal(runId)
933
+ .then((terminal) => {
934
+ if (terminal) persistRecommendRunSnapshot(terminal);
935
+ })
936
+ .catch(() => null)
937
+ .finally(() => {
938
+ closeRecommendRunSession(runId).catch(() => {});
939
+ });
940
+ }
941
+
942
+ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
943
+ const parsed = parseRecommendPipelineRequest(args);
944
+ const gate = evaluateRecommendPipelineGate(parsed, args);
945
+ if (gate) return gate;
946
+ const normalized = normalizeRecommendStartInput(args, parsed);
947
+
948
+ let session;
949
+ try {
950
+ session = await recommendConnectorImpl({
951
+ host: normalized.host,
952
+ port: normalized.port,
953
+ targetUrlIncludes: normalized.targetUrlIncludes,
954
+ allowNavigate: normalized.allowNavigate,
955
+ slowLive: normalized.slowLive
956
+ });
957
+ } catch (error) {
958
+ return {
959
+ status: "FAILED",
960
+ error: {
961
+ code: "BOSS_RECOMMEND_PAGE_NOT_READY",
962
+ message: error?.message || "Boss recommend page is not ready",
963
+ retryable: true
964
+ }
965
+ };
966
+ }
967
+
968
+ let started;
969
+ try {
970
+ started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session));
971
+ } catch (error) {
972
+ await session.close?.();
973
+ return {
974
+ status: "FAILED",
975
+ error: {
976
+ code: "RECOMMEND_RUN_START_FAILED",
977
+ message: error?.message || "Failed to start recommend run",
978
+ retryable: true
979
+ }
980
+ };
981
+ }
982
+
983
+ recommendRunMeta.set(started.runId, {
984
+ session,
985
+ methodLog: session.methodLog || [],
986
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
987
+ args: clonePlain(args, {}),
988
+ normalized,
989
+ parsed,
990
+ chrome: {
991
+ host: normalized.host,
992
+ port: normalized.port,
993
+ target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
994
+ target_id: session.target?.id || null
995
+ },
996
+ health: session.health || null
997
+ });
998
+ trackRecommendRun(started.runId);
999
+ const persistedStarted = persistRecommendRunSnapshot(started);
1000
+
1001
+ return {
1002
+ status: "ACCEPTED",
1003
+ run_id: persistedStarted.run_id,
1004
+ state: persistedStarted.state,
1005
+ run: persistedStarted,
1006
+ poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
1007
+ review: parsed.review,
1008
+ message: normalized.postAction === "none"
1009
+ ? "Recommend pipeline run started through the shared CDP-only recommend service. No post-action was requested."
1010
+ : `Recommend pipeline run started through the shared CDP-only recommend service with post_action=${normalized.postAction}${args.dry_run_post_action === true ? " in dry-run mode" : ""}.`,
1011
+ post_action: {
1012
+ requested: normalized.postAction,
1013
+ execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
1014
+ max_greet_count: normalized.maxGreetCount
1015
+ },
1016
+ target_count_semantics: TARGET_COUNT_SEMANTICS
1017
+ };
1018
+ }
1019
+
1020
+ export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
1021
+ const started = await startRecommendPipelineRunInternal(args, { workspaceRoot });
1022
+ if (started.status !== "ACCEPTED") return started;
1023
+ return attachMethodEvidence(started, started.run_id);
1024
+ }
1025
+
1026
+ export function getRecommendPipelineRunTool({ args = {} } = {}) {
1027
+ const runId = normalizeRunId(args.run_id || args.runId);
1028
+ if (!runId) {
1029
+ return {
1030
+ status: "FAILED",
1031
+ error: {
1032
+ code: "INVALID_RUN_ID",
1033
+ message: "run_id is required",
1034
+ retryable: false
1035
+ }
1036
+ };
1037
+ }
1038
+ try {
1039
+ const run = recommendRunService.getRecommendRun(runId);
1040
+ const normalizedRun = persistRecommendRunSnapshot(run);
1041
+ return attachMethodEvidence({
1042
+ status: "RUN_STATUS",
1043
+ run: normalizedRun
1044
+ }, runId);
1045
+ } catch {
1046
+ const persisted = readRecommendRunState(runId);
1047
+ if (persisted) {
1048
+ return {
1049
+ status: "RUN_STATUS",
1050
+ run: persisted,
1051
+ persistence: {
1052
+ source: "disk",
1053
+ active_control_available: false
1054
+ },
1055
+ runtime_evaluate_used: false,
1056
+ method_summary: {},
1057
+ method_log: [],
1058
+ chrome: null
1059
+ };
1060
+ }
1061
+ return {
1062
+ status: "FAILED",
1063
+ error: {
1064
+ code: "RUN_NOT_FOUND",
1065
+ message: `No recommend run found for run_id=${runId}`,
1066
+ retryable: false
1067
+ }
1068
+ };
1069
+ }
1070
+ }
1071
+
1072
+ export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
1073
+ const runId = normalizeRunId(args.run_id || args.runId);
1074
+ try {
1075
+ const before = recommendRunService.getRecommendRun(runId);
1076
+ if (TERMINAL_STATUSES.has(before.status)) {
1077
+ const normalizedBefore = persistRecommendRunSnapshot(before);
1078
+ return attachMethodEvidence({
1079
+ status: "PAUSE_IGNORED",
1080
+ run: normalizedBefore,
1081
+ message: "目标任务已结束,无需暂停。"
1082
+ }, runId);
1083
+ }
1084
+ if (before.status === RUN_STATUS_PAUSED) {
1085
+ const normalizedBefore = persistRecommendRunSnapshot(before);
1086
+ return attachMethodEvidence({
1087
+ status: "PAUSE_IGNORED",
1088
+ run: normalizedBefore,
1089
+ message: "目标任务已经处于 paused 状态。"
1090
+ }, runId);
1091
+ }
1092
+ const run = recommendRunService.pauseRecommendRun(runId);
1093
+ const normalizedRun = persistRecommendRunSnapshot(run);
1094
+ return attachMethodEvidence({
1095
+ status: "PAUSE_REQUESTED",
1096
+ run: normalizedRun,
1097
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
1098
+ }, runId);
1099
+ } catch {
1100
+ const persisted = readRecommendRunState(runId);
1101
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1102
+ return {
1103
+ status: "PAUSE_IGNORED",
1104
+ run: persisted,
1105
+ message: "目标任务已结束,无需暂停。",
1106
+ runtime_evaluate_used: false,
1107
+ method_summary: {},
1108
+ method_log: [],
1109
+ chrome: null
1110
+ };
1111
+ }
1112
+ return getRecommendPipelineRunTool({ args });
1113
+ }
1114
+ }
1115
+
1116
+ export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
1117
+ const runId = normalizeRunId(args.run_id || args.runId);
1118
+ try {
1119
+ const before = recommendRunService.getRecommendRun(runId);
1120
+ if (TERMINAL_STATUSES.has(before.status)) {
1121
+ const normalizedBefore = persistRecommendRunSnapshot(before);
1122
+ return attachMethodEvidence({
1123
+ status: "FAILED",
1124
+ error: {
1125
+ code: "RUN_ALREADY_TERMINATED",
1126
+ message: "目标任务已结束,无法继续。",
1127
+ retryable: false
1128
+ },
1129
+ run: normalizedBefore
1130
+ }, runId);
1131
+ }
1132
+ if (before.status !== RUN_STATUS_PAUSED) {
1133
+ const normalizedBefore = persistRecommendRunSnapshot(before);
1134
+ return attachMethodEvidence({
1135
+ status: "FAILED",
1136
+ error: {
1137
+ code: "RUN_NOT_PAUSED",
1138
+ message: "仅 paused 状态的 run 才能继续。",
1139
+ retryable: true
1140
+ },
1141
+ run: normalizedBefore
1142
+ }, runId);
1143
+ }
1144
+ const run = recommendRunService.resumeRecommendRun(runId);
1145
+ const meta = getRecommendRunMeta(runId);
1146
+ if (meta) {
1147
+ meta.resumeCount = (meta.resumeCount || 0) + 1;
1148
+ meta.lastResumedAt = new Date().toISOString();
1149
+ }
1150
+ const normalizedRun = persistRecommendRunSnapshot(run);
1151
+ return attachMethodEvidence({
1152
+ status: "RESUME_REQUESTED",
1153
+ run: normalizedRun,
1154
+ poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
1155
+ message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
1156
+ }, runId);
1157
+ } catch {
1158
+ const persisted = readRecommendRunState(runId);
1159
+ if (persisted) {
1160
+ return {
1161
+ status: "FAILED",
1162
+ error: {
1163
+ code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1164
+ message: TERMINAL_STATUSES.has(persisted.state)
1165
+ ? "目标任务已结束,无法继续。"
1166
+ : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1167
+ retryable: !TERMINAL_STATUSES.has(persisted.state)
1168
+ },
1169
+ run: persisted,
1170
+ persistence: {
1171
+ source: "disk",
1172
+ active_control_available: false
1173
+ },
1174
+ runtime_evaluate_used: false,
1175
+ method_summary: {},
1176
+ method_log: [],
1177
+ chrome: null
1178
+ };
1179
+ }
1180
+ return getRecommendPipelineRunTool({ args });
1181
+ }
1182
+ }
1183
+
1184
+ export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
1185
+ const runId = normalizeRunId(args.run_id || args.runId);
1186
+ try {
1187
+ const before = recommendRunService.getRecommendRun(runId);
1188
+ if (TERMINAL_STATUSES.has(before.status)) {
1189
+ const normalizedBefore = persistRecommendRunSnapshot(before);
1190
+ return attachMethodEvidence({
1191
+ status: "CANCEL_IGNORED",
1192
+ run: normalizedBefore,
1193
+ message: "目标任务已结束,无需取消。"
1194
+ }, runId);
1195
+ }
1196
+ const run = recommendRunService.cancelRecommendRun(runId);
1197
+ const normalizedRun = persistRecommendRunSnapshot(run);
1198
+ return attachMethodEvidence({
1199
+ status: "CANCEL_REQUESTED",
1200
+ run: normalizedRun,
1201
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
1202
+ }, runId);
1203
+ } catch {
1204
+ const persisted = readRecommendRunState(runId);
1205
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1206
+ return {
1207
+ status: "CANCEL_IGNORED",
1208
+ run: persisted,
1209
+ message: "目标任务已结束,无需取消。",
1210
+ runtime_evaluate_used: false,
1211
+ method_summary: {},
1212
+ method_log: [],
1213
+ chrome: null
1214
+ };
1215
+ }
1216
+ return getRecommendPipelineRunTool({ args });
1217
+ }
1218
+ }
1219
+
1220
+ export function getRecommendMcpHealthSnapshot(runId) {
1221
+ const meta = getRecommendRunMeta(runId);
1222
+ return {
1223
+ health: compactHealth(meta.health || null),
1224
+ chrome: meta.chrome || null,
1225
+ method_summary: methodSummary(meta.methodLog || [])
1226
+ };
1227
+ }
1228
+
1229
+ export function __setRecommendMcpConnectorForTests(nextConnector) {
1230
+ recommendConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectRecommendChromeSession;
1231
+ }
1232
+
1233
+ export function __setRecommendMcpJobReaderForTests(nextReader) {
1234
+ recommendJobReaderImpl = typeof nextReader === "function" ? nextReader : readRecommendJobOptionsFromSession;
1235
+ }
1236
+
1237
+ export function __setRecommendMcpWorkflowForTests(nextWorkflow) {
1238
+ recommendWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecommendWorkflow;
1239
+ recommendRunService = createRecommendRunService({
1240
+ idPrefix: "mcp_recommend",
1241
+ workflow: (...args) => recommendWorkflowImpl(...args)
1242
+ });
1243
+ }
1244
+
1245
+ export function __resetRecommendMcpStateForTests() {
1246
+ for (const meta of recommendRunMeta.values()) {
1247
+ try {
1248
+ meta.session?.close?.();
1249
+ } catch {
1250
+ // Best-effort test cleanup.
1251
+ }
1252
+ }
1253
+ recommendRunMeta.clear();
1254
+ __setRecommendMcpConnectorForTests(null);
1255
+ __setRecommendMcpJobReaderForTests(null);
1256
+ __setRecommendMcpWorkflowForTests(null);
1257
+ }