@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.49

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 (55) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/detail.js +1 -1
  37. package/src/domains/recommend/filters.js +610 -610
  38. package/src/domains/recommend/index.js +10 -10
  39. package/src/domains/recommend/jobs.js +378 -316
  40. package/src/domains/recommend/refresh.js +491 -472
  41. package/src/domains/recommend/roots.js +80 -80
  42. package/src/domains/recommend/run-service.js +50 -29
  43. package/src/domains/recommend/scopes.js +246 -246
  44. package/src/domains/recruit/actions.js +277 -277
  45. package/src/domains/recruit/cards.js +74 -74
  46. package/src/domains/recruit/constants.js +167 -167
  47. package/src/domains/recruit/detail.js +461 -461
  48. package/src/domains/recruit/index.js +9 -9
  49. package/src/domains/recruit/instruction-parser.js +451 -451
  50. package/src/domains/recruit/refresh.js +44 -44
  51. package/src/domains/recruit/roots.js +68 -68
  52. package/src/domains/recruit/run-service.js +1207 -1207
  53. package/src/domains/recruit/search.js +1202 -1202
  54. package/src/recommend-mcp.js +22 -22
  55. package/src/recruit-mcp.js +1338 -1338
package/src/chat-mcp.js CHANGED
@@ -1,1780 +1,1780 @@
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
- connectToChromeTargetOrOpen,
8
- createBossLoginRequiredError,
9
- detectBossLoginState,
10
- enableDomains,
11
- getMainFrameUrl,
12
- isBossLoginUrl,
13
- waitForMainFrameUrl,
14
- sleep
15
- } from "./core/browser/index.js";
16
- import {
17
- RUN_STATUS_CANCELING,
18
- RUN_STATUS_CANCELED,
19
- RUN_STATUS_COMPLETED,
20
- RUN_STATUS_FAILED,
21
- RUN_STATUS_PAUSED
22
- } from "./core/run/index.js";
23
- import {
24
- buildLegacyScreenInputRows,
25
- cloneReportInput,
26
- writeLegacyScreenCsv
27
- } from "./core/reporting/legacy-csv.js";
28
- import {
29
- buildChatSelfHealConfig,
30
- HEALTH_STATUS,
31
- resolveChatSelfHealRoots,
32
- runSelfHealCheck
33
- } from "./core/self-heal/index.js";
34
- import {
35
- CHAT_TARGET_URL,
36
- closeChatResumeModal,
37
- closeChatJobDropdown,
38
- createChatRunService,
39
- getChatRoots,
40
- isForbiddenChatResumeTopLevelUrl,
41
- readChatJobOptions,
42
- runChatWorkflow
43
- } from "./domains/chat/index.js";
44
- import {
45
- buildTargetCountCompatibilityHints,
46
- getBossChatDataDir,
47
- getBossChatTargetCountValue,
48
- normalizeTargetCountInput,
49
- resolveBossConfiguredOutputDir,
50
- resolveBossChatRuntimeLayout,
51
- resolveHumanBehaviorForRun,
52
- resolveBossScreeningConfig
53
- } from "./chat-runtime-config.js";
54
- import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
55
-
56
- const DEFAULT_CHAT_HOST = "127.0.0.1";
57
- const DEFAULT_CHAT_PORT = 9222;
58
- const DEFAULT_CHAT_POLL_AFTER_SEC = 10;
59
- const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
60
- const CHAT_ALL_MAX_CANDIDATES = 100000;
61
- 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";
62
- const RUN_MODE_ASYNC = "async";
63
-
64
- const CHAT_REQUIRED_FIELDS = Object.freeze([
65
- "job",
66
- "start_from",
67
- "target_count",
68
- "criteria"
69
- ]);
70
-
71
- const TERMINAL_STATUSES = new Set([
72
- RUN_STATUS_COMPLETED,
73
- RUN_STATUS_FAILED,
74
- RUN_STATUS_CANCELED
75
- ]);
76
-
77
- const ARTIFACT_STATUSES = new Set([
78
- RUN_STATUS_COMPLETED,
79
- RUN_STATUS_FAILED,
80
- RUN_STATUS_CANCELED,
81
- RUN_STATUS_PAUSED
82
- ]);
83
-
84
- const STALE_PROCESS_STATUSES = new Set([
85
- "queued",
86
- "running",
87
- RUN_STATUS_CANCELING
88
- ]);
89
-
90
- const CHAT_REQUEST_RESUME_ACTIONS = new Set([
91
- "request_cv",
92
- "ask_cv",
93
- "request_resume",
94
- "求简历",
95
- "索要简历"
96
- ]);
97
-
98
- const CHAT_DISABLE_REQUEST_RESUME_ACTIONS = new Set([
99
- "none",
100
- "no",
101
- "false",
102
- "off",
103
- "skip",
104
- "do_nothing",
105
- "nothing",
106
- "不做",
107
- "什么都不做",
108
- "无",
109
- "不用",
110
- "不求简历",
111
- "不请求简历"
112
- ]);
113
-
114
- let chatWorkflowImpl = runChatWorkflow;
115
- let chatConnectorImpl = connectChatChromeSession;
116
- let chatJobReaderImpl = readChatJobOptionsFromSession;
117
- let chatRunService = createChatRunService({
118
- idPrefix: "mcp_chat",
119
- workflow: (...args) => chatWorkflowImpl(...args),
120
- onSnapshot: persistChatLifecycleSnapshot
121
- });
122
- const chatRunMeta = new Map();
123
-
124
- function normalizeText(value) {
125
- return String(value || "").replace(/\s+/g, " ").trim();
126
- }
127
-
128
- function parsePositiveInteger(raw, fallback) {
129
- const parsed = Number.parseInt(String(raw || ""), 10);
130
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
131
- }
132
-
133
- function parseNonNegativeInteger(raw, fallback) {
134
- const parsed = Number.parseInt(String(raw ?? ""), 10);
135
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
136
- }
137
-
138
- function methodSummary(methodLog = []) {
139
- const summary = {};
140
- for (const entry of methodLog || []) {
141
- summary[entry.method] = (summary[entry.method] || 0) + 1;
142
- }
143
- return summary;
144
- }
145
-
146
- function clonePlain(value, fallback = null) {
147
- try {
148
- return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
149
- } catch {
150
- return fallback;
151
- }
152
- }
153
-
154
- function normalizeRunId(runId) {
155
- const normalized = normalizeText(runId);
156
- if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
157
- return normalized;
158
- }
159
-
160
- function getChatRunsDir() {
161
- return path.join(getBossChatDataDir(), "runs");
162
- }
163
-
164
- function getChatRunArtifacts(runId) {
165
- const normalized = normalizeRunId(runId);
166
- if (!normalized) return null;
167
- const runsDir = getChatRunsDir();
168
- const outputDir = resolveBossConfiguredOutputDir("", runsDir);
169
- return {
170
- runs_dir: runsDir,
171
- output_dir: outputDir,
172
- run_state_path: path.join(runsDir, `${normalized}.json`),
173
- checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
174
- output_csv: path.join(outputDir, `${normalized}.results.csv`),
175
- report_json: path.join(outputDir, `${normalized}.report.json`)
176
- };
177
- }
178
-
179
- function ensureDirectory(dirPath) {
180
- fs.mkdirSync(dirPath, { recursive: true });
181
- }
182
-
183
- function writeJsonAtomic(filePath, payload) {
184
- ensureDirectory(path.dirname(filePath));
185
- const tempPath = `${filePath}.tmp`;
186
- fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
187
- fs.renameSync(tempPath, filePath);
188
- }
189
-
190
- function readJsonFile(filePath) {
191
- try {
192
- if (!fs.existsSync(filePath)) return null;
193
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
194
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
195
- } catch {
196
- return null;
197
- }
198
- }
199
-
200
- function selectedChatJobForCsv(meta = {}, snapshot = {}) {
201
- const job = normalizeText(
202
- meta.normalized?.job
203
- || meta.args?.job
204
- || snapshot.context?.job
205
- || ""
206
- );
207
- return {
208
- value: job,
209
- title: job,
210
- label: job
211
- };
212
- }
213
-
214
- function buildChatCsvInputRows(snapshot = {}, meta = {}) {
215
- const normalized = meta.normalized || {};
216
- const context = snapshot.context || {};
217
- const postAction = shouldRequestChatResume(meta.args, context)
218
- ? "request_cv"
219
- : normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
220
- const searchParams = {
221
- job: normalized.job || meta.args?.job || context.job || "",
222
- start_from: normalized.startFrom || meta.args?.start_from || context.start_from || "",
223
- target_count: normalized.publicTargetCount ?? normalized.targetCount ?? snapshot.progress?.target_count ?? "",
224
- detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || context.detail_source || ""
225
- };
226
- return buildLegacyScreenInputRows({
227
- instruction: meta.args?.instruction || "启动boss聊天任务",
228
- selectedPage: "chat",
229
- selectedJob: selectedChatJobForCsv(meta, snapshot),
230
- userSearchParams: cloneReportInput(searchParams, {}),
231
- effectiveSearchParams: cloneReportInput(searchParams, {}),
232
- screenParams: {
233
- criteria: normalized.criteria || meta.args?.criteria || context.criteria || "",
234
- target_count: searchParams.target_count,
235
- post_action: postAction,
236
- max_greet_count: meta.args?.max_greet_count ?? ""
237
- },
238
- followUp: meta.args?.follow_up || null,
239
- extraRows: [
240
- ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
241
- ["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"]
242
- ]
243
- });
244
- }
245
-
246
- function writeChatLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
247
- writeLegacyScreenCsv(filePath, {
248
- inputRows: buildChatCsvInputRows(snapshot, meta),
249
- results: rows
250
- });
251
- }
252
-
253
- function readChatRunState(runId) {
254
- const artifacts = getChatRunArtifacts(runId);
255
- if (!artifacts) return null;
256
- return readJsonFile(artifacts.run_state_path);
257
- }
258
-
259
- function toIsoOrNull(value) {
260
- const normalized = normalizeText(value);
261
- return normalized || null;
262
- }
263
-
264
- function secondsBetween(startedAt, endedAt) {
265
- const startMs = Date.parse(startedAt || "");
266
- const endMs = Date.parse(endedAt || "") || Date.now();
267
- if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
268
- return Math.max(1, Math.round((endMs - startMs) / 1000));
269
- }
270
-
271
- function countPostActionResults(results = []) {
272
- let requested = 0;
273
- let requestSatisfied = 0;
274
- let requestSkipped = 0;
275
- for (const row of results || []) {
276
- const action = row?.post_action || {};
277
- if (action.requested) requestSatisfied += 1;
278
- if (action.skipped) requestSkipped += 1;
279
- if (action.requested && !action.skipped) requested += 1;
280
- }
281
- return {
282
- requested,
283
- request_satisfied: requestSatisfied,
284
- request_skipped: requestSkipped
285
- };
286
- }
287
-
288
- function normalizeLegacyProgress(progress = {}, summary = null) {
289
- const countedRequests = countPostActionResults(Array.isArray(summary?.results) ? summary.results : []);
290
- const processed = Number.isInteger(progress.processed)
291
- ? progress.processed
292
- : Number.isInteger(summary?.processed)
293
- ? summary.processed
294
- : 0;
295
- const screened = Number.isInteger(progress.screened)
296
- ? progress.screened
297
- : Number.isInteger(summary?.screened)
298
- ? summary.screened
299
- : processed;
300
- const passed = Number.isInteger(progress.passed)
301
- ? progress.passed
302
- : Number.isInteger(summary?.passed)
303
- ? summary.passed
304
- : 0;
305
- const requested = Number.isInteger(progress.requested)
306
- ? progress.requested
307
- : Number.isInteger(summary?.requested)
308
- ? summary.requested
309
- : countedRequests.requested;
310
- const requestSatisfied = Number.isInteger(progress.request_satisfied)
311
- ? progress.request_satisfied
312
- : Number.isInteger(summary?.request_satisfied)
313
- ? summary.request_satisfied
314
- : countedRequests.request_satisfied;
315
- const requestSkipped = Number.isInteger(progress.request_skipped)
316
- ? progress.request_skipped
317
- : Number.isInteger(summary?.request_skipped)
318
- ? summary.request_skipped
319
- : countedRequests.request_skipped;
320
- return {
321
- ...progress,
322
- processed,
323
- inspected: processed,
324
- screened,
325
- passed,
326
- requested,
327
- request_satisfied: requestSatisfied,
328
- request_skipped: requestSkipped,
329
- skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
330
- greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0
331
- };
332
- }
333
-
334
- function completionReason(status) {
335
- if (status === RUN_STATUS_COMPLETED) return "completed";
336
- if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
337
- if (status === RUN_STATUS_FAILED) return "failed";
338
- if (status === RUN_STATUS_PAUSED) return "paused";
339
- return null;
340
- }
341
-
342
- function getChatRunMeta(runId) {
343
- return chatRunMeta.get(runId) || {};
344
- }
345
-
346
- function ensureChatRunArtifacts(snapshot) {
347
- const artifacts = getChatRunArtifacts(snapshot?.runId || snapshot?.run_id);
348
- if (!artifacts) return null;
349
-
350
- const meta = getChatRunMeta(snapshot?.runId || snapshot?.run_id);
351
- const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
352
- ? snapshot.checkpoint
353
- : {};
354
- writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
355
- if (meta) meta.checkpointPath = artifacts.checkpoint_path;
356
-
357
- const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
358
- const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
359
- const artifactSummary = summary || (checkpointResults.length ? {
360
- domain: "chat",
361
- partial: true,
362
- partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
363
- results: checkpointResults
364
- } : ARTIFACT_STATUSES.has(snapshot?.status || snapshot?.state) ? {
365
- domain: "chat",
366
- partial: (snapshot?.status || snapshot?.state) !== RUN_STATUS_COMPLETED,
367
- partial_reason: snapshot?.status || snapshot?.state || "unknown",
368
- completion_reason: completionReason(snapshot?.status || snapshot?.state),
369
- results: []
370
- } : null);
371
- if (artifactSummary) {
372
- const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
373
- writeChatLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
374
- writeJsonAtomic(artifacts.report_json, {
375
- run_id: snapshot.runId || snapshot.run_id,
376
- status: snapshot.status || snapshot.state,
377
- phase: snapshot.phase || snapshot.stage,
378
- progress: snapshot.progress || {},
379
- context: snapshot.context || {},
380
- checkpoint,
381
- summary: artifactSummary,
382
- generated_at: new Date().toISOString()
383
- });
384
- if (meta) {
385
- meta.outputCsvPath = artifacts.output_csv;
386
- meta.reportJsonPath = artifacts.report_json;
387
- }
388
- }
389
-
390
- return artifacts;
391
- }
392
-
393
- function persistChatCheckpointSnapshot(normalized) {
394
- const artifacts = getChatRunArtifacts(normalized?.run_id || normalized?.runId);
395
- if (!artifacts) return;
396
- const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
397
- ? normalized.checkpoint
398
- : {};
399
- writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
400
- const meta = getChatRunMeta(normalized?.run_id || normalized?.runId);
401
- if (meta) meta.checkpointPath = artifacts.checkpoint_path;
402
- }
403
-
404
- function isPidAlive(pid) {
405
- const numericPid = Number(pid);
406
- if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
407
- if (numericPid === process.pid) return true;
408
- try {
409
- process.kill(numericPid, 0);
410
- return true;
411
- } catch (error) {
412
- return error?.code === "EPERM";
413
- }
414
- }
415
-
416
- function snapshotFromPersistedChatRun(persisted = {}) {
417
- return {
418
- runId: persisted.run_id || persisted.runId,
419
- name: persisted.name || persisted.run_id || persisted.runId,
420
- status: persisted.status || persisted.state,
421
- phase: persisted.stage || persisted.phase,
422
- progress: persisted.progress || {},
423
- context: persisted.context || {},
424
- checkpoint: persisted.checkpoint || {},
425
- startedAt: persisted.started_at || persisted.startedAt,
426
- updatedAt: persisted.updated_at || persisted.updatedAt,
427
- completedAt: persisted.completed_at || persisted.completedAt || null,
428
- error: persisted.error || null,
429
- summary: persisted.summary || null
430
- };
431
- }
432
-
433
- function persistDiskChatRun(runId, payload) {
434
- const artifacts = getChatRunArtifacts(runId);
435
- if (!artifacts) return payload;
436
- writeJsonAtomic(artifacts.run_state_path, payload);
437
- return payload;
438
- }
439
-
440
- function attachLegacyArtifactsToPersistedChatRun(persisted = {}) {
441
- const runId = normalizeRunId(persisted.run_id || persisted.runId);
442
- if (!runId) return persisted;
443
- const snapshot = snapshotFromPersistedChatRun(persisted);
444
- const result = buildLegacyChatResult(snapshot);
445
- const artifacts = getChatRunArtifacts(runId);
446
- const next = {
447
- ...persisted,
448
- result,
449
- resume: {
450
- ...(persisted.resume || {}),
451
- checkpoint_path: result?.checkpoint_path || persisted.resume?.checkpoint_path || artifacts?.checkpoint_path || null,
452
- output_csv: result?.output_csv || persisted.resume?.output_csv || artifacts?.output_csv || null
453
- },
454
- artifacts: artifacts || persisted.artifacts || null
455
- };
456
- return persistDiskChatRun(runId, next);
457
- }
458
-
459
- function finalizePersistedChatRun(persisted = {}, {
460
- status = RUN_STATUS_FAILED,
461
- error = null,
462
- message = ""
463
- } = {}) {
464
- const runId = normalizeRunId(persisted.run_id || persisted.runId);
465
- if (!runId) return persisted;
466
- const now = new Date().toISOString();
467
- const normalizedError = status === RUN_STATUS_FAILED
468
- ? {
469
- name: error?.name || "Error",
470
- code: error?.code || "STALE_RUN_PROCESS_EXITED",
471
- message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
472
- }
473
- : null;
474
- const next = {
475
- ...persisted,
476
- run_id: runId,
477
- state: status,
478
- status,
479
- stage: persisted.stage || persisted.phase || "chat:stale",
480
- updated_at: now,
481
- heartbeat_at: now,
482
- completed_at: persisted.completed_at || now,
483
- last_message: normalizedError?.message || message || status,
484
- control: {
485
- ...(persisted.control || {}),
486
- cancel_requested: false
487
- },
488
- error: normalizedError,
489
- summary: persisted.summary || null
490
- };
491
- return attachLegacyArtifactsToPersistedChatRun(next);
492
- }
493
-
494
- function persistedChatRunArtifactMissing(persisted = {}) {
495
- const runId = normalizeRunId(persisted.run_id || persisted.runId);
496
- const artifacts = getChatRunArtifacts(runId);
497
- const outputCsv = persisted.result?.output_csv
498
- || persisted.resume?.output_csv
499
- || persisted.artifacts?.output_csv
500
- || artifacts?.output_csv;
501
- const reportJson = persisted.result?.report_json
502
- || persisted.artifacts?.report_json
503
- || artifacts?.report_json;
504
- return Boolean(
505
- !outputCsv
506
- || !reportJson
507
- || !fs.existsSync(outputCsv)
508
- || !fs.existsSync(reportJson)
509
- );
510
- }
511
-
512
- function reconcilePersistedChatRun(persisted = {}, { cancelStale = false } = {}) {
513
- const status = persisted.status || persisted.state;
514
- if (STALE_PROCESS_STATUSES.has(status) && !isPidAlive(persisted.pid)) {
515
- const shouldCancel = cancelStale || status === RUN_STATUS_CANCELING || persisted.control?.cancel_requested === true;
516
- return {
517
- run: finalizePersistedChatRun(persisted, {
518
- status: shouldCancel ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
519
- error: shouldCancel ? null : {
520
- code: "STALE_RUN_PROCESS_EXITED",
521
- message: `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
522
- },
523
- message: shouldCancel
524
- ? "Boss chat run was canceled after its worker process was no longer active."
525
- : `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
526
- }),
527
- stale_finalized: true
528
- };
529
- }
530
- if (ARTIFACT_STATUSES.has(status) && persistedChatRunArtifactMissing(persisted)) {
531
- return {
532
- run: attachLegacyArtifactsToPersistedChatRun(persisted),
533
- artifacts_repaired: true
534
- };
535
- }
536
- return {
537
- run: persisted
538
- };
539
- }
540
-
541
- function buildLegacyChatResult(snapshot) {
542
- if (!snapshot) return null;
543
- const artifacts = ensureChatRunArtifacts(snapshot);
544
- const meta = getChatRunMeta(snapshot.runId);
545
- const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
546
- const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
547
- const resultRows = Array.isArray(summary?.results)
548
- ? summary.results
549
- : Array.isArray(checkpoint.results)
550
- ? checkpoint.results
551
- : [];
552
- const progress = normalizeLegacyProgress(snapshot.progress, summary);
553
- return {
554
- run_id: snapshot.runId,
555
- state: snapshot.status,
556
- status: snapshot.status,
557
- completion_reason: completionReason(snapshot.status),
558
- requested_count: progress.requested,
559
- request_satisfied_count: progress.request_satisfied,
560
- request_skipped_count: progress.request_skipped,
561
- processed_count: progress.processed,
562
- inspected_count: progress.processed,
563
- screened_count: progress.screened,
564
- passed_count: progress.passed,
565
- skipped_count: progress.skipped,
566
- detail_opened: progress.detail_opened || summary?.detail_opened || 0,
567
- llm_screened: progress.llm_screened || summary?.llm_screened || 0,
568
- output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
569
- report_json: artifacts?.report_json || meta.reportJsonPath || null,
570
- checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
571
- started_at: snapshot.startedAt,
572
- completed_at: snapshot.completedAt || null,
573
- duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
574
- error: snapshot.error || null,
575
- results: resultRows
576
- };
577
- }
578
-
579
- function normalizeRunSnapshot(snapshot) {
580
- if (!snapshot) return null;
581
- const meta = getChatRunMeta(snapshot.runId);
582
- const artifacts = getChatRunArtifacts(snapshot.runId);
583
- const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
584
- const progress = normalizeLegacyProgress(snapshot.progress, summary);
585
- const legacyResult = (
586
- TERMINAL_STATUSES.has(snapshot.status)
587
- || snapshot.status === RUN_STATUS_PAUSED
588
- ) ? buildLegacyChatResult({ ...snapshot, progress }) : null;
589
- const oldContext = {
590
- workspace_root: meta.workspaceRoot || null,
591
- profile: meta.normalized?.profile || meta.args?.profile || "default",
592
- job: meta.normalized?.job || meta.args?.job || "",
593
- start_from: meta.normalized?.startFrom || meta.args?.start_from || "",
594
- criteria: meta.normalized?.criteria || meta.args?.criteria || "",
595
- greeting_text: meta.normalized?.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT,
596
- target_count: meta.normalized?.publicTargetCount ?? null,
597
- target_count_semantics: TARGET_COUNT_SEMANTICS
598
- };
599
- return {
600
- ...snapshot,
601
- progress,
602
- run_id: snapshot.runId,
603
- mode: RUN_MODE_ASYNC,
604
- state: snapshot.status,
605
- stage: snapshot.phase,
606
- started_at: snapshot.startedAt,
607
- updated_at: snapshot.updatedAt,
608
- completed_at: toIsoOrNull(snapshot.completedAt),
609
- heartbeat_at: snapshot.updatedAt,
610
- pid: process.pid || null,
611
- last_message: snapshot.error?.message || snapshot.phase || null,
612
- context: {
613
- ...(snapshot.context || {}),
614
- ...oldContext,
615
- shared_run_context: snapshot.context || {}
616
- },
617
- control: {
618
- pause_requested: snapshot.status === RUN_STATUS_PAUSED,
619
- pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
620
- pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_boss_chat_run" : null,
621
- cancel_requested: snapshot.status === RUN_STATUS_CANCELING
622
- },
623
- resume: {
624
- checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
625
- pause_control_path: artifacts?.run_state_path || null,
626
- output_csv: legacyResult?.output_csv || null,
627
- resume_count: meta.resumeCount || 0,
628
- last_resumed_at: meta.lastResumedAt || null,
629
- last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
630
- },
631
- result: legacyResult,
632
- artifacts
633
- };
634
- }
635
-
636
- function persistChatRunSnapshot(snapshot, {
637
- persistActiveCheckpoint = false
638
- } = {}) {
639
- const normalized = normalizeRunSnapshot(snapshot);
640
- if (!normalized?.run_id) return normalized;
641
- const artifacts = getChatRunArtifacts(normalized.run_id);
642
- if (!artifacts) return normalized;
643
- if (persistActiveCheckpoint) {
644
- persistChatCheckpointSnapshot(normalized);
645
- }
646
- const payload = {
647
- run_id: normalized.run_id,
648
- mode: normalized.mode,
649
- state: normalized.state,
650
- status: normalized.status,
651
- stage: normalized.stage,
652
- started_at: normalized.started_at,
653
- updated_at: normalized.updated_at,
654
- heartbeat_at: normalized.heartbeat_at,
655
- completed_at: normalized.completed_at,
656
- pid: normalized.pid,
657
- progress: normalized.progress,
658
- last_message: normalized.last_message,
659
- context: normalized.context,
660
- control: normalized.control,
661
- resume: normalized.resume,
662
- error: normalized.error,
663
- result: normalized.result,
664
- summary: normalized.summary,
665
- artifacts: normalized.artifacts
666
- };
667
- writeJsonAtomic(artifacts.run_state_path, payload);
668
- return normalized;
669
- }
670
-
671
- function persistChatLifecycleSnapshot(snapshot, event = {}) {
672
- return persistChatRunSnapshot(snapshot, {
673
- persistActiveCheckpoint: event?.type === "checkpoint"
674
- });
675
- }
676
-
677
- function attachMethodEvidence(payload, runId) {
678
- const meta = getChatRunMeta(runId);
679
- assertNoForbiddenCdpCalls(meta.methodLog || []);
680
- return {
681
- ...payload,
682
- runtime_evaluate_used: false,
683
- method_summary: methodSummary(meta.methodLog || []),
684
- method_log: meta.methodLog || [],
685
- chrome: meta.chrome || null
686
- };
687
- }
688
-
689
- function shouldNavigateToChat(url) {
690
- const text = String(url || "");
691
- return !text.includes("/web/chat/index")
692
- || text.includes("/web/chat/recommend")
693
- || text.includes("/web/chat/search");
694
- }
695
-
696
- function isRecoverableChatTargetUrl(url) {
697
- const text = String(url || "");
698
- return text.includes("zhipin.com/web/chat")
699
- || isForbiddenChatResumeTopLevelUrl(text);
700
- }
701
-
702
- async function waitForHealthyChat(client, config, {
703
- timeoutMs = 90000,
704
- intervalMs = 1000
705
- } = {}) {
706
- const started = Date.now();
707
- let lastCheck = null;
708
- while (Date.now() - started <= timeoutMs) {
709
- const loginDetection = await detectBossLoginState(client).catch(() => null);
710
- if (loginDetection?.requires_login) {
711
- return {
712
- status: "login_required",
713
- summary: "Boss login is required",
714
- loginDetection
715
- };
716
- }
717
- const roots = await resolveChatSelfHealRoots(client, config);
718
- lastCheck = await runSelfHealCheck({
719
- client,
720
- domain: "chat",
721
- roots: roots.roots,
722
- selectorProbes: config.selectorProbes,
723
- accessibilityProbes: config.accessibilityProbes,
724
- viewportProbes: config.viewportProbes
725
- });
726
- if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
727
- await sleep(intervalMs);
728
- }
729
- return lastCheck;
730
- }
731
-
732
- async function connectChatChromeSession({
733
- host = DEFAULT_CHAT_HOST,
734
- port = DEFAULT_CHAT_PORT,
735
- targetUrlIncludes = CHAT_TARGET_URL,
736
- allowNavigate = true,
737
- slowLive = false
738
- } = {}) {
739
- const session = await connectToChromeTargetOrOpen({
740
- host,
741
- port,
742
- targetUrlIncludes,
743
- targetUrl: CHAT_TARGET_URL,
744
- allowNavigate,
745
- slowLive,
746
- fallbackTargetPredicate: (target) => (
747
- target?.type === "page"
748
- && (isRecoverableChatTargetUrl(target?.url) || String(target?.url || "").includes("zhipin.com"))
749
- )
750
- });
751
-
752
- const { client, target } = session;
753
- await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
754
- if (typeof client?.Network?.setCacheDisabled === "function") {
755
- await client.Network.setCacheDisabled({ cacheDisabled: true });
756
- }
757
- await bringPageToFront(client);
758
-
759
- const targetUrl = String(target?.url || "");
760
- let navigation = {
761
- navigated: false,
762
- url: targetUrl
763
- };
764
- if (allowNavigate && shouldNavigateToChat(targetUrl)) {
765
- await client.Page.navigate({ url: CHAT_TARGET_URL });
766
- const settleMs = slowLive ? 10000 : 5000;
767
- const waited = await waitForMainFrameUrl(
768
- client,
769
- (url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
770
- { timeoutMs: settleMs, intervalMs: 500 }
771
- );
772
- navigation = {
773
- navigated: true,
774
- url: CHAT_TARGET_URL,
775
- settle_ms: settleMs,
776
- observed_url: waited.url || null,
777
- observed_url_ok: waited.ok
778
- };
779
- }
780
- let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
781
- if (allowNavigate && shouldNavigateToChat(currentUrl) && !isBossLoginUrl(currentUrl)) {
782
- await client.Page.navigate({ url: CHAT_TARGET_URL });
783
- const settleMs = slowLive ? 10000 : 5000;
784
- const waited = await waitForMainFrameUrl(
785
- client,
786
- (url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
787
- { timeoutMs: settleMs, intervalMs: 500 }
788
- );
789
- navigation = {
790
- navigated: true,
791
- url: CHAT_TARGET_URL,
792
- settle_ms: settleMs,
793
- observed_url: waited.url || null,
794
- observed_url_ok: waited.ok,
795
- reason: "observed_url_mismatch"
796
- };
797
- currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
798
- }
799
- const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
800
- requires_login: isBossLoginUrl(currentUrl),
801
- reason: "login_detection_failed",
802
- current_url: currentUrl
803
- }));
804
- if (loginDetection.requires_login) {
805
- await session.close?.();
806
- throw createBossLoginRequiredError({
807
- domain: "chat",
808
- currentUrl: loginDetection.current_url || currentUrl,
809
- targetUrl: CHAT_TARGET_URL,
810
- loginDetection,
811
- chrome: session.chrome || null
812
- });
813
- }
814
- if (shouldNavigateToChat(currentUrl)) {
815
- await session.close?.();
816
- throw new Error(`Boss chat page did not navigate to ${CHAT_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
817
- }
818
-
819
- const selfHealConfig = buildChatSelfHealConfig();
820
- const health = await waitForHealthyChat(client, selfHealConfig, {
821
- timeoutMs: slowLive ? 180000 : 90000,
822
- intervalMs: slowLive ? 1200 : 800
823
- });
824
- if (health?.loginDetection?.requires_login) {
825
- await session.close?.();
826
- throw createBossLoginRequiredError({
827
- domain: "chat",
828
- currentUrl: health.loginDetection.current_url || currentUrl,
829
- targetUrl: CHAT_TARGET_URL,
830
- loginDetection: health.loginDetection,
831
- chrome: session.chrome || null
832
- });
833
- }
834
- if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
835
- const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
836
- const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
837
- requires_login: isBossLoginUrl(latestUrl),
838
- reason: "login_detection_failed",
839
- current_url: latestUrl
840
- }));
841
- if (latestLoginDetection.requires_login) {
842
- await session.close?.();
843
- throw createBossLoginRequiredError({
844
- domain: "chat",
845
- currentUrl: latestLoginDetection.current_url || latestUrl,
846
- targetUrl: CHAT_TARGET_URL,
847
- loginDetection: latestLoginDetection,
848
- chrome: session.chrome || null
849
- });
850
- }
851
- throw new Error(`Boss chat page is not healthy: ${health?.status || "missing"}`);
852
- }
853
-
854
- return {
855
- ...session,
856
- navigation,
857
- health
858
- };
859
- }
860
-
861
- async function readChatJobOptionsFromSession(session) {
862
- const roots = await getChatRoots(session.client);
863
- const result = await readChatJobOptions(session.client, roots.rootNodes.top);
864
- try {
865
- result.menu_close = await closeChatJobDropdown(session.client, roots.rootNodes.top);
866
- } catch (error) {
867
- result.menu_close = {
868
- ok: false,
869
- closed: false,
870
- reason: "close_failed",
871
- error: error?.message || String(error)
872
- };
873
- }
874
- return result;
875
- }
876
-
877
- function normalizeChatStartInput(args = {}, configResolution = null) {
878
- const target = normalizeTargetCountInput(getBossChatTargetCountValue(args));
879
- const explicitGreetingText = normalizeText(args.greeting_text || args.greetingText || args.greeting);
880
- const configuredGreetingText = normalizeText(configResolution?.config?.greetingMessage || configResolution?.config?.greetingText);
881
- return {
882
- profile: normalizeText(args.profile) || "default",
883
- job: normalizeText(args.job),
884
- startFrom: normalizeText(args.start_from).toLowerCase(),
885
- criteria: normalizeText(args.criteria),
886
- greetingText: explicitGreetingText || configuredGreetingText,
887
- target,
888
- targetCount: target.targetCount,
889
- publicTargetCount: target.publicValue,
890
- host: normalizeText(args.host) || DEFAULT_CHAT_HOST,
891
- port: parsePositiveInteger(
892
- args.port,
893
- configResolution?.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT
894
- ),
895
- targetUrlIncludes: normalizeText(args.target_url_includes) || CHAT_TARGET_URL,
896
- allowNavigate: args.allow_navigate !== false,
897
- slowLive: args.slow_live === true
898
- };
899
- }
900
-
901
- function buildChatNextCallExample(args, missingFields, normalized) {
902
- const example = {};
903
- if (normalized.job) example.job = normalized.job;
904
- if (normalized.startFrom) example.start_from = normalized.startFrom;
905
- if (normalized.target.provided && !normalized.target.parseError) {
906
- example.target_count = normalized.publicTargetCount ?? normalized.targetCount;
907
- } else if (missingFields.includes("target_count")) {
908
- example.target_count = "all";
909
- }
910
- if (normalized.criteria) example.criteria = normalized.criteria;
911
- if (normalizeText(args.greeting_text || args.greetingText || args.greeting)) {
912
- example.greeting_text = normalizeText(args.greeting_text || args.greetingText || args.greeting);
913
- }
914
- return Object.keys(example).length ? example : null;
915
- }
916
-
917
- function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
918
- const missing = [];
919
- if (!normalized.job) missing.push("job");
920
- if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
921
- if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
922
- if (!normalized.criteria) missing.push("criteria");
923
- return missing;
924
- }
925
-
926
- function buildTargetCountDiagnostics(args, missingFields, normalized) {
927
- if (!missingFields.includes("target_count")) return {};
928
- const hints = buildTargetCountCompatibilityHints({
929
- argumentName: "target_count",
930
- recommendedArgumentPatch: { target_count: "all" }
931
- });
932
- const received = getBossChatTargetCountValue(args);
933
- const nextCallExample = {
934
- ...(normalizeText(args.job) ? { job: normalizeText(args.job) } : {}),
935
- ...(normalizeText(args.start_from) ? { start_from: normalizeText(args.start_from).toLowerCase() } : {}),
936
- target_count: "all",
937
- ...(normalizeText(args.criteria) ? { criteria: normalizeText(args.criteria) } : {})
938
- };
939
- return {
940
- ...hints,
941
- received_target_count: received,
942
- target_count_parse_error: normalized.target.parseError || null,
943
- next_call_example: nextCallExample
944
- };
945
- }
946
-
947
- function buildJobQuestionOptions(jobOptions = []) {
948
- return (jobOptions || []).map((option) => ({
949
- label: option.label,
950
- value: option.value,
951
- index: option.index,
952
- active: option.active === true
953
- }));
954
- }
955
-
956
- function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions = [] }) {
957
- const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
958
- return missingFields.map((field) => {
959
- if (field === "job") {
960
- return {
961
- field,
962
- question: "请提供 Boss chat 岗位,支持岗位名、编号或页面中的岗位 value。",
963
- value: normalized.job || null,
964
- options: buildJobQuestionOptions(jobOptions)
965
- };
966
- }
967
- if (field === "start_from") {
968
- return {
969
- field,
970
- question: "请确认 chat 起始范围。",
971
- value: normalized.startFrom || null,
972
- options: [
973
- { label: "未读", value: "unread" },
974
- { label: "全部", value: "all" }
975
- ]
976
- };
977
- }
978
- if (field === "target_count") {
979
- return {
980
- field,
981
- ...diagnostics,
982
- question: "请提供 target_count,使用正整数或 all(扫到底)。",
983
- value: normalized.publicTargetCount ?? null,
984
- options: Array.isArray(diagnostics.options) ? diagnostics.options : [],
985
- parse_error: normalized.target.parseError || null
986
- };
987
- }
988
- if (field === "criteria") {
989
- return {
990
- field,
991
- question: "请提供自然语言筛选 criteria。",
992
- value: normalized.criteria || null
993
- };
994
- }
995
- return {
996
- field,
997
- question: `请提供 ${field}。`,
998
- value: null
999
- };
1000
- });
1001
- }
1002
-
1003
- async function buildNeedInputResponse({ args, missingFields, normalized }) {
1004
- const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
1005
- return {
1006
- status: "NEED_INPUT",
1007
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
1008
- missing_fields: missingFields,
1009
- ...diagnostics,
1010
- pending_questions: buildPendingChatQuestions({ args, missingFields, normalized }),
1011
- job_options: [],
1012
- error: {
1013
- code: "MISSING_REQUIRED_FIELDS",
1014
- message: "缺少必要字段。请补齐 job、start_from、target_count、criteria 后再启动 Boss chat CDP-only run。",
1015
- retryable: true
1016
- }
1017
- };
1018
- }
1019
-
1020
- function shouldRequestChatResume(args = {}, context = {}) {
1021
- const action = normalizeText(args.post_action || args.action).toLowerCase();
1022
- if (
1023
- args.request_cv === false
1024
- || args.request_resume === false
1025
- || args.ask_cv === false
1026
- || args.execute_post_action === false
1027
- || args.no_request_cv === true
1028
- || args.no_request_resume === true
1029
- || CHAT_DISABLE_REQUEST_RESUME_ACTIONS.has(action)
1030
- ) {
1031
- return false;
1032
- }
1033
- if (
1034
- args.request_cv === true
1035
- || args.request_resume === true
1036
- || args.ask_cv === true
1037
- || args.execute_post_action === true
1038
- || CHAT_REQUEST_RESUME_ACTIONS.has(action)
1039
- ) {
1040
- return true;
1041
- }
1042
- if (typeof context.request_resume_for_passed === "boolean") {
1043
- return context.request_resume_for_passed;
1044
- }
1045
- return true;
1046
- }
1047
-
1048
- function isDebugTestMode(args = {}) {
1049
- return args.debug_test_mode === true || args.allow_debug_test_mode === true;
1050
- }
1051
-
1052
- function normalizeScreeningModeArg(args = {}) {
1053
- const raw = normalizeText(args.screening_mode || args.screeningMode || "");
1054
- if (args.use_llm === false) return "deterministic";
1055
- return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
1056
- ? "deterministic"
1057
- : "llm";
1058
- }
1059
-
1060
- function collectChatDebugTestOptions(args = {}) {
1061
- const reasons = [];
1062
- if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
1063
- if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
1064
- if (args.dry_run === true || args.dry_run_request_cv === true) reasons.push("dry_run_request_cv");
1065
- return reasons;
1066
- }
1067
-
1068
- function shouldUseChatLlm(args = {}) {
1069
- return normalizeScreeningModeArg(args) !== "deterministic";
1070
- }
1071
-
1072
- function getRunOptions(args, normalized, session, { workspaceRoot = "", configResolution = null } = {}) {
1073
- const slowLive = args.slow_live === true;
1074
- const isAllTarget = normalized.publicTargetCount === "all";
1075
- const processedLimit = parsePositiveInteger(
1076
- args.max_candidates,
1077
- isAllTarget ? CHAT_ALL_MAX_CANDIDATES : CHAT_ALL_MAX_CANDIDATES
1078
- );
1079
- const shouldRequestResume = shouldRequestChatResume(args);
1080
- const useLlm = shouldUseChatLlm(args);
1081
- const resolvedConfig = configResolution || (useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false });
1082
- const humanBehavior = resolveHumanBehaviorForRun(args, resolvedConfig?.config || {});
1083
- return {
1084
- client: session.client,
1085
- targetUrl: CHAT_TARGET_URL,
1086
- job: normalized.job,
1087
- startFrom: normalized.startFrom,
1088
- criteria: normalized.criteria,
1089
- maxCandidates: processedLimit,
1090
- targetPassCount: isAllTarget ? null : normalized.targetCount,
1091
- processUntilListEnd: isAllTarget,
1092
- detailLimit: parseNonNegativeInteger(args.detail_limit, useLlm || shouldRequestResume ? processedLimit : 0),
1093
- detailSource: normalizeText(args.detail_source) || "cascade",
1094
- closeResume: true,
1095
- requestResumeForPassed: shouldRequestResume,
1096
- dryRunRequestCv: args.dry_run === true || args.dry_run_request_cv === true,
1097
- greetingText: normalized.greetingText || DEFAULT_CHAT_GREETING_TEXT,
1098
- delayMs: parseNonNegativeInteger(args.delay_ms, 0),
1099
- cardTimeoutMs: slowLive ? 180000 : 90000,
1100
- readyTimeoutMs: slowLive ? 120000 : 60000,
1101
- onlineResumeButtonTimeoutMs: parsePositiveInteger(
1102
- args.online_resume_button_timeout_ms,
1103
- slowLive ? 30000 : 15000
1104
- ),
1105
- resumeDomTimeoutMs: slowLive ? 120000 : 60000,
1106
- maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
1107
- imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
1108
- llmConfig: resolvedConfig.ok ? {
1109
- ...resolvedConfig.config
1110
- } : null,
1111
- llmTimeoutMs: parsePositiveInteger(
1112
- args.llm_timeout_ms,
1113
- parsePositiveInteger(resolvedConfig.config?.llmTimeoutMs || resolvedConfig.config?.timeoutMs, slowLive ? 180000 : 120000)
1114
- ),
1115
- llmImageLimit: parsePositiveInteger(
1116
- args.llm_image_limit,
1117
- parsePositiveInteger(resolvedConfig.config?.llmImageLimit || resolvedConfig.config?.imageLimit, 8)
1118
- ),
1119
- llmImageDetail: normalizeText(
1120
- args.llm_image_detail || resolvedConfig.config?.llmImageDetail || resolvedConfig.config?.imageDetail
1121
- ) || "low",
1122
- screeningMode: normalizeScreeningModeArg(args),
1123
- listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
1124
- listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
1125
- listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
1126
- listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
1127
- listFallbackPoint: null,
1128
- imageOutputDir: resolveBossConfiguredOutputDir("", getChatRunsDir()),
1129
- humanRestEnabled: humanBehavior.restEnabled,
1130
- humanBehavior,
1131
- name: "mcp-boss-chat-run"
1132
- };
1133
- }
1134
-
1135
- async function closeChatRunSession(runId) {
1136
- const meta = chatRunMeta.get(runId);
1137
- if (!meta || meta.closed) return;
1138
- try {
1139
- try {
1140
- if (meta.session?.client) {
1141
- await closeChatResumeModal(meta.session.client, { attemptsLimit: 2 });
1142
- }
1143
- } catch {
1144
- // Cleanup is best-effort once the run has settled.
1145
- }
1146
- assertNoForbiddenCdpCalls(meta.methodLog || []);
1147
- } finally {
1148
- meta.closed = true;
1149
- try {
1150
- await meta.session?.close?.();
1151
- } catch {
1152
- // Nothing actionable for the caller once the run has settled.
1153
- }
1154
- }
1155
- }
1156
-
1157
- async function waitForChatRunTerminal(runId) {
1158
- while (true) {
1159
- try {
1160
- const snapshot = chatRunService.getChatRun(runId);
1161
- if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
1162
- } catch {
1163
- return null;
1164
- }
1165
- await sleep(1000);
1166
- }
1167
- }
1168
-
1169
- function trackChatRun(runId) {
1170
- waitForChatRunTerminal(runId)
1171
- .then((terminal) => {
1172
- if (terminal) persistChatRunSnapshot(terminal);
1173
- })
1174
- .catch(() => null)
1175
- .finally(() => {
1176
- closeChatRunSession(runId).catch(() => {});
1177
- });
1178
- }
1179
-
1180
- async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
1181
- const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
1182
- const normalized = normalizeChatStartInput(args, defaultConfigResolution);
1183
- const missingFields = getMissingChatStartFields(args, normalized);
1184
- if (missingFields.length) {
1185
- return buildNeedInputResponse({
1186
- args,
1187
- missingFields,
1188
- normalized
1189
- });
1190
- }
1191
-
1192
- const shouldRequestResume = shouldRequestChatResume(args);
1193
- const useLlm = shouldUseChatLlm(args);
1194
- const debugTestOptions = collectChatDebugTestOptions(args);
1195
- if (debugTestOptions.length && !isDebugTestMode(args)) {
1196
- return {
1197
- status: "FAILED",
1198
- error: {
1199
- code: "DEBUG_TEST_MODE_REQUIRED",
1200
- message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1201
- retryable: false
1202
- },
1203
- debug_test_options: debugTestOptions
1204
- };
1205
- }
1206
- const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
1207
- if (useLlm && !configResolution?.ok) {
1208
- return {
1209
- status: "FAILED",
1210
- error: {
1211
- code: "SCREEN_CONFIG_ERROR",
1212
- message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
1213
- retryable: true
1214
- }
1215
- };
1216
- }
1217
-
1218
- let session;
1219
- try {
1220
- session = await chatConnectorImpl({
1221
- host: normalized.host,
1222
- port: normalized.port,
1223
- targetUrlIncludes: normalized.targetUrlIncludes,
1224
- allowNavigate: normalized.allowNavigate,
1225
- slowLive: normalized.slowLive
1226
- });
1227
- } catch (error) {
1228
- const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1229
- return {
1230
- status: "FAILED",
1231
- error: {
1232
- code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1233
- message: error?.message || "Boss chat page is not ready",
1234
- requires_login: Boolean(error?.requires_login),
1235
- login_url: error?.login_url || null,
1236
- login_detection: error?.login_detection || null,
1237
- chrome: error?.chrome || null,
1238
- current_url: error?.current_url || null,
1239
- target_url: error?.target_url || CHAT_TARGET_URL,
1240
- retryable: true
1241
- },
1242
- chrome: error?.chrome || null
1243
- };
1244
- }
1245
-
1246
- let started;
1247
- try {
1248
- started = chatRunService.startChatRun(getRunOptions(args, normalized, session, { workspaceRoot, configResolution }));
1249
- } catch (error) {
1250
- await session.close?.();
1251
- return {
1252
- status: "FAILED",
1253
- error: {
1254
- code: "CHAT_RUN_START_FAILED",
1255
- message: error?.message || "Failed to start Boss chat run",
1256
- retryable: true
1257
- }
1258
- };
1259
- }
1260
-
1261
- chatRunMeta.set(started.runId, {
1262
- session,
1263
- methodLog: session.methodLog || [],
1264
- workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
1265
- args: clonePlain(args, {}),
1266
- normalized,
1267
- chrome: {
1268
- host: normalized.host,
1269
- port: normalized.port,
1270
- target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1271
- target_id: session.target?.id || null,
1272
- auto_launch: session.chrome || null
1273
- },
1274
- health: session.health || null
1275
- });
1276
- trackChatRun(started.runId);
1277
- const persistedStarted = persistChatRunSnapshot(started);
1278
-
1279
- return {
1280
- status: "ACCEPTED",
1281
- run_id: persistedStarted.run_id,
1282
- state: persistedStarted.state,
1283
- run: persistedStarted,
1284
- poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1285
- message: shouldRequestResume
1286
- ? "Boss chat run started through the shared CDP-only chat service. Passed candidates will follow the configured request-CV sequence."
1287
- : "Boss chat run started through the shared CDP-only chat service.",
1288
- target_count_semantics: TARGET_COUNT_SEMANTICS
1289
- };
1290
- }
1291
-
1292
- export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
1293
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
1294
- const normalized = normalizeChatStartInput(args, configResolution);
1295
- let session;
1296
- try {
1297
- session = await chatConnectorImpl({
1298
- host: normalized.host,
1299
- port: normalized.port,
1300
- targetUrlIncludes: normalized.targetUrlIncludes,
1301
- allowNavigate: normalized.allowNavigate,
1302
- slowLive: normalized.slowLive
1303
- });
1304
- } catch (error) {
1305
- const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1306
- return {
1307
- status: "FAILED",
1308
- stage: "chat_run_setup",
1309
- error: {
1310
- code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1311
- message: error?.message || "Boss chat page is not ready",
1312
- requires_login: Boolean(error?.requires_login),
1313
- login_url: error?.login_url || null,
1314
- login_detection: error?.login_detection || null,
1315
- chrome: error?.chrome || null,
1316
- current_url: error?.current_url || null,
1317
- target_url: error?.target_url || CHAT_TARGET_URL,
1318
- retryable: true
1319
- },
1320
- runtime_evaluate_used: false,
1321
- method_summary: {},
1322
- method_log: [],
1323
- chrome: {
1324
- host: normalized.host,
1325
- port: normalized.port,
1326
- target_url: CHAT_TARGET_URL,
1327
- auto_launch: error?.chrome || null
1328
- }
1329
- };
1330
- }
1331
-
1332
- try {
1333
- const jobs = await chatJobReaderImpl(session, {
1334
- workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
1335
- args: clonePlain(args, {}),
1336
- normalized
1337
- });
1338
- const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
1339
- const missingFields = getMissingChatStartFields(args, normalized);
1340
- const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
1341
- const nextCallExample = buildChatNextCallExample(args, missingFields, normalized);
1342
- const selectedJob = jobOptions.find((option) => {
1343
- const job = normalizeText(normalized.job).toLowerCase();
1344
- if (!job) return option.active === true;
1345
- return [option.value, option.label, option.title]
1346
- .map((value) => normalizeText(value).toLowerCase())
1347
- .includes(job);
1348
- }) || null;
1349
-
1350
- assertNoForbiddenCdpCalls(session.methodLog || []);
1351
- return {
1352
- status: missingFields.length ? "NEED_INPUT" : "READY",
1353
- stage: "chat_run_setup",
1354
- page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1355
- required_fields: CHAT_REQUIRED_FIELDS.slice(),
1356
- missing_fields: missingFields,
1357
- job_options: jobOptions,
1358
- selected_job: selectedJob,
1359
- selected_job_label: jobs?.selected_label || selectedJob?.label || "",
1360
- job_options_source: jobs?.source || "",
1361
- job_options_selector: jobs?.selector || "",
1362
- pending_questions: buildPendingChatQuestions({
1363
- args,
1364
- missingFields,
1365
- normalized,
1366
- jobOptions
1367
- }),
1368
- ...diagnostics,
1369
- ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
1370
- message: missingFields.length
1371
- ? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count / criteria。"
1372
- : "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening.",
1373
- runtime_evaluate_used: false,
1374
- method_summary: methodSummary(session.methodLog || []),
1375
- method_log: session.methodLog || [],
1376
- chrome: {
1377
- host: normalized.host,
1378
- port: normalized.port,
1379
- target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1380
- target_id: session.target?.id || null,
1381
- auto_launch: session.chrome || null
1382
- }
1383
- };
1384
- } catch (error) {
1385
- const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1386
- return {
1387
- status: "FAILED",
1388
- stage: "chat_run_setup",
1389
- error: {
1390
- code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PREPARE_FAILED",
1391
- message: error?.message || "Boss chat CDP-only prepare failed",
1392
- requires_login: Boolean(error?.requires_login),
1393
- login_url: error?.login_url || null,
1394
- login_detection: error?.login_detection || null,
1395
- chrome: error?.chrome || null,
1396
- current_url: error?.current_url || null,
1397
- target_url: error?.target_url || CHAT_TARGET_URL,
1398
- retryable: true
1399
- },
1400
- runtime_evaluate_used: false,
1401
- method_summary: methodSummary(session.methodLog || []),
1402
- method_log: session.methodLog || [],
1403
- chrome: {
1404
- host: normalized.host,
1405
- port: normalized.port,
1406
- target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1407
- target_id: session.target?.id || null,
1408
- auto_launch: session.chrome || null
1409
- }
1410
- };
1411
- } finally {
1412
- try {
1413
- assertNoForbiddenCdpCalls(session.methodLog || []);
1414
- } finally {
1415
- await session.close?.();
1416
- }
1417
- }
1418
- }
1419
-
1420
- export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} } = {}) {
1421
- const configResolution = resolveBossScreeningConfig(workspaceRoot);
1422
- const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
1423
- const host = normalizeText(args.host) || DEFAULT_CHAT_HOST;
1424
- const port = parsePositiveInteger(args.port, configResolution.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT);
1425
- const targetUrlIncludes = normalizeText(args.target_url_includes) || CHAT_TARGET_URL;
1426
- const allowNavigate = args.allow_navigate !== false;
1427
- const slowLive = args.slow_live === true;
1428
- const basePayload = {
1429
- server: "boss-chat",
1430
- mode: "cdp-only",
1431
- cdp_only: true,
1432
- cli_dir: null,
1433
- cli_path: null,
1434
- config_path: configResolution.config_path || null,
1435
- config_dir: configResolution.config_dir || null,
1436
- output_dir: configResolution.ok ? configResolution.config.outputDir || null : null,
1437
- debug_port: port,
1438
- shared_llm_config: configResolution.ok === true,
1439
- data_dir: runtimeLayout.data_dir,
1440
- data_dir_source: runtimeLayout.data_dir_source,
1441
- legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
1442
- migration_source_dir: runtimeLayout.migration_source_dir,
1443
- migration_pending: runtimeLayout.migration_pending
1444
- };
1445
-
1446
- if (!configResolution.ok) {
1447
- return {
1448
- status: "FAILED",
1449
- ...basePayload,
1450
- error: configResolution.error,
1451
- runtime_evaluate_used: false,
1452
- method_summary: {},
1453
- method_log: [],
1454
- chrome: {
1455
- host,
1456
- port,
1457
- target_url: targetUrlIncludes
1458
- }
1459
- };
1460
- }
1461
-
1462
- let session;
1463
- try {
1464
- session = await chatConnectorImpl({
1465
- host,
1466
- port,
1467
- targetUrlIncludes,
1468
- allowNavigate,
1469
- slowLive
1470
- });
1471
- assertNoForbiddenCdpCalls(session.methodLog || []);
1472
- return {
1473
- status: "OK",
1474
- ...basePayload,
1475
- page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1476
- health: session.health || null,
1477
- runtime_evaluate_used: false,
1478
- method_summary: methodSummary(session.methodLog || []),
1479
- method_log: session.methodLog || [],
1480
- chrome: {
1481
- host,
1482
- port,
1483
- target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1484
- target_id: session.target?.id || null,
1485
- auto_launch: session.chrome || null
1486
- },
1487
- message: "Boss chat CDP-only health check passed with shared self-heal probes."
1488
- };
1489
- } catch (error) {
1490
- const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1491
- return {
1492
- status: "FAILED",
1493
- ...basePayload,
1494
- error: {
1495
- code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1496
- message: error?.message || "Boss chat page is not ready",
1497
- requires_login: Boolean(error?.requires_login),
1498
- login_url: error?.login_url || null,
1499
- login_detection: error?.login_detection || null,
1500
- chrome: error?.chrome || null,
1501
- current_url: error?.current_url || null,
1502
- target_url: error?.target_url || CHAT_TARGET_URL,
1503
- retryable: true
1504
- },
1505
- runtime_evaluate_used: false,
1506
- method_summary: methodSummary(session?.methodLog || []),
1507
- method_log: session?.methodLog || [],
1508
- chrome: {
1509
- host,
1510
- port,
1511
- target_url: session?.navigation?.url || session?.target?.url || targetUrlIncludes,
1512
- target_id: session?.target?.id || null,
1513
- auto_launch: error?.chrome || session?.chrome || null
1514
- }
1515
- };
1516
- } finally {
1517
- if (session?.methodLog) assertNoForbiddenCdpCalls(session.methodLog);
1518
- await session?.close?.();
1519
- }
1520
- }
1521
-
1522
- export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
1523
- const started = await startBossChatRunInternal(args, { workspaceRoot });
1524
- if (started.status !== "ACCEPTED") return started;
1525
- return attachMethodEvidence(started, started.run_id);
1526
- }
1527
-
1528
- export function getBossChatRunTool({ args = {} } = {}) {
1529
- const runId = normalizeRunId(args.run_id || args.runId);
1530
- if (!runId) {
1531
- return {
1532
- status: "FAILED",
1533
- error: {
1534
- code: "INVALID_RUN_ID",
1535
- message: "run_id is required",
1536
- retryable: false
1537
- }
1538
- };
1539
- }
1540
- try {
1541
- const run = chatRunService.getChatRun(runId);
1542
- const normalizedRun = persistChatRunSnapshot(run);
1543
- return attachMethodEvidence({
1544
- status: "RUN_STATUS",
1545
- run: normalizedRun
1546
- }, runId);
1547
- } catch {
1548
- const persisted = readChatRunState(runId);
1549
- if (persisted) {
1550
- const reconciled = reconcilePersistedChatRun(persisted);
1551
- return {
1552
- status: "RUN_STATUS",
1553
- run: reconciled.run,
1554
- persistence: {
1555
- source: "disk",
1556
- active_control_available: false,
1557
- stale_finalized: reconciled.stale_finalized === true,
1558
- artifacts_repaired: reconciled.artifacts_repaired === true
1559
- },
1560
- runtime_evaluate_used: false,
1561
- method_summary: {},
1562
- method_log: [],
1563
- chrome: null
1564
- };
1565
- }
1566
- return {
1567
- status: "FAILED",
1568
- error: {
1569
- code: "RUN_NOT_FOUND",
1570
- message: `No Boss chat run found for run_id=${runId}`,
1571
- retryable: false
1572
- }
1573
- };
1574
- }
1575
- }
1576
-
1577
- export function pauseBossChatRunTool({ args = {} } = {}) {
1578
- const runId = normalizeRunId(args.run_id || args.runId);
1579
- try {
1580
- const before = chatRunService.getChatRun(runId);
1581
- if (TERMINAL_STATUSES.has(before.status)) {
1582
- const normalizedBefore = persistChatRunSnapshot(before);
1583
- return attachMethodEvidence({
1584
- status: "PAUSE_IGNORED",
1585
- run: normalizedBefore,
1586
- message: "目标任务已结束,无需暂停。"
1587
- }, runId);
1588
- }
1589
- if (before.status === RUN_STATUS_PAUSED) {
1590
- const normalizedBefore = persistChatRunSnapshot(before);
1591
- return attachMethodEvidence({
1592
- status: "PAUSE_IGNORED",
1593
- run: normalizedBefore,
1594
- message: "目标任务已经处于 paused 状态。"
1595
- }, runId);
1596
- }
1597
- const run = chatRunService.pauseChatRun(runId);
1598
- const normalizedRun = persistChatRunSnapshot(run);
1599
- return attachMethodEvidence({
1600
- status: "PAUSE_REQUESTED",
1601
- run: normalizedRun,
1602
- message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
1603
- }, runId);
1604
- } catch {
1605
- const persisted = readChatRunState(runId);
1606
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1607
- const reconciled = reconcilePersistedChatRun(persisted);
1608
- return {
1609
- status: "PAUSE_IGNORED",
1610
- run: reconciled.run,
1611
- message: "目标任务已结束,无需暂停。",
1612
- runtime_evaluate_used: false,
1613
- method_summary: {},
1614
- method_log: [],
1615
- chrome: null
1616
- };
1617
- }
1618
- return getBossChatRunTool({ args });
1619
- }
1620
- }
1621
-
1622
- export function resumeBossChatRunTool({ args = {} } = {}) {
1623
- const runId = normalizeRunId(args.run_id || args.runId);
1624
- try {
1625
- const before = chatRunService.getChatRun(runId);
1626
- if (TERMINAL_STATUSES.has(before.status)) {
1627
- const normalizedBefore = persistChatRunSnapshot(before);
1628
- return attachMethodEvidence({
1629
- status: "FAILED",
1630
- error: {
1631
- code: "RUN_ALREADY_TERMINATED",
1632
- message: "目标任务已结束,无法继续。",
1633
- retryable: false
1634
- },
1635
- run: normalizedBefore
1636
- }, runId);
1637
- }
1638
- if (before.status !== RUN_STATUS_PAUSED) {
1639
- const normalizedBefore = persistChatRunSnapshot(before);
1640
- return attachMethodEvidence({
1641
- status: "FAILED",
1642
- error: {
1643
- code: "RUN_NOT_PAUSED",
1644
- message: "仅 paused 状态的 run 才能继续。",
1645
- retryable: true
1646
- },
1647
- run: normalizedBefore
1648
- }, runId);
1649
- }
1650
- const run = chatRunService.resumeChatRun(runId);
1651
- const meta = getChatRunMeta(runId);
1652
- if (meta) {
1653
- meta.resumeCount = (meta.resumeCount || 0) + 1;
1654
- meta.lastResumedAt = new Date().toISOString();
1655
- }
1656
- const normalizedRun = persistChatRunSnapshot(run);
1657
- return attachMethodEvidence({
1658
- status: "RESUME_REQUESTED",
1659
- run: normalizedRun,
1660
- poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1661
- message: "已恢复 Boss chat run,请使用 get_boss_chat_run 按需轮询。"
1662
- }, runId);
1663
- } catch {
1664
- const persisted = readChatRunState(runId);
1665
- if (persisted) {
1666
- const reconciled = reconcilePersistedChatRun(persisted);
1667
- const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
1668
- return {
1669
- status: "FAILED",
1670
- error: {
1671
- code: TERMINAL_STATUSES.has(reconciledStatus) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1672
- message: TERMINAL_STATUSES.has(reconciledStatus)
1673
- ? "目标任务已结束,无法继续。"
1674
- : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1675
- retryable: !TERMINAL_STATUSES.has(reconciledStatus)
1676
- },
1677
- run: reconciled.run,
1678
- persistence: {
1679
- source: "disk",
1680
- active_control_available: false,
1681
- stale_finalized: reconciled.stale_finalized === true,
1682
- artifacts_repaired: reconciled.artifacts_repaired === true
1683
- },
1684
- runtime_evaluate_used: false,
1685
- method_summary: {},
1686
- method_log: [],
1687
- chrome: null
1688
- };
1689
- }
1690
- return getBossChatRunTool({ args });
1691
- }
1692
- }
1693
-
1694
- export function cancelBossChatRunTool({ args = {} } = {}) {
1695
- const runId = normalizeRunId(args.run_id || args.runId);
1696
- try {
1697
- const before = chatRunService.getChatRun(runId);
1698
- if (TERMINAL_STATUSES.has(before.status)) {
1699
- const normalizedBefore = persistChatRunSnapshot(before);
1700
- return attachMethodEvidence({
1701
- status: "CANCEL_IGNORED",
1702
- run: normalizedBefore,
1703
- message: "目标任务已结束,无需取消。"
1704
- }, runId);
1705
- }
1706
- const run = chatRunService.cancelChatRun(runId);
1707
- const normalizedRun = persistChatRunSnapshot(run);
1708
- return attachMethodEvidence({
1709
- status: "CANCEL_REQUESTED",
1710
- run: normalizedRun,
1711
- message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
1712
- }, runId);
1713
- } catch {
1714
- const persisted = readChatRunState(runId);
1715
- if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1716
- const reconciled = reconcilePersistedChatRun(persisted);
1717
- return {
1718
- status: "CANCEL_IGNORED",
1719
- run: reconciled.run,
1720
- message: "目标任务已结束,无需取消。",
1721
- runtime_evaluate_used: false,
1722
- method_summary: {},
1723
- method_log: [],
1724
- chrome: null
1725
- };
1726
- }
1727
- if (persisted) {
1728
- const reconciled = reconcilePersistedChatRun(persisted, { cancelStale: true });
1729
- if (reconciled.stale_finalized) {
1730
- return {
1731
- status: "CANCEL_REQUESTED",
1732
- run: reconciled.run,
1733
- message: "该 run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
1734
- persistence: {
1735
- source: "disk",
1736
- active_control_available: false,
1737
- stale_finalized: true,
1738
- artifacts_repaired: reconciled.artifacts_repaired === true
1739
- },
1740
- runtime_evaluate_used: false,
1741
- method_summary: {},
1742
- method_log: [],
1743
- chrome: null
1744
- };
1745
- }
1746
- }
1747
- return getBossChatRunTool({ args });
1748
- }
1749
- }
1750
-
1751
- export function __setChatMcpConnectorForTests(nextConnector) {
1752
- chatConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectChatChromeSession;
1753
- }
1754
-
1755
- export function __setChatMcpJobReaderForTests(nextReader) {
1756
- chatJobReaderImpl = typeof nextReader === "function" ? nextReader : readChatJobOptionsFromSession;
1757
- }
1758
-
1759
- export function __setChatMcpWorkflowForTests(nextWorkflow) {
1760
- chatWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runChatWorkflow;
1761
- chatRunService = createChatRunService({
1762
- idPrefix: "mcp_chat",
1763
- workflow: (...args) => chatWorkflowImpl(...args),
1764
- onSnapshot: persistChatLifecycleSnapshot
1765
- });
1766
- }
1767
-
1768
- export function __resetChatMcpStateForTests() {
1769
- for (const meta of chatRunMeta.values()) {
1770
- try {
1771
- meta.session?.close?.();
1772
- } catch {
1773
- // Best-effort test cleanup.
1774
- }
1775
- }
1776
- chatRunMeta.clear();
1777
- __setChatMcpConnectorForTests(null);
1778
- __setChatMcpJobReaderForTests(null);
1779
- __setChatMcpWorkflowForTests(null);
1780
- }
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
+ connectToChromeTargetOrOpen,
8
+ createBossLoginRequiredError,
9
+ detectBossLoginState,
10
+ enableDomains,
11
+ getMainFrameUrl,
12
+ isBossLoginUrl,
13
+ waitForMainFrameUrl,
14
+ sleep
15
+ } from "./core/browser/index.js";
16
+ import {
17
+ RUN_STATUS_CANCELING,
18
+ RUN_STATUS_CANCELED,
19
+ RUN_STATUS_COMPLETED,
20
+ RUN_STATUS_FAILED,
21
+ RUN_STATUS_PAUSED
22
+ } from "./core/run/index.js";
23
+ import {
24
+ buildLegacyScreenInputRows,
25
+ cloneReportInput,
26
+ writeLegacyScreenCsv
27
+ } from "./core/reporting/legacy-csv.js";
28
+ import {
29
+ buildChatSelfHealConfig,
30
+ HEALTH_STATUS,
31
+ resolveChatSelfHealRoots,
32
+ runSelfHealCheck
33
+ } from "./core/self-heal/index.js";
34
+ import {
35
+ CHAT_TARGET_URL,
36
+ closeChatResumeModal,
37
+ closeChatJobDropdown,
38
+ createChatRunService,
39
+ getChatRoots,
40
+ isForbiddenChatResumeTopLevelUrl,
41
+ readChatJobOptions,
42
+ runChatWorkflow
43
+ } from "./domains/chat/index.js";
44
+ import {
45
+ buildTargetCountCompatibilityHints,
46
+ getBossChatDataDir,
47
+ getBossChatTargetCountValue,
48
+ normalizeTargetCountInput,
49
+ resolveBossConfiguredOutputDir,
50
+ resolveBossChatRuntimeLayout,
51
+ resolveHumanBehaviorForRun,
52
+ resolveBossScreeningConfig
53
+ } from "./chat-runtime-config.js";
54
+ import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
55
+
56
+ const DEFAULT_CHAT_HOST = "127.0.0.1";
57
+ const DEFAULT_CHAT_PORT = 9222;
58
+ const DEFAULT_CHAT_POLL_AFTER_SEC = 10;
59
+ const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
60
+ const CHAT_ALL_MAX_CANDIDATES = 100000;
61
+ 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";
62
+ const RUN_MODE_ASYNC = "async";
63
+
64
+ const CHAT_REQUIRED_FIELDS = Object.freeze([
65
+ "job",
66
+ "start_from",
67
+ "target_count",
68
+ "criteria"
69
+ ]);
70
+
71
+ const TERMINAL_STATUSES = new Set([
72
+ RUN_STATUS_COMPLETED,
73
+ RUN_STATUS_FAILED,
74
+ RUN_STATUS_CANCELED
75
+ ]);
76
+
77
+ const ARTIFACT_STATUSES = new Set([
78
+ RUN_STATUS_COMPLETED,
79
+ RUN_STATUS_FAILED,
80
+ RUN_STATUS_CANCELED,
81
+ RUN_STATUS_PAUSED
82
+ ]);
83
+
84
+ const STALE_PROCESS_STATUSES = new Set([
85
+ "queued",
86
+ "running",
87
+ RUN_STATUS_CANCELING
88
+ ]);
89
+
90
+ const CHAT_REQUEST_RESUME_ACTIONS = new Set([
91
+ "request_cv",
92
+ "ask_cv",
93
+ "request_resume",
94
+ "求简历",
95
+ "索要简历"
96
+ ]);
97
+
98
+ const CHAT_DISABLE_REQUEST_RESUME_ACTIONS = new Set([
99
+ "none",
100
+ "no",
101
+ "false",
102
+ "off",
103
+ "skip",
104
+ "do_nothing",
105
+ "nothing",
106
+ "不做",
107
+ "什么都不做",
108
+ "无",
109
+ "不用",
110
+ "不求简历",
111
+ "不请求简历"
112
+ ]);
113
+
114
+ let chatWorkflowImpl = runChatWorkflow;
115
+ let chatConnectorImpl = connectChatChromeSession;
116
+ let chatJobReaderImpl = readChatJobOptionsFromSession;
117
+ let chatRunService = createChatRunService({
118
+ idPrefix: "mcp_chat",
119
+ workflow: (...args) => chatWorkflowImpl(...args),
120
+ onSnapshot: persistChatLifecycleSnapshot
121
+ });
122
+ const chatRunMeta = new Map();
123
+
124
+ function normalizeText(value) {
125
+ return String(value || "").replace(/\s+/g, " ").trim();
126
+ }
127
+
128
+ function parsePositiveInteger(raw, fallback) {
129
+ const parsed = Number.parseInt(String(raw || ""), 10);
130
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
131
+ }
132
+
133
+ function parseNonNegativeInteger(raw, fallback) {
134
+ const parsed = Number.parseInt(String(raw ?? ""), 10);
135
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
136
+ }
137
+
138
+ function methodSummary(methodLog = []) {
139
+ const summary = {};
140
+ for (const entry of methodLog || []) {
141
+ summary[entry.method] = (summary[entry.method] || 0) + 1;
142
+ }
143
+ return summary;
144
+ }
145
+
146
+ function clonePlain(value, fallback = null) {
147
+ try {
148
+ return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
149
+ } catch {
150
+ return fallback;
151
+ }
152
+ }
153
+
154
+ function normalizeRunId(runId) {
155
+ const normalized = normalizeText(runId);
156
+ if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
157
+ return normalized;
158
+ }
159
+
160
+ function getChatRunsDir() {
161
+ return path.join(getBossChatDataDir(), "runs");
162
+ }
163
+
164
+ function getChatRunArtifacts(runId) {
165
+ const normalized = normalizeRunId(runId);
166
+ if (!normalized) return null;
167
+ const runsDir = getChatRunsDir();
168
+ const outputDir = resolveBossConfiguredOutputDir("", runsDir);
169
+ return {
170
+ runs_dir: runsDir,
171
+ output_dir: outputDir,
172
+ run_state_path: path.join(runsDir, `${normalized}.json`),
173
+ checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
174
+ output_csv: path.join(outputDir, `${normalized}.results.csv`),
175
+ report_json: path.join(outputDir, `${normalized}.report.json`)
176
+ };
177
+ }
178
+
179
+ function ensureDirectory(dirPath) {
180
+ fs.mkdirSync(dirPath, { recursive: true });
181
+ }
182
+
183
+ function writeJsonAtomic(filePath, payload) {
184
+ ensureDirectory(path.dirname(filePath));
185
+ const tempPath = `${filePath}.tmp`;
186
+ fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
187
+ fs.renameSync(tempPath, filePath);
188
+ }
189
+
190
+ function readJsonFile(filePath) {
191
+ try {
192
+ if (!fs.existsSync(filePath)) return null;
193
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
194
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ function selectedChatJobForCsv(meta = {}, snapshot = {}) {
201
+ const job = normalizeText(
202
+ meta.normalized?.job
203
+ || meta.args?.job
204
+ || snapshot.context?.job
205
+ || ""
206
+ );
207
+ return {
208
+ value: job,
209
+ title: job,
210
+ label: job
211
+ };
212
+ }
213
+
214
+ function buildChatCsvInputRows(snapshot = {}, meta = {}) {
215
+ const normalized = meta.normalized || {};
216
+ const context = snapshot.context || {};
217
+ const postAction = shouldRequestChatResume(meta.args, context)
218
+ ? "request_cv"
219
+ : normalizeText(meta.args?.post_action || meta.args?.action || "") || "none";
220
+ const searchParams = {
221
+ job: normalized.job || meta.args?.job || context.job || "",
222
+ start_from: normalized.startFrom || meta.args?.start_from || context.start_from || "",
223
+ target_count: normalized.publicTargetCount ?? normalized.targetCount ?? snapshot.progress?.target_count ?? "",
224
+ detail_source: meta.args?.detail_source || snapshot.summary?.detail_source || context.detail_source || ""
225
+ };
226
+ return buildLegacyScreenInputRows({
227
+ instruction: meta.args?.instruction || "启动boss聊天任务",
228
+ selectedPage: "chat",
229
+ selectedJob: selectedChatJobForCsv(meta, snapshot),
230
+ userSearchParams: cloneReportInput(searchParams, {}),
231
+ effectiveSearchParams: cloneReportInput(searchParams, {}),
232
+ screenParams: {
233
+ criteria: normalized.criteria || meta.args?.criteria || context.criteria || "",
234
+ target_count: searchParams.target_count,
235
+ post_action: postAction,
236
+ max_greet_count: meta.args?.max_greet_count ?? ""
237
+ },
238
+ followUp: meta.args?.follow_up || null,
239
+ extraRows: [
240
+ ["chat_params.greeting_text", normalized.greetingText || meta.args?.greeting_text || meta.args?.greetingText || context.greeting_text || DEFAULT_CHAT_GREETING_TEXT],
241
+ ["chat_params.profile", normalized.profile || meta.args?.profile || context.profile || "default"]
242
+ ]
243
+ });
244
+ }
245
+
246
+ function writeChatLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
247
+ writeLegacyScreenCsv(filePath, {
248
+ inputRows: buildChatCsvInputRows(snapshot, meta),
249
+ results: rows
250
+ });
251
+ }
252
+
253
+ function readChatRunState(runId) {
254
+ const artifacts = getChatRunArtifacts(runId);
255
+ if (!artifacts) return null;
256
+ return readJsonFile(artifacts.run_state_path);
257
+ }
258
+
259
+ function toIsoOrNull(value) {
260
+ const normalized = normalizeText(value);
261
+ return normalized || null;
262
+ }
263
+
264
+ function secondsBetween(startedAt, endedAt) {
265
+ const startMs = Date.parse(startedAt || "");
266
+ const endMs = Date.parse(endedAt || "") || Date.now();
267
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
268
+ return Math.max(1, Math.round((endMs - startMs) / 1000));
269
+ }
270
+
271
+ function countPostActionResults(results = []) {
272
+ let requested = 0;
273
+ let requestSatisfied = 0;
274
+ let requestSkipped = 0;
275
+ for (const row of results || []) {
276
+ const action = row?.post_action || {};
277
+ if (action.requested) requestSatisfied += 1;
278
+ if (action.skipped) requestSkipped += 1;
279
+ if (action.requested && !action.skipped) requested += 1;
280
+ }
281
+ return {
282
+ requested,
283
+ request_satisfied: requestSatisfied,
284
+ request_skipped: requestSkipped
285
+ };
286
+ }
287
+
288
+ function normalizeLegacyProgress(progress = {}, summary = null) {
289
+ const countedRequests = countPostActionResults(Array.isArray(summary?.results) ? summary.results : []);
290
+ const processed = Number.isInteger(progress.processed)
291
+ ? progress.processed
292
+ : Number.isInteger(summary?.processed)
293
+ ? summary.processed
294
+ : 0;
295
+ const screened = Number.isInteger(progress.screened)
296
+ ? progress.screened
297
+ : Number.isInteger(summary?.screened)
298
+ ? summary.screened
299
+ : processed;
300
+ const passed = Number.isInteger(progress.passed)
301
+ ? progress.passed
302
+ : Number.isInteger(summary?.passed)
303
+ ? summary.passed
304
+ : 0;
305
+ const requested = Number.isInteger(progress.requested)
306
+ ? progress.requested
307
+ : Number.isInteger(summary?.requested)
308
+ ? summary.requested
309
+ : countedRequests.requested;
310
+ const requestSatisfied = Number.isInteger(progress.request_satisfied)
311
+ ? progress.request_satisfied
312
+ : Number.isInteger(summary?.request_satisfied)
313
+ ? summary.request_satisfied
314
+ : countedRequests.request_satisfied;
315
+ const requestSkipped = Number.isInteger(progress.request_skipped)
316
+ ? progress.request_skipped
317
+ : Number.isInteger(summary?.request_skipped)
318
+ ? summary.request_skipped
319
+ : countedRequests.request_skipped;
320
+ return {
321
+ ...progress,
322
+ processed,
323
+ inspected: processed,
324
+ screened,
325
+ passed,
326
+ requested,
327
+ request_satisfied: requestSatisfied,
328
+ request_skipped: requestSkipped,
329
+ skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
330
+ greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0
331
+ };
332
+ }
333
+
334
+ function completionReason(status) {
335
+ if (status === RUN_STATUS_COMPLETED) return "completed";
336
+ if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
337
+ if (status === RUN_STATUS_FAILED) return "failed";
338
+ if (status === RUN_STATUS_PAUSED) return "paused";
339
+ return null;
340
+ }
341
+
342
+ function getChatRunMeta(runId) {
343
+ return chatRunMeta.get(runId) || {};
344
+ }
345
+
346
+ function ensureChatRunArtifacts(snapshot) {
347
+ const artifacts = getChatRunArtifacts(snapshot?.runId || snapshot?.run_id);
348
+ if (!artifacts) return null;
349
+
350
+ const meta = getChatRunMeta(snapshot?.runId || snapshot?.run_id);
351
+ const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
352
+ ? snapshot.checkpoint
353
+ : {};
354
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
355
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
356
+
357
+ const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
358
+ const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
359
+ const artifactSummary = summary || (checkpointResults.length ? {
360
+ domain: "chat",
361
+ partial: true,
362
+ partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
363
+ results: checkpointResults
364
+ } : ARTIFACT_STATUSES.has(snapshot?.status || snapshot?.state) ? {
365
+ domain: "chat",
366
+ partial: (snapshot?.status || snapshot?.state) !== RUN_STATUS_COMPLETED,
367
+ partial_reason: snapshot?.status || snapshot?.state || "unknown",
368
+ completion_reason: completionReason(snapshot?.status || snapshot?.state),
369
+ results: []
370
+ } : null);
371
+ if (artifactSummary) {
372
+ const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
373
+ writeChatLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
374
+ writeJsonAtomic(artifacts.report_json, {
375
+ run_id: snapshot.runId || snapshot.run_id,
376
+ status: snapshot.status || snapshot.state,
377
+ phase: snapshot.phase || snapshot.stage,
378
+ progress: snapshot.progress || {},
379
+ context: snapshot.context || {},
380
+ checkpoint,
381
+ summary: artifactSummary,
382
+ generated_at: new Date().toISOString()
383
+ });
384
+ if (meta) {
385
+ meta.outputCsvPath = artifacts.output_csv;
386
+ meta.reportJsonPath = artifacts.report_json;
387
+ }
388
+ }
389
+
390
+ return artifacts;
391
+ }
392
+
393
+ function persistChatCheckpointSnapshot(normalized) {
394
+ const artifacts = getChatRunArtifacts(normalized?.run_id || normalized?.runId);
395
+ if (!artifacts) return;
396
+ const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
397
+ ? normalized.checkpoint
398
+ : {};
399
+ writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
400
+ const meta = getChatRunMeta(normalized?.run_id || normalized?.runId);
401
+ if (meta) meta.checkpointPath = artifacts.checkpoint_path;
402
+ }
403
+
404
+ function isPidAlive(pid) {
405
+ const numericPid = Number(pid);
406
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
407
+ if (numericPid === process.pid) return true;
408
+ try {
409
+ process.kill(numericPid, 0);
410
+ return true;
411
+ } catch (error) {
412
+ return error?.code === "EPERM";
413
+ }
414
+ }
415
+
416
+ function snapshotFromPersistedChatRun(persisted = {}) {
417
+ return {
418
+ runId: persisted.run_id || persisted.runId,
419
+ name: persisted.name || persisted.run_id || persisted.runId,
420
+ status: persisted.status || persisted.state,
421
+ phase: persisted.stage || persisted.phase,
422
+ progress: persisted.progress || {},
423
+ context: persisted.context || {},
424
+ checkpoint: persisted.checkpoint || {},
425
+ startedAt: persisted.started_at || persisted.startedAt,
426
+ updatedAt: persisted.updated_at || persisted.updatedAt,
427
+ completedAt: persisted.completed_at || persisted.completedAt || null,
428
+ error: persisted.error || null,
429
+ summary: persisted.summary || null
430
+ };
431
+ }
432
+
433
+ function persistDiskChatRun(runId, payload) {
434
+ const artifacts = getChatRunArtifacts(runId);
435
+ if (!artifacts) return payload;
436
+ writeJsonAtomic(artifacts.run_state_path, payload);
437
+ return payload;
438
+ }
439
+
440
+ function attachLegacyArtifactsToPersistedChatRun(persisted = {}) {
441
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
442
+ if (!runId) return persisted;
443
+ const snapshot = snapshotFromPersistedChatRun(persisted);
444
+ const result = buildLegacyChatResult(snapshot);
445
+ const artifacts = getChatRunArtifacts(runId);
446
+ const next = {
447
+ ...persisted,
448
+ result,
449
+ resume: {
450
+ ...(persisted.resume || {}),
451
+ checkpoint_path: result?.checkpoint_path || persisted.resume?.checkpoint_path || artifacts?.checkpoint_path || null,
452
+ output_csv: result?.output_csv || persisted.resume?.output_csv || artifacts?.output_csv || null
453
+ },
454
+ artifacts: artifacts || persisted.artifacts || null
455
+ };
456
+ return persistDiskChatRun(runId, next);
457
+ }
458
+
459
+ function finalizePersistedChatRun(persisted = {}, {
460
+ status = RUN_STATUS_FAILED,
461
+ error = null,
462
+ message = ""
463
+ } = {}) {
464
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
465
+ if (!runId) return persisted;
466
+ const now = new Date().toISOString();
467
+ const normalizedError = status === RUN_STATUS_FAILED
468
+ ? {
469
+ name: error?.name || "Error",
470
+ code: error?.code || "STALE_RUN_PROCESS_EXITED",
471
+ message: error?.message || message || "Boss chat run process exited before it wrote a terminal state."
472
+ }
473
+ : null;
474
+ const next = {
475
+ ...persisted,
476
+ run_id: runId,
477
+ state: status,
478
+ status,
479
+ stage: persisted.stage || persisted.phase || "chat:stale",
480
+ updated_at: now,
481
+ heartbeat_at: now,
482
+ completed_at: persisted.completed_at || now,
483
+ last_message: normalizedError?.message || message || status,
484
+ control: {
485
+ ...(persisted.control || {}),
486
+ cancel_requested: false
487
+ },
488
+ error: normalizedError,
489
+ summary: persisted.summary || null
490
+ };
491
+ return attachLegacyArtifactsToPersistedChatRun(next);
492
+ }
493
+
494
+ function persistedChatRunArtifactMissing(persisted = {}) {
495
+ const runId = normalizeRunId(persisted.run_id || persisted.runId);
496
+ const artifacts = getChatRunArtifacts(runId);
497
+ const outputCsv = persisted.result?.output_csv
498
+ || persisted.resume?.output_csv
499
+ || persisted.artifacts?.output_csv
500
+ || artifacts?.output_csv;
501
+ const reportJson = persisted.result?.report_json
502
+ || persisted.artifacts?.report_json
503
+ || artifacts?.report_json;
504
+ return Boolean(
505
+ !outputCsv
506
+ || !reportJson
507
+ || !fs.existsSync(outputCsv)
508
+ || !fs.existsSync(reportJson)
509
+ );
510
+ }
511
+
512
+ function reconcilePersistedChatRun(persisted = {}, { cancelStale = false } = {}) {
513
+ const status = persisted.status || persisted.state;
514
+ if (STALE_PROCESS_STATUSES.has(status) && !isPidAlive(persisted.pid)) {
515
+ const shouldCancel = cancelStale || status === RUN_STATUS_CANCELING || persisted.control?.cancel_requested === true;
516
+ return {
517
+ run: finalizePersistedChatRun(persisted, {
518
+ status: shouldCancel ? RUN_STATUS_CANCELED : RUN_STATUS_FAILED,
519
+ error: shouldCancel ? null : {
520
+ code: "STALE_RUN_PROCESS_EXITED",
521
+ message: `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
522
+ },
523
+ message: shouldCancel
524
+ ? "Boss chat run was canceled after its worker process was no longer active."
525
+ : `Boss chat run process is no longer alive for pid=${persisted.pid || "unknown"}.`
526
+ }),
527
+ stale_finalized: true
528
+ };
529
+ }
530
+ if (ARTIFACT_STATUSES.has(status) && persistedChatRunArtifactMissing(persisted)) {
531
+ return {
532
+ run: attachLegacyArtifactsToPersistedChatRun(persisted),
533
+ artifacts_repaired: true
534
+ };
535
+ }
536
+ return {
537
+ run: persisted
538
+ };
539
+ }
540
+
541
+ function buildLegacyChatResult(snapshot) {
542
+ if (!snapshot) return null;
543
+ const artifacts = ensureChatRunArtifacts(snapshot);
544
+ const meta = getChatRunMeta(snapshot.runId);
545
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
546
+ const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
547
+ const resultRows = Array.isArray(summary?.results)
548
+ ? summary.results
549
+ : Array.isArray(checkpoint.results)
550
+ ? checkpoint.results
551
+ : [];
552
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
553
+ return {
554
+ run_id: snapshot.runId,
555
+ state: snapshot.status,
556
+ status: snapshot.status,
557
+ completion_reason: completionReason(snapshot.status),
558
+ requested_count: progress.requested,
559
+ request_satisfied_count: progress.request_satisfied,
560
+ request_skipped_count: progress.request_skipped,
561
+ processed_count: progress.processed,
562
+ inspected_count: progress.processed,
563
+ screened_count: progress.screened,
564
+ passed_count: progress.passed,
565
+ skipped_count: progress.skipped,
566
+ detail_opened: progress.detail_opened || summary?.detail_opened || 0,
567
+ llm_screened: progress.llm_screened || summary?.llm_screened || 0,
568
+ output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
569
+ report_json: artifacts?.report_json || meta.reportJsonPath || null,
570
+ checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
571
+ started_at: snapshot.startedAt,
572
+ completed_at: snapshot.completedAt || null,
573
+ duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
574
+ error: snapshot.error || null,
575
+ results: resultRows
576
+ };
577
+ }
578
+
579
+ function normalizeRunSnapshot(snapshot) {
580
+ if (!snapshot) return null;
581
+ const meta = getChatRunMeta(snapshot.runId);
582
+ const artifacts = getChatRunArtifacts(snapshot.runId);
583
+ const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
584
+ const progress = normalizeLegacyProgress(snapshot.progress, summary);
585
+ const legacyResult = (
586
+ TERMINAL_STATUSES.has(snapshot.status)
587
+ || snapshot.status === RUN_STATUS_PAUSED
588
+ ) ? buildLegacyChatResult({ ...snapshot, progress }) : null;
589
+ const oldContext = {
590
+ workspace_root: meta.workspaceRoot || null,
591
+ profile: meta.normalized?.profile || meta.args?.profile || "default",
592
+ job: meta.normalized?.job || meta.args?.job || "",
593
+ start_from: meta.normalized?.startFrom || meta.args?.start_from || "",
594
+ criteria: meta.normalized?.criteria || meta.args?.criteria || "",
595
+ greeting_text: meta.normalized?.greetingText || meta.args?.greeting_text || meta.args?.greetingText || DEFAULT_CHAT_GREETING_TEXT,
596
+ target_count: meta.normalized?.publicTargetCount ?? null,
597
+ target_count_semantics: TARGET_COUNT_SEMANTICS
598
+ };
599
+ return {
600
+ ...snapshot,
601
+ progress,
602
+ run_id: snapshot.runId,
603
+ mode: RUN_MODE_ASYNC,
604
+ state: snapshot.status,
605
+ stage: snapshot.phase,
606
+ started_at: snapshot.startedAt,
607
+ updated_at: snapshot.updatedAt,
608
+ completed_at: toIsoOrNull(snapshot.completedAt),
609
+ heartbeat_at: snapshot.updatedAt,
610
+ pid: process.pid || null,
611
+ last_message: snapshot.error?.message || snapshot.phase || null,
612
+ context: {
613
+ ...(snapshot.context || {}),
614
+ ...oldContext,
615
+ shared_run_context: snapshot.context || {}
616
+ },
617
+ control: {
618
+ pause_requested: snapshot.status === RUN_STATUS_PAUSED,
619
+ pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
620
+ pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_boss_chat_run" : null,
621
+ cancel_requested: snapshot.status === RUN_STATUS_CANCELING
622
+ },
623
+ resume: {
624
+ checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
625
+ pause_control_path: artifacts?.run_state_path || null,
626
+ output_csv: legacyResult?.output_csv || null,
627
+ resume_count: meta.resumeCount || 0,
628
+ last_resumed_at: meta.lastResumedAt || null,
629
+ last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
630
+ },
631
+ result: legacyResult,
632
+ artifacts
633
+ };
634
+ }
635
+
636
+ function persistChatRunSnapshot(snapshot, {
637
+ persistActiveCheckpoint = false
638
+ } = {}) {
639
+ const normalized = normalizeRunSnapshot(snapshot);
640
+ if (!normalized?.run_id) return normalized;
641
+ const artifacts = getChatRunArtifacts(normalized.run_id);
642
+ if (!artifacts) return normalized;
643
+ if (persistActiveCheckpoint) {
644
+ persistChatCheckpointSnapshot(normalized);
645
+ }
646
+ const payload = {
647
+ run_id: normalized.run_id,
648
+ mode: normalized.mode,
649
+ state: normalized.state,
650
+ status: normalized.status,
651
+ stage: normalized.stage,
652
+ started_at: normalized.started_at,
653
+ updated_at: normalized.updated_at,
654
+ heartbeat_at: normalized.heartbeat_at,
655
+ completed_at: normalized.completed_at,
656
+ pid: normalized.pid,
657
+ progress: normalized.progress,
658
+ last_message: normalized.last_message,
659
+ context: normalized.context,
660
+ control: normalized.control,
661
+ resume: normalized.resume,
662
+ error: normalized.error,
663
+ result: normalized.result,
664
+ summary: normalized.summary,
665
+ artifacts: normalized.artifacts
666
+ };
667
+ writeJsonAtomic(artifacts.run_state_path, payload);
668
+ return normalized;
669
+ }
670
+
671
+ function persistChatLifecycleSnapshot(snapshot, event = {}) {
672
+ return persistChatRunSnapshot(snapshot, {
673
+ persistActiveCheckpoint: event?.type === "checkpoint"
674
+ });
675
+ }
676
+
677
+ function attachMethodEvidence(payload, runId) {
678
+ const meta = getChatRunMeta(runId);
679
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
680
+ return {
681
+ ...payload,
682
+ runtime_evaluate_used: false,
683
+ method_summary: methodSummary(meta.methodLog || []),
684
+ method_log: meta.methodLog || [],
685
+ chrome: meta.chrome || null
686
+ };
687
+ }
688
+
689
+ function shouldNavigateToChat(url) {
690
+ const text = String(url || "");
691
+ return !text.includes("/web/chat/index")
692
+ || text.includes("/web/chat/recommend")
693
+ || text.includes("/web/chat/search");
694
+ }
695
+
696
+ function isRecoverableChatTargetUrl(url) {
697
+ const text = String(url || "");
698
+ return text.includes("zhipin.com/web/chat")
699
+ || isForbiddenChatResumeTopLevelUrl(text);
700
+ }
701
+
702
+ async function waitForHealthyChat(client, config, {
703
+ timeoutMs = 90000,
704
+ intervalMs = 1000
705
+ } = {}) {
706
+ const started = Date.now();
707
+ let lastCheck = null;
708
+ while (Date.now() - started <= timeoutMs) {
709
+ const loginDetection = await detectBossLoginState(client).catch(() => null);
710
+ if (loginDetection?.requires_login) {
711
+ return {
712
+ status: "login_required",
713
+ summary: "Boss login is required",
714
+ loginDetection
715
+ };
716
+ }
717
+ const roots = await resolveChatSelfHealRoots(client, config);
718
+ lastCheck = await runSelfHealCheck({
719
+ client,
720
+ domain: "chat",
721
+ roots: roots.roots,
722
+ selectorProbes: config.selectorProbes,
723
+ accessibilityProbes: config.accessibilityProbes,
724
+ viewportProbes: config.viewportProbes
725
+ });
726
+ if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
727
+ await sleep(intervalMs);
728
+ }
729
+ return lastCheck;
730
+ }
731
+
732
+ async function connectChatChromeSession({
733
+ host = DEFAULT_CHAT_HOST,
734
+ port = DEFAULT_CHAT_PORT,
735
+ targetUrlIncludes = CHAT_TARGET_URL,
736
+ allowNavigate = true,
737
+ slowLive = false
738
+ } = {}) {
739
+ const session = await connectToChromeTargetOrOpen({
740
+ host,
741
+ port,
742
+ targetUrlIncludes,
743
+ targetUrl: CHAT_TARGET_URL,
744
+ allowNavigate,
745
+ slowLive,
746
+ fallbackTargetPredicate: (target) => (
747
+ target?.type === "page"
748
+ && (isRecoverableChatTargetUrl(target?.url) || String(target?.url || "").includes("zhipin.com"))
749
+ )
750
+ });
751
+
752
+ const { client, target } = session;
753
+ await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
754
+ if (typeof client?.Network?.setCacheDisabled === "function") {
755
+ await client.Network.setCacheDisabled({ cacheDisabled: true });
756
+ }
757
+ await bringPageToFront(client);
758
+
759
+ const targetUrl = String(target?.url || "");
760
+ let navigation = {
761
+ navigated: false,
762
+ url: targetUrl
763
+ };
764
+ if (allowNavigate && shouldNavigateToChat(targetUrl)) {
765
+ await client.Page.navigate({ url: CHAT_TARGET_URL });
766
+ const settleMs = slowLive ? 10000 : 5000;
767
+ const waited = await waitForMainFrameUrl(
768
+ client,
769
+ (url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
770
+ { timeoutMs: settleMs, intervalMs: 500 }
771
+ );
772
+ navigation = {
773
+ navigated: true,
774
+ url: CHAT_TARGET_URL,
775
+ settle_ms: settleMs,
776
+ observed_url: waited.url || null,
777
+ observed_url_ok: waited.ok
778
+ };
779
+ }
780
+ let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
781
+ if (allowNavigate && shouldNavigateToChat(currentUrl) && !isBossLoginUrl(currentUrl)) {
782
+ await client.Page.navigate({ url: CHAT_TARGET_URL });
783
+ const settleMs = slowLive ? 10000 : 5000;
784
+ const waited = await waitForMainFrameUrl(
785
+ client,
786
+ (url) => isBossLoginUrl(url) || !shouldNavigateToChat(url),
787
+ { timeoutMs: settleMs, intervalMs: 500 }
788
+ );
789
+ navigation = {
790
+ navigated: true,
791
+ url: CHAT_TARGET_URL,
792
+ settle_ms: settleMs,
793
+ observed_url: waited.url || null,
794
+ observed_url_ok: waited.ok,
795
+ reason: "observed_url_mismatch"
796
+ };
797
+ currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
798
+ }
799
+ const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
800
+ requires_login: isBossLoginUrl(currentUrl),
801
+ reason: "login_detection_failed",
802
+ current_url: currentUrl
803
+ }));
804
+ if (loginDetection.requires_login) {
805
+ await session.close?.();
806
+ throw createBossLoginRequiredError({
807
+ domain: "chat",
808
+ currentUrl: loginDetection.current_url || currentUrl,
809
+ targetUrl: CHAT_TARGET_URL,
810
+ loginDetection,
811
+ chrome: session.chrome || null
812
+ });
813
+ }
814
+ if (shouldNavigateToChat(currentUrl)) {
815
+ await session.close?.();
816
+ throw new Error(`Boss chat page did not navigate to ${CHAT_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
817
+ }
818
+
819
+ const selfHealConfig = buildChatSelfHealConfig();
820
+ const health = await waitForHealthyChat(client, selfHealConfig, {
821
+ timeoutMs: slowLive ? 180000 : 90000,
822
+ intervalMs: slowLive ? 1200 : 800
823
+ });
824
+ if (health?.loginDetection?.requires_login) {
825
+ await session.close?.();
826
+ throw createBossLoginRequiredError({
827
+ domain: "chat",
828
+ currentUrl: health.loginDetection.current_url || currentUrl,
829
+ targetUrl: CHAT_TARGET_URL,
830
+ loginDetection: health.loginDetection,
831
+ chrome: session.chrome || null
832
+ });
833
+ }
834
+ if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
835
+ const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
836
+ const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
837
+ requires_login: isBossLoginUrl(latestUrl),
838
+ reason: "login_detection_failed",
839
+ current_url: latestUrl
840
+ }));
841
+ if (latestLoginDetection.requires_login) {
842
+ await session.close?.();
843
+ throw createBossLoginRequiredError({
844
+ domain: "chat",
845
+ currentUrl: latestLoginDetection.current_url || latestUrl,
846
+ targetUrl: CHAT_TARGET_URL,
847
+ loginDetection: latestLoginDetection,
848
+ chrome: session.chrome || null
849
+ });
850
+ }
851
+ throw new Error(`Boss chat page is not healthy: ${health?.status || "missing"}`);
852
+ }
853
+
854
+ return {
855
+ ...session,
856
+ navigation,
857
+ health
858
+ };
859
+ }
860
+
861
+ async function readChatJobOptionsFromSession(session) {
862
+ const roots = await getChatRoots(session.client);
863
+ const result = await readChatJobOptions(session.client, roots.rootNodes.top);
864
+ try {
865
+ result.menu_close = await closeChatJobDropdown(session.client, roots.rootNodes.top);
866
+ } catch (error) {
867
+ result.menu_close = {
868
+ ok: false,
869
+ closed: false,
870
+ reason: "close_failed",
871
+ error: error?.message || String(error)
872
+ };
873
+ }
874
+ return result;
875
+ }
876
+
877
+ function normalizeChatStartInput(args = {}, configResolution = null) {
878
+ const target = normalizeTargetCountInput(getBossChatTargetCountValue(args));
879
+ const explicitGreetingText = normalizeText(args.greeting_text || args.greetingText || args.greeting);
880
+ const configuredGreetingText = normalizeText(configResolution?.config?.greetingMessage || configResolution?.config?.greetingText);
881
+ return {
882
+ profile: normalizeText(args.profile) || "default",
883
+ job: normalizeText(args.job),
884
+ startFrom: normalizeText(args.start_from).toLowerCase(),
885
+ criteria: normalizeText(args.criteria),
886
+ greetingText: explicitGreetingText || configuredGreetingText,
887
+ target,
888
+ targetCount: target.targetCount,
889
+ publicTargetCount: target.publicValue,
890
+ host: normalizeText(args.host) || DEFAULT_CHAT_HOST,
891
+ port: parsePositiveInteger(
892
+ args.port,
893
+ configResolution?.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT
894
+ ),
895
+ targetUrlIncludes: normalizeText(args.target_url_includes) || CHAT_TARGET_URL,
896
+ allowNavigate: args.allow_navigate !== false,
897
+ slowLive: args.slow_live === true
898
+ };
899
+ }
900
+
901
+ function buildChatNextCallExample(args, missingFields, normalized) {
902
+ const example = {};
903
+ if (normalized.job) example.job = normalized.job;
904
+ if (normalized.startFrom) example.start_from = normalized.startFrom;
905
+ if (normalized.target.provided && !normalized.target.parseError) {
906
+ example.target_count = normalized.publicTargetCount ?? normalized.targetCount;
907
+ } else if (missingFields.includes("target_count")) {
908
+ example.target_count = "all";
909
+ }
910
+ if (normalized.criteria) example.criteria = normalized.criteria;
911
+ if (normalizeText(args.greeting_text || args.greetingText || args.greeting)) {
912
+ example.greeting_text = normalizeText(args.greeting_text || args.greetingText || args.greeting);
913
+ }
914
+ return Object.keys(example).length ? example : null;
915
+ }
916
+
917
+ function getMissingChatStartFields(args = {}, normalized = normalizeChatStartInput(args)) {
918
+ const missing = [];
919
+ if (!normalized.job) missing.push("job");
920
+ if (!["unread", "all"].includes(normalized.startFrom)) missing.push("start_from");
921
+ if (!normalized.target.provided || normalized.target.parseError) missing.push("target_count");
922
+ if (!normalized.criteria) missing.push("criteria");
923
+ return missing;
924
+ }
925
+
926
+ function buildTargetCountDiagnostics(args, missingFields, normalized) {
927
+ if (!missingFields.includes("target_count")) return {};
928
+ const hints = buildTargetCountCompatibilityHints({
929
+ argumentName: "target_count",
930
+ recommendedArgumentPatch: { target_count: "all" }
931
+ });
932
+ const received = getBossChatTargetCountValue(args);
933
+ const nextCallExample = {
934
+ ...(normalizeText(args.job) ? { job: normalizeText(args.job) } : {}),
935
+ ...(normalizeText(args.start_from) ? { start_from: normalizeText(args.start_from).toLowerCase() } : {}),
936
+ target_count: "all",
937
+ ...(normalizeText(args.criteria) ? { criteria: normalizeText(args.criteria) } : {})
938
+ };
939
+ return {
940
+ ...hints,
941
+ received_target_count: received,
942
+ target_count_parse_error: normalized.target.parseError || null,
943
+ next_call_example: nextCallExample
944
+ };
945
+ }
946
+
947
+ function buildJobQuestionOptions(jobOptions = []) {
948
+ return (jobOptions || []).map((option) => ({
949
+ label: option.label,
950
+ value: option.value,
951
+ index: option.index,
952
+ active: option.active === true
953
+ }));
954
+ }
955
+
956
+ function buildPendingChatQuestions({ args, missingFields, normalized, jobOptions = [] }) {
957
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
958
+ return missingFields.map((field) => {
959
+ if (field === "job") {
960
+ return {
961
+ field,
962
+ question: "请提供 Boss chat 岗位,支持岗位名、编号或页面中的岗位 value。",
963
+ value: normalized.job || null,
964
+ options: buildJobQuestionOptions(jobOptions)
965
+ };
966
+ }
967
+ if (field === "start_from") {
968
+ return {
969
+ field,
970
+ question: "请确认 chat 起始范围。",
971
+ value: normalized.startFrom || null,
972
+ options: [
973
+ { label: "未读", value: "unread" },
974
+ { label: "全部", value: "all" }
975
+ ]
976
+ };
977
+ }
978
+ if (field === "target_count") {
979
+ return {
980
+ field,
981
+ ...diagnostics,
982
+ question: "请提供 target_count,使用正整数或 all(扫到底)。",
983
+ value: normalized.publicTargetCount ?? null,
984
+ options: Array.isArray(diagnostics.options) ? diagnostics.options : [],
985
+ parse_error: normalized.target.parseError || null
986
+ };
987
+ }
988
+ if (field === "criteria") {
989
+ return {
990
+ field,
991
+ question: "请提供自然语言筛选 criteria。",
992
+ value: normalized.criteria || null
993
+ };
994
+ }
995
+ return {
996
+ field,
997
+ question: `请提供 ${field}。`,
998
+ value: null
999
+ };
1000
+ });
1001
+ }
1002
+
1003
+ async function buildNeedInputResponse({ args, missingFields, normalized }) {
1004
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
1005
+ return {
1006
+ status: "NEED_INPUT",
1007
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
1008
+ missing_fields: missingFields,
1009
+ ...diagnostics,
1010
+ pending_questions: buildPendingChatQuestions({ args, missingFields, normalized }),
1011
+ job_options: [],
1012
+ error: {
1013
+ code: "MISSING_REQUIRED_FIELDS",
1014
+ message: "缺少必要字段。请补齐 job、start_from、target_count、criteria 后再启动 Boss chat CDP-only run。",
1015
+ retryable: true
1016
+ }
1017
+ };
1018
+ }
1019
+
1020
+ function shouldRequestChatResume(args = {}, context = {}) {
1021
+ const action = normalizeText(args.post_action || args.action).toLowerCase();
1022
+ if (
1023
+ args.request_cv === false
1024
+ || args.request_resume === false
1025
+ || args.ask_cv === false
1026
+ || args.execute_post_action === false
1027
+ || args.no_request_cv === true
1028
+ || args.no_request_resume === true
1029
+ || CHAT_DISABLE_REQUEST_RESUME_ACTIONS.has(action)
1030
+ ) {
1031
+ return false;
1032
+ }
1033
+ if (
1034
+ args.request_cv === true
1035
+ || args.request_resume === true
1036
+ || args.ask_cv === true
1037
+ || args.execute_post_action === true
1038
+ || CHAT_REQUEST_RESUME_ACTIONS.has(action)
1039
+ ) {
1040
+ return true;
1041
+ }
1042
+ if (typeof context.request_resume_for_passed === "boolean") {
1043
+ return context.request_resume_for_passed;
1044
+ }
1045
+ return true;
1046
+ }
1047
+
1048
+ function isDebugTestMode(args = {}) {
1049
+ return args.debug_test_mode === true || args.allow_debug_test_mode === true;
1050
+ }
1051
+
1052
+ function normalizeScreeningModeArg(args = {}) {
1053
+ const raw = normalizeText(args.screening_mode || args.screeningMode || "");
1054
+ if (args.use_llm === false) return "deterministic";
1055
+ return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
1056
+ ? "deterministic"
1057
+ : "llm";
1058
+ }
1059
+
1060
+ function collectChatDebugTestOptions(args = {}) {
1061
+ const reasons = [];
1062
+ if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
1063
+ if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
1064
+ if (args.dry_run === true || args.dry_run_request_cv === true) reasons.push("dry_run_request_cv");
1065
+ return reasons;
1066
+ }
1067
+
1068
+ function shouldUseChatLlm(args = {}) {
1069
+ return normalizeScreeningModeArg(args) !== "deterministic";
1070
+ }
1071
+
1072
+ function getRunOptions(args, normalized, session, { workspaceRoot = "", configResolution = null } = {}) {
1073
+ const slowLive = args.slow_live === true;
1074
+ const isAllTarget = normalized.publicTargetCount === "all";
1075
+ const processedLimit = parsePositiveInteger(
1076
+ args.max_candidates,
1077
+ isAllTarget ? CHAT_ALL_MAX_CANDIDATES : CHAT_ALL_MAX_CANDIDATES
1078
+ );
1079
+ const shouldRequestResume = shouldRequestChatResume(args);
1080
+ const useLlm = shouldUseChatLlm(args);
1081
+ const resolvedConfig = configResolution || (useLlm ? resolveBossScreeningConfig(workspaceRoot) : { ok: false });
1082
+ const humanBehavior = resolveHumanBehaviorForRun(args, resolvedConfig?.config || {});
1083
+ return {
1084
+ client: session.client,
1085
+ targetUrl: CHAT_TARGET_URL,
1086
+ job: normalized.job,
1087
+ startFrom: normalized.startFrom,
1088
+ criteria: normalized.criteria,
1089
+ maxCandidates: processedLimit,
1090
+ targetPassCount: isAllTarget ? null : normalized.targetCount,
1091
+ processUntilListEnd: isAllTarget,
1092
+ detailLimit: parseNonNegativeInteger(args.detail_limit, useLlm || shouldRequestResume ? processedLimit : 0),
1093
+ detailSource: normalizeText(args.detail_source) || "cascade",
1094
+ closeResume: true,
1095
+ requestResumeForPassed: shouldRequestResume,
1096
+ dryRunRequestCv: args.dry_run === true || args.dry_run_request_cv === true,
1097
+ greetingText: normalized.greetingText || DEFAULT_CHAT_GREETING_TEXT,
1098
+ delayMs: parseNonNegativeInteger(args.delay_ms, 0),
1099
+ cardTimeoutMs: slowLive ? 180000 : 90000,
1100
+ readyTimeoutMs: slowLive ? 120000 : 60000,
1101
+ onlineResumeButtonTimeoutMs: parsePositiveInteger(
1102
+ args.online_resume_button_timeout_ms,
1103
+ slowLive ? 30000 : 15000
1104
+ ),
1105
+ resumeDomTimeoutMs: slowLive ? 120000 : 60000,
1106
+ maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
1107
+ imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
1108
+ llmConfig: resolvedConfig.ok ? {
1109
+ ...resolvedConfig.config
1110
+ } : null,
1111
+ llmTimeoutMs: parsePositiveInteger(
1112
+ args.llm_timeout_ms,
1113
+ parsePositiveInteger(resolvedConfig.config?.llmTimeoutMs || resolvedConfig.config?.timeoutMs, slowLive ? 180000 : 120000)
1114
+ ),
1115
+ llmImageLimit: parsePositiveInteger(
1116
+ args.llm_image_limit,
1117
+ parsePositiveInteger(resolvedConfig.config?.llmImageLimit || resolvedConfig.config?.imageLimit, 8)
1118
+ ),
1119
+ llmImageDetail: normalizeText(
1120
+ args.llm_image_detail || resolvedConfig.config?.llmImageDetail || resolvedConfig.config?.imageDetail
1121
+ ) || "low",
1122
+ screeningMode: normalizeScreeningModeArg(args),
1123
+ listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 200),
1124
+ listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
1125
+ listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
1126
+ listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
1127
+ listFallbackPoint: null,
1128
+ imageOutputDir: resolveBossConfiguredOutputDir("", getChatRunsDir()),
1129
+ humanRestEnabled: humanBehavior.restEnabled,
1130
+ humanBehavior,
1131
+ name: "mcp-boss-chat-run"
1132
+ };
1133
+ }
1134
+
1135
+ async function closeChatRunSession(runId) {
1136
+ const meta = chatRunMeta.get(runId);
1137
+ if (!meta || meta.closed) return;
1138
+ try {
1139
+ try {
1140
+ if (meta.session?.client) {
1141
+ await closeChatResumeModal(meta.session.client, { attemptsLimit: 2 });
1142
+ }
1143
+ } catch {
1144
+ // Cleanup is best-effort once the run has settled.
1145
+ }
1146
+ assertNoForbiddenCdpCalls(meta.methodLog || []);
1147
+ } finally {
1148
+ meta.closed = true;
1149
+ try {
1150
+ await meta.session?.close?.();
1151
+ } catch {
1152
+ // Nothing actionable for the caller once the run has settled.
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ async function waitForChatRunTerminal(runId) {
1158
+ while (true) {
1159
+ try {
1160
+ const snapshot = chatRunService.getChatRun(runId);
1161
+ if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
1162
+ } catch {
1163
+ return null;
1164
+ }
1165
+ await sleep(1000);
1166
+ }
1167
+ }
1168
+
1169
+ function trackChatRun(runId) {
1170
+ waitForChatRunTerminal(runId)
1171
+ .then((terminal) => {
1172
+ if (terminal) persistChatRunSnapshot(terminal);
1173
+ })
1174
+ .catch(() => null)
1175
+ .finally(() => {
1176
+ closeChatRunSession(runId).catch(() => {});
1177
+ });
1178
+ }
1179
+
1180
+ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
1181
+ const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
1182
+ const normalized = normalizeChatStartInput(args, defaultConfigResolution);
1183
+ const missingFields = getMissingChatStartFields(args, normalized);
1184
+ if (missingFields.length) {
1185
+ return buildNeedInputResponse({
1186
+ args,
1187
+ missingFields,
1188
+ normalized
1189
+ });
1190
+ }
1191
+
1192
+ const shouldRequestResume = shouldRequestChatResume(args);
1193
+ const useLlm = shouldUseChatLlm(args);
1194
+ const debugTestOptions = collectChatDebugTestOptions(args);
1195
+ if (debugTestOptions.length && !isDebugTestMode(args)) {
1196
+ return {
1197
+ status: "FAILED",
1198
+ error: {
1199
+ code: "DEBUG_TEST_MODE_REQUIRED",
1200
+ message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
1201
+ retryable: false
1202
+ },
1203
+ debug_test_options: debugTestOptions
1204
+ };
1205
+ }
1206
+ const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
1207
+ if (useLlm && !configResolution?.ok) {
1208
+ return {
1209
+ status: "FAILED",
1210
+ error: {
1211
+ code: "SCREEN_CONFIG_ERROR",
1212
+ message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
1213
+ retryable: true
1214
+ }
1215
+ };
1216
+ }
1217
+
1218
+ let session;
1219
+ try {
1220
+ session = await chatConnectorImpl({
1221
+ host: normalized.host,
1222
+ port: normalized.port,
1223
+ targetUrlIncludes: normalized.targetUrlIncludes,
1224
+ allowNavigate: normalized.allowNavigate,
1225
+ slowLive: normalized.slowLive
1226
+ });
1227
+ } catch (error) {
1228
+ const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1229
+ return {
1230
+ status: "FAILED",
1231
+ error: {
1232
+ code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1233
+ message: error?.message || "Boss chat page is not ready",
1234
+ requires_login: Boolean(error?.requires_login),
1235
+ login_url: error?.login_url || null,
1236
+ login_detection: error?.login_detection || null,
1237
+ chrome: error?.chrome || null,
1238
+ current_url: error?.current_url || null,
1239
+ target_url: error?.target_url || CHAT_TARGET_URL,
1240
+ retryable: true
1241
+ },
1242
+ chrome: error?.chrome || null
1243
+ };
1244
+ }
1245
+
1246
+ let started;
1247
+ try {
1248
+ started = chatRunService.startChatRun(getRunOptions(args, normalized, session, { workspaceRoot, configResolution }));
1249
+ } catch (error) {
1250
+ await session.close?.();
1251
+ return {
1252
+ status: "FAILED",
1253
+ error: {
1254
+ code: "CHAT_RUN_START_FAILED",
1255
+ message: error?.message || "Failed to start Boss chat run",
1256
+ retryable: true
1257
+ }
1258
+ };
1259
+ }
1260
+
1261
+ chatRunMeta.set(started.runId, {
1262
+ session,
1263
+ methodLog: session.methodLog || [],
1264
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
1265
+ args: clonePlain(args, {}),
1266
+ normalized,
1267
+ chrome: {
1268
+ host: normalized.host,
1269
+ port: normalized.port,
1270
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1271
+ target_id: session.target?.id || null,
1272
+ auto_launch: session.chrome || null
1273
+ },
1274
+ health: session.health || null
1275
+ });
1276
+ trackChatRun(started.runId);
1277
+ const persistedStarted = persistChatRunSnapshot(started);
1278
+
1279
+ return {
1280
+ status: "ACCEPTED",
1281
+ run_id: persistedStarted.run_id,
1282
+ state: persistedStarted.state,
1283
+ run: persistedStarted,
1284
+ poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1285
+ message: shouldRequestResume
1286
+ ? "Boss chat run started through the shared CDP-only chat service. Passed candidates will follow the configured request-CV sequence."
1287
+ : "Boss chat run started through the shared CDP-only chat service.",
1288
+ target_count_semantics: TARGET_COUNT_SEMANTICS
1289
+ };
1290
+ }
1291
+
1292
+ export async function prepareBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
1293
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1294
+ const normalized = normalizeChatStartInput(args, configResolution);
1295
+ let session;
1296
+ try {
1297
+ session = await chatConnectorImpl({
1298
+ host: normalized.host,
1299
+ port: normalized.port,
1300
+ targetUrlIncludes: normalized.targetUrlIncludes,
1301
+ allowNavigate: normalized.allowNavigate,
1302
+ slowLive: normalized.slowLive
1303
+ });
1304
+ } catch (error) {
1305
+ const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1306
+ return {
1307
+ status: "FAILED",
1308
+ stage: "chat_run_setup",
1309
+ error: {
1310
+ code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1311
+ message: error?.message || "Boss chat page is not ready",
1312
+ requires_login: Boolean(error?.requires_login),
1313
+ login_url: error?.login_url || null,
1314
+ login_detection: error?.login_detection || null,
1315
+ chrome: error?.chrome || null,
1316
+ current_url: error?.current_url || null,
1317
+ target_url: error?.target_url || CHAT_TARGET_URL,
1318
+ retryable: true
1319
+ },
1320
+ runtime_evaluate_used: false,
1321
+ method_summary: {},
1322
+ method_log: [],
1323
+ chrome: {
1324
+ host: normalized.host,
1325
+ port: normalized.port,
1326
+ target_url: CHAT_TARGET_URL,
1327
+ auto_launch: error?.chrome || null
1328
+ }
1329
+ };
1330
+ }
1331
+
1332
+ try {
1333
+ const jobs = await chatJobReaderImpl(session, {
1334
+ workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
1335
+ args: clonePlain(args, {}),
1336
+ normalized
1337
+ });
1338
+ const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
1339
+ const missingFields = getMissingChatStartFields(args, normalized);
1340
+ const diagnostics = buildTargetCountDiagnostics(args, missingFields, normalized);
1341
+ const nextCallExample = buildChatNextCallExample(args, missingFields, normalized);
1342
+ const selectedJob = jobOptions.find((option) => {
1343
+ const job = normalizeText(normalized.job).toLowerCase();
1344
+ if (!job) return option.active === true;
1345
+ return [option.value, option.label, option.title]
1346
+ .map((value) => normalizeText(value).toLowerCase())
1347
+ .includes(job);
1348
+ }) || null;
1349
+
1350
+ assertNoForbiddenCdpCalls(session.methodLog || []);
1351
+ return {
1352
+ status: missingFields.length ? "NEED_INPUT" : "READY",
1353
+ stage: "chat_run_setup",
1354
+ page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1355
+ required_fields: CHAT_REQUIRED_FIELDS.slice(),
1356
+ missing_fields: missingFields,
1357
+ job_options: jobOptions,
1358
+ selected_job: selectedJob,
1359
+ selected_job_label: jobs?.selected_label || selectedJob?.label || "",
1360
+ job_options_source: jobs?.source || "",
1361
+ job_options_selector: jobs?.selector || "",
1362
+ pending_questions: buildPendingChatQuestions({
1363
+ args,
1364
+ missingFields,
1365
+ normalized,
1366
+ jobOptions
1367
+ }),
1368
+ ...diagnostics,
1369
+ ...(nextCallExample ? { next_call_example: nextCallExample } : {}),
1370
+ message: missingFields.length
1371
+ ? "已通过 CDP-only 读取 Boss 聊天页岗位列表,请补齐 job / start_from / target_count / criteria。"
1372
+ : "Boss chat CDP-only preflight is ready. Use start_boss_chat_run to start screening.",
1373
+ runtime_evaluate_used: false,
1374
+ method_summary: methodSummary(session.methodLog || []),
1375
+ method_log: session.methodLog || [],
1376
+ chrome: {
1377
+ host: normalized.host,
1378
+ port: normalized.port,
1379
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1380
+ target_id: session.target?.id || null,
1381
+ auto_launch: session.chrome || null
1382
+ }
1383
+ };
1384
+ } catch (error) {
1385
+ const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1386
+ return {
1387
+ status: "FAILED",
1388
+ stage: "chat_run_setup",
1389
+ error: {
1390
+ code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PREPARE_FAILED",
1391
+ message: error?.message || "Boss chat CDP-only prepare failed",
1392
+ requires_login: Boolean(error?.requires_login),
1393
+ login_url: error?.login_url || null,
1394
+ login_detection: error?.login_detection || null,
1395
+ chrome: error?.chrome || null,
1396
+ current_url: error?.current_url || null,
1397
+ target_url: error?.target_url || CHAT_TARGET_URL,
1398
+ retryable: true
1399
+ },
1400
+ runtime_evaluate_used: false,
1401
+ method_summary: methodSummary(session.methodLog || []),
1402
+ method_log: session.methodLog || [],
1403
+ chrome: {
1404
+ host: normalized.host,
1405
+ port: normalized.port,
1406
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1407
+ target_id: session.target?.id || null,
1408
+ auto_launch: session.chrome || null
1409
+ }
1410
+ };
1411
+ } finally {
1412
+ try {
1413
+ assertNoForbiddenCdpCalls(session.methodLog || []);
1414
+ } finally {
1415
+ await session.close?.();
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ export async function bossChatHealthCheckTool({ workspaceRoot = "", args = {} } = {}) {
1421
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
1422
+ const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
1423
+ const host = normalizeText(args.host) || DEFAULT_CHAT_HOST;
1424
+ const port = parsePositiveInteger(args.port, configResolution.ok ? configResolution.config.debugPort : DEFAULT_CHAT_PORT);
1425
+ const targetUrlIncludes = normalizeText(args.target_url_includes) || CHAT_TARGET_URL;
1426
+ const allowNavigate = args.allow_navigate !== false;
1427
+ const slowLive = args.slow_live === true;
1428
+ const basePayload = {
1429
+ server: "boss-chat",
1430
+ mode: "cdp-only",
1431
+ cdp_only: true,
1432
+ cli_dir: null,
1433
+ cli_path: null,
1434
+ config_path: configResolution.config_path || null,
1435
+ config_dir: configResolution.config_dir || null,
1436
+ output_dir: configResolution.ok ? configResolution.config.outputDir || null : null,
1437
+ debug_port: port,
1438
+ shared_llm_config: configResolution.ok === true,
1439
+ data_dir: runtimeLayout.data_dir,
1440
+ data_dir_source: runtimeLayout.data_dir_source,
1441
+ legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
1442
+ migration_source_dir: runtimeLayout.migration_source_dir,
1443
+ migration_pending: runtimeLayout.migration_pending
1444
+ };
1445
+
1446
+ if (!configResolution.ok) {
1447
+ return {
1448
+ status: "FAILED",
1449
+ ...basePayload,
1450
+ error: configResolution.error,
1451
+ runtime_evaluate_used: false,
1452
+ method_summary: {},
1453
+ method_log: [],
1454
+ chrome: {
1455
+ host,
1456
+ port,
1457
+ target_url: targetUrlIncludes
1458
+ }
1459
+ };
1460
+ }
1461
+
1462
+ let session;
1463
+ try {
1464
+ session = await chatConnectorImpl({
1465
+ host,
1466
+ port,
1467
+ targetUrlIncludes,
1468
+ allowNavigate,
1469
+ slowLive
1470
+ });
1471
+ assertNoForbiddenCdpCalls(session.methodLog || []);
1472
+ return {
1473
+ status: "OK",
1474
+ ...basePayload,
1475
+ page_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1476
+ health: session.health || null,
1477
+ runtime_evaluate_used: false,
1478
+ method_summary: methodSummary(session.methodLog || []),
1479
+ method_log: session.methodLog || [],
1480
+ chrome: {
1481
+ host,
1482
+ port,
1483
+ target_url: session.navigation?.url || session.target?.url || CHAT_TARGET_URL,
1484
+ target_id: session.target?.id || null,
1485
+ auto_launch: session.chrome || null
1486
+ },
1487
+ message: "Boss chat CDP-only health check passed with shared self-heal probes."
1488
+ };
1489
+ } catch (error) {
1490
+ const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
1491
+ return {
1492
+ status: "FAILED",
1493
+ ...basePayload,
1494
+ error: {
1495
+ code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_CHAT_PAGE_NOT_READY",
1496
+ message: error?.message || "Boss chat page is not ready",
1497
+ requires_login: Boolean(error?.requires_login),
1498
+ login_url: error?.login_url || null,
1499
+ login_detection: error?.login_detection || null,
1500
+ chrome: error?.chrome || null,
1501
+ current_url: error?.current_url || null,
1502
+ target_url: error?.target_url || CHAT_TARGET_URL,
1503
+ retryable: true
1504
+ },
1505
+ runtime_evaluate_used: false,
1506
+ method_summary: methodSummary(session?.methodLog || []),
1507
+ method_log: session?.methodLog || [],
1508
+ chrome: {
1509
+ host,
1510
+ port,
1511
+ target_url: session?.navigation?.url || session?.target?.url || targetUrlIncludes,
1512
+ target_id: session?.target?.id || null,
1513
+ auto_launch: error?.chrome || session?.chrome || null
1514
+ }
1515
+ };
1516
+ } finally {
1517
+ if (session?.methodLog) assertNoForbiddenCdpCalls(session.methodLog);
1518
+ await session?.close?.();
1519
+ }
1520
+ }
1521
+
1522
+ export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {}) {
1523
+ const started = await startBossChatRunInternal(args, { workspaceRoot });
1524
+ if (started.status !== "ACCEPTED") return started;
1525
+ return attachMethodEvidence(started, started.run_id);
1526
+ }
1527
+
1528
+ export function getBossChatRunTool({ args = {} } = {}) {
1529
+ const runId = normalizeRunId(args.run_id || args.runId);
1530
+ if (!runId) {
1531
+ return {
1532
+ status: "FAILED",
1533
+ error: {
1534
+ code: "INVALID_RUN_ID",
1535
+ message: "run_id is required",
1536
+ retryable: false
1537
+ }
1538
+ };
1539
+ }
1540
+ try {
1541
+ const run = chatRunService.getChatRun(runId);
1542
+ const normalizedRun = persistChatRunSnapshot(run);
1543
+ return attachMethodEvidence({
1544
+ status: "RUN_STATUS",
1545
+ run: normalizedRun
1546
+ }, runId);
1547
+ } catch {
1548
+ const persisted = readChatRunState(runId);
1549
+ if (persisted) {
1550
+ const reconciled = reconcilePersistedChatRun(persisted);
1551
+ return {
1552
+ status: "RUN_STATUS",
1553
+ run: reconciled.run,
1554
+ persistence: {
1555
+ source: "disk",
1556
+ active_control_available: false,
1557
+ stale_finalized: reconciled.stale_finalized === true,
1558
+ artifacts_repaired: reconciled.artifacts_repaired === true
1559
+ },
1560
+ runtime_evaluate_used: false,
1561
+ method_summary: {},
1562
+ method_log: [],
1563
+ chrome: null
1564
+ };
1565
+ }
1566
+ return {
1567
+ status: "FAILED",
1568
+ error: {
1569
+ code: "RUN_NOT_FOUND",
1570
+ message: `No Boss chat run found for run_id=${runId}`,
1571
+ retryable: false
1572
+ }
1573
+ };
1574
+ }
1575
+ }
1576
+
1577
+ export function pauseBossChatRunTool({ args = {} } = {}) {
1578
+ const runId = normalizeRunId(args.run_id || args.runId);
1579
+ try {
1580
+ const before = chatRunService.getChatRun(runId);
1581
+ if (TERMINAL_STATUSES.has(before.status)) {
1582
+ const normalizedBefore = persistChatRunSnapshot(before);
1583
+ return attachMethodEvidence({
1584
+ status: "PAUSE_IGNORED",
1585
+ run: normalizedBefore,
1586
+ message: "目标任务已结束,无需暂停。"
1587
+ }, runId);
1588
+ }
1589
+ if (before.status === RUN_STATUS_PAUSED) {
1590
+ const normalizedBefore = persistChatRunSnapshot(before);
1591
+ return attachMethodEvidence({
1592
+ status: "PAUSE_IGNORED",
1593
+ run: normalizedBefore,
1594
+ message: "目标任务已经处于 paused 状态。"
1595
+ }, runId);
1596
+ }
1597
+ const run = chatRunService.pauseChatRun(runId);
1598
+ const normalizedRun = persistChatRunSnapshot(run);
1599
+ return attachMethodEvidence({
1600
+ status: "PAUSE_REQUESTED",
1601
+ run: normalizedRun,
1602
+ message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
1603
+ }, runId);
1604
+ } catch {
1605
+ const persisted = readChatRunState(runId);
1606
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1607
+ const reconciled = reconcilePersistedChatRun(persisted);
1608
+ return {
1609
+ status: "PAUSE_IGNORED",
1610
+ run: reconciled.run,
1611
+ message: "目标任务已结束,无需暂停。",
1612
+ runtime_evaluate_used: false,
1613
+ method_summary: {},
1614
+ method_log: [],
1615
+ chrome: null
1616
+ };
1617
+ }
1618
+ return getBossChatRunTool({ args });
1619
+ }
1620
+ }
1621
+
1622
+ export function resumeBossChatRunTool({ args = {} } = {}) {
1623
+ const runId = normalizeRunId(args.run_id || args.runId);
1624
+ try {
1625
+ const before = chatRunService.getChatRun(runId);
1626
+ if (TERMINAL_STATUSES.has(before.status)) {
1627
+ const normalizedBefore = persistChatRunSnapshot(before);
1628
+ return attachMethodEvidence({
1629
+ status: "FAILED",
1630
+ error: {
1631
+ code: "RUN_ALREADY_TERMINATED",
1632
+ message: "目标任务已结束,无法继续。",
1633
+ retryable: false
1634
+ },
1635
+ run: normalizedBefore
1636
+ }, runId);
1637
+ }
1638
+ if (before.status !== RUN_STATUS_PAUSED) {
1639
+ const normalizedBefore = persistChatRunSnapshot(before);
1640
+ return attachMethodEvidence({
1641
+ status: "FAILED",
1642
+ error: {
1643
+ code: "RUN_NOT_PAUSED",
1644
+ message: "仅 paused 状态的 run 才能继续。",
1645
+ retryable: true
1646
+ },
1647
+ run: normalizedBefore
1648
+ }, runId);
1649
+ }
1650
+ const run = chatRunService.resumeChatRun(runId);
1651
+ const meta = getChatRunMeta(runId);
1652
+ if (meta) {
1653
+ meta.resumeCount = (meta.resumeCount || 0) + 1;
1654
+ meta.lastResumedAt = new Date().toISOString();
1655
+ }
1656
+ const normalizedRun = persistChatRunSnapshot(run);
1657
+ return attachMethodEvidence({
1658
+ status: "RESUME_REQUESTED",
1659
+ run: normalizedRun,
1660
+ poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
1661
+ message: "已恢复 Boss chat run,请使用 get_boss_chat_run 按需轮询。"
1662
+ }, runId);
1663
+ } catch {
1664
+ const persisted = readChatRunState(runId);
1665
+ if (persisted) {
1666
+ const reconciled = reconcilePersistedChatRun(persisted);
1667
+ const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
1668
+ return {
1669
+ status: "FAILED",
1670
+ error: {
1671
+ code: TERMINAL_STATUSES.has(reconciledStatus) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
1672
+ message: TERMINAL_STATUSES.has(reconciledStatus)
1673
+ ? "目标任务已结束,无法继续。"
1674
+ : "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
1675
+ retryable: !TERMINAL_STATUSES.has(reconciledStatus)
1676
+ },
1677
+ run: reconciled.run,
1678
+ persistence: {
1679
+ source: "disk",
1680
+ active_control_available: false,
1681
+ stale_finalized: reconciled.stale_finalized === true,
1682
+ artifacts_repaired: reconciled.artifacts_repaired === true
1683
+ },
1684
+ runtime_evaluate_used: false,
1685
+ method_summary: {},
1686
+ method_log: [],
1687
+ chrome: null
1688
+ };
1689
+ }
1690
+ return getBossChatRunTool({ args });
1691
+ }
1692
+ }
1693
+
1694
+ export function cancelBossChatRunTool({ args = {} } = {}) {
1695
+ const runId = normalizeRunId(args.run_id || args.runId);
1696
+ try {
1697
+ const before = chatRunService.getChatRun(runId);
1698
+ if (TERMINAL_STATUSES.has(before.status)) {
1699
+ const normalizedBefore = persistChatRunSnapshot(before);
1700
+ return attachMethodEvidence({
1701
+ status: "CANCEL_IGNORED",
1702
+ run: normalizedBefore,
1703
+ message: "目标任务已结束,无需取消。"
1704
+ }, runId);
1705
+ }
1706
+ const run = chatRunService.cancelChatRun(runId);
1707
+ const normalizedRun = persistChatRunSnapshot(run);
1708
+ return attachMethodEvidence({
1709
+ status: "CANCEL_REQUESTED",
1710
+ run: normalizedRun,
1711
+ message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
1712
+ }, runId);
1713
+ } catch {
1714
+ const persisted = readChatRunState(runId);
1715
+ if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
1716
+ const reconciled = reconcilePersistedChatRun(persisted);
1717
+ return {
1718
+ status: "CANCEL_IGNORED",
1719
+ run: reconciled.run,
1720
+ message: "目标任务已结束,无需取消。",
1721
+ runtime_evaluate_used: false,
1722
+ method_summary: {},
1723
+ method_log: [],
1724
+ chrome: null
1725
+ };
1726
+ }
1727
+ if (persisted) {
1728
+ const reconciled = reconcilePersistedChatRun(persisted, { cancelStale: true });
1729
+ if (reconciled.stale_finalized) {
1730
+ return {
1731
+ status: "CANCEL_REQUESTED",
1732
+ run: reconciled.run,
1733
+ message: "该 run 的后台进程已经不在,已将磁盘状态安全标记为 canceled 并生成结果文件。",
1734
+ persistence: {
1735
+ source: "disk",
1736
+ active_control_available: false,
1737
+ stale_finalized: true,
1738
+ artifacts_repaired: reconciled.artifacts_repaired === true
1739
+ },
1740
+ runtime_evaluate_used: false,
1741
+ method_summary: {},
1742
+ method_log: [],
1743
+ chrome: null
1744
+ };
1745
+ }
1746
+ }
1747
+ return getBossChatRunTool({ args });
1748
+ }
1749
+ }
1750
+
1751
+ export function __setChatMcpConnectorForTests(nextConnector) {
1752
+ chatConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectChatChromeSession;
1753
+ }
1754
+
1755
+ export function __setChatMcpJobReaderForTests(nextReader) {
1756
+ chatJobReaderImpl = typeof nextReader === "function" ? nextReader : readChatJobOptionsFromSession;
1757
+ }
1758
+
1759
+ export function __setChatMcpWorkflowForTests(nextWorkflow) {
1760
+ chatWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runChatWorkflow;
1761
+ chatRunService = createChatRunService({
1762
+ idPrefix: "mcp_chat",
1763
+ workflow: (...args) => chatWorkflowImpl(...args),
1764
+ onSnapshot: persistChatLifecycleSnapshot
1765
+ });
1766
+ }
1767
+
1768
+ export function __resetChatMcpStateForTests() {
1769
+ for (const meta of chatRunMeta.values()) {
1770
+ try {
1771
+ meta.session?.close?.();
1772
+ } catch {
1773
+ // Best-effort test cleanup.
1774
+ }
1775
+ }
1776
+ chatRunMeta.clear();
1777
+ __setChatMcpConnectorForTests(null);
1778
+ __setChatMcpJobReaderForTests(null);
1779
+ __setChatMcpWorkflowForTests(null);
1780
+ }