@reconcrap/boss-recommend-mcp 2.0.56 → 2.0.57
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/package.json +2 -1
- package/src/chat-mcp.js +397 -3
- package/src/core/browser/index.js +1 -0
- package/src/core/self-heal/viewport.js +1 -1
- package/src/detached-worker.js +99 -0
- package/src/domains/chat/run-service.js +8 -6
- package/src/domains/recruit/run-service.js +2 -0
- package/src/index.js +35 -2
- package/src/recruit-mcp.js +534 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.57",
|
|
4
4
|
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"boss",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"src/chat-mcp.js",
|
|
89
89
|
"src/chat-runtime-config.js",
|
|
90
90
|
"src/cli.js",
|
|
91
|
+
"src/detached-worker.js",
|
|
91
92
|
"src/index.js",
|
|
92
93
|
"src/parser.js",
|
|
93
94
|
"src/recommend-mcp.js",
|
package/src/chat-mcp.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import {
|
|
5
7
|
assertNoForbiddenCdpCalls,
|
|
6
8
|
bringPageToFront,
|
|
@@ -18,7 +20,8 @@ import {
|
|
|
18
20
|
RUN_STATUS_CANCELED,
|
|
19
21
|
RUN_STATUS_COMPLETED,
|
|
20
22
|
RUN_STATUS_FAILED,
|
|
21
|
-
RUN_STATUS_PAUSED
|
|
23
|
+
RUN_STATUS_PAUSED,
|
|
24
|
+
RUN_STATUS_RUNNING
|
|
22
25
|
} from "./core/run/index.js";
|
|
23
26
|
import {
|
|
24
27
|
buildLegacyScreenInputRows,
|
|
@@ -60,6 +63,8 @@ const DEFAULT_CHAT_GREETING_TEXT = "Hi同学,能麻烦发下简历吗?";
|
|
|
60
63
|
const CHAT_ALL_MAX_CANDIDATES = 100000;
|
|
61
64
|
const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; numeric targets scan until that many candidates pass or the list ends; all/全部/扫到底 scans to the end";
|
|
62
65
|
const RUN_MODE_ASYNC = "async";
|
|
66
|
+
const DETACHED_WORKER_SCRIPT = fileURLToPath(new URL("./detached-worker.js", import.meta.url));
|
|
67
|
+
const DETACHED_WORKER_POLL_MS = 1000;
|
|
63
68
|
|
|
64
69
|
const CHAT_REQUIRED_FIELDS = Object.freeze([
|
|
65
70
|
"job",
|
|
@@ -170,6 +175,9 @@ function getChatRunArtifacts(runId) {
|
|
|
170
175
|
runs_dir: runsDir,
|
|
171
176
|
output_dir: outputDir,
|
|
172
177
|
run_state_path: path.join(runsDir, `${normalized}.json`),
|
|
178
|
+
detached_args_path: path.join(runsDir, `${normalized}.detached-args.json`),
|
|
179
|
+
worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
|
|
180
|
+
worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
|
|
173
181
|
checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
|
|
174
182
|
output_csv: path.join(outputDir, `${normalized}.results.csv`),
|
|
175
183
|
report_json: path.join(outputDir, `${normalized}.report.json`)
|
|
@@ -256,6 +264,156 @@ function readChatRunState(runId) {
|
|
|
256
264
|
return readJsonFile(artifacts.run_state_path);
|
|
257
265
|
}
|
|
258
266
|
|
|
267
|
+
function writeChatRunState(runId, payload) {
|
|
268
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
269
|
+
if (!artifacts) return null;
|
|
270
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
271
|
+
return payload;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createDetachedChatRunId() {
|
|
275
|
+
return `mcp_chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildInitialChatDetachedState(runId, {
|
|
279
|
+
workspaceRoot = "",
|
|
280
|
+
args = {},
|
|
281
|
+
normalized = {},
|
|
282
|
+
pid = process.pid
|
|
283
|
+
} = {}) {
|
|
284
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
285
|
+
const now = new Date().toISOString();
|
|
286
|
+
const isAllTarget = normalized.publicTargetCount === "all";
|
|
287
|
+
const processedLimit = isAllTarget ? CHAT_ALL_MAX_CANDIDATES : Math.max(1, Number(normalized.targetCount) || 1);
|
|
288
|
+
return {
|
|
289
|
+
run_id: runId,
|
|
290
|
+
mode: RUN_MODE_ASYNC,
|
|
291
|
+
state: "queued",
|
|
292
|
+
status: "queued",
|
|
293
|
+
stage: "queued",
|
|
294
|
+
started_at: now,
|
|
295
|
+
updated_at: now,
|
|
296
|
+
heartbeat_at: now,
|
|
297
|
+
completed_at: null,
|
|
298
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
|
|
299
|
+
progress: {
|
|
300
|
+
target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
|
|
301
|
+
target_pass_count: isAllTarget ? null : normalized.targetCount ?? null,
|
|
302
|
+
processed_limit: processedLimit,
|
|
303
|
+
processed: 0,
|
|
304
|
+
screened: 0,
|
|
305
|
+
detail_opened: 0,
|
|
306
|
+
llm_screened: 0,
|
|
307
|
+
passed: 0,
|
|
308
|
+
skipped: 0,
|
|
309
|
+
requested: 0,
|
|
310
|
+
request_satisfied: 0,
|
|
311
|
+
request_skipped: 0,
|
|
312
|
+
greet_count: 0
|
|
313
|
+
},
|
|
314
|
+
last_message: "Boss chat detached worker is queued.",
|
|
315
|
+
context: {
|
|
316
|
+
domain: "chat",
|
|
317
|
+
target_url: CHAT_TARGET_URL,
|
|
318
|
+
workspace_root: normalizeText(workspaceRoot) || process.cwd(),
|
|
319
|
+
profile: normalized.profile || args.profile || "default",
|
|
320
|
+
job: normalized.job || args.job || "",
|
|
321
|
+
start_from: normalized.startFrom || args.start_from || "",
|
|
322
|
+
criteria: normalized.criteria || args.criteria || "",
|
|
323
|
+
greeting_text: normalized.greetingText || args.greeting_text || args.greetingText || DEFAULT_CHAT_GREETING_TEXT,
|
|
324
|
+
target_count: normalized.publicTargetCount ?? normalized.targetCount ?? null,
|
|
325
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
326
|
+
request_resume_for_passed: shouldRequestChatResume(args),
|
|
327
|
+
detached_worker: true
|
|
328
|
+
},
|
|
329
|
+
control: {
|
|
330
|
+
pause_requested: false,
|
|
331
|
+
pause_requested_at: null,
|
|
332
|
+
pause_requested_by: null,
|
|
333
|
+
cancel_requested: false
|
|
334
|
+
},
|
|
335
|
+
resume: {
|
|
336
|
+
checkpoint_path: artifacts?.checkpoint_path || null,
|
|
337
|
+
pause_control_path: artifacts?.run_state_path || null,
|
|
338
|
+
output_csv: null,
|
|
339
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
340
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
341
|
+
resume_count: 0,
|
|
342
|
+
last_resumed_at: null,
|
|
343
|
+
last_paused_at: null
|
|
344
|
+
},
|
|
345
|
+
error: null,
|
|
346
|
+
result: null,
|
|
347
|
+
summary: null,
|
|
348
|
+
artifacts
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function patchPersistedChatControl(runId, controlPatch = {}, {
|
|
353
|
+
status = "RUN_STATUS",
|
|
354
|
+
message = "",
|
|
355
|
+
lastMessage = ""
|
|
356
|
+
} = {}) {
|
|
357
|
+
const current = readChatRunState(runId);
|
|
358
|
+
if (!current) return null;
|
|
359
|
+
const state = normalizeText(current.state || current.status);
|
|
360
|
+
if (TERMINAL_STATUSES.has(state)) return null;
|
|
361
|
+
const now = new Date().toISOString();
|
|
362
|
+
const patched = {
|
|
363
|
+
...current,
|
|
364
|
+
updated_at: now,
|
|
365
|
+
heartbeat_at: now,
|
|
366
|
+
last_message: lastMessage || message || current.last_message || "",
|
|
367
|
+
control: {
|
|
368
|
+
...(current.control || {}),
|
|
369
|
+
...controlPatch
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
writeChatRunState(runId, patched);
|
|
373
|
+
return {
|
|
374
|
+
status,
|
|
375
|
+
run: patched,
|
|
376
|
+
message,
|
|
377
|
+
persistence: {
|
|
378
|
+
source: "disk",
|
|
379
|
+
active_control_available: false,
|
|
380
|
+
detached_control_requested: true
|
|
381
|
+
},
|
|
382
|
+
runtime_evaluate_used: false,
|
|
383
|
+
method_summary: {},
|
|
384
|
+
method_log: [],
|
|
385
|
+
chrome: null
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function launchDetachedChatWorker(runId) {
|
|
390
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
391
|
+
if (!artifacts) throw new Error("Invalid chat run_id");
|
|
392
|
+
fs.mkdirSync(path.dirname(artifacts.worker_stdout_path), { recursive: true });
|
|
393
|
+
const stdoutFd = fs.openSync(artifacts.worker_stdout_path, "a");
|
|
394
|
+
const stderrFd = fs.openSync(artifacts.worker_stderr_path, "a");
|
|
395
|
+
let child;
|
|
396
|
+
try {
|
|
397
|
+
child = spawn(process.execPath, [
|
|
398
|
+
DETACHED_WORKER_SCRIPT,
|
|
399
|
+
"--domain",
|
|
400
|
+
"chat",
|
|
401
|
+
"--run-id",
|
|
402
|
+
runId
|
|
403
|
+
], {
|
|
404
|
+
detached: true,
|
|
405
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
406
|
+
windowsHide: true,
|
|
407
|
+
env: process.env
|
|
408
|
+
});
|
|
409
|
+
} finally {
|
|
410
|
+
fs.closeSync(stdoutFd);
|
|
411
|
+
fs.closeSync(stderrFd);
|
|
412
|
+
}
|
|
413
|
+
if (typeof child?.unref === "function") child.unref();
|
|
414
|
+
return child;
|
|
415
|
+
}
|
|
416
|
+
|
|
259
417
|
function toIsoOrNull(value) {
|
|
260
418
|
const normalized = normalizeText(value);
|
|
261
419
|
return normalized || null;
|
|
@@ -491,6 +649,27 @@ function finalizePersistedChatRun(persisted = {}, {
|
|
|
491
649
|
return attachLegacyArtifactsToPersistedChatRun(next);
|
|
492
650
|
}
|
|
493
651
|
|
|
652
|
+
export function markBossChatDetachedWorkerFailed(runId, error, options = {}) {
|
|
653
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
654
|
+
if (!normalizedRunId) return null;
|
|
655
|
+
const persisted = readChatRunState(normalizedRunId) || buildInitialChatDetachedState(normalizedRunId, {});
|
|
656
|
+
const state = normalizeText(persisted.state || persisted.status);
|
|
657
|
+
if (TERMINAL_STATUSES.has(state)) return persisted;
|
|
658
|
+
const errorPayload = {
|
|
659
|
+
name: error?.name || "Error",
|
|
660
|
+
code: options.code || error?.code || "CHAT_WORKER_UNHANDLED_EXCEPTION",
|
|
661
|
+
message: normalizeText(error?.message || error || options.message) || "Boss chat detached worker exited unexpectedly."
|
|
662
|
+
};
|
|
663
|
+
if (normalizeText(error?.stack || "")) {
|
|
664
|
+
errorPayload.stack = String(error.stack).slice(0, 8000);
|
|
665
|
+
}
|
|
666
|
+
return finalizePersistedChatRun(persisted, {
|
|
667
|
+
status: RUN_STATUS_FAILED,
|
|
668
|
+
error: errorPayload,
|
|
669
|
+
message: errorPayload.message
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
494
673
|
function persistedChatRunArtifactMissing(persisted = {}) {
|
|
495
674
|
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
496
675
|
const artifacts = getChatRunArtifacts(runId);
|
|
@@ -568,6 +747,8 @@ function buildLegacyChatResult(snapshot) {
|
|
|
568
747
|
output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
|
|
569
748
|
report_json: artifacts?.report_json || meta.reportJsonPath || null,
|
|
570
749
|
checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
|
|
750
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
751
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
571
752
|
started_at: snapshot.startedAt,
|
|
572
753
|
completed_at: snapshot.completedAt || null,
|
|
573
754
|
duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
|
|
@@ -624,6 +805,8 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
624
805
|
checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
|
|
625
806
|
pause_control_path: artifacts?.run_state_path || null,
|
|
626
807
|
output_csv: legacyResult?.output_csv || null,
|
|
808
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
809
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
627
810
|
resume_count: meta.resumeCount || 0,
|
|
628
811
|
last_resumed_at: meta.lastResumedAt || null,
|
|
629
812
|
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
@@ -1177,7 +1360,7 @@ function trackChatRun(runId) {
|
|
|
1177
1360
|
});
|
|
1178
1361
|
}
|
|
1179
1362
|
|
|
1180
|
-
async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {}) {
|
|
1363
|
+
async function startBossChatRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
1181
1364
|
const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1182
1365
|
const normalized = normalizeChatStartInput(args, defaultConfigResolution);
|
|
1183
1366
|
const missingFields = getMissingChatStartFields(args, normalized);
|
|
@@ -1245,7 +1428,10 @@ async function startBossChatRunInternal(args = {}, { workspaceRoot = "" } = {})
|
|
|
1245
1428
|
|
|
1246
1429
|
let started;
|
|
1247
1430
|
try {
|
|
1248
|
-
started = chatRunService.startChatRun(
|
|
1431
|
+
started = chatRunService.startChatRun({
|
|
1432
|
+
...getRunOptions(args, normalized, session, { workspaceRoot, configResolution }),
|
|
1433
|
+
runId
|
|
1434
|
+
});
|
|
1249
1435
|
} catch (error) {
|
|
1250
1436
|
await session.close?.();
|
|
1251
1437
|
return {
|
|
@@ -1525,6 +1711,178 @@ export async function startBossChatRunTool({ workspaceRoot = "", args = {} } = {
|
|
|
1525
1711
|
return attachMethodEvidence(started, started.run_id);
|
|
1526
1712
|
}
|
|
1527
1713
|
|
|
1714
|
+
export async function startBossChatDetachedRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1715
|
+
const defaultConfigResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1716
|
+
const normalized = normalizeChatStartInput(args, defaultConfigResolution);
|
|
1717
|
+
const missingFields = getMissingChatStartFields(args, normalized);
|
|
1718
|
+
if (missingFields.length) {
|
|
1719
|
+
return buildNeedInputResponse({
|
|
1720
|
+
args,
|
|
1721
|
+
missingFields,
|
|
1722
|
+
normalized
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const useLlm = shouldUseChatLlm(args);
|
|
1727
|
+
const debugTestOptions = collectChatDebugTestOptions(args);
|
|
1728
|
+
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1729
|
+
return {
|
|
1730
|
+
status: "FAILED",
|
|
1731
|
+
error: {
|
|
1732
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1733
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1734
|
+
retryable: false
|
|
1735
|
+
},
|
|
1736
|
+
debug_test_options: debugTestOptions
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
const configResolution = useLlm ? resolveBossScreeningConfig(workspaceRoot) : null;
|
|
1740
|
+
if (useLlm && !configResolution?.ok) {
|
|
1741
|
+
return {
|
|
1742
|
+
status: "FAILED",
|
|
1743
|
+
error: {
|
|
1744
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1745
|
+
message: configResolution?.error?.message || "screening-config.json is required for chat LLM screening",
|
|
1746
|
+
retryable: true
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const runId = createDetachedChatRunId();
|
|
1752
|
+
const artifacts = getChatRunArtifacts(runId);
|
|
1753
|
+
const initial = buildInitialChatDetachedState(runId, {
|
|
1754
|
+
workspaceRoot,
|
|
1755
|
+
args,
|
|
1756
|
+
normalized,
|
|
1757
|
+
pid: process.pid
|
|
1758
|
+
});
|
|
1759
|
+
try {
|
|
1760
|
+
writeJsonAtomic(artifacts.detached_args_path, {
|
|
1761
|
+
domain: "chat",
|
|
1762
|
+
run_id: runId,
|
|
1763
|
+
workspace_root: normalizeText(workspaceRoot) || process.cwd(),
|
|
1764
|
+
args: clonePlain(args, {})
|
|
1765
|
+
});
|
|
1766
|
+
writeChatRunState(runId, initial);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
return {
|
|
1769
|
+
status: "FAILED",
|
|
1770
|
+
error: {
|
|
1771
|
+
code: "CHAT_RUN_STATE_IO_ERROR",
|
|
1772
|
+
message: `Unable to write Boss chat detached run state: ${error?.message || error}`,
|
|
1773
|
+
retryable: false
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
let child;
|
|
1779
|
+
try {
|
|
1780
|
+
child = launchDetachedChatWorker(runId);
|
|
1781
|
+
const now = new Date().toISOString();
|
|
1782
|
+
const latest = readChatRunState(runId) || initial;
|
|
1783
|
+
const latestState = normalizeText(latest.state || latest.status);
|
|
1784
|
+
if (TERMINAL_STATUSES.has(latestState)) {
|
|
1785
|
+
return {
|
|
1786
|
+
status: "FAILED",
|
|
1787
|
+
error: latest.error || {
|
|
1788
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1789
|
+
message: "Boss chat detached worker exited during launch.",
|
|
1790
|
+
retryable: true
|
|
1791
|
+
},
|
|
1792
|
+
run: latest,
|
|
1793
|
+
runtime_evaluate_used: false,
|
|
1794
|
+
method_summary: {},
|
|
1795
|
+
method_log: [],
|
|
1796
|
+
chrome: null
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
const queued = {
|
|
1800
|
+
...latest,
|
|
1801
|
+
pid: child.pid || process.pid,
|
|
1802
|
+
updated_at: now,
|
|
1803
|
+
heartbeat_at: now,
|
|
1804
|
+
last_message: "Boss chat detached worker launched."
|
|
1805
|
+
};
|
|
1806
|
+
writeChatRunState(runId, queued);
|
|
1807
|
+
return {
|
|
1808
|
+
status: "ACCEPTED",
|
|
1809
|
+
run_id: runId,
|
|
1810
|
+
state: "queued",
|
|
1811
|
+
run: queued,
|
|
1812
|
+
poll_after_sec: DEFAULT_CHAT_POLL_AFTER_SEC,
|
|
1813
|
+
message: "Boss chat run started in a detached worker. It can continue after the MCP host returns or is recycled.",
|
|
1814
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
1815
|
+
detached_worker: true,
|
|
1816
|
+
runtime_evaluate_used: false,
|
|
1817
|
+
method_summary: {},
|
|
1818
|
+
method_log: [],
|
|
1819
|
+
chrome: null
|
|
1820
|
+
};
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
const failed = markBossChatDetachedWorkerFailed(runId, error, {
|
|
1823
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1824
|
+
message: "Unable to launch Boss chat detached worker."
|
|
1825
|
+
});
|
|
1826
|
+
return {
|
|
1827
|
+
status: "FAILED",
|
|
1828
|
+
error: failed?.error || {
|
|
1829
|
+
code: "CHAT_WORKER_LAUNCH_FAILED",
|
|
1830
|
+
message: error?.message || "Unable to launch Boss chat detached worker.",
|
|
1831
|
+
retryable: true
|
|
1832
|
+
},
|
|
1833
|
+
run: failed || readChatRunState(runId),
|
|
1834
|
+
runtime_evaluate_used: false,
|
|
1835
|
+
method_summary: {},
|
|
1836
|
+
method_log: [],
|
|
1837
|
+
chrome: null
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
export async function runBossChatDetachedWorker({ runId } = {}) {
|
|
1843
|
+
const normalizedRunId = normalizeRunId(runId);
|
|
1844
|
+
if (!normalizedRunId) return { ok: false, error: "run_id is required" };
|
|
1845
|
+
const artifacts = getChatRunArtifacts(normalizedRunId);
|
|
1846
|
+
const spec = readJsonFile(artifacts?.detached_args_path || "");
|
|
1847
|
+
if (!spec) {
|
|
1848
|
+
const error = new Error(`Boss chat detached args were not found for run_id=${normalizedRunId}`);
|
|
1849
|
+
markBossChatDetachedWorkerFailed(normalizedRunId, error, { code: "CHAT_WORKER_ARGS_MISSING" });
|
|
1850
|
+
return { ok: false, error: error.message };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const started = await startBossChatRunInternal(spec.args || {}, {
|
|
1854
|
+
workspaceRoot: spec.workspace_root || "",
|
|
1855
|
+
runId: normalizedRunId
|
|
1856
|
+
});
|
|
1857
|
+
if (started?.status !== "ACCEPTED") {
|
|
1858
|
+
const failedError = started?.error || {
|
|
1859
|
+
code: "CHAT_WORKER_START_FAILED",
|
|
1860
|
+
message: started?.status || "Boss chat detached worker failed to start.",
|
|
1861
|
+
retryable: true
|
|
1862
|
+
};
|
|
1863
|
+
markBossChatDetachedWorkerFailed(normalizedRunId, failedError, {
|
|
1864
|
+
code: failedError.code || "CHAT_WORKER_START_FAILED"
|
|
1865
|
+
});
|
|
1866
|
+
return { ok: false, error: failedError.message || "Boss chat detached worker failed to start." };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
while (true) {
|
|
1870
|
+
const payload = getBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1871
|
+
const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
|
|
1872
|
+
if (TERMINAL_STATUSES.has(state)) break;
|
|
1873
|
+
const persisted = readChatRunState(normalizedRunId);
|
|
1874
|
+
if (persisted?.control?.cancel_requested === true) {
|
|
1875
|
+
cancelBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1876
|
+
} else if (persisted?.control?.pause_requested === true && state === RUN_STATUS_RUNNING) {
|
|
1877
|
+
pauseBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1878
|
+
} else if (persisted?.control?.pause_requested === false && state === RUN_STATUS_PAUSED) {
|
|
1879
|
+
resumeBossChatRunTool({ args: { run_id: normalizedRunId } });
|
|
1880
|
+
}
|
|
1881
|
+
await sleep(DETACHED_WORKER_POLL_MS);
|
|
1882
|
+
}
|
|
1883
|
+
return { ok: true };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1528
1886
|
export function getBossChatRunTool({ args = {} } = {}) {
|
|
1529
1887
|
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1530
1888
|
if (!runId) {
|
|
@@ -1615,6 +1973,20 @@ export function pauseBossChatRunTool({ args = {} } = {}) {
|
|
|
1615
1973
|
chrome: null
|
|
1616
1974
|
};
|
|
1617
1975
|
}
|
|
1976
|
+
if (persisted) {
|
|
1977
|
+
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1978
|
+
if (reconciled.stale_finalized) return getBossChatRunTool({ args });
|
|
1979
|
+
return patchPersistedChatControl(runId, {
|
|
1980
|
+
pause_requested: true,
|
|
1981
|
+
pause_requested_at: new Date().toISOString(),
|
|
1982
|
+
pause_requested_by: "pause_boss_chat_run",
|
|
1983
|
+
cancel_requested: false
|
|
1984
|
+
}, {
|
|
1985
|
+
status: "PAUSE_REQUESTED",
|
|
1986
|
+
message: "暂停请求已写入 detached chat run 控制文件。",
|
|
1987
|
+
lastMessage: "暂停请求已写入 detached chat run 控制文件。"
|
|
1988
|
+
}) || getBossChatRunTool({ args });
|
|
1989
|
+
}
|
|
1618
1990
|
return getBossChatRunTool({ args });
|
|
1619
1991
|
}
|
|
1620
1992
|
}
|
|
@@ -1665,6 +2037,18 @@ export function resumeBossChatRunTool({ args = {} } = {}) {
|
|
|
1665
2037
|
if (persisted) {
|
|
1666
2038
|
const reconciled = reconcilePersistedChatRun(persisted);
|
|
1667
2039
|
const reconciledStatus = reconciled.run?.status || reconciled.run?.state;
|
|
2040
|
+
if (!TERMINAL_STATUSES.has(reconciledStatus)) {
|
|
2041
|
+
return patchPersistedChatControl(runId, {
|
|
2042
|
+
pause_requested: false,
|
|
2043
|
+
pause_requested_at: null,
|
|
2044
|
+
pause_requested_by: null,
|
|
2045
|
+
cancel_requested: false
|
|
2046
|
+
}, {
|
|
2047
|
+
status: "RESUME_REQUESTED",
|
|
2048
|
+
message: "恢复请求已写入 detached chat run 控制文件。",
|
|
2049
|
+
lastMessage: "恢复请求已写入 detached chat run 控制文件。"
|
|
2050
|
+
}) || getBossChatRunTool({ args });
|
|
2051
|
+
}
|
|
1668
2052
|
return {
|
|
1669
2053
|
status: "FAILED",
|
|
1670
2054
|
error: {
|
|
@@ -1743,6 +2127,16 @@ export function cancelBossChatRunTool({ args = {} } = {}) {
|
|
|
1743
2127
|
chrome: null
|
|
1744
2128
|
};
|
|
1745
2129
|
}
|
|
2130
|
+
return patchPersistedChatControl(runId, {
|
|
2131
|
+
pause_requested: true,
|
|
2132
|
+
pause_requested_at: new Date().toISOString(),
|
|
2133
|
+
pause_requested_by: "cancel_boss_chat_run",
|
|
2134
|
+
cancel_requested: true
|
|
2135
|
+
}, {
|
|
2136
|
+
status: "CANCEL_REQUESTED",
|
|
2137
|
+
message: "取消请求已写入 detached chat run 控制文件。",
|
|
2138
|
+
lastMessage: "取消请求已写入 detached chat run 控制文件。"
|
|
2139
|
+
}) || getBossChatRunTool({ args });
|
|
1746
2140
|
}
|
|
1747
2141
|
return getBossChatRunTool({ args });
|
|
1748
2142
|
}
|
|
@@ -326,7 +326,7 @@ export async function toggleWindowStateForViewportRecovery(client, {
|
|
|
326
326
|
const currentInfo = await getCurrentWindowInfo(client);
|
|
327
327
|
const currentState = normalizeText(currentInfo?.bounds?.windowState || "").toLowerCase();
|
|
328
328
|
const sequence = currentState === "normal"
|
|
329
|
-
? ["maximized"
|
|
329
|
+
? ["maximized"]
|
|
330
330
|
: ["normal", "maximized"];
|
|
331
331
|
const attempts = [];
|
|
332
332
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
markBossChatDetachedWorkerFailed,
|
|
4
|
+
runBossChatDetachedWorker
|
|
5
|
+
} from "./chat-mcp.js";
|
|
6
|
+
import {
|
|
7
|
+
markBossRecruitDetachedWorkerFailed,
|
|
8
|
+
runBossRecruitDetachedWorker
|
|
9
|
+
} from "./recruit-mcp.js";
|
|
10
|
+
|
|
11
|
+
function normalizeText(value) {
|
|
12
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv = []) {
|
|
16
|
+
const args = {};
|
|
17
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
18
|
+
const item = argv[index];
|
|
19
|
+
if (item === "--domain") {
|
|
20
|
+
args.domain = normalizeText(argv[index + 1]).toLowerCase();
|
|
21
|
+
index += 1;
|
|
22
|
+
} else if (item === "--run-id") {
|
|
23
|
+
args.runId = normalizeText(argv[index + 1]);
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function markFailed(domain, runId, error, options = {}) {
|
|
31
|
+
if (domain === "chat") {
|
|
32
|
+
return markBossChatDetachedWorkerFailed(runId, error, options);
|
|
33
|
+
}
|
|
34
|
+
if (domain === "recruit") {
|
|
35
|
+
return markBossRecruitDetachedWorkerFailed(runId, error, options);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function installFailureHandlers(domain, runId) {
|
|
41
|
+
let handled = false;
|
|
42
|
+
const failOnce = (error, options = {}) => {
|
|
43
|
+
if (handled) return;
|
|
44
|
+
handled = true;
|
|
45
|
+
try {
|
|
46
|
+
markFailed(domain, runId, error, options);
|
|
47
|
+
} catch (markError) {
|
|
48
|
+
console.error("[boss-recommend-mcp] failed to persist detached worker failure", markError);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
process.on("uncaughtException", (error) => {
|
|
53
|
+
console.error("[boss-recommend-mcp] detached worker uncaught exception", error);
|
|
54
|
+
failOnce(error, { code: "DETACHED_WORKER_UNCAUGHT_EXCEPTION" });
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
process.on("unhandledRejection", (reason) => {
|
|
59
|
+
console.error("[boss-recommend-mcp] detached worker unhandled rejection", reason);
|
|
60
|
+
const error = reason instanceof Error ? reason : new Error(normalizeText(reason) || "Unhandled promise rejection");
|
|
61
|
+
failOnce(error, { code: "DETACHED_WORKER_UNHANDLED_REJECTION" });
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
66
|
+
process.on(signal, () => {
|
|
67
|
+
const error = new Error(`detached ${domain} worker received ${signal}`);
|
|
68
|
+
console.error("[boss-recommend-mcp] detached worker received signal", signal);
|
|
69
|
+
failOnce(error, { code: "DETACHED_WORKER_SIGNAL" });
|
|
70
|
+
const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
|
|
71
|
+
process.exit(signalExitCodes[signal] || 1);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
const options = parseArgs(process.argv.slice(2));
|
|
78
|
+
if (!options.domain || !options.runId) {
|
|
79
|
+
console.error("[boss-recommend-mcp] detached worker requires --domain and --run-id");
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
installFailureHandlers(options.domain, options.runId);
|
|
84
|
+
const result = options.domain === "chat"
|
|
85
|
+
? await runBossChatDetachedWorker({ runId: options.runId })
|
|
86
|
+
: options.domain === "recruit"
|
|
87
|
+
? await runBossRecruitDetachedWorker({ runId: options.runId })
|
|
88
|
+
: { ok: false, error: `Unsupported detached worker domain: ${options.domain}` };
|
|
89
|
+
if (!result?.ok) {
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await main().catch((error) => {
|
|
95
|
+
const options = parseArgs(process.argv.slice(2));
|
|
96
|
+
console.error("[boss-recommend-mcp] detached worker failed", error);
|
|
97
|
+
markFailed(options.domain, options.runId, error, { code: "DETACHED_WORKER_FAILED" });
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|
|
@@ -1876,9 +1876,10 @@ export function createChatRunService({
|
|
|
1876
1876
|
} = {}) {
|
|
1877
1877
|
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1878
1878
|
|
|
1879
|
-
function startChatRun({
|
|
1880
|
-
|
|
1881
|
-
|
|
1879
|
+
function startChatRun({
|
|
1880
|
+
runId = "",
|
|
1881
|
+
client,
|
|
1882
|
+
targetUrl = CHAT_TARGET_URL,
|
|
1882
1883
|
job = "",
|
|
1883
1884
|
startFrom = "all",
|
|
1884
1885
|
criteria = "",
|
|
@@ -1924,9 +1925,10 @@ export function createChatRunService({
|
|
|
1924
1925
|
legacyEnabled: humanRestEnabled === true || llmConfig?.humanRestEnabled === true
|
|
1925
1926
|
});
|
|
1926
1927
|
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1927
|
-
return manager.startRun({
|
|
1928
|
-
|
|
1929
|
-
|
|
1928
|
+
return manager.startRun({
|
|
1929
|
+
runId,
|
|
1930
|
+
name,
|
|
1931
|
+
context: {
|
|
1930
1932
|
domain: "chat",
|
|
1931
1933
|
target_url: targetUrl,
|
|
1932
1934
|
criteria_present: Boolean(criteria),
|
|
@@ -1146,6 +1146,7 @@ export function createRecruitRunService({
|
|
|
1146
1146
|
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1147
1147
|
|
|
1148
1148
|
function startRecruitRun({
|
|
1149
|
+
runId = "",
|
|
1149
1150
|
client,
|
|
1150
1151
|
targetUrl = "",
|
|
1151
1152
|
criteria = "",
|
|
@@ -1189,6 +1190,7 @@ export function createRecruitRunService({
|
|
|
1189
1190
|
});
|
|
1190
1191
|
const effectiveHumanRestEnabled = effectiveHumanBehavior.restEnabled;
|
|
1191
1192
|
return manager.startRun({
|
|
1193
|
+
runId,
|
|
1192
1194
|
name,
|
|
1193
1195
|
context: {
|
|
1194
1196
|
domain: "recruit",
|