@reconcrap/boss-recommend-mcp 2.1.11 → 2.1.13

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