@reconcrap/boss-recommend-mcp 2.0.35 → 2.0.37
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 +0 -0
- package/config/screening-config.example.json +1 -1
- package/package.json +119 -119
- package/src/core/run/index.js +7 -2
- package/src/domains/recommend/refresh.js +140 -73
- package/src/domains/recommend/run-service.js +11 -1
- package/src/index.js +283 -35
- package/src/recommend-mcp.js +212 -49
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"boss",
|
|
7
|
-
"mcp",
|
|
8
|
-
"codex",
|
|
9
|
-
"recruiting",
|
|
10
|
-
"boss-zhipin",
|
|
11
|
-
"recommend"
|
|
12
|
-
],
|
|
13
|
-
"type": "module",
|
|
14
|
-
"main": "src/index.js",
|
|
15
|
-
"bin": {
|
|
16
|
-
"boss-recommend-mcp": "bin/boss-recommend-mcp.js"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"start": "node src/index.js",
|
|
20
|
-
"cli": "node src/cli.js",
|
|
21
|
-
"install:local": "node src/cli.js install",
|
|
22
|
-
"postinstall": "node scripts/postinstall.cjs",
|
|
23
|
-
"test:parser": "node src/test-parser.js",
|
|
24
|
-
"test:run-state": "node src/test-run-state.js",
|
|
25
|
-
"test:cdp-browser": "node src/test-cdp-browser.js",
|
|
26
|
-
"test:core-capture": "node src/test-core-capture.js",
|
|
27
|
-
"test:core-cv-capture-target": "node src/test-core-cv-capture-target.js",
|
|
28
|
-
"test:core-cv-acquisition": "node src/test-core-cv-acquisition.js",
|
|
29
|
-
"test:core-greet-quota": "node src/test-core-greet-quota.js",
|
|
30
|
-
"test:core-infinite-list": "node src/test-core-infinite-list.js",
|
|
31
|
-
"test:core-reporting": "node src/test-core-reporting.js",
|
|
32
|
-
"test:core-run": "node src/test-core-run.js",
|
|
33
|
-
"test:core-screening": "node src/test-core-screening.js",
|
|
34
|
-
"test:core-self-heal": "node src/test-core-self-heal.js",
|
|
35
|
-
"test:installer-migration": "node src/test-installer-migration.js",
|
|
36
|
-
"test:recommend-actions": "node src/test-recommend-actions.js",
|
|
37
|
-
"test:recommend-domain": "node src/test-recommend-domain.js",
|
|
38
|
-
"test:recommend-run-service": "node src/test-recommend-run-service.js",
|
|
39
|
-
"test:recommend-mcp": "node src/test-recommend-mcp.js",
|
|
40
|
-
"test:recruit-domain": "node src/test-recruit-domain.js",
|
|
41
|
-
"test:recruit-mcp": "node src/test-recruit-mcp.js",
|
|
42
|
-
"test:chat-domain": "node src/test-chat-domain.js",
|
|
43
|
-
"test:chat-run-service": "node src/test-chat-run-service.js",
|
|
44
|
-
"test:chat-mcp": "node src/test-chat-mcp.js",
|
|
45
|
-
"test:async": "node src/test-index-async.js",
|
|
46
|
-
"test:runtime-scan": "node src/test-runtime-scan.js",
|
|
47
|
-
"scan:runtime": "node scripts/scan-forbidden-runtime.js",
|
|
48
|
-
"scan:runtime:json": "node scripts/scan-forbidden-runtime.js --json",
|
|
49
|
-
"scan:runtime:strict": "node scripts/scan-forbidden-runtime.js --fail-on-findings",
|
|
50
|
-
"scan:runtime:package": "node scripts/scan-forbidden-runtime.js --package-surface",
|
|
51
|
-
"scan:runtime:package:strict": "node scripts/scan-forbidden-runtime.js --package-surface --fail-on-legacy",
|
|
52
|
-
"scan:legacy-boundary": "node scripts/scan-legacy-boundary.js",
|
|
53
|
-
"scan:package-boundary": "node scripts/scan-package-boundary.js",
|
|
54
|
-
"gate:phase9-static": "node scripts/phase9-static-gate.js",
|
|
55
|
-
"gate:phase10-complete": "node scripts/phase10-completion-gate.js",
|
|
56
|
-
"live:cdp-smoke": "node scripts/live-cdp-smoke.js",
|
|
57
|
-
"live:run-lifecycle": "node scripts/live-run-lifecycle-smoke.js",
|
|
58
|
-
"live:screening": "node scripts/live-screening-smoke.js",
|
|
59
|
-
"live:detail": "node scripts/live-detail-smoke.js",
|
|
60
|
-
"live:infinite-list": "node scripts/live-infinite-list-smoke.js",
|
|
61
|
-
"live:scroll-end": "node scripts/live-scroll-end-screenshot.js",
|
|
62
|
-
"live:refresh-round": "node scripts/live-refresh-round-smoke.js",
|
|
63
|
-
"live:self-heal": "node scripts/live-self-heal-smoke.js",
|
|
64
|
-
"live:recommend-actions": "node scripts/live-recommend-actions-smoke.js",
|
|
65
|
-
"live:recommend-phase10-full": "node scripts/live-recommend-phase10-full.js",
|
|
66
|
-
"live:recommend-domain": "node scripts/live-recommend-domain-smoke.js",
|
|
67
|
-
"live:recommend-run-service": "node scripts/live-recommend-run-service-smoke.js",
|
|
68
|
-
"live:recommend-mcp": "node scripts/live-recommend-mcp-smoke.js",
|
|
69
|
-
"live:search-phase10-full": "node scripts/live-search-phase10-full.js",
|
|
70
|
-
"live:recruit-domain": "node scripts/live-recruit-domain-smoke.js",
|
|
71
|
-
"live:recruit-run-service": "node scripts/live-recruit-run-service-smoke.js",
|
|
72
|
-
"live:recruit-mcp": "node scripts/live-recruit-mcp-smoke.js",
|
|
73
|
-
"live:chat-domain": "node scripts/live-chat-domain-smoke.js",
|
|
74
|
-
"live:chat-run-service": "node scripts/live-chat-run-service-smoke.js",
|
|
75
|
-
"live:chat-mcp": "node scripts/live-chat-mcp-smoke.js",
|
|
76
|
-
"live:cv-capture-target": "node scripts/live-cv-capture-target-smoke.js",
|
|
77
|
-
"live:chat-phase10-full": "node scripts/live-chat-phase10-full.js",
|
|
78
|
-
"live:chat-image-screening": "node scripts/live-chat-image-screening-smoke.js"
|
|
79
|
-
},
|
|
80
|
-
"files": [
|
|
81
|
-
"bin",
|
|
82
|
-
"config/screening-config.example.json",
|
|
83
|
-
"skills",
|
|
84
|
-
"scripts/postinstall.cjs",
|
|
85
|
-
"src/core",
|
|
86
|
-
"src/domains",
|
|
87
|
-
"src/chat-mcp.js",
|
|
88
|
-
"src/chat-runtime-config.js",
|
|
89
|
-
"src/cli.js",
|
|
90
|
-
"src/index.js",
|
|
91
|
-
"src/parser.js",
|
|
92
|
-
"src/recommend-mcp.js",
|
|
93
|
-
"src/recruit-mcp.js",
|
|
94
|
-
"src/run-state.js",
|
|
95
|
-
"README.md"
|
|
96
|
-
],
|
|
97
|
-
"dependencies": {
|
|
98
|
-
"chrome-remote-interface": "^0.33.3",
|
|
99
|
-
"sharp": "^0.34.4",
|
|
100
|
-
"ws": "^8.19.0"
|
|
101
|
-
},
|
|
102
|
-
"engines": {
|
|
103
|
-
"node": ">=18"
|
|
104
|
-
},
|
|
105
|
-
"publishConfig": {
|
|
106
|
-
"access": "public"
|
|
107
|
-
},
|
|
108
|
-
"license": "MIT",
|
|
109
|
-
"devDependencies": {},
|
|
110
|
-
"repository": {
|
|
111
|
-
"type": "git",
|
|
112
|
-
"url": "git+https://github.com/reconcrap-cpu/boss-recommend-mcp.git"
|
|
113
|
-
},
|
|
114
|
-
"author": "",
|
|
115
|
-
"bugs": {
|
|
116
|
-
"url": "https://github.com/reconcrap-cpu/boss-recommend-mcp/issues"
|
|
117
|
-
},
|
|
118
|
-
"homepage": "https://github.com/reconcrap-cpu/boss-recommend-mcp#readme"
|
|
119
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@reconcrap/boss-recommend-mcp",
|
|
3
|
+
"version": "2.0.37",
|
|
4
|
+
"description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"boss",
|
|
7
|
+
"mcp",
|
|
8
|
+
"codex",
|
|
9
|
+
"recruiting",
|
|
10
|
+
"boss-zhipin",
|
|
11
|
+
"recommend"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "src/index.js",
|
|
15
|
+
"bin": {
|
|
16
|
+
"boss-recommend-mcp": "bin/boss-recommend-mcp.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node src/index.js",
|
|
20
|
+
"cli": "node src/cli.js",
|
|
21
|
+
"install:local": "node src/cli.js install",
|
|
22
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
23
|
+
"test:parser": "node src/test-parser.js",
|
|
24
|
+
"test:run-state": "node src/test-run-state.js",
|
|
25
|
+
"test:cdp-browser": "node src/test-cdp-browser.js",
|
|
26
|
+
"test:core-capture": "node src/test-core-capture.js",
|
|
27
|
+
"test:core-cv-capture-target": "node src/test-core-cv-capture-target.js",
|
|
28
|
+
"test:core-cv-acquisition": "node src/test-core-cv-acquisition.js",
|
|
29
|
+
"test:core-greet-quota": "node src/test-core-greet-quota.js",
|
|
30
|
+
"test:core-infinite-list": "node src/test-core-infinite-list.js",
|
|
31
|
+
"test:core-reporting": "node src/test-core-reporting.js",
|
|
32
|
+
"test:core-run": "node src/test-core-run.js",
|
|
33
|
+
"test:core-screening": "node src/test-core-screening.js",
|
|
34
|
+
"test:core-self-heal": "node src/test-core-self-heal.js",
|
|
35
|
+
"test:installer-migration": "node src/test-installer-migration.js",
|
|
36
|
+
"test:recommend-actions": "node src/test-recommend-actions.js",
|
|
37
|
+
"test:recommend-domain": "node src/test-recommend-domain.js",
|
|
38
|
+
"test:recommend-run-service": "node src/test-recommend-run-service.js",
|
|
39
|
+
"test:recommend-mcp": "node src/test-recommend-mcp.js",
|
|
40
|
+
"test:recruit-domain": "node src/test-recruit-domain.js",
|
|
41
|
+
"test:recruit-mcp": "node src/test-recruit-mcp.js",
|
|
42
|
+
"test:chat-domain": "node src/test-chat-domain.js",
|
|
43
|
+
"test:chat-run-service": "node src/test-chat-run-service.js",
|
|
44
|
+
"test:chat-mcp": "node src/test-chat-mcp.js",
|
|
45
|
+
"test:async": "node src/test-index-async.js",
|
|
46
|
+
"test:runtime-scan": "node src/test-runtime-scan.js",
|
|
47
|
+
"scan:runtime": "node scripts/scan-forbidden-runtime.js",
|
|
48
|
+
"scan:runtime:json": "node scripts/scan-forbidden-runtime.js --json",
|
|
49
|
+
"scan:runtime:strict": "node scripts/scan-forbidden-runtime.js --fail-on-findings",
|
|
50
|
+
"scan:runtime:package": "node scripts/scan-forbidden-runtime.js --package-surface",
|
|
51
|
+
"scan:runtime:package:strict": "node scripts/scan-forbidden-runtime.js --package-surface --fail-on-legacy",
|
|
52
|
+
"scan:legacy-boundary": "node scripts/scan-legacy-boundary.js",
|
|
53
|
+
"scan:package-boundary": "node scripts/scan-package-boundary.js",
|
|
54
|
+
"gate:phase9-static": "node scripts/phase9-static-gate.js",
|
|
55
|
+
"gate:phase10-complete": "node scripts/phase10-completion-gate.js",
|
|
56
|
+
"live:cdp-smoke": "node scripts/live-cdp-smoke.js",
|
|
57
|
+
"live:run-lifecycle": "node scripts/live-run-lifecycle-smoke.js",
|
|
58
|
+
"live:screening": "node scripts/live-screening-smoke.js",
|
|
59
|
+
"live:detail": "node scripts/live-detail-smoke.js",
|
|
60
|
+
"live:infinite-list": "node scripts/live-infinite-list-smoke.js",
|
|
61
|
+
"live:scroll-end": "node scripts/live-scroll-end-screenshot.js",
|
|
62
|
+
"live:refresh-round": "node scripts/live-refresh-round-smoke.js",
|
|
63
|
+
"live:self-heal": "node scripts/live-self-heal-smoke.js",
|
|
64
|
+
"live:recommend-actions": "node scripts/live-recommend-actions-smoke.js",
|
|
65
|
+
"live:recommend-phase10-full": "node scripts/live-recommend-phase10-full.js",
|
|
66
|
+
"live:recommend-domain": "node scripts/live-recommend-domain-smoke.js",
|
|
67
|
+
"live:recommend-run-service": "node scripts/live-recommend-run-service-smoke.js",
|
|
68
|
+
"live:recommend-mcp": "node scripts/live-recommend-mcp-smoke.js",
|
|
69
|
+
"live:search-phase10-full": "node scripts/live-search-phase10-full.js",
|
|
70
|
+
"live:recruit-domain": "node scripts/live-recruit-domain-smoke.js",
|
|
71
|
+
"live:recruit-run-service": "node scripts/live-recruit-run-service-smoke.js",
|
|
72
|
+
"live:recruit-mcp": "node scripts/live-recruit-mcp-smoke.js",
|
|
73
|
+
"live:chat-domain": "node scripts/live-chat-domain-smoke.js",
|
|
74
|
+
"live:chat-run-service": "node scripts/live-chat-run-service-smoke.js",
|
|
75
|
+
"live:chat-mcp": "node scripts/live-chat-mcp-smoke.js",
|
|
76
|
+
"live:cv-capture-target": "node scripts/live-cv-capture-target-smoke.js",
|
|
77
|
+
"live:chat-phase10-full": "node scripts/live-chat-phase10-full.js",
|
|
78
|
+
"live:chat-image-screening": "node scripts/live-chat-image-screening-smoke.js"
|
|
79
|
+
},
|
|
80
|
+
"files": [
|
|
81
|
+
"bin",
|
|
82
|
+
"config/screening-config.example.json",
|
|
83
|
+
"skills",
|
|
84
|
+
"scripts/postinstall.cjs",
|
|
85
|
+
"src/core",
|
|
86
|
+
"src/domains",
|
|
87
|
+
"src/chat-mcp.js",
|
|
88
|
+
"src/chat-runtime-config.js",
|
|
89
|
+
"src/cli.js",
|
|
90
|
+
"src/index.js",
|
|
91
|
+
"src/parser.js",
|
|
92
|
+
"src/recommend-mcp.js",
|
|
93
|
+
"src/recruit-mcp.js",
|
|
94
|
+
"src/run-state.js",
|
|
95
|
+
"README.md"
|
|
96
|
+
],
|
|
97
|
+
"dependencies": {
|
|
98
|
+
"chrome-remote-interface": "^0.33.3",
|
|
99
|
+
"sharp": "^0.34.4",
|
|
100
|
+
"ws": "^8.19.0"
|
|
101
|
+
},
|
|
102
|
+
"engines": {
|
|
103
|
+
"node": ">=18"
|
|
104
|
+
},
|
|
105
|
+
"publishConfig": {
|
|
106
|
+
"access": "public"
|
|
107
|
+
},
|
|
108
|
+
"license": "MIT",
|
|
109
|
+
"devDependencies": {},
|
|
110
|
+
"repository": {
|
|
111
|
+
"type": "git",
|
|
112
|
+
"url": "git+https://github.com/reconcrap-cpu/boss-recommend-mcp.git"
|
|
113
|
+
},
|
|
114
|
+
"author": "",
|
|
115
|
+
"bugs": {
|
|
116
|
+
"url": "https://github.com/reconcrap-cpu/boss-recommend-mcp/issues"
|
|
117
|
+
},
|
|
118
|
+
"homepage": "https://github.com/reconcrap-cpu/boss-recommend-mcp#readme"
|
|
119
|
+
}
|
package/src/core/run/index.js
CHANGED
|
@@ -47,6 +47,7 @@ function snapshotFromEntry(entry) {
|
|
|
47
47
|
return clone({
|
|
48
48
|
runId: run.runId,
|
|
49
49
|
name: run.name,
|
|
50
|
+
pid: run.pid,
|
|
50
51
|
status: run.status,
|
|
51
52
|
phase: run.phase,
|
|
52
53
|
progress: run.progress,
|
|
@@ -205,11 +206,14 @@ export function createRunLifecycleManager({
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
function startRun({ name, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
209
|
+
function startRun({ runId: requestedRunId = "", name, pid = process.pid, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
209
210
|
if (typeof task !== "function") {
|
|
210
211
|
throw new Error("startRun requires a task function");
|
|
211
212
|
}
|
|
212
|
-
const runId = createRunId(idPrefix);
|
|
213
|
+
const runId = String(requestedRunId || "").trim() || createRunId(idPrefix);
|
|
214
|
+
if (runs.has(runId)) {
|
|
215
|
+
throw new Error(`Run already exists: ${runId}`);
|
|
216
|
+
}
|
|
213
217
|
const startedAt = now();
|
|
214
218
|
const entry = {
|
|
215
219
|
controller: new AbortController(),
|
|
@@ -219,6 +223,7 @@ export function createRunLifecycleManager({
|
|
|
219
223
|
run: {
|
|
220
224
|
runId,
|
|
221
225
|
name: name || runId,
|
|
226
|
+
pid: Number.isInteger(pid) && pid > 0 ? pid : process.pid,
|
|
222
227
|
status: RUN_STATUS_QUEUED,
|
|
223
228
|
phase: "queued",
|
|
224
229
|
progress,
|
|
@@ -88,6 +88,105 @@ export function buildRecommendFilterSelectionOptions(filter = {}, {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function refreshFailureReason(method = "") {
|
|
92
|
+
return method === "page_navigate" ? "page_navigate_failed" : "page_reload_failed";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function applyRefreshMethod(client, method, {
|
|
96
|
+
jobLabel = "",
|
|
97
|
+
pageScope = "recommend",
|
|
98
|
+
fallbackPageScope = "recommend",
|
|
99
|
+
filter = {},
|
|
100
|
+
targetUrl = RECOMMEND_TARGET_URL,
|
|
101
|
+
forceRecentNotView = true,
|
|
102
|
+
cardTimeoutMs = 30000,
|
|
103
|
+
reloadSettleMs = 8000
|
|
104
|
+
} = {}) {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
let currentRootState = null;
|
|
107
|
+
let jobSelection = null;
|
|
108
|
+
let pageScopeResult = null;
|
|
109
|
+
let filterResult = null;
|
|
110
|
+
try {
|
|
111
|
+
if (method === "page_navigate") {
|
|
112
|
+
await client.Page.navigate({ url: targetUrl || RECOMMEND_TARGET_URL });
|
|
113
|
+
} else {
|
|
114
|
+
await client.Page.reload({ ignoreCache: true });
|
|
115
|
+
}
|
|
116
|
+
if (reloadSettleMs > 0) await sleep(reloadSettleMs);
|
|
117
|
+
currentRootState = await waitForRecommendRoots(client, {
|
|
118
|
+
timeoutMs: Math.max(45000, reloadSettleMs * 6),
|
|
119
|
+
intervalMs: 500
|
|
120
|
+
});
|
|
121
|
+
if (!currentRootState?.iframe?.documentNodeId) {
|
|
122
|
+
throw new Error("Recommend iframe was not ready after refresh reload");
|
|
123
|
+
}
|
|
124
|
+
if (jobLabel) {
|
|
125
|
+
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
126
|
+
jobLabel,
|
|
127
|
+
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
128
|
+
});
|
|
129
|
+
if (!jobSelection.selected) {
|
|
130
|
+
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
131
|
+
}
|
|
132
|
+
currentRootState = await getRecommendRoots(client);
|
|
133
|
+
}
|
|
134
|
+
pageScopeResult = await selectRecommendPageScope(
|
|
135
|
+
client,
|
|
136
|
+
currentRootState.iframe.documentNodeId,
|
|
137
|
+
{
|
|
138
|
+
pageScope,
|
|
139
|
+
fallbackScope: fallbackPageScope,
|
|
140
|
+
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
141
|
+
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
if (!pageScopeResult.selected) {
|
|
145
|
+
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
146
|
+
}
|
|
147
|
+
currentRootState = await getRecommendRoots(client);
|
|
148
|
+
filterResult = await selectAndConfirmFirstSafeFilter(
|
|
149
|
+
client,
|
|
150
|
+
currentRootState.iframe.documentNodeId,
|
|
151
|
+
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
152
|
+
);
|
|
153
|
+
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
154
|
+
timeoutMs: cardTimeoutMs,
|
|
155
|
+
intervalMs: 500
|
|
156
|
+
});
|
|
157
|
+
if (!cardNodeIds.length) {
|
|
158
|
+
throw new Error("No recommend candidate cards were found after refresh reload");
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
method,
|
|
163
|
+
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
164
|
+
job_selection: jobSelection,
|
|
165
|
+
page_scope: pageScopeResult,
|
|
166
|
+
filter: filterResult,
|
|
167
|
+
card_count: cardNodeIds.length,
|
|
168
|
+
root_state: currentRootState,
|
|
169
|
+
forced_recent_not_view: forceRecentNotView,
|
|
170
|
+
elapsed_ms: Date.now() - started
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
method,
|
|
176
|
+
reason: refreshFailureReason(method),
|
|
177
|
+
error: error?.message || String(error),
|
|
178
|
+
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
179
|
+
job_selection: jobSelection,
|
|
180
|
+
page_scope: pageScopeResult,
|
|
181
|
+
filter: filterResult,
|
|
182
|
+
card_count: 0,
|
|
183
|
+
root_state: currentRootState,
|
|
184
|
+
forced_recent_not_view: forceRecentNotView,
|
|
185
|
+
elapsed_ms: Date.now() - started
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
91
190
|
export async function refreshRecommendListAtEnd(client, {
|
|
92
191
|
rootState = null,
|
|
93
192
|
jobLabel = "",
|
|
@@ -103,9 +202,10 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
103
202
|
reloadSettleMs = 8000
|
|
104
203
|
} = {}) {
|
|
105
204
|
const attempts = [];
|
|
106
|
-
let currentRootState = rootState ||
|
|
205
|
+
let currentRootState = rootState || null;
|
|
107
206
|
|
|
108
207
|
if (preferEndRefreshButton) {
|
|
208
|
+
currentRootState = currentRootState || await getRecommendRoots(client);
|
|
109
209
|
const buttonResult = await clickRecommendEndRefreshButton(
|
|
110
210
|
client,
|
|
111
211
|
currentRootState.iframe.documentNodeId,
|
|
@@ -159,82 +259,49 @@ export async function refreshRecommendListAtEnd(client, {
|
|
|
159
259
|
}
|
|
160
260
|
}
|
|
161
261
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
262
|
+
const fallbackMethods = [];
|
|
263
|
+
if (forceNavigate && typeof client?.Page?.navigate === "function") {
|
|
264
|
+
fallbackMethods.push("page_navigate");
|
|
265
|
+
}
|
|
266
|
+
if (typeof client?.Page?.reload === "function") {
|
|
267
|
+
fallbackMethods.push("page_reload");
|
|
268
|
+
}
|
|
269
|
+
if (!fallbackMethods.length) {
|
|
270
|
+
fallbackMethods.push("page_reload");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let lastRefreshResult = null;
|
|
274
|
+
for (const method of fallbackMethods) {
|
|
275
|
+
const refreshResult = await applyRefreshMethod(client, method, {
|
|
276
|
+
jobLabel,
|
|
277
|
+
pageScope,
|
|
278
|
+
fallbackPageScope,
|
|
279
|
+
filter,
|
|
280
|
+
targetUrl,
|
|
281
|
+
forceRecentNotView,
|
|
282
|
+
cardTimeoutMs,
|
|
283
|
+
reloadSettleMs
|
|
177
284
|
});
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
jobSelection = await selectRecommendJob(client, currentRootState.iframe.documentNodeId, {
|
|
184
|
-
jobLabel,
|
|
185
|
-
settleMs: reloadSettleMs > 10000 ? 12000 : 6000
|
|
186
|
-
});
|
|
187
|
-
if (!jobSelection.selected) {
|
|
188
|
-
throw new Error(`Requested recommend job was not selected after refresh reload: ${jobSelection.reason}`);
|
|
189
|
-
}
|
|
190
|
-
currentRootState = await getRecommendRoots(client);
|
|
285
|
+
if (refreshResult.ok) {
|
|
286
|
+
return {
|
|
287
|
+
...refreshResult,
|
|
288
|
+
attempts
|
|
289
|
+
};
|
|
191
290
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
settleMs: reloadSettleMs > 10000 ? 3000 : 1200,
|
|
199
|
-
timeoutMs: Math.max(10000, Math.min(cardTimeoutMs, 60000))
|
|
200
|
-
}
|
|
201
|
-
);
|
|
202
|
-
if (!pageScopeResult.selected) {
|
|
203
|
-
throw new Error(`Recommend page scope was not selected after refresh reload: ${pageScopeResult.reason || pageScope}`);
|
|
204
|
-
}
|
|
205
|
-
currentRootState = await getRecommendRoots(client);
|
|
206
|
-
const filterResult = await selectAndConfirmFirstSafeFilter(
|
|
207
|
-
client,
|
|
208
|
-
currentRootState.iframe.documentNodeId,
|
|
209
|
-
buildRecommendFilterSelectionOptions(filter, { forceRecentNotView })
|
|
210
|
-
);
|
|
211
|
-
const cardNodeIds = await waitForRecommendCardNodeIds(client, currentRootState.iframe.documentNodeId, {
|
|
212
|
-
timeoutMs: cardTimeoutMs,
|
|
213
|
-
intervalMs: 500
|
|
214
|
-
});
|
|
215
|
-
return {
|
|
216
|
-
ok: cardNodeIds.length > 0,
|
|
217
|
-
method,
|
|
218
|
-
attempts,
|
|
219
|
-
target_url: method === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
220
|
-
job_selection: jobSelection,
|
|
221
|
-
page_scope: pageScopeResult,
|
|
222
|
-
filter: filterResult,
|
|
223
|
-
card_count: cardNodeIds.length,
|
|
224
|
-
root_state: currentRootState,
|
|
225
|
-
forced_recent_not_view: forceRecentNotView
|
|
226
|
-
};
|
|
227
|
-
} catch (error) {
|
|
228
|
-
return {
|
|
291
|
+
attempts.push(refreshResult);
|
|
292
|
+
lastRefreshResult = refreshResult;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
...(lastRefreshResult || {
|
|
229
297
|
ok: false,
|
|
230
|
-
method:
|
|
231
|
-
reason:
|
|
232
|
-
error:
|
|
233
|
-
attempts,
|
|
234
|
-
target_url: fallbackMethod === "page_navigate" ? (targetUrl || RECOMMEND_TARGET_URL) : null,
|
|
298
|
+
method: fallbackMethods[fallbackMethods.length - 1] || "page_reload",
|
|
299
|
+
reason: "refresh_failed",
|
|
300
|
+
error: "Recommend refresh did not run",
|
|
235
301
|
card_count: 0,
|
|
236
302
|
root_state: currentRootState,
|
|
237
303
|
forced_recent_not_view: forceRecentNotView
|
|
238
|
-
}
|
|
239
|
-
|
|
304
|
+
}),
|
|
305
|
+
attempts
|
|
306
|
+
};
|
|
240
307
|
}
|
|
@@ -355,16 +355,22 @@ function compactRefreshAttempt(refreshAttempt) {
|
|
|
355
355
|
return {
|
|
356
356
|
ok: Boolean(refreshAttempt.ok),
|
|
357
357
|
method: refreshAttempt.method || "",
|
|
358
|
+
reason: refreshAttempt.reason || null,
|
|
359
|
+
error: refreshAttempt.error || null,
|
|
358
360
|
forced_recent_not_view: Boolean(refreshAttempt.forced_recent_not_view),
|
|
359
361
|
target_url: refreshAttempt.target_url || null,
|
|
360
362
|
card_count: refreshAttempt.card_count || 0,
|
|
363
|
+
elapsed_ms: refreshAttempt.elapsed_ms || 0,
|
|
361
364
|
attempts: (refreshAttempt.attempts || []).map((attempt) => ({
|
|
362
365
|
ok: Boolean(attempt.ok),
|
|
363
366
|
method: attempt.method || "",
|
|
364
367
|
reason: attempt.reason || null,
|
|
368
|
+
error: attempt.error || null,
|
|
365
369
|
label: attempt.label || null,
|
|
366
370
|
before_card_count: attempt.before_card_count || 0,
|
|
367
|
-
after_card_count: attempt.after_card_count || 0
|
|
371
|
+
after_card_count: attempt.after_card_count || 0,
|
|
372
|
+
card_count: attempt.card_count || 0,
|
|
373
|
+
elapsed_ms: attempt.elapsed_ms || 0
|
|
368
374
|
})),
|
|
369
375
|
job_selection: compactJobSelection(refreshAttempt.job_selection),
|
|
370
376
|
page_scope: compactPageScopeSelection(refreshAttempt.page_scope),
|
|
@@ -1260,6 +1266,8 @@ export function createRecommendRunService({
|
|
|
1260
1266
|
const manager = lifecycle || createRunLifecycleManager({ idPrefix, onSnapshot });
|
|
1261
1267
|
|
|
1262
1268
|
function startRecommendRun({
|
|
1269
|
+
runId = "",
|
|
1270
|
+
pid = process.pid,
|
|
1263
1271
|
client,
|
|
1264
1272
|
targetUrl = "",
|
|
1265
1273
|
criteria = "",
|
|
@@ -1307,7 +1315,9 @@ export function createRecommendRunService({
|
|
|
1307
1315
|
const candidateLimit = Math.max(1, Number(maxCandidates) || 1);
|
|
1308
1316
|
const normalizedDetailLimit = detailLimit == null ? null : Math.max(0, Number(detailLimit) || 0);
|
|
1309
1317
|
return manager.startRun({
|
|
1318
|
+
runId,
|
|
1310
1319
|
name,
|
|
1320
|
+
pid,
|
|
1311
1321
|
context: {
|
|
1312
1322
|
domain: "recommend",
|
|
1313
1323
|
target_url: targetUrl,
|
package/src/index.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
getRecommendPipelineRunTool,
|
|
47
47
|
listRecommendJobsTool,
|
|
48
48
|
pauseRecommendPipelineRunTool,
|
|
49
|
+
prepareRecommendPipelineRunTool,
|
|
49
50
|
resumeRecommendPipelineRunTool,
|
|
50
51
|
startRecommendPipelineRunTool
|
|
51
52
|
} from "./recommend-mcp.js";
|
|
@@ -116,6 +117,16 @@ const FRAMING_LINE = "line";
|
|
|
116
117
|
const DETACHED_WORKER_FLAG = "--detached-worker";
|
|
117
118
|
const DETACHED_WORKER_RUN_ID_FLAG = "--run-id";
|
|
118
119
|
const DETACHED_WORKER_RESUME_FLAG = "--resume";
|
|
120
|
+
const AGENT_RUNTIME_HINT_KEYS = [
|
|
121
|
+
"CODEX_CI",
|
|
122
|
+
"CODEX_THREAD_ID",
|
|
123
|
+
"CODEX_HOME",
|
|
124
|
+
"OPENCLAW_HOME",
|
|
125
|
+
"OPENCLAW",
|
|
126
|
+
"TRAE_CN",
|
|
127
|
+
"TRAE_HOME",
|
|
128
|
+
"TRAE_AGENT"
|
|
129
|
+
];
|
|
119
130
|
const featuredCalibrationUnsupportedCode = "FEATURED_CALIBRATION_UNSUPPORTED_CDP_ONLY";
|
|
120
131
|
const recommendSelfHealApplyUnsupportedCode = "RECOMMEND_SELF_HEAL_APPLY_UNSUPPORTED_CDP_ONLY";
|
|
121
132
|
const detachedLegacyPipelineUnsupportedCode = "DETACHED_LEGACY_PIPELINE_UNSUPPORTED_CDP_ONLY";
|
|
@@ -138,6 +149,31 @@ function normalizeText(value) {
|
|
|
138
149
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
function clonePlain(value, fallback = null) {
|
|
153
|
+
try {
|
|
154
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
155
|
+
} catch {
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isLikelyAgentRuntime() {
|
|
161
|
+
for (const key of AGENT_RUNTIME_HINT_KEYS) {
|
|
162
|
+
if (normalizeText(process.env[key] || "")) return true;
|
|
163
|
+
}
|
|
164
|
+
const originHints = [
|
|
165
|
+
normalizeText(process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || ""),
|
|
166
|
+
normalizeText(process.env.TERM_PROGRAM || "")
|
|
167
|
+
].join(" ").toLowerCase();
|
|
168
|
+
return /codex|openclaw|trae/.test(originHints);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldStartRecommendDetached() {
|
|
172
|
+
if (normalizeText(process.env.BOSS_RECOMMEND_CDP_INPROC || "") === "1") return false;
|
|
173
|
+
if (normalizeText(process.env.BOSS_RECOMMEND_CDP_DETACHED || "") === "1") return true;
|
|
174
|
+
return isLikelyAgentRuntime();
|
|
175
|
+
}
|
|
176
|
+
|
|
141
177
|
function isUnlimitedTargetCountToken(value) {
|
|
142
178
|
const token = normalizeText(value).toLowerCase();
|
|
143
179
|
if (!token) return false;
|
|
@@ -255,17 +291,65 @@ function getDefaultAcceptedMessage(args = {}) {
|
|
|
255
291
|
return `异步流水线已启动(detached)。默认不自动轮询;如需进度请按需调用 get_recommend_pipeline_run(建议至少每 ${recommendedMinutes} 分钟查询一次)。`;
|
|
256
292
|
}
|
|
257
293
|
|
|
258
|
-
function getRunArtifacts(runId) {
|
|
259
|
-
const normalizedRunId = normalizeText(runId);
|
|
260
|
-
return {
|
|
261
|
-
run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
|
|
262
|
-
checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
|
|
263
|
-
};
|
|
264
|
-
}
|
|
294
|
+
function getRunArtifacts(runId) {
|
|
295
|
+
const normalizedRunId = normalizeText(runId);
|
|
296
|
+
return {
|
|
297
|
+
run_state_path: path.join(getRunsDir(), `${normalizedRunId}.json`),
|
|
298
|
+
checkpoint_path: path.join(getRunsDir(), `${normalizedRunId}.checkpoint.json`)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function writeRawRunState(runId, payload) {
|
|
303
|
+
const artifacts = getRunArtifacts(runId);
|
|
304
|
+
fs.mkdirSync(path.dirname(artifacts.run_state_path), { recursive: true });
|
|
305
|
+
const tempPath = `${artifacts.run_state_path}.tmp`;
|
|
306
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
307
|
+
fs.renameSync(tempPath, artifacts.run_state_path);
|
|
308
|
+
return payload;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function readRawRunState(runId) {
|
|
312
|
+
const artifacts = getRunArtifacts(runId);
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(fs.readFileSync(artifacts.run_state_path, "utf8"));
|
|
315
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function patchRawRunState(runId, patch) {
|
|
322
|
+
const current = readRawRunState(runId);
|
|
323
|
+
if (!current) return null;
|
|
324
|
+
const now = new Date().toISOString();
|
|
325
|
+
const next = {
|
|
326
|
+
...current,
|
|
327
|
+
...patch,
|
|
328
|
+
run_id: current.run_id || runId,
|
|
329
|
+
updated_at: now,
|
|
330
|
+
heartbeat_at: current.heartbeat_at || now,
|
|
331
|
+
control: {
|
|
332
|
+
...(current.control || {}),
|
|
333
|
+
...(patch.control || {})
|
|
334
|
+
},
|
|
335
|
+
resume: {
|
|
336
|
+
...(current.resume || {}),
|
|
337
|
+
...(patch.resume || {})
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
return writeRawRunState(runId, next);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createDetachedRecommendRunId() {
|
|
344
|
+
const suffix = Math.random().toString(36).slice(2, 10);
|
|
345
|
+
return `mcp_recommend_${Date.now().toString(36)}_${suffix}`;
|
|
346
|
+
}
|
|
265
347
|
|
|
266
348
|
function buildRunContext(workspaceRoot, args = {}) {
|
|
349
|
+
const clonedArgs = clonePlain(args, {});
|
|
267
350
|
return {
|
|
268
351
|
workspace_root: path.resolve(workspaceRoot),
|
|
352
|
+
args: clonedArgs,
|
|
269
353
|
instruction: String(args?.instruction || ""),
|
|
270
354
|
confirmation: args?.confirmation && typeof args.confirmation === "object" ? args.confirmation : {},
|
|
271
355
|
overrides: args?.overrides && typeof args.overrides === "object" ? args.overrides : {},
|
|
@@ -273,25 +357,40 @@ function buildRunContext(workspaceRoot, args = {}) {
|
|
|
273
357
|
};
|
|
274
358
|
}
|
|
275
359
|
|
|
276
|
-
function resolveRunContext(snapshot) {
|
|
277
|
-
const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
|
|
278
|
-
const
|
|
279
|
-
? snapshot.context.
|
|
280
|
-
:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
360
|
+
function resolveRunContext(snapshot) {
|
|
361
|
+
const workspaceRoot = normalizeText(snapshot?.context?.workspace_root || "");
|
|
362
|
+
const storedArgs = snapshot?.context?.args && typeof snapshot.context.args === "object" && !Array.isArray(snapshot.context.args)
|
|
363
|
+
? clonePlain(snapshot.context.args, {})
|
|
364
|
+
: null;
|
|
365
|
+
const instruction = typeof storedArgs?.instruction === "string"
|
|
366
|
+
? storedArgs.instruction
|
|
367
|
+
: typeof snapshot?.context?.instruction === "string"
|
|
368
|
+
? snapshot.context.instruction
|
|
369
|
+
: "";
|
|
370
|
+
const confirmation = storedArgs?.confirmation && typeof storedArgs.confirmation === "object" && !Array.isArray(storedArgs.confirmation)
|
|
371
|
+
? storedArgs.confirmation
|
|
372
|
+
: snapshot?.context?.confirmation && typeof snapshot.context.confirmation === "object"
|
|
373
|
+
? snapshot.context.confirmation
|
|
374
|
+
: {};
|
|
375
|
+
const overrides = storedArgs?.overrides && typeof storedArgs.overrides === "object" && !Array.isArray(storedArgs.overrides)
|
|
376
|
+
? storedArgs.overrides
|
|
377
|
+
: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
|
|
378
|
+
? snapshot.context.overrides
|
|
379
|
+
: {};
|
|
380
|
+
const followUp = storedArgs && Object.prototype.hasOwnProperty.call(storedArgs, "follow_up")
|
|
381
|
+
? storedArgs.follow_up
|
|
382
|
+
: snapshot?.context?.follow_up && typeof snapshot.context.follow_up === "object"
|
|
383
|
+
? snapshot.context.follow_up
|
|
384
|
+
: null;
|
|
385
|
+
if (!workspaceRoot || !instruction.trim()) return null;
|
|
386
|
+
return {
|
|
387
|
+
workspaceRoot,
|
|
388
|
+
args: {
|
|
389
|
+
...(storedArgs || {}),
|
|
285
390
|
instruction,
|
|
286
|
-
confirmation
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
overrides: snapshot?.context?.overrides && typeof snapshot.context.overrides === "object"
|
|
290
|
-
? snapshot.context.overrides
|
|
291
|
-
: {},
|
|
292
|
-
follow_up: snapshot?.context?.follow_up && typeof snapshot.context.follow_up === "object"
|
|
293
|
-
? snapshot.context.follow_up
|
|
294
|
-
: null
|
|
391
|
+
confirmation,
|
|
392
|
+
overrides,
|
|
393
|
+
follow_up: followUp
|
|
295
394
|
}
|
|
296
395
|
};
|
|
297
396
|
}
|
|
@@ -1827,35 +1926,184 @@ async function runDetachedWorker({ runId, resumeRun = false, workerPid = process
|
|
|
1827
1926
|
: "detached worker 已启动,准备执行。"
|
|
1828
1927
|
});
|
|
1829
1928
|
|
|
1830
|
-
await
|
|
1831
|
-
runId: normalizedRunId,
|
|
1832
|
-
mode: RUN_MODE_ASYNC,
|
|
1929
|
+
const started = await startRecommendPipelineRunTool({
|
|
1833
1930
|
workspaceRoot: executionContext.workspaceRoot,
|
|
1834
1931
|
args: executionContext.args,
|
|
1835
|
-
|
|
1836
|
-
resumeRun
|
|
1932
|
+
runId: normalizedRunId
|
|
1837
1933
|
});
|
|
1934
|
+
if (started?.status !== "ACCEPTED") {
|
|
1935
|
+
const failedPayload = started?.error || {
|
|
1936
|
+
code: "RUN_WORKER_START_FAILED",
|
|
1937
|
+
message: started?.status || "detached recommend worker failed to start",
|
|
1938
|
+
retryable: true
|
|
1939
|
+
};
|
|
1940
|
+
safeUpdateRunState(normalizedRunId, {
|
|
1941
|
+
state: RUN_STATE_FAILED,
|
|
1942
|
+
stage: snapshot.stage || RUN_STAGE_PREFLIGHT,
|
|
1943
|
+
last_message: failedPayload.message,
|
|
1944
|
+
error: failedPayload,
|
|
1945
|
+
result: {
|
|
1946
|
+
status: "FAILED",
|
|
1947
|
+
error: failedPayload
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
return { ok: false, error: failedPayload.message };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
while (true) {
|
|
1954
|
+
const payload = getRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1955
|
+
const state = normalizeText(payload?.run?.state || payload?.run?.status || "");
|
|
1956
|
+
if (TERMINAL_RUN_STATES.has(state)) break;
|
|
1957
|
+
const persisted = readRawRunState(normalizedRunId);
|
|
1958
|
+
if (persisted?.control?.cancel_requested === true) {
|
|
1959
|
+
cancelRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1960
|
+
} else if (persisted?.control?.pause_requested === true && state === RUN_STATE_RUNNING) {
|
|
1961
|
+
pauseRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1962
|
+
} else if (persisted?.control?.pause_requested === false && state === RUN_STATE_PAUSED) {
|
|
1963
|
+
resumeRecommendPipelineRunTool({ args: { run_id: normalizedRunId } });
|
|
1964
|
+
}
|
|
1965
|
+
await sleepMs(1000);
|
|
1966
|
+
}
|
|
1838
1967
|
return { ok: true };
|
|
1839
1968
|
}
|
|
1840
|
-
|
|
1969
|
+
|
|
1841
1970
|
async function handleStartRunTool({ workspaceRoot, args }) {
|
|
1842
|
-
|
|
1971
|
+
if (!shouldStartRecommendDetached()) {
|
|
1972
|
+
return startRecommendPipelineRunTool({ workspaceRoot, args });
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const prepared = prepareRecommendPipelineRunTool({ workspaceRoot, args });
|
|
1976
|
+
if (prepared.status !== "READY") return prepared;
|
|
1977
|
+
|
|
1978
|
+
cleanupExpiredRuns();
|
|
1979
|
+
const runId = createDetachedRecommendRunId();
|
|
1980
|
+
try {
|
|
1981
|
+
initializeRunStateOrThrow(runId, RUN_MODE_ASYNC, workspaceRoot, args, process.pid);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
return {
|
|
1984
|
+
status: "FAILED",
|
|
1985
|
+
error: {
|
|
1986
|
+
code: "RUN_STATE_IO_ERROR",
|
|
1987
|
+
message: `无法写入运行状态目录:${error.message || "unknown"}`,
|
|
1988
|
+
retryable: false
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
let worker;
|
|
1994
|
+
try {
|
|
1995
|
+
worker = launchDetachedRunWorker({ runId });
|
|
1996
|
+
safeUpdateRunState(runId, { pid: worker.pid || process.pid });
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
const failed = buildWorkerLaunchFailedPayload(error?.message || "无法启动 detached recommend worker。");
|
|
1999
|
+
safeUpdateRunState(runId, {
|
|
2000
|
+
state: RUN_STATE_FAILED,
|
|
2001
|
+
stage: RUN_STAGE_PREFLIGHT,
|
|
2002
|
+
last_message: failed.error.message,
|
|
2003
|
+
error: failed.error,
|
|
2004
|
+
result: failed
|
|
2005
|
+
});
|
|
2006
|
+
return failed;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const run = readRunState(runId);
|
|
2010
|
+
return {
|
|
2011
|
+
status: "ACCEPTED",
|
|
2012
|
+
run_id: runId,
|
|
2013
|
+
state: "queued",
|
|
2014
|
+
run,
|
|
2015
|
+
poll_after_sec: getRecommendedPollAfterSec(args),
|
|
2016
|
+
message: getDefaultAcceptedMessage(args),
|
|
2017
|
+
post_action: prepared.post_action,
|
|
2018
|
+
target_count_semantics: prepared.target_count_semantics,
|
|
2019
|
+
review: prepared.review
|
|
2020
|
+
};
|
|
1843
2021
|
}
|
|
1844
2022
|
|
|
1845
2023
|
function handleGetRunTool(args) {
|
|
1846
2024
|
return getRecommendPipelineRunTool({ args });
|
|
1847
2025
|
}
|
|
1848
2026
|
|
|
2027
|
+
function patchDetachedRecommendControl(args, controlPatch, {
|
|
2028
|
+
status,
|
|
2029
|
+
message,
|
|
2030
|
+
lastMessage
|
|
2031
|
+
} = {}) {
|
|
2032
|
+
const runId = normalizeText(args?.run_id || args?.runId || "");
|
|
2033
|
+
if (!runId) return null;
|
|
2034
|
+
const current = readRawRunState(runId);
|
|
2035
|
+
const state = normalizeText(current?.state || current?.status || "");
|
|
2036
|
+
if (!current || TERMINAL_RUN_STATES.has(state)) return null;
|
|
2037
|
+
const patched = patchRawRunState(runId, {
|
|
2038
|
+
last_message: lastMessage || message || current.last_message || "",
|
|
2039
|
+
control: controlPatch
|
|
2040
|
+
});
|
|
2041
|
+
if (!patched) return null;
|
|
2042
|
+
return {
|
|
2043
|
+
status,
|
|
2044
|
+
run: patched,
|
|
2045
|
+
message,
|
|
2046
|
+
persistence: {
|
|
2047
|
+
source: "disk",
|
|
2048
|
+
active_control_available: false,
|
|
2049
|
+
detached_control_requested: true
|
|
2050
|
+
},
|
|
2051
|
+
runtime_evaluate_used: false,
|
|
2052
|
+
method_summary: {},
|
|
2053
|
+
method_log: [],
|
|
2054
|
+
chrome: null
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1849
2058
|
function handleCancelRunTool(args) {
|
|
1850
|
-
|
|
2059
|
+
const result = cancelRecommendPipelineRunTool({ args });
|
|
2060
|
+
if (result?.status === "RUN_STATUS" && result?.persistence?.active_control_available === false) {
|
|
2061
|
+
return patchDetachedRecommendControl(args, {
|
|
2062
|
+
pause_requested: true,
|
|
2063
|
+
pause_requested_at: new Date().toISOString(),
|
|
2064
|
+
pause_requested_by: TOOL_CANCEL_RUN,
|
|
2065
|
+
cancel_requested: true
|
|
2066
|
+
}, {
|
|
2067
|
+
status: "CANCEL_REQUESTED",
|
|
2068
|
+
message: "已收到取消请求,将由 detached worker 在下一个安全边界停止。",
|
|
2069
|
+
lastMessage: "已收到取消请求,将由 detached worker 在下一个安全边界停止。"
|
|
2070
|
+
}) || result;
|
|
2071
|
+
}
|
|
2072
|
+
return result;
|
|
1851
2073
|
}
|
|
1852
2074
|
|
|
1853
2075
|
function handlePauseRunTool(args) {
|
|
1854
|
-
|
|
2076
|
+
const result = pauseRecommendPipelineRunTool({ args });
|
|
2077
|
+
if (result?.status === "RUN_STATUS" && result?.persistence?.active_control_available === false) {
|
|
2078
|
+
return patchDetachedRecommendControl(args, {
|
|
2079
|
+
pause_requested: true,
|
|
2080
|
+
pause_requested_at: new Date().toISOString(),
|
|
2081
|
+
pause_requested_by: TOOL_PAUSE_RUN,
|
|
2082
|
+
cancel_requested: false
|
|
2083
|
+
}, {
|
|
2084
|
+
status: "PAUSE_REQUESTED",
|
|
2085
|
+
message: "暂停请求已写入 detached run 控制文件。",
|
|
2086
|
+
lastMessage: "暂停请求已写入 detached run 控制文件。"
|
|
2087
|
+
}) || result;
|
|
2088
|
+
}
|
|
2089
|
+
return result;
|
|
1855
2090
|
}
|
|
1856
2091
|
|
|
1857
2092
|
function handleResumeRunTool(args) {
|
|
1858
|
-
|
|
2093
|
+
const result = resumeRecommendPipelineRunTool({ args });
|
|
2094
|
+
if (result?.status === "FAILED" && result?.error?.code === "RUN_NOT_ACTIVE") {
|
|
2095
|
+
return patchDetachedRecommendControl(args, {
|
|
2096
|
+
pause_requested: false,
|
|
2097
|
+
pause_requested_at: null,
|
|
2098
|
+
pause_requested_by: null,
|
|
2099
|
+
cancel_requested: false
|
|
2100
|
+
}, {
|
|
2101
|
+
status: "RESUME_REQUESTED",
|
|
2102
|
+
message: "恢复请求已写入 detached run 控制文件。",
|
|
2103
|
+
lastMessage: "恢复请求已写入 detached run 控制文件。"
|
|
2104
|
+
}) || result;
|
|
2105
|
+
}
|
|
2106
|
+
return result;
|
|
1859
2107
|
}
|
|
1860
2108
|
|
|
1861
2109
|
function handleGetFeaturedCalibrationStatusTool(workspaceRoot) {
|
package/src/recommend-mcp.js
CHANGED
|
@@ -140,6 +140,15 @@ function clonePlain(value, fallback = null) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
function plainRecord(value) {
|
|
144
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function nonEmptyRecord(value) {
|
|
148
|
+
const record = plainRecord(value);
|
|
149
|
+
return Object.keys(record).length ? record : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
function normalizeRunId(runId) {
|
|
144
153
|
const normalized = normalizeText(runId);
|
|
145
154
|
if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
|
|
@@ -191,11 +200,29 @@ function recommendSearchParamsForCsv(searchParams = {}) {
|
|
|
191
200
|
};
|
|
192
201
|
}
|
|
193
202
|
|
|
194
|
-
function
|
|
203
|
+
function getSnapshotRequestContext(snapshot = {}) {
|
|
204
|
+
const context = plainRecord(snapshot?.context);
|
|
205
|
+
const shared = plainRecord(context.shared_run_context);
|
|
206
|
+
return {
|
|
207
|
+
context,
|
|
208
|
+
confirmation: nonEmptyRecord(context.confirmation) || plainRecord(shared.confirmation),
|
|
209
|
+
overrides: nonEmptyRecord(context.overrides) || plainRecord(shared.overrides),
|
|
210
|
+
followUp: context.follow_up ?? shared.follow_up ?? null,
|
|
211
|
+
shared
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
|
|
216
|
+
const { confirmation, overrides, shared } = getSnapshotRequestContext(snapshot);
|
|
195
217
|
const value = normalizeText(
|
|
196
218
|
meta.args?.confirmation?.job_value
|
|
197
219
|
|| meta.normalized?.job
|
|
198
220
|
|| meta.args?.overrides?.job
|
|
221
|
+
|| confirmation.job_value
|
|
222
|
+
|| overrides.job
|
|
223
|
+
|| shared.confirmation?.job_value
|
|
224
|
+
|| shared.overrides?.job
|
|
225
|
+
|| shared.job_label
|
|
199
226
|
|| ""
|
|
200
227
|
);
|
|
201
228
|
return {
|
|
@@ -206,21 +233,28 @@ function selectedRecommendJobForCsv(meta = {}) {
|
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
236
|
+
const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
|
|
237
|
+
const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
|
|
238
|
+
school_tag: overrides.school_tag ?? confirmation.school_tag_value,
|
|
239
|
+
degree: overrides.degree ?? confirmation.degree_value,
|
|
240
|
+
gender: overrides.gender ?? confirmation.gender_value,
|
|
241
|
+
recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value
|
|
242
|
+
});
|
|
243
|
+
const parsedScreenParams = meta.parsed?.screenParams || {};
|
|
244
|
+
const screenParams = {
|
|
245
|
+
criteria: parsedScreenParams.criteria || meta.normalized?.criteria || overrides.criteria || "",
|
|
246
|
+
target_count: parsedScreenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || overrides.target_count || confirmation.target_count_value || shared.max_candidates || "",
|
|
247
|
+
post_action: parsedScreenParams.post_action || overrides.post_action || confirmation.post_action_value || shared.post_action || "none",
|
|
248
|
+
max_greet_count: parsedScreenParams.max_greet_count ?? overrides.max_greet_count ?? confirmation.max_greet_count_value ?? shared.max_greet_count ?? ""
|
|
249
|
+
};
|
|
211
250
|
return buildLegacyScreenInputRows({
|
|
212
|
-
instruction: meta.args?.instruction || "",
|
|
251
|
+
instruction: meta.args?.instruction || context.instruction || shared.instruction || "",
|
|
213
252
|
selectedPage: "recommend",
|
|
214
|
-
selectedJob: selectedRecommendJobForCsv(meta),
|
|
253
|
+
selectedJob: selectedRecommendJobForCsv(meta, snapshot),
|
|
215
254
|
userSearchParams: cloneReportInput(searchParams, {}),
|
|
216
255
|
effectiveSearchParams: cloneReportInput(searchParams, {}),
|
|
217
|
-
screenParams
|
|
218
|
-
|
|
219
|
-
target_count: screenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || "",
|
|
220
|
-
post_action: screenParams.post_action || "none",
|
|
221
|
-
max_greet_count: screenParams.max_greet_count ?? ""
|
|
222
|
-
},
|
|
223
|
-
followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || null
|
|
256
|
+
screenParams,
|
|
257
|
+
followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || followUp || overrides.follow_up || null
|
|
224
258
|
});
|
|
225
259
|
}
|
|
226
260
|
|
|
@@ -237,6 +271,16 @@ function readRecommendRunState(runId) {
|
|
|
237
271
|
return readJsonFile(artifacts.run_state_path);
|
|
238
272
|
}
|
|
239
273
|
|
|
274
|
+
function isProcessAlive(pid) {
|
|
275
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
276
|
+
try {
|
|
277
|
+
process.kill(pid, 0);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
240
284
|
function getRecommendRunMeta(runId) {
|
|
241
285
|
return recommendRunMeta.get(runId) || {};
|
|
242
286
|
}
|
|
@@ -475,12 +519,14 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
475
519
|
|| snapshot.status === RUN_STATUS_PAUSED
|
|
476
520
|
) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
|
|
477
521
|
const recovery = buildConstrainedAgentRecovery(snapshot, meta, artifacts);
|
|
522
|
+
const snapshotContext = plainRecord(snapshot.context);
|
|
523
|
+
const metaArgs = plainRecord(meta.args);
|
|
478
524
|
const oldContext = {
|
|
479
|
-
workspace_root: meta.workspaceRoot || null,
|
|
480
|
-
instruction:
|
|
481
|
-
confirmation: clonePlain(
|
|
482
|
-
overrides: clonePlain(
|
|
483
|
-
follow_up: clonePlain(
|
|
525
|
+
workspace_root: meta.workspaceRoot || snapshotContext.workspace_root || null,
|
|
526
|
+
instruction: metaArgs.instruction || snapshotContext.instruction || "",
|
|
527
|
+
confirmation: clonePlain(metaArgs.confirmation ?? snapshotContext.confirmation ?? {}, {}),
|
|
528
|
+
overrides: clonePlain(metaArgs.overrides ?? snapshotContext.overrides ?? {}, {}),
|
|
529
|
+
follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
|
|
484
530
|
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
485
531
|
};
|
|
486
532
|
return {
|
|
@@ -494,12 +540,12 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
494
540
|
updated_at: snapshot.updatedAt,
|
|
495
541
|
completed_at: toIsoOrNull(snapshot.completedAt),
|
|
496
542
|
heartbeat_at: snapshot.updatedAt,
|
|
497
|
-
pid: process.pid || null,
|
|
543
|
+
pid: Number.isInteger(snapshot.pid) && snapshot.pid > 0 ? snapshot.pid : process.pid || null,
|
|
498
544
|
last_message: snapshot.error?.message || snapshot.phase || null,
|
|
499
545
|
context: {
|
|
500
|
-
...
|
|
546
|
+
...snapshotContext,
|
|
501
547
|
...oldContext,
|
|
502
|
-
shared_run_context:
|
|
548
|
+
shared_run_context: snapshotContext
|
|
503
549
|
},
|
|
504
550
|
control: {
|
|
505
551
|
pause_requested: snapshot.status === RUN_STATUS_PAUSED,
|
|
@@ -521,6 +567,41 @@ function normalizeRunSnapshot(snapshot) {
|
|
|
521
567
|
};
|
|
522
568
|
}
|
|
523
569
|
|
|
570
|
+
function mergePersistedControlRequest(normalized, existing) {
|
|
571
|
+
const control = {
|
|
572
|
+
...(normalized?.control || {})
|
|
573
|
+
};
|
|
574
|
+
if (!normalized || TERMINAL_STATUSES.has(normalized.state)) return control;
|
|
575
|
+
const existingControl = plainRecord(existing?.control);
|
|
576
|
+
if (existingControl.cancel_requested === true) {
|
|
577
|
+
return {
|
|
578
|
+
...control,
|
|
579
|
+
pause_requested: true,
|
|
580
|
+
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
581
|
+
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
|
|
582
|
+
cancel_requested: true
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
|
|
586
|
+
return {
|
|
587
|
+
...control,
|
|
588
|
+
pause_requested: true,
|
|
589
|
+
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
590
|
+
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_recommend_pipeline_run"
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
|
|
594
|
+
return {
|
|
595
|
+
...control,
|
|
596
|
+
pause_requested: false,
|
|
597
|
+
pause_requested_at: null,
|
|
598
|
+
pause_requested_by: null,
|
|
599
|
+
cancel_requested: false
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return control;
|
|
603
|
+
}
|
|
604
|
+
|
|
524
605
|
function persistRecommendRunSnapshot(snapshot, {
|
|
525
606
|
persistActiveCheckpoint = false
|
|
526
607
|
} = {}) {
|
|
@@ -528,6 +609,8 @@ function persistRecommendRunSnapshot(snapshot, {
|
|
|
528
609
|
if (!normalized?.run_id) return normalized;
|
|
529
610
|
const artifacts = getRecommendRunArtifacts(normalized.run_id);
|
|
530
611
|
if (!artifacts) return normalized;
|
|
612
|
+
const existing = readJsonFile(artifacts.run_state_path);
|
|
613
|
+
normalized.control = mergePersistedControlRequest(normalized, existing);
|
|
531
614
|
if (persistActiveCheckpoint) {
|
|
532
615
|
persistRecommendCheckpointSnapshot(normalized);
|
|
533
616
|
}
|
|
@@ -557,6 +640,38 @@ function persistRecommendRunSnapshot(snapshot, {
|
|
|
557
640
|
return normalized;
|
|
558
641
|
}
|
|
559
642
|
|
|
643
|
+
function reconcilePersistedRecommendRunIfNeeded(persisted) {
|
|
644
|
+
if (!persisted || typeof persisted !== "object") return persisted;
|
|
645
|
+
const persistedState = normalizeText(persisted.state || persisted.status);
|
|
646
|
+
if (TERMINAL_STATUSES.has(persistedState)) return persisted;
|
|
647
|
+
if (isProcessAlive(persisted.pid)) return persisted;
|
|
648
|
+
|
|
649
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
650
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
651
|
+
const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
|
|
652
|
+
const now = new Date().toISOString();
|
|
653
|
+
const error = {
|
|
654
|
+
code: "RUN_PROCESS_EXITED",
|
|
655
|
+
message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`,
|
|
656
|
+
retryable: true
|
|
657
|
+
};
|
|
658
|
+
return persistRecommendRunSnapshot({
|
|
659
|
+
runId,
|
|
660
|
+
name: persisted.name || runId,
|
|
661
|
+
status: RUN_STATUS_FAILED,
|
|
662
|
+
phase: persisted.stage || persisted.phase || "recommend:orphaned",
|
|
663
|
+
progress: persisted.progress || {},
|
|
664
|
+
context: persisted.context || {},
|
|
665
|
+
checkpoint: checkpoint || persisted.checkpoint || {},
|
|
666
|
+
startedAt: persisted.started_at || persisted.startedAt || now,
|
|
667
|
+
updatedAt: now,
|
|
668
|
+
completedAt: now,
|
|
669
|
+
pid: Number.isInteger(persisted.pid) && persisted.pid > 0 ? persisted.pid : null,
|
|
670
|
+
error,
|
|
671
|
+
summary: persisted.summary || null
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
560
675
|
function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
|
|
561
676
|
return persistRecommendRunSnapshot(snapshot, {
|
|
562
677
|
persistActiveCheckpoint: event?.type === "checkpoint"
|
|
@@ -1154,6 +1269,47 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
|
|
|
1154
1269
|
};
|
|
1155
1270
|
}
|
|
1156
1271
|
|
|
1272
|
+
function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
|
|
1273
|
+
const parsed = parseRecommendPipelineRequest(args);
|
|
1274
|
+
const gate = evaluateRecommendPipelineGate(parsed, args);
|
|
1275
|
+
if (gate) return { response: gate };
|
|
1276
|
+
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1277
|
+
const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
|
|
1278
|
+
const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
|
|
1279
|
+
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1280
|
+
return {
|
|
1281
|
+
response: {
|
|
1282
|
+
status: "FAILED",
|
|
1283
|
+
error: {
|
|
1284
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1285
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1286
|
+
retryable: false
|
|
1287
|
+
},
|
|
1288
|
+
debug_test_options: debugTestOptions
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
if (normalized.screeningMode === "llm" && !configResolution.ok) {
|
|
1293
|
+
return {
|
|
1294
|
+
response: {
|
|
1295
|
+
status: "FAILED",
|
|
1296
|
+
error: {
|
|
1297
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1298
|
+
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
1299
|
+
retryable: true
|
|
1300
|
+
},
|
|
1301
|
+
config_path: configResolution.config_path || null,
|
|
1302
|
+
candidate_paths: configResolution.candidate_paths || []
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
parsed,
|
|
1308
|
+
configResolution,
|
|
1309
|
+
normalized
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1157
1313
|
async function closeRecommendRunSession(runId) {
|
|
1158
1314
|
const meta = recommendRunMeta.get(runId);
|
|
1159
1315
|
if (!meta || meta.closed) return;
|
|
@@ -1199,34 +1355,19 @@ function trackRecommendRun(runId) {
|
|
|
1199
1355
|
});
|
|
1200
1356
|
}
|
|
1201
1357
|
|
|
1202
|
-
async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
|
|
1209
|
-
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1358
|
+
async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
1359
|
+
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1360
|
+
if (prepared.response) return prepared.response;
|
|
1361
|
+
const { parsed, configResolution, normalized } = prepared;
|
|
1362
|
+
const fixedRunId = normalizeRunId(runId);
|
|
1363
|
+
if (runId && !fixedRunId) {
|
|
1210
1364
|
return {
|
|
1211
1365
|
status: "FAILED",
|
|
1212
1366
|
error: {
|
|
1213
|
-
code: "
|
|
1214
|
-
message:
|
|
1367
|
+
code: "INVALID_RUN_ID",
|
|
1368
|
+
message: "run_id is invalid",
|
|
1215
1369
|
retryable: false
|
|
1216
|
-
}
|
|
1217
|
-
debug_test_options: debugTestOptions
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
if (normalized.screeningMode === "llm" && !configResolution.ok) {
|
|
1221
|
-
return {
|
|
1222
|
-
status: "FAILED",
|
|
1223
|
-
error: {
|
|
1224
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
1225
|
-
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
1226
|
-
retryable: true
|
|
1227
|
-
},
|
|
1228
|
-
config_path: configResolution.config_path || null,
|
|
1229
|
-
candidate_paths: configResolution.candidate_paths || []
|
|
1370
|
+
}
|
|
1230
1371
|
};
|
|
1231
1372
|
}
|
|
1232
1373
|
|
|
@@ -1260,7 +1401,11 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
1260
1401
|
|
|
1261
1402
|
let started;
|
|
1262
1403
|
try {
|
|
1263
|
-
started = recommendRunService.startRecommendRun(
|
|
1404
|
+
started = recommendRunService.startRecommendRun({
|
|
1405
|
+
...getRunOptions(args, parsed, normalized, session, configResolution),
|
|
1406
|
+
runId: fixedRunId || undefined,
|
|
1407
|
+
pid: process.pid
|
|
1408
|
+
});
|
|
1264
1409
|
} catch (error) {
|
|
1265
1410
|
await session.close?.();
|
|
1266
1411
|
return {
|
|
@@ -1311,8 +1456,24 @@ async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = ""
|
|
|
1311
1456
|
};
|
|
1312
1457
|
}
|
|
1313
1458
|
|
|
1314
|
-
export
|
|
1315
|
-
const
|
|
1459
|
+
export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1460
|
+
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1461
|
+
if (prepared.response) return prepared.response;
|
|
1462
|
+
const { parsed, normalized } = prepared;
|
|
1463
|
+
return {
|
|
1464
|
+
status: "READY",
|
|
1465
|
+
review: parsed.review,
|
|
1466
|
+
post_action: {
|
|
1467
|
+
requested: normalized.postAction,
|
|
1468
|
+
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1469
|
+
max_greet_count: normalized.maxGreetCount
|
|
1470
|
+
},
|
|
1471
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {}, runId = "" } = {}) {
|
|
1476
|
+
const started = await startRecommendPipelineRunInternal(args, { workspaceRoot, runId });
|
|
1316
1477
|
if (started.status !== "ACCEPTED") return started;
|
|
1317
1478
|
return attachMethodEvidence(started, started.run_id);
|
|
1318
1479
|
}
|
|
@@ -1339,12 +1500,14 @@ export function getRecommendPipelineRunTool({ args = {} } = {}) {
|
|
|
1339
1500
|
} catch {
|
|
1340
1501
|
const persisted = readRecommendRunState(runId);
|
|
1341
1502
|
if (persisted) {
|
|
1503
|
+
const reconciled = reconcilePersistedRecommendRunIfNeeded(persisted);
|
|
1342
1504
|
return {
|
|
1343
1505
|
status: "RUN_STATUS",
|
|
1344
|
-
run:
|
|
1506
|
+
run: reconciled,
|
|
1345
1507
|
persistence: {
|
|
1346
1508
|
source: "disk",
|
|
1347
|
-
active_control_available: false
|
|
1509
|
+
active_control_available: false,
|
|
1510
|
+
stale_process_reconciled: reconciled?.state !== persisted.state
|
|
1348
1511
|
},
|
|
1349
1512
|
runtime_evaluate_used: false,
|
|
1350
1513
|
method_summary: {},
|