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