@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,1333 @@
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
+ buildChatSelfHealConfig,
25
+ HEALTH_STATUS,
26
+ resolveChatSelfHealRoots,
27
+ runSelfHealCheck
28
+ } from "./core/self-heal/index.js";
29
+ import {
30
+ CHAT_TARGET_URL,
31
+ closeChatResumeModal,
32
+ createChatRunService,
33
+ getChatRoots,
34
+ isForbiddenChatResumeTopLevelUrl,
35
+ readChatJobOptions,
36
+ runChatWorkflow
37
+ } from "./domains/chat/index.js";
38
+ import {
39
+ buildTargetCountCompatibilityHints,
40
+ getBossChatDataDir,
41
+ getBossChatTargetCountValue,
42
+ normalizeTargetCountInput,
43
+ resolveBossChatRuntimeLayout,
44
+ resolveBossScreeningConfig
45
+ } from "./chat-runtime-config.js";
46
+
47
+ const DEFAULT_CHAT_HOST = "127.0.0.1";
48
+ const DEFAULT_CHAT_PORT = 9222;
49
+ const DEFAULT_CHAT_POLL_AFTER_SEC = 10;
50
+ const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
51
+ const CHAT_ALL_MAX_CANDIDATES = 100000;
52
+ const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; numeric targets scan until that many candidates pass or the list ends; all/全部/扫到底 scans to the end";
53
+ const RUN_MODE_ASYNC = "async";
54
+
55
+ const CHAT_REQUIRED_FIELDS = Object.freeze([
56
+ "job",
57
+ "start_from",
58
+ "target_count",
59
+ "criteria"
60
+ ]);
61
+
62
+ const TERMINAL_STATUSES = new Set([
63
+ RUN_STATUS_COMPLETED,
64
+ RUN_STATUS_FAILED,
65
+ RUN_STATUS_CANCELED
66
+ ]);
67
+
68
+ let chatWorkflowImpl = runChatWorkflow;
69
+ let chatConnectorImpl = connectChatChromeSession;
70
+ let chatJobReaderImpl = readChatJobOptionsFromSession;
71
+ let chatRunService = createChatRunService({
72
+ idPrefix: "mcp_chat",
73
+ workflow: (...args) => chatWorkflowImpl(...args)
74
+ });
75
+ const chatRunMeta = new Map();
76
+
77
+ function normalizeText(value) {
78
+ return String(value || "").replace(/\s+/g, " ").trim();
79
+ }
80
+
81
+ function parsePositiveInteger(raw, fallback) {
82
+ const parsed = Number.parseInt(String(raw || ""), 10);
83
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
84
+ }
85
+
86
+ function parseNonNegativeInteger(raw, fallback) {
87
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
88
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
89
+ }
90
+
91
+ function methodSummary(methodLog = []) {
92
+ const summary = {};
93
+ for (const entry of methodLog || []) {
94
+ summary[entry.method] = (summary[entry.method] || 0) + 1;
95
+ }
96
+ return summary;
97
+ }
98
+
99
+ function clonePlain(value, fallback = null) {
100
+ try {
101
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
102
+ } catch {
103
+ return fallback;
104
+ }
105
+ }
106
+
107
+ function normalizeRunId(runId) {
108
+ const normalized = normalizeText(runId);
109
+ if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
110
+ return normalized;
111
+ }
112
+
113
+ function getChatRunsDir() {
114
+ return path.join(getBossChatDataDir(), "runs");
115
+ }
116
+
117
+ function getChatRunArtifacts(runId) {
118
+ const normalized = normalizeRunId(runId);
119
+ if (!normalized) return null;
120
+ const runsDir = getChatRunsDir();
121
+ return {
122
+ runs_dir: runsDir,
123
+ run_state_path: path.join(runsDir, `${normalized}.json`),
124
+ checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
125
+ output_csv: path.join(runsDir, `${normalized}.results.csv`),
126
+ report_json: path.join(runsDir, `${normalized}.report.json`)
127
+ };
128
+ }
129
+
130
+ function ensureDirectory(dirPath) {
131
+ fs.mkdirSync(dirPath, { recursive: true });
132
+ }
133
+
134
+ function writeJsonAtomic(filePath, payload) {
135
+ ensureDirectory(path.dirname(filePath));
136
+ const tempPath = `${filePath}.tmp`;
137
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
138
+ fs.renameSync(tempPath, filePath);
139
+ }
140
+
141
+ function readJsonFile(filePath) {
142
+ try {
143
+ if (!fs.existsSync(filePath)) return null;
144
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
145
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function selectedChatJobForCsv(meta = {}) {
152
+ const job = normalizeText(meta.normalized?.job || meta.args?.job || "");
153
+ return {
154
+ value: job,
155
+ title: job,
156
+ label: job
157
+ };
158
+ }
159
+
160
+ function buildChatCsvInputRows(snapshot = {}, meta = {}) {
161
+ const normalized = meta.normalized || {};
162
+ const postAction = shouldRequestChatResume(meta.args)
163
+ ? "request_cv"
164
+ : normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
165
+ const searchParams = {
166
+ job: normalized.job || meta.args?.job || "",
167
+ start_from: normalized.startFrom || meta.args?.start_from || "",
168
+ target_count: normalized.publicTargetCount ?? normalized.targetCount ?? snapshot.progress?.target_count ?? "",
169
+ detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || snapshot.context?.detail_source || ""
170
+ };
171
+ return buildLegacyScreenInputRows({
172
+ instruction: meta.args?.instruction || "启动boss聊天任务",
173
+ selectedPage: "chat",
174
+ selectedJob: selectedChatJobForCsv(meta),
175
+ userSearchParams: cloneReportInput(searchParams, {}),
176
+ effectiveSearchParams: cloneReportInput(searchParams, {}),
177
+ screenParams: {
178
+ criteria: normalized.criteria || meta.args?.criteria || "",
179
+ target_count: searchParams.target_count,
180
+ post_action: postAction,
181
+ max_greet_count: meta.args?.max_greet_count ?? ""
182
+ },
183
+ followUp: meta.args?.follow_up || null,
184
+ extraRows: [
185
+ ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT],
186
+ ["chat_params.profile", normalized.profile || meta.args?.profile || "default"]
187
+ ]
188
+ });
189
+ }
190
+
191
+ function writeChatLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
192
+ writeLegacyScreenCsv(filePath, {
193
+ inputRows: buildChatCsvInputRows(snapshot, meta),
194
+ results: rows
195
+ });
196
+ }
197
+
198
+ function readChatRunState(runId) {
199
+ const artifacts = getChatRunArtifacts(runId);
200
+ if (!artifacts) return null;
201
+ return readJsonFile(artifacts.run_state_path);
202
+ }
203
+
204
+ function toIsoOrNull(value) {
205
+ const normalized = normalizeText(value);
206
+ return normalized || null;
207
+ }
208
+
209
+ function secondsBetween(startedAt, endedAt) {
210
+ const startMs = Date.parse(startedAt || "");
211
+ const endMs = Date.parse(endedAt || "") || Date.now();
212
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
213
+ return Math.max(1, Math.round((endMs - startMs) / 1000));
214
+ }
215
+
216
+ function countPostActionResults(results = []) {
217
+ let requested = 0;
218
+ let requestSatisfied = 0;
219
+ let requestSkipped = 0;
220
+ for (const row of results || []) {
221
+ const action = row?.post_action || {};
222
+ if (action.requested) requestSatisfied += 1;
223
+ if (action.skipped) requestSkipped += 1;
224
+ if (action.requested && !action.skipped) requested += 1;
225
+ }
226
+ return {
227
+ requested,
228
+ request_satisfied: requestSatisfied,
229
+ request_skipped: requestSkipped
230
+ };
231
+ }
232
+
233
+ function normalizeLegacyProgress(progress = {}, summary = null) {
234
+ const countedRequests = countPostActionResults(Array.isArray(summary?.results) ? summary.results : []);
235
+ const processed = Number.isInteger(progress.processed)
236
+ ? progress.processed
237
+ : Number.isInteger(summary?.processed)
238
+ ? summary.processed
239
+ : 0;
240
+ const screened = Number.isInteger(progress.screened)
241
+ ? progress.screened
242
+ : Number.isInteger(summary?.screened)
243
+ ? summary.screened
244
+ : processed;
245
+ const passed = Number.isInteger(progress.passed)
246
+ ? progress.passed
247
+ : Number.isInteger(summary?.passed)
248
+ ? summary.passed
249
+ : 0;
250
+ const requested = Number.isInteger(progress.requested)
251
+ ? progress.requested
252
+ : Number.isInteger(summary?.requested)
253
+ ? summary.requested
254
+ : countedRequests.requested;
255
+ const requestSatisfied = Number.isInteger(progress.request_satisfied)
256
+ ? progress.request_satisfied
257
+ : Number.isInteger(summary?.request_satisfied)
258
+ ? summary.request_satisfied
259
+ : countedRequests.request_satisfied;
260
+ const requestSkipped = Number.isInteger(progress.request_skipped)
261
+ ? progress.request_skipped
262
+ : Number.isInteger(summary?.request_skipped)
263
+ ? summary.request_skipped
264
+ : countedRequests.request_skipped;
265
+ return {
266
+ ...progress,
267
+ processed,
268
+ inspected: processed,
269
+ screened,
270
+ passed,
271
+ requested,
272
+ request_satisfied: requestSatisfied,
273
+ request_skipped: requestSkipped,
274
+ skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
275
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0
276
+ };
277
+ }
278
+
279
+ function completionReason(status) {
280
+ if (status === RUN_STATUS_COMPLETED) return "completed";
281
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
282
+ if (status === RUN_STATUS_FAILED) return "failed";
283
+ if (status === RUN_STATUS_PAUSED) return "paused";
284
+ return null;
285
+ }
286
+
287
+ function getChatRunMeta(runId) {
288
+ return chatRunMeta.get(runId) || {};
289
+ }
290
+
291
+ function ensureChatRunArtifacts(snapshot) {
292
+ const artifacts = getChatRunArtifacts(snapshot?.runId || snapshot?.run_id);
293
+ if (!artifacts) return null;
294
+
295
+ const meta = getChatRunMeta(snapshot?.runId || snapshot?.run_id);
296
+ const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
297
+ ? snapshot.checkpoint
298
+ : {};
299
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
300
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
301
+
302
+ const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
303
+ if (summary) {
304
+ const rows = Array.isArray(summary.results) ? summary.results : [];
305
+ writeChatLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
306
+ writeJsonAtomic(artifacts.report_json, {
307
+ run_id: snapshot.runId || snapshot.run_id,
308
+ status: snapshot.status || snapshot.state,
309
+ phase: snapshot.phase || snapshot.stage,
310
+ progress: snapshot.progress || {},
311
+ context: snapshot.context || {},
312
+ checkpoint,
313
+ summary,
314
+ generated_at: new Date().toISOString()
315
+ });
316
+ if (meta) {
317
+ meta.outputCsvPath = artifacts.output_csv;
318
+ meta.reportJsonPath = artifacts.report_json;
319
+ }
320
+ }
321
+
322
+ return artifacts;
323
+ }
324
+
325
+ function buildLegacyChatResult(snapshot) {
326
+ if (!snapshot) return null;
327
+ const artifacts = ensureChatRunArtifacts(snapshot);
328
+ const meta = getChatRunMeta(snapshot.runId);
329
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
330
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
331
+ return {
332
+ run_id: snapshot.runId,
333
+ state: snapshot.status,
334
+ status: snapshot.status,
335
+ completion_reason: completionReason(snapshot.status),
336
+ requested_count: progress.requested,
337
+ request_satisfied_count: progress.request_satisfied,
338
+ request_skipped_count: progress.request_skipped,
339
+ processed_count: progress.processed,
340
+ inspected_count: progress.processed,
341
+ screened_count: progress.screened,
342
+ passed_count: progress.passed,
343
+ skipped_count: progress.skipped,
344
+ detail_opened: progress.detail_opened || summary?.detail_opened || 0,
345
+ llm_screened: progress.llm_screened || summary?.llm_screened || 0,
346
+ output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
347
+ report_json: artifacts?.report_json || meta.reportJsonPath || null,
348
+ checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
349
+ started_at: snapshot.startedAt,
350
+ completed_at: snapshot.completedAt || null,
351
+ duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
352
+ error: snapshot.error || null,
353
+ results: Array.isArray(summary?.results) ? summary.results : []
354
+ };
355
+ }
356
+
357
+ function normalizeRunSnapshot(snapshot) {
358
+ if (!snapshot) return null;
359
+ const meta = getChatRunMeta(snapshot.runId);
360
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
361
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
362
+ const legacyResult = (
363
+ TERMINAL_STATUSES.has(snapshot.status)
364
+ || snapshot.status === RUN_STATUS_PAUSED
365
+ ) ? buildLegacyChatResult({ ...snapshot, progress }) : null;
366
+ const oldContext = {
367
+ workspace_root: meta.workspaceRoot || null,
368
+ profile: meta.normalized?.profile || meta.args?.profile || "default",
369
+ job: meta.normalized?.job || meta.args?.job || "",
370
+ start_from: meta.normalized?.startFrom || meta.args?.start_from || "",
371
+ criteria: meta.normalized?.criteria || meta.args?.criteria || "",
372
+ greeting_text: meta.normalized?.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT,
373
+ target_count: meta.normalized?.publicTargetCount ?? null,
374
+ target_count_semantics: TARGET_COUNT_SEMANTICS
375
+ };
376
+ return {
377
+ ...snapshot,
378
+ progress,
379
+ run_id: snapshot.runId,
380
+ mode: RUN_MODE_ASYNC,
381
+ state: snapshot.status,
382
+ stage: snapshot.phase,
383
+ started_at: snapshot.startedAt,
384
+ updated_at: snapshot.updatedAt,
385
+ completed_at: toIsoOrNull(snapshot.completedAt),
386
+ heartbeat_at: snapshot.updatedAt,
387
+ pid: process.pid || null,
388
+ last_message: snapshot.error?.message || snapshot.phase || null,
389
+ context: {
390
+ ...(snapshot.context || {}),
391
+ ...oldContext,
392
+ shared_run_context: snapshot.context || {}
393
+ },
394
+ control: {
395
+ pause_requested: snapshot.status === RUN_STATUS_PAUSED,
396
+ pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
397
+ pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_boss_chat_run" : null,
398
+ cancel_requested: snapshot.status === RUN_STATUS_CANCELING
399
+ },
400
+ resume: {
401
+ checkpoint_path: legacyResult?.checkpoint_path || null,
402
+ pause_control_path: getChatRunArtifacts(snapshot.runId)?.run_state_path || null,
403
+ output_csv: legacyResult?.output_csv || null,
404
+ resume_count: meta.resumeCount || 0,
405
+ last_resumed_at: meta.lastResumedAt || null,
406
+ last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
407
+ },
408
+ result: legacyResult,
409
+ artifacts: getChatRunArtifacts(snapshot.runId)
410
+ };
411
+ }
412
+
413
+ function persistChatRunSnapshot(snapshot) {
414
+ const normalized = normalizeRunSnapshot(snapshot);
415
+ if (!normalized?.run_id) return normalized;
416
+ const artifacts = getChatRunArtifacts(normalized.run_id);
417
+ if (!artifacts) return normalized;
418
+ const payload = {
419
+ run_id: normalized.run_id,
420
+ mode: normalized.mode,
421
+ state: normalized.state,
422
+ status: normalized.status,
423
+ stage: normalized.stage,
424
+ started_at: normalized.started_at,
425
+ updated_at: normalized.updated_at,
426
+ heartbeat_at: normalized.heartbeat_at,
427
+ completed_at: normalized.completed_at,
428
+ pid: normalized.pid,
429
+ progress: normalized.progress,
430
+ last_message: normalized.last_message,
431
+ context: normalized.context,
432
+ control: normalized.control,
433
+ resume: normalized.resume,
434
+ error: normalized.error,
435
+ result: normalized.result,
436
+ summary: normalized.summary,
437
+ artifacts: normalized.artifacts
438
+ };
439
+ writeJsonAtomic(artifacts.run_state_path, payload);
440
+ return normalized;
441
+ }
442
+
443
+ function attachMethodEvidence(payload, runId) {
444
+ const meta = getChatRunMeta(runId);
445
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
446
+ return {
447
+ ...payload,
448
+ runtime_evaluate_used: false,
449
+ method_summary: methodSummary(meta.methodLog || []),
450
+ method_log: meta.methodLog || [],
451
+ chrome: meta.chrome || null
452
+ };
453
+ }
454
+
455
+ function shouldNavigateToChat(url) {
456
+ const text = String(url || "");
457
+ return !text.includes("/web/chat/index")
458
+ || text.includes("/web/chat/recommend")
459
+ || text.includes("/web/chat/search");
460
+ }
461
+
462
+ function isRecoverableChatTargetUrl(url) {
463
+ const text = String(url || "");
464
+ return text.includes("zhipin.com/web/chat")
465
+ || isForbiddenChatResumeTopLevelUrl(text);
466
+ }
467
+
468
+ async function waitForHealthyChat(client, config, {
469
+ timeoutMs = 90000,
470
+ intervalMs = 1000
471
+ } = {}) {
472
+ const started = Date.now();
473
+ let lastCheck = null;
474
+ while (Date.now() - started <= timeoutMs) {
475
+ const roots = await resolveChatSelfHealRoots(client, config);
476
+ lastCheck = await runSelfHealCheck({
477
+ client,
478
+ domain: "chat",
479
+ roots: roots.roots,
480
+ selectorProbes: config.selectorProbes,
481
+ accessibilityProbes: config.accessibilityProbes
482
+ });
483
+ if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
484
+ await sleep(intervalMs);
485
+ }
486
+ return lastCheck;
487
+ }
488
+
489
+ async function connectChatChromeSession({
490
+ host = DEFAULT_CHAT_HOST,
491
+ port = DEFAULT_CHAT_PORT,
492
+ targetUrlIncludes = CHAT_TARGET_URL,
493
+ allowNavigate = true,
494
+ slowLive = false
495
+ } = {}) {
496
+ let session;
497
+ try {
498
+ session = await connectToChromeTarget({
499
+ host,
500
+ port,
501
+ targetUrlIncludes
502
+ });
503
+ } catch (error) {
504
+ if (!allowNavigate) throw error;
505
+ session = await connectToChromeTarget({
506
+ host,
507
+ port,
508
+ targetPredicate: (target) => (
509
+ target?.type === "page"
510
+ && isRecoverableChatTargetUrl(target?.url)
511
+ )
512
+ });
513
+ }
514
+
515
+ const { client, target } = session;
516
+ await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
517
+ if (typeof client?.Network?.setCacheDisabled === "function") {
518
+ await client.Network.setCacheDisabled({ cacheDisabled: true });
519
+ }
520
+ await bringPageToFront(client);
521
+
522
+ const targetUrl = String(target?.url || "");
523
+ let navigation = {
524
+ navigated: false,
525
+ url: targetUrl
526
+ };
527
+ if (allowNavigate && shouldNavigateToChat(targetUrl)) {
528
+ await client.Page.navigate({ url: CHAT_TARGET_URL });
529
+ const settleMs = slowLive ? 10000 : 5000;
530
+ await sleep(settleMs);
531
+ navigation = {
532
+ navigated: true,
533
+ url: CHAT_TARGET_URL,
534
+ settle_ms: settleMs
535
+ };
536
+ }
537
+
538
+ const selfHealConfig = buildChatSelfHealConfig();
539
+ const health = await waitForHealthyChat(client, selfHealConfig, {
540
+ timeoutMs: slowLive ? 180000 : 90000,
541
+ intervalMs: slowLive ? 1200 : 800
542
+ });
543
+ if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
544
+ throw new Error(`Boss chat page is not healthy: ${health?.status || "missing"}`);
545
+ }
546
+
547
+ return {
548
+ ...session,
549
+ navigation,
550
+ health
551
+ };
552
+ }
553
+
554
+ async function readChatJobOptionsFromSession(session) {
555
+ const roots = await getChatRoots(session.client);
556
+ return readChatJobOptions(session.client, roots.rootNodes.top);
557
+ }
558
+
559
+ function normalizeChatStartInput(args = {}) {
560
+ const target = normalizeTargetCountInput(getBossChatTargetCountValue(args));
561
+ return {
562
+ profile: normalizeText(args.profile) || "default",
563
+ job: normalizeText(args.job),
564
+ startFrom: normalizeText(args.start_from).toLowerCase(),
565
+ criteria: normalizeText(args.criteria),
566
+ greetingText: normalizeText(args.greeting_text || args.greetingText || args.greeting),
567
+ target,
568
+ targetCount: target.targetCount,
569
+ publicTargetCount: target.publicValue,
570
+ host: normalizeText(args.host) || DEFAULT_CHAT_HOST,
571
+ port: parsePositiveInteger(args.port, DEFAULT_CHAT_PORT),
572
+ targetUrlIncludes: normalizeText(args.target_url_includes) || CHAT_TARGET_URL,
573
+ allowNavigate: args.allow_navigate !== false,
574
+ slowLive: args.slow_live === true
575
+ };
576
+ }
577
+
578
+ function buildChatNextCallExample(args, missingFields, normalized) {
579
+ const example = {};
580
+ if (normalized.job) example.job = normalized.job;
581
+ if (normalized.startFrom) example.start_from = normalized.startFrom;
582
+ if (normalized.target.provided && !normalized.target.parseError) {
583
+ example.target_count = normalized.publicTargetCount ?? normalized.targetCount;
584
+ } else if (missingFields.includes("target_count")) {
585
+ example.target_count = "all";
586
+ }
587
+ if (normalized.criteria) example.criteria = normalized.criteria;
588
+ if (normalizeText(args.greeting_text || args.greetingText || args.greeting)) {
589
+ example.greeting_text = normalizeText(args.greeting_text || args.greetingText || args.greeting);
590
+ }
591
+ return Object.keys(example).length ? example : null;
592
+ }
593
+
594
+ function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
595
+ const missing = [];
596
+ if (!normalized.job) missing.push("job");
597
+ if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
598
+ if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
599
+ if (!normalized.criteria) missing.push("criteria");
600
+ return missing;
601
+ }
602
+
603
+ function buildTargetCountDiagnostics(args, missingFields, normalized) {
604
+ if (!missingFields.includes("target_count")) return {};
605
+ const hints = buildTargetCountCompatibilityHints({
606
+ argumentName: "target_count",
607
+ recommendedArgumentPatch: { target_count: "all" }
608
+ });
609
+ const received = getBossChatTargetCountValue(args);
610
+ const nextCallExample = {
611
+ ...(normalizeText(args.job) ? { job: normalizeText(args.job) } : {}),
612
+ ...(normalizeText(args.start_from) ? { start_from: normalizeText(args.start_from).toLowerCase() } : {}),
613
+ target_count: "all",
614
+ ...(normalizeText(args.criteria) ? { criteria: normalizeText(args.criteria) } : {})
615
+ };
616
+ return {
617
+ ...hints,
618
+ received_target_count: received,
619
+ target_count_parse_error: normalized.target.parseError || null,
620
+ next_call_example: nextCallExample
621
+ };
622
+ }
623
+
624
+ function buildJobQuestionOptions(jobOptions = []) {
625
+ return (jobOptions || []).map((option) => ({
626
+ label: option.label,
627
+ value: option.value,
628
+ index: option.index,
629
+ active: option.active === true
630
+ }));
631
+ }
632
+
633
+ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions = [] }) {
634
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
635
+ return missingFields.map((field) => {
636
+ if (field === "job") {
637
+ return {
638
+ field,
639
+ question: "请提供 Boss chat 岗位,支持岗位名、编号或页面中的岗位 value。",
640
+ value: normalized.job || null,
641
+ options: buildJobQuestionOptions(jobOptions)
642
+ };
643
+ }
644
+ if (field === "start_from") {
645
+ return {
646
+ field,
647
+ question: "请确认 chat 起始范围。",
648
+ value: normalized.startFrom || null,
649
+ options: [
650
+ { label: "未读", value: "unread" },
651
+ { label: "全部", value: "all" }
652
+ ]
653
+ };
654
+ }
655
+ if (field === "target_count") {
656
+ return {
657
+ field,
658
+ ...diagnostics,
659
+ question: "请提供 target_count,使用正整数或 all(扫到底)。",
660
+ value: normalized.publicTargetCount ?? null,
661
+ options: Array.isArray(diagnostics.options) ? diagnostics.options : [],
662
+ parse_error: normalized.target.parseError || null
663
+ };
664
+ }
665
+ if (field === "criteria") {
666
+ return {
667
+ field,
668
+ question: "请提供自然语言筛选 criteria。",
669
+ value: normalized.criteria || null
670
+ };
671
+ }
672
+ return {
673
+ field,
674
+ question: `请提供 ${field}。`,
675
+ value: null
676
+ };
677
+ });
678
+ }
679
+
680
+ async function buildNeedInputResponse({ args, missingFields, normalized }) {
681
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
682
+ return {
683
+ status: "NEED_INPUT",
684
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
685
+ missing_fields: missingFields,
686
+ ...diagnostics,
687
+ pending_questions: buildPendingChatQuestions({ args, missingFields, normalized }),
688
+ job_options: [],
689
+ error: {
690
+ code: "MISSING_REQUIRED_FIELDS",
691
+ message: "缺少必要字段。请补齐 job、start_from、target_count、criteria 后再启动 Boss chat CDP-only run。",
692
+ retryable: true
693
+ }
694
+ };
695
+ }
696
+
697
+ function shouldRequestChatResume(args = {}) {
698
+ return (
699
+ args.request_cv === true
700
+ || args.request_resume === true
701
+ || args.ask_cv === true
702
+ || args.execute_post_action === true
703
+ || ["request_cv", "ask_cv", "request_resume", "求简历", "索要简历"].includes(normalizeText(args.post_action || args.action))
704
+ );
705
+ }
706
+
707
+ function shouldUseChatLlm(args = {}, shouldRequestResume = false) {
708
+ if (args.use_llm === false) return false;
709
+ return (
710
+ args.use_llm === true
711
+ || shouldRequestResume
712
+ || parseNonNegativeInteger(args.detail_limit, 0) > 0
713
+ );
714
+ }
715
+
716
+ function getRunOptions(args, normalized, session, { workspaceRoot = "" } = {}) {
717
+ const slowLive = args.slow_live === true;
718
+ const isAllTarget = normalized.publicTargetCount === "all";
719
+ const processedLimit = parsePositiveInteger(
720
+ args.max_candidates,
721
+ isAllTarget ? CHAT_ALL_MAX_CANDIDATES : CHAT_ALL_MAX_CANDIDATES
722
+ );
723
+ const shouldRequestResume = shouldRequestChatResume(args);
724
+ const useLlm = shouldUseChatLlm(args, shouldRequestResume);
725
+ const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false };
726
+ const configFile = configResolution.ok ? readJsonFile(configResolution.config_path) : null;
727
+ return {
728
+ client: session.client,
729
+ targetUrl: CHAT_TARGET_URL,
730
+ job: normalized.job,
731
+ startFrom: normalized.startFrom,
732
+ criteria: normalized.criteria,
733
+ maxCandidates: processedLimit,
734
+ targetPassCount: isAllTarget ? null : normalized.targetCount,
735
+ processUntilListEnd: isAllTarget,
736
+ detailLimit: parseNonNegativeInteger(args.detail_limit, useLlm || shouldRequestResume ? processedLimit : 0),
737
+ detailSource: normalizeText(args.detail_source) || "cascade",
738
+ closeResume: true,
739
+ requestResumeForPassed: shouldRequestResume,
740
+ dryRunRequestCv: args.dry_run === true || args.dry_run_request_cv === true,
741
+ greetingText: normalized.greetingText || DEFAULT_CHAT_GREETING_TEXT,
742
+ delayMs: parseNonNegativeInteger(args.delay_ms, 0),
743
+ cardTimeoutMs: slowLive ? 180000 : 90000,
744
+ readyTimeoutMs: slowLive ? 120000 : 60000,
745
+ onlineResumeButtonTimeoutMs: parsePositiveInteger(
746
+ args.online_resume_button_timeout_ms,
747
+ slowLive ? 30000 : 15000
748
+ ),
749
+ resumeDomTimeoutMs: slowLive ? 120000 : 60000,
750
+ maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
751
+ imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
752
+ llmConfig: configResolution.ok ? {
753
+ ...configResolution.config,
754
+ apiKey: configFile?.apiKey || ""
755
+ } : null,
756
+ llmTimeoutMs: parsePositiveInteger(args.llm_timeout_ms, slowLive ? 180000 : 120000),
757
+ llmImageLimit: parsePositiveInteger(args.llm_image_limit, 8),
758
+ llmImageDetail: normalizeText(args.llm_image_detail) || "high",
759
+ listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
760
+ listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
761
+ listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
762
+ listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
763
+ listFallbackPoint: null,
764
+ name: "mcp-boss-chat-run"
765
+ };
766
+ }
767
+
768
+ async function closeChatRunSession(runId) {
769
+ const meta = chatRunMeta.get(runId);
770
+ if (!meta || meta.closed) return;
771
+ try {
772
+ try {
773
+ if (meta.session?.client) {
774
+ await closeChatResumeModal(meta.session.client, { attemptsLimit: 2 });
775
+ }
776
+ } catch {
777
+ // Cleanup is best-effort once the run has settled.
778
+ }
779
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
780
+ } finally {
781
+ meta.closed = true;
782
+ try {
783
+ await meta.session?.close?.();
784
+ } catch {
785
+ // Nothing actionable for the caller once the run has settled.
786
+ }
787
+ }
788
+ }
789
+
790
+ async function waitForChatRunTerminal(runId) {
791
+ while (true) {
792
+ try {
793
+ const snapshot = chatRunService.getChatRun(runId);
794
+ if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
795
+ } catch {
796
+ return null;
797
+ }
798
+ await sleep(1000);
799
+ }
800
+ }
801
+
802
+ function trackChatRun(runId) {
803
+ waitForChatRunTerminal(runId)
804
+ .then((terminal) => {
805
+ if (terminal) persistChatRunSnapshot(terminal);
806
+ })
807
+ .catch(() => null)
808
+ .finally(() => {
809
+ closeChatRunSession(runId).catch(() => {});
810
+ });
811
+ }
812
+
813
+ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
814
+ const normalized = normalizeChatStartInput(args);
815
+ const missingFields = getMissingChatStartFields(args, normalized);
816
+ if (missingFields.length) {
817
+ return buildNeedInputResponse({
818
+ args,
819
+ missingFields,
820
+ normalized
821
+ });
822
+ }
823
+
824
+ const shouldRequestResume = shouldRequestChatResume(args);
825
+ const useLlm = shouldUseChatLlm(args, shouldRequestResume);
826
+ const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
827
+ if (useLlm && !configResolution?.ok) {
828
+ return {
829
+ status: "FAILED",
830
+ error: {
831
+ code: "SCREEN_CONFIG_ERROR",
832
+ message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
833
+ retryable: true
834
+ }
835
+ };
836
+ }
837
+
838
+ let session;
839
+ try {
840
+ session = await chatConnectorImpl({
841
+ host: normalized.host,
842
+ port: normalized.port,
843
+ targetUrlIncludes: normalized.targetUrlIncludes,
844
+ allowNavigate: normalized.allowNavigate,
845
+ slowLive: normalized.slowLive
846
+ });
847
+ } catch (error) {
848
+ return {
849
+ status: "FAILED",
850
+ error: {
851
+ code: "BOSS_CHAT_PAGE_NOT_READY",
852
+ message: error?.message || "Boss chat page is not ready",
853
+ retryable: true
854
+ }
855
+ };
856
+ }
857
+
858
+ let started;
859
+ try {
860
+ started = chatRunService.startChatRun(getRunOptions(args, normalized, session, { workspaceRoot }));
861
+ } catch (error) {
862
+ await session.close?.();
863
+ return {
864
+ status: "FAILED",
865
+ error: {
866
+ code: "CHAT_RUN_START_FAILED",
867
+ message: error?.message || "Failed to start Boss chat run",
868
+ retryable: true
869
+ }
870
+ };
871
+ }
872
+
873
+ chatRunMeta.set(started.runId, {
874
+ session,
875
+ methodLog: session.methodLog || [],
876
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
877
+ args: clonePlain(args, {}),
878
+ normalized,
879
+ chrome: {
880
+ host: normalized.host,
881
+ port: normalized.port,
882
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
883
+ target_id: session.target?.id || null
884
+ },
885
+ health: session.health || null
886
+ });
887
+ trackChatRun(started.runId);
888
+ const persistedStarted = persistChatRunSnapshot(started);
889
+
890
+ return {
891
+ status: "ACCEPTED",
892
+ run_id: persistedStarted.run_id,
893
+ state: persistedStarted.state,
894
+ run: persistedStarted,
895
+ poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
896
+ message: shouldRequestResume
897
+ ? "Boss chat run started through the shared CDP-only chat service. Passed candidates will follow the configured request-CV sequence."
898
+ : "Boss chat run started through the shared CDP-only chat service.",
899
+ target_count_semantics: TARGET_COUNT_SEMANTICS
900
+ };
901
+ }
902
+
903
+ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
904
+ const normalized = normalizeChatStartInput(args);
905
+ let session;
906
+ try {
907
+ session = await chatConnectorImpl({
908
+ host: normalized.host,
909
+ port: normalized.port,
910
+ targetUrlIncludes: normalized.targetUrlIncludes,
911
+ allowNavigate: normalized.allowNavigate,
912
+ slowLive: normalized.slowLive
913
+ });
914
+ } catch (error) {
915
+ return {
916
+ status: "FAILED",
917
+ stage: "chat_run_setup",
918
+ error: {
919
+ code: "BOSS_CHAT_PAGE_NOT_READY",
920
+ message: error?.message || "Boss chat page is not ready",
921
+ retryable: true
922
+ },
923
+ runtime_evaluate_used: false,
924
+ method_summary: {},
925
+ method_log: [],
926
+ chrome: {
927
+ host: normalized.host,
928
+ port: normalized.port,
929
+ target_url: CHAT_TARGET_URL
930
+ }
931
+ };
932
+ }
933
+
934
+ try {
935
+ const jobs = await chatJobReaderImpl(session, {
936
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
937
+ args: clonePlain(args, {}),
938
+ normalized
939
+ });
940
+ const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
941
+ const missingFields = getMissingChatStartFields(args, normalized);
942
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
943
+ const nextCallExample = buildChatNextCallExample(args, missingFields, normalized);
944
+ const selectedJob = jobOptions.find((option) => {
945
+ const job = normalizeText(normalized.job).toLowerCase();
946
+ if (!job) return option.active === true;
947
+ return [option.value, option.label, option.title]
948
+ .map((value) => normalizeText(value).toLowerCase())
949
+ .includes(job);
950
+ }) || null;
951
+
952
+ assertNoForbiddenCdpCalls(session.methodLog || []);
953
+ return {
954
+ status: missingFields.length ? "NEED_INPUT" : "READY",
955
+ stage: "chat_run_setup",
956
+ page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
957
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
958
+ missing_fields: missingFields,
959
+ job_options: jobOptions,
960
+ selected_job: selectedJob,
961
+ selected_job_label: jobs?.selected_label || selectedJob?.label || "",
962
+ job_options_source: jobs?.source || "",
963
+ job_options_selector: jobs?.selector || "",
964
+ pending_questions: buildPendingChatQuestions({
965
+ args,
966
+ missingFields,
967
+ normalized,
968
+ jobOptions
969
+ }),
970
+ ...diagnostics,
971
+ ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
972
+ message: missingFields.length
973
+ ? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count / criteria。"
974
+ : "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening.",
975
+ runtime_evaluate_used: false,
976
+ method_summary: methodSummary(session.methodLog || []),
977
+ method_log: session.methodLog || [],
978
+ chrome: {
979
+ host: normalized.host,
980
+ port: normalized.port,
981
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
982
+ target_id: session.target?.id || null
983
+ }
984
+ };
985
+ } catch (error) {
986
+ return {
987
+ status: "FAILED",
988
+ stage: "chat_run_setup",
989
+ error: {
990
+ code: "BOSS_CHAT_PREPARE_FAILED",
991
+ message: error?.message || "Boss chat CDP-only prepare failed",
992
+ retryable: true
993
+ },
994
+ runtime_evaluate_used: false,
995
+ method_summary: methodSummary(session.methodLog || []),
996
+ method_log: session.methodLog || [],
997
+ chrome: {
998
+ host: normalized.host,
999
+ port: normalized.port,
1000
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1001
+ target_id: session.target?.id || null
1002
+ }
1003
+ };
1004
+ } finally {
1005
+ try {
1006
+ assertNoForbiddenCdpCalls(session.methodLog || []);
1007
+ } finally {
1008
+ await session.close?.();
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} } = {}) {
1014
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1015
+ const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
1016
+ const host = normalizeText(args.host) || DEFAULT_CHAT_HOST;
1017
+ const port = parsePositiveInteger(args.port, configResolution.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT);
1018
+ const targetUrlIncludes = normalizeText(args.target_url_includes) || CHAT_TARGET_URL;
1019
+ const allowNavigate = args.allow_navigate !== false;
1020
+ const slowLive = args.slow_live === true;
1021
+ const basePayload = {
1022
+ server: "boss-chat",
1023
+ mode: "cdp-only",
1024
+ cdp_only: true,
1025
+ cli_dir: null,
1026
+ cli_path: null,
1027
+ config_path: configResolution.config_path || null,
1028
+ config_dir: configResolution.config_dir || null,
1029
+ debug_port: port,
1030
+ shared_llm_config: configResolution.ok === true,
1031
+ data_dir: runtimeLayout.data_dir,
1032
+ data_dir_source: runtimeLayout.data_dir_source,
1033
+ legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
1034
+ migration_source_dir: runtimeLayout.migration_source_dir,
1035
+ migration_pending: runtimeLayout.migration_pending
1036
+ };
1037
+
1038
+ if (!configResolution.ok) {
1039
+ return {
1040
+ status: "FAILED",
1041
+ ...basePayload,
1042
+ error: configResolution.error,
1043
+ runtime_evaluate_used: false,
1044
+ method_summary: {},
1045
+ method_log: [],
1046
+ chrome: {
1047
+ host,
1048
+ port,
1049
+ target_url: targetUrlIncludes
1050
+ }
1051
+ };
1052
+ }
1053
+
1054
+ let session;
1055
+ try {
1056
+ session = await chatConnectorImpl({
1057
+ host,
1058
+ port,
1059
+ targetUrlIncludes,
1060
+ allowNavigate,
1061
+ slowLive
1062
+ });
1063
+ assertNoForbiddenCdpCalls(session.methodLog || []);
1064
+ return {
1065
+ status: "OK",
1066
+ ...basePayload,
1067
+ page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1068
+ health: session.health || null,
1069
+ runtime_evaluate_used: false,
1070
+ method_summary: methodSummary(session.methodLog || []),
1071
+ method_log: session.methodLog || [],
1072
+ chrome: {
1073
+ host,
1074
+ port,
1075
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1076
+ target_id: session.target?.id || null
1077
+ },
1078
+ message: "Boss chat CDP-only health check passed with shared self-heal probes."
1079
+ };
1080
+ } catch (error) {
1081
+ return {
1082
+ status: "FAILED",
1083
+ ...basePayload,
1084
+ error: {
1085
+ code: "BOSS_CHAT_PAGE_NOT_READY",
1086
+ message: error?.message || "Boss chat page is not ready",
1087
+ retryable: true
1088
+ },
1089
+ runtime_evaluate_used: false,
1090
+ method_summary: methodSummary(session?.methodLog || []),
1091
+ method_log: session?.methodLog || [],
1092
+ chrome: {
1093
+ host,
1094
+ port,
1095
+ target_url: session?.navigation?.url || session?.target?.url || targetUrlIncludes,
1096
+ target_id: session?.target?.id || null
1097
+ }
1098
+ };
1099
+ } finally {
1100
+ if (session?.methodLog) assertNoForbiddenCdpCalls(session.methodLog);
1101
+ await session?.close?.();
1102
+ }
1103
+ }
1104
+
1105
+ export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
1106
+ const started = await startBossChatRunInternal(args, { workspaceRoot });
1107
+ if (started.status !== "ACCEPTED") return started;
1108
+ return attachMethodEvidence(started, started.run_id);
1109
+ }
1110
+
1111
+ export function getBossChatRunTool({ args = {} } = {}) {
1112
+ const runId = normalizeRunId(args.run_id || args.runId);
1113
+ if (!runId) {
1114
+ return {
1115
+ status: "FAILED",
1116
+ error: {
1117
+ code: "INVALID_RUN_ID",
1118
+ message: "run_id is required",
1119
+ retryable: false
1120
+ }
1121
+ };
1122
+ }
1123
+ try {
1124
+ const run = chatRunService.getChatRun(runId);
1125
+ const normalizedRun = persistChatRunSnapshot(run);
1126
+ return attachMethodEvidence({
1127
+ status: "RUN_STATUS",
1128
+ run: normalizedRun
1129
+ }, runId);
1130
+ } catch {
1131
+ const persisted = readChatRunState(runId);
1132
+ if (persisted) {
1133
+ return {
1134
+ status: "RUN_STATUS",
1135
+ run: persisted,
1136
+ persistence: {
1137
+ source: "disk",
1138
+ active_control_available: false
1139
+ },
1140
+ runtime_evaluate_used: false,
1141
+ method_summary: {},
1142
+ method_log: [],
1143
+ chrome: null
1144
+ };
1145
+ }
1146
+ return {
1147
+ status: "FAILED",
1148
+ error: {
1149
+ code: "RUN_NOT_FOUND",
1150
+ message: `No Boss chat run found for run_id=${runId}`,
1151
+ retryable: false
1152
+ }
1153
+ };
1154
+ }
1155
+ }
1156
+
1157
+ export function pauseBossChatRunTool({ args = {} } = {}) {
1158
+ const runId = normalizeRunId(args.run_id || args.runId);
1159
+ try {
1160
+ const before = chatRunService.getChatRun(runId);
1161
+ if (TERMINAL_STATUSES.has(before.status)) {
1162
+ const normalizedBefore = persistChatRunSnapshot(before);
1163
+ return attachMethodEvidence({
1164
+ status: "PAUSE_IGNORED",
1165
+ run: normalizedBefore,
1166
+ message: "目标任务已结束,无需暂停。"
1167
+ }, runId);
1168
+ }
1169
+ if (before.status === RUN_STATUS_PAUSED) {
1170
+ const normalizedBefore = persistChatRunSnapshot(before);
1171
+ return attachMethodEvidence({
1172
+ status: "PAUSE_IGNORED",
1173
+ run: normalizedBefore,
1174
+ message: "目标任务已经处于 paused 状态。"
1175
+ }, runId);
1176
+ }
1177
+ const run = chatRunService.pauseChatRun(runId);
1178
+ const normalizedRun = persistChatRunSnapshot(run);
1179
+ return attachMethodEvidence({
1180
+ status: "PAUSE_REQUESTED",
1181
+ run: normalizedRun,
1182
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
1183
+ }, runId);
1184
+ } catch {
1185
+ const persisted = readChatRunState(runId);
1186
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1187
+ return {
1188
+ status: "PAUSE_IGNORED",
1189
+ run: persisted,
1190
+ message: "目标任务已结束,无需暂停。",
1191
+ runtime_evaluate_used: false,
1192
+ method_summary: {},
1193
+ method_log: [],
1194
+ chrome: null
1195
+ };
1196
+ }
1197
+ return getBossChatRunTool({ args });
1198
+ }
1199
+ }
1200
+
1201
+ export function resumeBossChatRunTool({ args = {} } = {}) {
1202
+ const runId = normalizeRunId(args.run_id || args.runId);
1203
+ try {
1204
+ const before = chatRunService.getChatRun(runId);
1205
+ if (TERMINAL_STATUSES.has(before.status)) {
1206
+ const normalizedBefore = persistChatRunSnapshot(before);
1207
+ return attachMethodEvidence({
1208
+ status: "FAILED",
1209
+ error: {
1210
+ code: "RUN_ALREADY_TERMINATED",
1211
+ message: "目标任务已结束,无法继续。",
1212
+ retryable: false
1213
+ },
1214
+ run: normalizedBefore
1215
+ }, runId);
1216
+ }
1217
+ if (before.status !== RUN_STATUS_PAUSED) {
1218
+ const normalizedBefore = persistChatRunSnapshot(before);
1219
+ return attachMethodEvidence({
1220
+ status: "FAILED",
1221
+ error: {
1222
+ code: "RUN_NOT_PAUSED",
1223
+ message: "仅 paused 状态的 run 才能继续。",
1224
+ retryable: true
1225
+ },
1226
+ run: normalizedBefore
1227
+ }, runId);
1228
+ }
1229
+ const run = chatRunService.resumeChatRun(runId);
1230
+ const meta = getChatRunMeta(runId);
1231
+ if (meta) {
1232
+ meta.resumeCount = (meta.resumeCount || 0) + 1;
1233
+ meta.lastResumedAt = new Date().toISOString();
1234
+ }
1235
+ const normalizedRun = persistChatRunSnapshot(run);
1236
+ return attachMethodEvidence({
1237
+ status: "RESUME_REQUESTED",
1238
+ run: normalizedRun,
1239
+ poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1240
+ message: "已恢复 Boss chat run,请使用 get_boss_chat_run 按需轮询。"
1241
+ }, runId);
1242
+ } catch {
1243
+ const persisted = readChatRunState(runId);
1244
+ if (persisted) {
1245
+ return {
1246
+ status: "FAILED",
1247
+ error: {
1248
+ code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1249
+ message: TERMINAL_STATUSES.has(persisted.state)
1250
+ ? "目标任务已结束,无法继续。"
1251
+ : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1252
+ retryable: !TERMINAL_STATUSES.has(persisted.state)
1253
+ },
1254
+ run: persisted,
1255
+ persistence: {
1256
+ source: "disk",
1257
+ active_control_available: false
1258
+ },
1259
+ runtime_evaluate_used: false,
1260
+ method_summary: {},
1261
+ method_log: [],
1262
+ chrome: null
1263
+ };
1264
+ }
1265
+ return getBossChatRunTool({ args });
1266
+ }
1267
+ }
1268
+
1269
+ export function cancelBossChatRunTool({ args = {} } = {}) {
1270
+ const runId = normalizeRunId(args.run_id || args.runId);
1271
+ try {
1272
+ const before = chatRunService.getChatRun(runId);
1273
+ if (TERMINAL_STATUSES.has(before.status)) {
1274
+ const normalizedBefore = persistChatRunSnapshot(before);
1275
+ return attachMethodEvidence({
1276
+ status: "CANCEL_IGNORED",
1277
+ run: normalizedBefore,
1278
+ message: "目标任务已结束,无需取消。"
1279
+ }, runId);
1280
+ }
1281
+ const run = chatRunService.cancelChatRun(runId);
1282
+ const normalizedRun = persistChatRunSnapshot(run);
1283
+ return attachMethodEvidence({
1284
+ status: "CANCEL_REQUESTED",
1285
+ run: normalizedRun,
1286
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
1287
+ }, runId);
1288
+ } catch {
1289
+ const persisted = readChatRunState(runId);
1290
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1291
+ return {
1292
+ status: "CANCEL_IGNORED",
1293
+ run: persisted,
1294
+ message: "目标任务已结束,无需取消。",
1295
+ runtime_evaluate_used: false,
1296
+ method_summary: {},
1297
+ method_log: [],
1298
+ chrome: null
1299
+ };
1300
+ }
1301
+ return getBossChatRunTool({ args });
1302
+ }
1303
+ }
1304
+
1305
+ export function __setChatMcpConnectorForTests(nextConnector) {
1306
+ chatConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectChatChromeSession;
1307
+ }
1308
+
1309
+ export function __setChatMcpJobReaderForTests(nextReader) {
1310
+ chatJobReaderImpl = typeof nextReader === "function" ? nextReader : readChatJobOptionsFromSession;
1311
+ }
1312
+
1313
+ export function __setChatMcpWorkflowForTests(nextWorkflow) {
1314
+ chatWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runChatWorkflow;
1315
+ chatRunService = createChatRunService({
1316
+ idPrefix: "mcp_chat",
1317
+ workflow: (...args) => chatWorkflowImpl(...args)
1318
+ });
1319
+ }
1320
+
1321
+ export function __resetChatMcpStateForTests() {
1322
+ for (const meta of chatRunMeta.values()) {
1323
+ try {
1324
+ meta.session?.close?.();
1325
+ } catch {
1326
+ // Best-effort test cleanup.
1327
+ }
1328
+ }
1329
+ chatRunMeta.clear();
1330
+ __setChatMcpConnectorForTests(null);
1331
+ __setChatMcpJobReaderForTests(null);
1332
+ __setChatMcpWorkflowForTests(null);
1333
+ }