@reconcrap/boss-recommend-mcp 2.0.51 → 2.0.53
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/README.md +2 -2
- package/bin/boss-recommend-mcp.js +0 -0
- package/config/screening-config.example.json +1 -1
- package/package.json +120 -120
- package/src/cli.js +47 -4
- package/src/core/run/index.js +310 -310
- package/src/domains/recommend/detail.js +544 -544
- package/src/domains/recommend/run-service.js +1235 -1235
- package/src/index.js +16 -5
- package/src/recommend-mcp.js +1701 -1701
- package/src/run-state.js +358 -358
package/src/recommend-mcp.js
CHANGED
|
@@ -1,1222 +1,1222 @@
|
|
|
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
|
-
buildRecommendSelfHealConfig,
|
|
30
|
-
HEALTH_STATUS,
|
|
31
|
-
resolveRecommendSelfHealRoots,
|
|
32
|
-
runSelfHealCheck
|
|
33
|
-
} from "./core/self-heal/index.js";
|
|
34
|
-
import {
|
|
35
|
-
closeRecommendJobDropdown,
|
|
36
|
-
closeRecommendDetail,
|
|
37
|
-
createRecommendRunService,
|
|
38
|
-
getRecommendRoots,
|
|
39
|
-
listRecommendJobOptions,
|
|
40
|
-
RECOMMEND_TARGET_URL,
|
|
41
|
-
runRecommendWorkflow
|
|
42
|
-
} from "./domains/recommend/index.js";
|
|
43
|
-
import {
|
|
44
|
-
parseRecommendInstruction
|
|
45
|
-
} from "./parser.js";
|
|
46
|
-
import { getRunsDir } from "./run-state.js";
|
|
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
|
+
buildRecommendSelfHealConfig,
|
|
30
|
+
HEALTH_STATUS,
|
|
31
|
+
resolveRecommendSelfHealRoots,
|
|
32
|
+
runSelfHealCheck
|
|
33
|
+
} from "./core/self-heal/index.js";
|
|
34
|
+
import {
|
|
35
|
+
closeRecommendJobDropdown,
|
|
36
|
+
closeRecommendDetail,
|
|
37
|
+
createRecommendRunService,
|
|
38
|
+
getRecommendRoots,
|
|
39
|
+
listRecommendJobOptions,
|
|
40
|
+
RECOMMEND_TARGET_URL,
|
|
41
|
+
runRecommendWorkflow
|
|
42
|
+
} from "./domains/recommend/index.js";
|
|
43
|
+
import {
|
|
44
|
+
parseRecommendInstruction
|
|
45
|
+
} from "./parser.js";
|
|
46
|
+
import { getRunsDir } from "./run-state.js";
|
|
47
47
|
import {
|
|
48
48
|
resolveBossConfiguredOutputDir,
|
|
49
49
|
resolveHumanBehaviorForRun,
|
|
50
50
|
resolveBossScreeningConfig
|
|
51
51
|
} from "./chat-runtime-config.js";
|
|
52
|
-
import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
|
|
53
|
-
|
|
54
|
-
const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
|
|
55
|
-
const DEFAULT_RECOMMEND_PORT = 9222;
|
|
56
|
-
const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
|
|
57
|
-
const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
|
|
58
|
-
const RUN_MODE_ASYNC = "async";
|
|
59
|
-
|
|
60
|
-
const TERMINAL_STATUSES = new Set([
|
|
61
|
-
RUN_STATUS_COMPLETED,
|
|
62
|
-
RUN_STATUS_FAILED,
|
|
63
|
-
RUN_STATUS_CANCELED
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
let recommendWorkflowImpl = runRecommendWorkflow;
|
|
67
|
-
let recommendConnectorImpl = connectRecommendChromeSession;
|
|
68
|
-
let recommendJobReaderImpl = readRecommendJobOptionsFromSession;
|
|
69
|
-
let recommendRunService = createRecommendRunService({
|
|
70
|
-
idPrefix: "mcp_recommend",
|
|
71
|
-
workflow: (...args) => recommendWorkflowImpl(...args),
|
|
72
|
-
onSnapshot: persistRecommendLifecycleSnapshot
|
|
73
|
-
});
|
|
74
|
-
const recommendRunMeta = new Map();
|
|
75
|
-
|
|
76
|
-
function normalizeText(value) {
|
|
77
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function parsePositiveInteger(raw, fallback) {
|
|
81
|
-
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
82
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function parseNonNegativeInteger(raw, fallback) {
|
|
86
|
-
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
87
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isDebugTestMode(args = {}) {
|
|
91
|
-
return args.debug_test_mode === true || args.allow_debug_test_mode === true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function normalizeScreeningModeArg(args = {}) {
|
|
95
|
-
const raw = normalizeText(args.screening_mode || args.screeningMode || "");
|
|
96
|
-
if (args.use_llm === false) return "deterministic";
|
|
97
|
-
return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
|
|
98
|
-
? "deterministic"
|
|
99
|
-
: "llm";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
|
|
103
|
-
const reasons = [];
|
|
104
|
-
if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
|
|
105
|
-
if (args.allow_card_only_screening === true) reasons.push("allow_card_only_screening");
|
|
106
|
-
if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
|
|
107
|
-
if (args.no_filter === true) reasons.push("no_filter");
|
|
108
|
-
if (args.filter_enabled === false) reasons.push("filter_enabled=false");
|
|
109
|
-
if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
|
|
110
|
-
if (args.execute_post_action === false && normalized.postAction && normalized.postAction !== "none") {
|
|
111
|
-
reasons.push("execute_post_action=false");
|
|
112
|
-
}
|
|
113
|
-
return reasons;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolveRecommendDetailLimit(args = {}, normalized = {}) {
|
|
117
|
-
const fallback = parsePositiveInteger(normalized.targetCount, 5);
|
|
118
|
-
const requested = parseNonNegativeInteger(args.detail_limit, fallback);
|
|
119
|
-
if (requested === 0 && !isDebugTestMode(args)) {
|
|
120
|
-
return fallback;
|
|
121
|
-
}
|
|
122
|
-
if (requested === 0 && args.allow_card_only_screening !== true) {
|
|
123
|
-
return fallback;
|
|
124
|
-
}
|
|
125
|
-
return requested;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function methodSummary(methodLog = []) {
|
|
129
|
-
const summary = {};
|
|
130
|
-
for (const entry of methodLog || []) {
|
|
131
|
-
summary[entry.method] = (summary[entry.method] || 0) + 1;
|
|
132
|
-
}
|
|
133
|
-
return summary;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function clonePlain(value, fallback = null) {
|
|
137
|
-
try {
|
|
138
|
-
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
139
|
-
} catch {
|
|
140
|
-
return fallback;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function plainRecord(value) {
|
|
145
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function nonEmptyRecord(value) {
|
|
149
|
-
const record = plainRecord(value);
|
|
150
|
-
return Object.keys(record).length ? record : null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function normalizeRunId(runId) {
|
|
154
|
-
const normalized = normalizeText(runId);
|
|
155
|
-
if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
|
|
156
|
-
return normalized;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getRecommendRunArtifacts(runId) {
|
|
160
|
-
const normalized = normalizeRunId(runId);
|
|
161
|
-
if (!normalized) return null;
|
|
162
|
-
const runsDir = getRunsDir();
|
|
163
|
-
const outputDir = resolveBossConfiguredOutputDir("", runsDir);
|
|
164
|
-
return {
|
|
165
|
-
runs_dir: runsDir,
|
|
166
|
-
output_dir: outputDir,
|
|
167
|
-
run_state_path: path.join(runsDir, `${normalized}.json`),
|
|
168
|
-
checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
|
|
169
|
-
worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
|
|
170
|
-
worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
|
|
171
|
-
output_csv: path.join(outputDir, `${normalized}.results.csv`),
|
|
172
|
-
report_json: path.join(outputDir, `${normalized}.report.json`)
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function ensureDirectory(dirPath) {
|
|
177
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function writeJsonAtomic(filePath, payload) {
|
|
181
|
-
ensureDirectory(path.dirname(filePath));
|
|
182
|
-
const tempPath = `${filePath}.tmp`;
|
|
183
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
184
|
-
fs.renameSync(tempPath, filePath);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function readJsonFile(filePath) {
|
|
188
|
-
try {
|
|
189
|
-
if (!fs.existsSync(filePath)) return null;
|
|
190
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
191
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
192
|
-
} catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function recommendSearchParamsForCsv(searchParams = {}) {
|
|
198
|
-
return {
|
|
199
|
-
school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
|
|
200
|
-
degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
|
|
201
|
-
gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
|
|
202
|
-
recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限"
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getSnapshotRequestContext(snapshot = {}) {
|
|
207
|
-
const context = plainRecord(snapshot?.context);
|
|
208
|
-
const shared = plainRecord(context.shared_run_context);
|
|
209
|
-
return {
|
|
210
|
-
context,
|
|
211
|
-
confirmation: nonEmptyRecord(context.confirmation) || plainRecord(shared.confirmation),
|
|
212
|
-
overrides: nonEmptyRecord(context.overrides) || plainRecord(shared.overrides),
|
|
213
|
-
followUp: context.follow_up ?? shared.follow_up ?? null,
|
|
214
|
-
shared
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
|
|
219
|
-
const { confirmation, overrides, shared } = getSnapshotRequestContext(snapshot);
|
|
220
|
-
const value = normalizeText(
|
|
221
|
-
meta.args?.confirmation?.job_value
|
|
222
|
-
|| meta.normalized?.job
|
|
223
|
-
|| meta.args?.overrides?.job
|
|
224
|
-
|| confirmation.job_value
|
|
225
|
-
|| overrides.job
|
|
226
|
-
|| shared.confirmation?.job_value
|
|
227
|
-
|| shared.overrides?.job
|
|
228
|
-
|| shared.job_label
|
|
229
|
-
|| ""
|
|
230
|
-
);
|
|
231
|
-
return {
|
|
232
|
-
value,
|
|
233
|
-
title: value,
|
|
234
|
-
label: value
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
|
|
239
|
-
const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
|
|
240
|
-
const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
|
|
241
|
-
school_tag: overrides.school_tag ?? confirmation.school_tag_value,
|
|
242
|
-
degree: overrides.degree ?? confirmation.degree_value,
|
|
243
|
-
gender: overrides.gender ?? confirmation.gender_value,
|
|
244
|
-
recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value
|
|
245
|
-
});
|
|
246
|
-
const parsedScreenParams = meta.parsed?.screenParams || {};
|
|
247
|
-
const screenParams = {
|
|
248
|
-
criteria: parsedScreenParams.criteria || meta.normalized?.criteria || overrides.criteria || "",
|
|
249
|
-
target_count: parsedScreenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || overrides.target_count || confirmation.target_count_value || shared.max_candidates || "",
|
|
250
|
-
post_action: parsedScreenParams.post_action || overrides.post_action || confirmation.post_action_value || shared.post_action || "none",
|
|
251
|
-
max_greet_count: parsedScreenParams.max_greet_count ?? overrides.max_greet_count ?? confirmation.max_greet_count_value ?? shared.max_greet_count ?? ""
|
|
252
|
-
};
|
|
253
|
-
return buildLegacyScreenInputRows({
|
|
254
|
-
instruction: meta.args?.instruction || context.instruction || shared.instruction || "",
|
|
255
|
-
selectedPage: "recommend",
|
|
256
|
-
selectedJob: selectedRecommendJobForCsv(meta, snapshot),
|
|
257
|
-
userSearchParams: cloneReportInput(searchParams, {}),
|
|
258
|
-
effectiveSearchParams: cloneReportInput(searchParams, {}),
|
|
259
|
-
screenParams,
|
|
260
|
-
followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || followUp || overrides.follow_up || null
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function writeRecommendLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
|
|
265
|
-
writeLegacyScreenCsv(filePath, {
|
|
266
|
-
inputRows: buildRecommendCsvInputRows(snapshot, meta),
|
|
267
|
-
results: rows
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function readRecommendRunState(runId) {
|
|
272
|
-
const artifacts = getRecommendRunArtifacts(runId);
|
|
273
|
-
if (!artifacts) return null;
|
|
274
|
-
return readJsonFile(artifacts.run_state_path);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function isProcessAlive(pid) {
|
|
278
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
279
|
-
try {
|
|
280
|
-
process.kill(pid, 0);
|
|
281
|
-
return true;
|
|
282
|
-
} catch {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function getRecommendRunMeta(runId) {
|
|
288
|
-
return recommendRunMeta.get(runId) || {};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function toIsoOrNull(value) {
|
|
292
|
-
const normalized = normalizeText(value);
|
|
293
|
-
return normalized || null;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function secondsBetween(startedAt, endedAt) {
|
|
297
|
-
const startMs = Date.parse(startedAt || "");
|
|
298
|
-
const endMs = Date.parse(endedAt || "") || Date.now();
|
|
299
|
-
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
|
|
300
|
-
return Math.max(1, Math.round((endMs - startMs) / 1000));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeLegacyProgress(progress = {}, summary = null) {
|
|
304
|
-
const processed = Number.isInteger(progress.processed)
|
|
305
|
-
? progress.processed
|
|
306
|
-
: Number.isInteger(summary?.processed)
|
|
307
|
-
? summary.processed
|
|
308
|
-
: 0;
|
|
309
|
-
const screened = Number.isInteger(progress.screened)
|
|
310
|
-
? progress.screened
|
|
311
|
-
: Number.isInteger(summary?.screened)
|
|
312
|
-
? summary.screened
|
|
313
|
-
: processed;
|
|
314
|
-
const passed = Number.isInteger(progress.passed)
|
|
315
|
-
? progress.passed
|
|
316
|
-
: Number.isInteger(summary?.passed)
|
|
317
|
-
? summary.passed
|
|
318
|
-
: 0;
|
|
319
|
-
return {
|
|
320
|
-
...progress,
|
|
321
|
-
processed,
|
|
322
|
-
inspected: processed,
|
|
323
|
-
screened,
|
|
324
|
-
passed,
|
|
325
|
-
skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
|
|
326
|
-
greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
|
|
327
|
-
post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function completionReason(status) {
|
|
332
|
-
if (status === RUN_STATUS_COMPLETED) return "completed";
|
|
333
|
-
if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
|
|
334
|
-
if (status === RUN_STATUS_FAILED) return "failed";
|
|
335
|
-
if (status === RUN_STATUS_PAUSED) return "paused";
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function normalizeErrorText(error = {}) {
|
|
340
|
-
return normalizeText([
|
|
341
|
-
error?.code || "",
|
|
342
|
-
error?.message || error || ""
|
|
343
|
-
].join(" "));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function classifyRecommendRecovery(error = {}) {
|
|
347
|
-
const text = normalizeErrorText(error);
|
|
348
|
-
if (!text) return null;
|
|
349
|
-
if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
|
|
350
|
-
if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
|
|
351
|
-
return "transient_stale_dom";
|
|
352
|
-
}
|
|
353
|
-
if (/IMAGE_CAPTURE_TIMEOUT|IMAGE_CAPTURE_TOTAL_TIMEOUT|Image fallback capture timed out/i.test(text)) {
|
|
354
|
-
return "transient_image_capture";
|
|
355
|
-
}
|
|
356
|
-
if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
|
|
357
|
-
return "transient_network_or_llm";
|
|
358
|
-
}
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
|
|
363
|
-
const error = snapshot?.error || snapshot?.result?.error || null;
|
|
364
|
-
const classification = classifyRecommendRecovery(error);
|
|
365
|
-
if (!classification) return null;
|
|
366
|
-
const canRestartSameRequest = classification !== "login_required";
|
|
367
|
-
return {
|
|
368
|
-
policy_version: 1,
|
|
369
|
-
classification,
|
|
370
|
-
safe_for_outer_ai_agent: true,
|
|
371
|
-
recommended_action: canRestartSameRequest
|
|
372
|
-
? "restart_same_recommend_request_only"
|
|
373
|
-
: "ask_user_to_login_then_retry_same_recommend_request",
|
|
374
|
-
package_requirement: "@reconcrap/boss-recommend-mcp@>=2.0.30",
|
|
375
|
-
run_id: snapshot?.runId || snapshot?.run_id || null,
|
|
376
|
-
retryable: true,
|
|
377
|
-
same_request_sources: {
|
|
378
|
-
instruction: "run.context.instruction",
|
|
379
|
-
confirmation: "run.context.confirmation",
|
|
380
|
-
overrides: "run.context.overrides",
|
|
381
|
-
follow_up: "run.context.follow_up"
|
|
382
|
-
},
|
|
383
|
-
constraints: [
|
|
384
|
-
"Do not change instruction, criteria, filters, job, page_scope, target_count, post_action, or max_greet_count.",
|
|
385
|
-
"Do not switch to search/recruit/chat and do not add follow_up.chat.",
|
|
386
|
-
"Do not summarize, translate, or rewrite criteria.",
|
|
387
|
-
"Do not ask the user to reconfirm business choices unless Boss login is required or the stored context is missing.",
|
|
388
|
-
"Use the same Chrome debug port and recommend page route."
|
|
389
|
-
],
|
|
390
|
-
artifacts: artifacts ? {
|
|
391
|
-
run_state_path: artifacts.run_state_path || null,
|
|
392
|
-
checkpoint_path: artifacts.checkpoint_path || null,
|
|
393
|
-
report_json: artifacts.report_json || null,
|
|
394
|
-
output_csv: artifacts.output_csv || null
|
|
395
|
-
} : null
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function ensureRecommendRunArtifacts(snapshot) {
|
|
400
|
-
const artifacts = getRecommendRunArtifacts(snapshot?.runId || snapshot?.run_id);
|
|
401
|
-
if (!artifacts) return null;
|
|
402
|
-
|
|
403
|
-
const meta = getRecommendRunMeta(snapshot?.runId || snapshot?.run_id);
|
|
404
|
-
const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
|
|
405
|
-
? snapshot.checkpoint
|
|
406
|
-
: {};
|
|
407
|
-
writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
|
|
408
|
-
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
409
|
-
|
|
410
|
-
const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
411
|
-
const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
|
|
412
|
-
const artifactSummary = summary || (checkpointResults.length ? {
|
|
413
|
-
domain: "recommend",
|
|
414
|
-
partial: true,
|
|
415
|
-
partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
|
|
416
|
-
results: checkpointResults
|
|
417
|
-
} : null);
|
|
418
|
-
if (artifactSummary) {
|
|
419
|
-
const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
|
|
420
|
-
writeRecommendLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
|
|
421
|
-
writeJsonAtomic(artifacts.report_json, {
|
|
422
|
-
run_id: snapshot.runId || snapshot.run_id,
|
|
423
|
-
status: snapshot.status || snapshot.state,
|
|
424
|
-
phase: snapshot.phase || snapshot.stage,
|
|
425
|
-
progress: snapshot.progress || {},
|
|
426
|
-
context: snapshot.context || {},
|
|
427
|
-
checkpoint,
|
|
428
|
-
error: snapshot.error || null,
|
|
429
|
-
last_message: snapshot.error?.message || snapshot.phase || snapshot.stage || null,
|
|
430
|
-
recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
|
|
431
|
-
summary: artifactSummary,
|
|
432
|
-
generated_at: new Date().toISOString()
|
|
433
|
-
});
|
|
434
|
-
if (meta) {
|
|
435
|
-
meta.outputCsvPath = artifacts.output_csv;
|
|
436
|
-
meta.reportJsonPath = artifacts.report_json;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return artifacts;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function persistRecommendCheckpointSnapshot(normalized) {
|
|
444
|
-
const artifacts = getRecommendRunArtifacts(normalized?.run_id || normalized?.runId);
|
|
445
|
-
if (!artifacts) return;
|
|
446
|
-
const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
|
|
447
|
-
? normalized.checkpoint
|
|
448
|
-
: {};
|
|
449
|
-
writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
|
|
450
|
-
const meta = getRecommendRunMeta(normalized?.run_id || normalized?.runId);
|
|
451
|
-
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function buildLegacyRecommendResult(snapshot) {
|
|
455
|
-
if (!snapshot) return null;
|
|
456
|
-
const artifacts = ensureRecommendRunArtifacts(snapshot);
|
|
457
|
-
const meta = getRecommendRunMeta(snapshot.runId);
|
|
458
|
-
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
459
|
-
const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
|
|
460
|
-
const resultRows = Array.isArray(summary?.results)
|
|
461
|
-
? summary.results
|
|
462
|
-
: Array.isArray(checkpoint.results)
|
|
463
|
-
? checkpoint.results
|
|
464
|
-
: [];
|
|
465
|
-
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
466
|
-
const targetCount = Number.isInteger(progress.target_count)
|
|
467
|
-
? progress.target_count
|
|
468
|
-
: Number.isInteger(snapshot.context?.max_candidates)
|
|
469
|
-
? snapshot.context.max_candidates
|
|
470
|
-
: meta.parsed?.screenParams?.target_count || null;
|
|
471
|
-
return {
|
|
472
|
-
status: snapshot.status === RUN_STATUS_COMPLETED
|
|
473
|
-
? "COMPLETED"
|
|
474
|
-
: snapshot.status === RUN_STATUS_CANCELED
|
|
475
|
-
? "CANCELED"
|
|
476
|
-
: snapshot.status === RUN_STATUS_PAUSED
|
|
477
|
-
? "PAUSED"
|
|
478
|
-
: snapshot.status === RUN_STATUS_FAILED
|
|
479
|
-
? "FAILED"
|
|
480
|
-
: snapshot.status,
|
|
481
|
-
run_id: snapshot.runId,
|
|
482
|
-
completion_reason: completionReason(snapshot.status),
|
|
483
|
-
requested_count: targetCount,
|
|
484
|
-
processed_count: progress.processed,
|
|
485
|
-
inspected_count: progress.processed,
|
|
486
|
-
screened_count: progress.screened,
|
|
487
|
-
passed_count: progress.passed,
|
|
488
|
-
skipped_count: progress.skipped,
|
|
489
|
-
detail_opened: progress.detail_opened || summary?.detail_opened || 0,
|
|
490
|
-
greet_count: progress.greet_count || 0,
|
|
491
|
-
post_action_clicked: progress.post_action_clicked || summary?.post_action_clicked || 0,
|
|
492
|
-
output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
|
|
493
|
-
report_json: artifacts?.report_json || meta.reportJsonPath || null,
|
|
494
|
-
checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
|
|
495
|
-
started_at: snapshot.startedAt,
|
|
496
|
-
completed_at: snapshot.completedAt || null,
|
|
497
|
-
duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
|
|
498
|
-
selected_job: {
|
|
499
|
-
title: meta.normalized?.job || meta.args?.confirmation?.job_value || meta.args?.overrides?.job || ""
|
|
500
|
-
},
|
|
501
|
-
selected_page_scope: summary?.page_scope || {
|
|
502
|
-
requested_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend",
|
|
503
|
-
effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
|
|
504
|
-
},
|
|
505
|
-
search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
|
|
506
|
-
screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
|
|
507
|
-
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
508
|
-
error: snapshot.error || null,
|
|
509
|
-
recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
|
|
510
|
-
results: resultRows
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function normalizeRunSnapshot(snapshot) {
|
|
515
|
-
if (!snapshot) return null;
|
|
516
|
-
const meta = getRecommendRunMeta(snapshot.runId);
|
|
517
|
-
const artifacts = getRecommendRunArtifacts(snapshot.runId);
|
|
518
|
-
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
519
|
-
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
520
|
-
const legacyResult = (
|
|
521
|
-
TERMINAL_STATUSES.has(snapshot.status)
|
|
522
|
-
|| snapshot.status === RUN_STATUS_PAUSED
|
|
523
|
-
) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
|
|
524
|
-
const recovery = buildConstrainedAgentRecovery(snapshot, meta, artifacts);
|
|
525
|
-
const snapshotContext = plainRecord(snapshot.context);
|
|
526
|
-
const metaArgs = plainRecord(meta.args);
|
|
527
|
-
const oldContext = {
|
|
528
|
-
workspace_root: meta.workspaceRoot || snapshotContext.workspace_root || null,
|
|
529
|
-
instruction: metaArgs.instruction || snapshotContext.instruction || "",
|
|
530
|
-
confirmation: clonePlain(metaArgs.confirmation ?? snapshotContext.confirmation ?? {}, {}),
|
|
531
|
-
overrides: clonePlain(metaArgs.overrides ?? snapshotContext.overrides ?? {}, {}),
|
|
532
|
-
follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
|
|
533
|
-
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
534
|
-
};
|
|
535
|
-
return {
|
|
536
|
-
...snapshot,
|
|
537
|
-
progress,
|
|
538
|
-
run_id: snapshot.runId,
|
|
539
|
-
mode: RUN_MODE_ASYNC,
|
|
540
|
-
state: snapshot.status,
|
|
541
|
-
stage: snapshot.phase,
|
|
542
|
-
started_at: snapshot.startedAt,
|
|
543
|
-
updated_at: snapshot.updatedAt,
|
|
544
|
-
completed_at: toIsoOrNull(snapshot.completedAt),
|
|
545
|
-
heartbeat_at: snapshot.updatedAt,
|
|
546
|
-
pid: Number.isInteger(snapshot.pid) && snapshot.pid > 0 ? snapshot.pid : process.pid || null,
|
|
547
|
-
last_message: snapshot.error?.message || snapshot.phase || null,
|
|
548
|
-
context: {
|
|
549
|
-
...snapshotContext,
|
|
550
|
-
...oldContext,
|
|
551
|
-
shared_run_context: snapshotContext
|
|
552
|
-
},
|
|
553
|
-
control: {
|
|
554
|
-
pause_requested: snapshot.status === RUN_STATUS_PAUSED,
|
|
555
|
-
pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
|
|
556
|
-
pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_recommend_pipeline_run" : null,
|
|
557
|
-
cancel_requested: snapshot.status === RUN_STATUS_CANCELING
|
|
558
|
-
},
|
|
559
|
-
resume: {
|
|
560
|
-
checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
|
|
561
|
-
pause_control_path: artifacts?.run_state_path || null,
|
|
562
|
-
output_csv: legacyResult?.output_csv || null,
|
|
563
|
-
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
564
|
-
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
565
|
-
resume_count: meta.resumeCount || 0,
|
|
566
|
-
last_resumed_at: meta.lastResumedAt || null,
|
|
567
|
-
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
568
|
-
},
|
|
569
|
-
recovery,
|
|
570
|
-
result: legacyResult,
|
|
571
|
-
artifacts
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function mergePersistedControlRequest(normalized, existing) {
|
|
576
|
-
const control = {
|
|
577
|
-
...(normalized?.control || {})
|
|
578
|
-
};
|
|
579
|
-
if (!normalized || TERMINAL_STATUSES.has(normalized.state)) return control;
|
|
580
|
-
const existingControl = plainRecord(existing?.control);
|
|
581
|
-
if (existingControl.cancel_requested === true) {
|
|
582
|
-
return {
|
|
583
|
-
...control,
|
|
584
|
-
pause_requested: true,
|
|
585
|
-
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
586
|
-
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
|
|
587
|
-
cancel_requested: true
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
|
|
591
|
-
return {
|
|
592
|
-
...control,
|
|
593
|
-
pause_requested: true,
|
|
594
|
-
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
595
|
-
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_recommend_pipeline_run"
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
|
|
599
|
-
return {
|
|
600
|
-
...control,
|
|
601
|
-
pause_requested: false,
|
|
602
|
-
pause_requested_at: null,
|
|
603
|
-
pause_requested_by: null,
|
|
604
|
-
cancel_requested: false
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
return control;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function persistRecommendRunSnapshot(snapshot, {
|
|
611
|
-
persistActiveCheckpoint = false
|
|
612
|
-
} = {}) {
|
|
613
|
-
const normalized = normalizeRunSnapshot(snapshot);
|
|
614
|
-
if (!normalized?.run_id) return normalized;
|
|
615
|
-
const artifacts = getRecommendRunArtifacts(normalized.run_id);
|
|
616
|
-
if (!artifacts) return normalized;
|
|
617
|
-
const existing = readJsonFile(artifacts.run_state_path);
|
|
618
|
-
normalized.control = mergePersistedControlRequest(normalized, existing);
|
|
619
|
-
if (persistActiveCheckpoint) {
|
|
620
|
-
persistRecommendCheckpointSnapshot(normalized);
|
|
621
|
-
}
|
|
622
|
-
const payload = {
|
|
623
|
-
run_id: normalized.run_id,
|
|
624
|
-
mode: normalized.mode,
|
|
625
|
-
state: normalized.state,
|
|
626
|
-
status: normalized.status,
|
|
627
|
-
stage: normalized.stage,
|
|
628
|
-
started_at: normalized.started_at,
|
|
629
|
-
updated_at: normalized.updated_at,
|
|
630
|
-
heartbeat_at: normalized.heartbeat_at,
|
|
631
|
-
completed_at: normalized.completed_at,
|
|
632
|
-
pid: normalized.pid,
|
|
633
|
-
progress: normalized.progress,
|
|
634
|
-
last_message: normalized.last_message,
|
|
635
|
-
context: normalized.context,
|
|
636
|
-
control: normalized.control,
|
|
637
|
-
resume: normalized.resume,
|
|
638
|
-
error: normalized.error,
|
|
639
|
-
recovery: normalized.recovery,
|
|
640
|
-
result: normalized.result,
|
|
641
|
-
summary: normalized.summary,
|
|
642
|
-
artifacts: normalized.artifacts
|
|
643
|
-
};
|
|
644
|
-
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
645
|
-
return normalized;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function reconcilePersistedRecommendRunIfNeeded(persisted) {
|
|
649
|
-
if (!persisted || typeof persisted !== "object") return persisted;
|
|
650
|
-
const persistedState = normalizeText(persisted.state || persisted.status);
|
|
651
|
-
if (TERMINAL_STATUSES.has(persistedState)) return persisted;
|
|
652
|
-
if (isProcessAlive(persisted.pid)) return persisted;
|
|
653
|
-
|
|
654
|
-
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
655
|
-
const artifacts = getRecommendRunArtifacts(runId);
|
|
656
|
-
const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
|
|
657
|
-
const now = new Date().toISOString();
|
|
658
|
-
const error = {
|
|
659
|
-
code: "RUN_PROCESS_EXITED",
|
|
660
|
-
message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`,
|
|
661
|
-
retryable: true
|
|
662
|
-
};
|
|
663
|
-
return persistRecommendRunSnapshot({
|
|
664
|
-
runId,
|
|
665
|
-
name: persisted.name || runId,
|
|
666
|
-
status: RUN_STATUS_FAILED,
|
|
667
|
-
phase: persisted.stage || persisted.phase || "recommend:orphaned",
|
|
668
|
-
progress: persisted.progress || {},
|
|
669
|
-
context: persisted.context || {},
|
|
670
|
-
checkpoint: checkpoint || persisted.checkpoint || {},
|
|
671
|
-
startedAt: persisted.started_at || persisted.startedAt || now,
|
|
672
|
-
updatedAt: now,
|
|
673
|
-
completedAt: now,
|
|
674
|
-
pid: Number.isInteger(persisted.pid) && persisted.pid > 0 ? persisted.pid : null,
|
|
675
|
-
error,
|
|
676
|
-
summary: persisted.summary || null
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
|
|
681
|
-
return persistRecommendRunSnapshot(snapshot, {
|
|
682
|
-
persistActiveCheckpoint: event?.type === "checkpoint"
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function attachMethodEvidence(payload, runId) {
|
|
687
|
-
const meta = getRecommendRunMeta(runId);
|
|
688
|
-
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
689
|
-
return {
|
|
690
|
-
...payload,
|
|
691
|
-
runtime_evaluate_used: false,
|
|
692
|
-
method_summary: methodSummary(meta.methodLog || []),
|
|
693
|
-
method_log: meta.methodLog || [],
|
|
694
|
-
chrome: meta.chrome || null
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function compactRecommendJobListOption(option, index) {
|
|
699
|
-
const label = normalizeText(option?.label);
|
|
700
|
-
const name = normalizeText(option?.label_without_salary || label);
|
|
701
|
-
return {
|
|
702
|
-
index,
|
|
703
|
-
name,
|
|
704
|
-
label,
|
|
705
|
-
label_without_salary: name,
|
|
706
|
-
current: Boolean(option?.current),
|
|
707
|
-
visible: Boolean(option?.visible)
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
async function readRecommendJobOptionsFromSession(session) {
|
|
712
|
-
const client = session?.client;
|
|
713
|
-
if (!client) throw new Error("Recommend Chrome session is missing a CDP client");
|
|
714
|
-
const rootState = await getRecommendRoots(client);
|
|
715
|
-
const frameNodeId = rootState?.iframe?.documentNodeId;
|
|
716
|
-
if (!frameNodeId) throw new Error("recommendFrame iframe document was not found");
|
|
717
|
-
|
|
718
|
-
let options = [];
|
|
719
|
-
try {
|
|
720
|
-
options = await listRecommendJobOptions(client, frameNodeId, {
|
|
721
|
-
openDropdown: true
|
|
722
|
-
});
|
|
723
|
-
} finally {
|
|
724
|
-
await closeRecommendJobDropdown(client).catch(() => {});
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const compacted = [];
|
|
728
|
-
const seen = new Set();
|
|
729
|
-
for (const option of options) {
|
|
730
|
-
const compact = compactRecommendJobListOption(option, compacted.length);
|
|
731
|
-
if (!compact.name && !compact.label) continue;
|
|
732
|
-
const key = `${compact.name}\n${compact.label}`;
|
|
733
|
-
if (seen.has(key)) continue;
|
|
734
|
-
seen.add(key);
|
|
735
|
-
compacted.push(compact);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return {
|
|
739
|
-
source: "recommend_job_dropdown",
|
|
740
|
-
selector: "recommend job selection dropdown",
|
|
741
|
-
job_options: compacted,
|
|
742
|
-
selected_job: compacted.find((option) => option.current) || null
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
|
|
747
|
-
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
748
|
-
const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
|
|
749
|
-
const port = parsePositiveInteger(
|
|
750
|
-
args.port,
|
|
751
|
-
configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
|
|
752
|
-
);
|
|
753
|
-
const targetUrlIncludes = normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL;
|
|
754
|
-
const allowNavigate = args.allow_navigate !== false;
|
|
755
|
-
const slowLive = args.slow_live === true;
|
|
756
|
-
let session;
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
session = await recommendConnectorImpl({
|
|
760
|
-
host,
|
|
761
|
-
port,
|
|
762
|
-
targetUrlIncludes,
|
|
763
|
-
allowNavigate,
|
|
764
|
-
slowLive
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
const jobs = await recommendJobReaderImpl(session, {
|
|
768
|
-
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
769
|
-
args: clonePlain(args, {}),
|
|
770
|
-
normalized: {
|
|
771
|
-
host,
|
|
772
|
-
port,
|
|
773
|
-
targetUrlIncludes,
|
|
774
|
-
allowNavigate,
|
|
775
|
-
slowLive
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
|
|
779
|
-
assertNoForbiddenCdpCalls(session.methodLog || []);
|
|
780
|
-
return {
|
|
781
|
-
status: "OK",
|
|
782
|
-
stage: "recommend_job_list",
|
|
783
|
-
cdp_only: true,
|
|
784
|
-
runtime_evaluate_used: false,
|
|
785
|
-
page_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
786
|
-
job_count: jobOptions.length,
|
|
787
|
-
job_names: jobOptions.map((option) => option.name || option.label).filter(Boolean),
|
|
788
|
-
job_full_labels: jobOptions.map((option) => option.label || option.name).filter(Boolean),
|
|
789
|
-
job_options: jobOptions,
|
|
790
|
-
selected_job: jobs?.selected_job || jobOptions.find((option) => option.current) || null,
|
|
791
|
-
source: jobs?.source || "recommend_job_dropdown",
|
|
792
|
-
selector: jobs?.selector || "",
|
|
793
|
-
message: "已通过 CDP-only 从推荐页岗位下拉框读取可用岗位。Cron/一次性任务里的 job 参数优先使用 job_names 中的完整岗位名。",
|
|
794
|
-
chrome: {
|
|
795
|
-
host,
|
|
796
|
-
port,
|
|
797
|
-
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
798
|
-
target_id: session.target?.id || null,
|
|
799
|
-
auto_launch: session.chrome || null
|
|
800
|
-
},
|
|
801
|
-
method_summary: methodSummary(session.methodLog || []),
|
|
802
|
-
method_log: session.methodLog || []
|
|
803
|
-
};
|
|
804
|
-
} catch (error) {
|
|
805
|
-
const methodLog = session?.methodLog || [];
|
|
806
|
-
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
807
|
-
return {
|
|
808
|
-
status: "FAILED",
|
|
809
|
-
stage: "recommend_job_list",
|
|
810
|
-
cdp_only: true,
|
|
811
|
-
runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
|
|
812
|
-
error: {
|
|
813
|
-
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "RECOMMEND_JOB_LIST_FAILED",
|
|
814
|
-
message: error?.message || "Failed to read recommend job list",
|
|
815
|
-
requires_login: Boolean(error?.requires_login),
|
|
816
|
-
login_url: error?.login_url || null,
|
|
817
|
-
login_detection: error?.login_detection || null,
|
|
818
|
-
current_url: error?.current_url || null,
|
|
819
|
-
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
820
|
-
chrome: error?.chrome || null,
|
|
821
|
-
retryable: true
|
|
822
|
-
},
|
|
823
|
-
chrome: {
|
|
824
|
-
host,
|
|
825
|
-
port,
|
|
826
|
-
target_url: targetUrlIncludes,
|
|
827
|
-
auto_launch: error?.chrome || session?.chrome || null
|
|
828
|
-
},
|
|
829
|
-
method_summary: methodSummary(methodLog),
|
|
830
|
-
method_log: methodLog
|
|
831
|
-
};
|
|
832
|
-
} finally {
|
|
833
|
-
if (session) {
|
|
834
|
-
try {
|
|
835
|
-
await session.close?.();
|
|
836
|
-
} catch {
|
|
837
|
-
// Best-effort cleanup after a read-only helper.
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function compactHealth(check) {
|
|
844
|
-
if (!check) return null;
|
|
845
|
-
return {
|
|
846
|
-
status: check.status,
|
|
847
|
-
summary: check.summary,
|
|
848
|
-
drift_report: check.drift_report,
|
|
849
|
-
probes: (check.probes || []).map((probe) => ({
|
|
850
|
-
id: probe.id,
|
|
851
|
-
type: probe.type,
|
|
852
|
-
status: probe.status,
|
|
853
|
-
count: probe.count,
|
|
854
|
-
required: probe.required
|
|
855
|
-
}))
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
async function waitForHealthyRecommend(client, config, {
|
|
860
|
-
timeoutMs = 90000,
|
|
861
|
-
intervalMs = 1000
|
|
862
|
-
} = {}) {
|
|
863
|
-
const started = Date.now();
|
|
864
|
-
let lastCheck = null;
|
|
865
|
-
while (Date.now() - started <= timeoutMs) {
|
|
866
|
-
const loginDetection = await detectBossLoginState(client).catch(() => null);
|
|
867
|
-
if (loginDetection?.requires_login) {
|
|
868
|
-
return {
|
|
869
|
-
status: "login_required",
|
|
870
|
-
summary: "Boss login is required",
|
|
871
|
-
loginDetection
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
const roots = await resolveRecommendSelfHealRoots(client, config);
|
|
875
|
-
lastCheck = await runSelfHealCheck({
|
|
876
|
-
client,
|
|
877
|
-
domain: "recommend",
|
|
878
|
-
roots: roots.roots,
|
|
879
|
-
selectorProbes: config.selectorProbes,
|
|
880
|
-
accessibilityProbes: config.accessibilityProbes,
|
|
881
|
-
viewportProbes: config.viewportProbes
|
|
882
|
-
});
|
|
883
|
-
if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
|
|
884
|
-
await sleep(intervalMs);
|
|
885
|
-
}
|
|
886
|
-
return lastCheck;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function shouldNavigateToRecommend(url) {
|
|
890
|
-
return !String(url || "").includes("/web/chat/recommend");
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
async function connectRecommendChromeSession({
|
|
894
|
-
host = DEFAULT_RECOMMEND_HOST,
|
|
895
|
-
port = DEFAULT_RECOMMEND_PORT,
|
|
896
|
-
targetUrlIncludes = RECOMMEND_TARGET_URL,
|
|
897
|
-
allowNavigate = true,
|
|
898
|
-
slowLive = false
|
|
899
|
-
} = {}) {
|
|
900
|
-
const session = await connectToChromeTargetOrOpen({
|
|
901
|
-
host,
|
|
902
|
-
port,
|
|
903
|
-
targetUrlIncludes,
|
|
904
|
-
targetUrl: RECOMMEND_TARGET_URL,
|
|
905
|
-
allowNavigate,
|
|
906
|
-
slowLive,
|
|
907
|
-
fallbackTargetPredicate: (target) => (
|
|
908
|
-
target?.type === "page"
|
|
909
|
-
&& String(target?.url || "").includes("zhipin.com")
|
|
910
|
-
)
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
const { client, target } = session;
|
|
914
|
-
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
915
|
-
if (typeof client?.Network?.setCacheDisabled === "function") {
|
|
916
|
-
await client.Network.setCacheDisabled({ cacheDisabled: true });
|
|
917
|
-
}
|
|
918
|
-
await bringPageToFront(client);
|
|
919
|
-
|
|
920
|
-
const targetUrl = String(target?.url || "");
|
|
921
|
-
let navigation = {
|
|
922
|
-
navigated: false,
|
|
923
|
-
url: targetUrl
|
|
924
|
-
};
|
|
925
|
-
if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
|
|
926
|
-
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
927
|
-
const settleMs = slowLive ? 12000 : 5000;
|
|
928
|
-
const waited = await waitForMainFrameUrl(
|
|
929
|
-
client,
|
|
930
|
-
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
931
|
-
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
932
|
-
);
|
|
933
|
-
navigation = {
|
|
934
|
-
navigated: true,
|
|
935
|
-
url: RECOMMEND_TARGET_URL,
|
|
936
|
-
settle_ms: settleMs,
|
|
937
|
-
observed_url: waited.url || null,
|
|
938
|
-
observed_url_ok: waited.ok
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
|
|
942
|
-
if (allowNavigate && shouldNavigateToRecommend(currentUrl) && !isBossLoginUrl(currentUrl)) {
|
|
943
|
-
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
944
|
-
const settleMs = slowLive ? 12000 : 5000;
|
|
945
|
-
const waited = await waitForMainFrameUrl(
|
|
946
|
-
client,
|
|
947
|
-
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
948
|
-
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
949
|
-
);
|
|
950
|
-
navigation = {
|
|
951
|
-
navigated: true,
|
|
952
|
-
url: RECOMMEND_TARGET_URL,
|
|
953
|
-
settle_ms: settleMs,
|
|
954
|
-
observed_url: waited.url || null,
|
|
955
|
-
observed_url_ok: waited.ok,
|
|
956
|
-
reason: "observed_url_mismatch"
|
|
957
|
-
};
|
|
958
|
-
currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
|
|
959
|
-
}
|
|
960
|
-
const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
|
|
961
|
-
requires_login: isBossLoginUrl(currentUrl),
|
|
962
|
-
reason: "login_detection_failed",
|
|
963
|
-
current_url: currentUrl
|
|
964
|
-
}));
|
|
965
|
-
if (loginDetection.requires_login) {
|
|
966
|
-
await session.close?.();
|
|
967
|
-
throw createBossLoginRequiredError({
|
|
968
|
-
domain: "recommend",
|
|
969
|
-
currentUrl: loginDetection.current_url || currentUrl,
|
|
970
|
-
targetUrl: RECOMMEND_TARGET_URL,
|
|
971
|
-
loginDetection,
|
|
972
|
-
chrome: session.chrome || null
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
if (shouldNavigateToRecommend(currentUrl)) {
|
|
976
|
-
await session.close?.();
|
|
977
|
-
throw new Error(`Boss recommend page did not navigate to ${RECOMMEND_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
const selfHealConfig = buildRecommendSelfHealConfig();
|
|
981
|
-
const health = await waitForHealthyRecommend(client, selfHealConfig, {
|
|
982
|
-
timeoutMs: slowLive ? 180000 : 90000,
|
|
983
|
-
intervalMs: slowLive ? 1200 : 800
|
|
984
|
-
});
|
|
985
|
-
if (health?.loginDetection?.requires_login) {
|
|
986
|
-
await session.close?.();
|
|
987
|
-
throw createBossLoginRequiredError({
|
|
988
|
-
domain: "recommend",
|
|
989
|
-
currentUrl: health.loginDetection.current_url || currentUrl,
|
|
990
|
-
targetUrl: RECOMMEND_TARGET_URL,
|
|
991
|
-
loginDetection: health.loginDetection,
|
|
992
|
-
chrome: session.chrome || null
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
|
|
996
|
-
const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
|
|
997
|
-
const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
|
|
998
|
-
requires_login: isBossLoginUrl(latestUrl),
|
|
999
|
-
reason: "login_detection_failed",
|
|
1000
|
-
current_url: latestUrl
|
|
1001
|
-
}));
|
|
1002
|
-
if (latestLoginDetection.requires_login) {
|
|
1003
|
-
await session.close?.();
|
|
1004
|
-
throw createBossLoginRequiredError({
|
|
1005
|
-
domain: "recommend",
|
|
1006
|
-
currentUrl: latestLoginDetection.current_url || latestUrl,
|
|
1007
|
-
targetUrl: RECOMMEND_TARGET_URL,
|
|
1008
|
-
loginDetection: latestLoginDetection,
|
|
1009
|
-
chrome: session.chrome || null
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return {
|
|
1016
|
-
...session,
|
|
1017
|
-
navigation,
|
|
1018
|
-
health
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function parseRecommendPipelineRequest(args = {}) {
|
|
1023
|
-
return parseRecommendInstruction({
|
|
1024
|
-
instruction: args.instruction,
|
|
1025
|
-
confirmation: args.confirmation,
|
|
1026
|
-
overrides: args.overrides
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
function buildRequiredConfirmations(parsed, args = {}) {
|
|
1031
|
-
const required = [];
|
|
1032
|
-
if (parsed.needs_page_confirmation) required.push("page_scope");
|
|
1033
|
-
if (parsed.needs_filters_confirmation) required.push("filters");
|
|
1034
|
-
if (parsed.needs_school_tag_confirmation) required.push("school_tag");
|
|
1035
|
-
if (parsed.needs_degree_confirmation) required.push("degree");
|
|
1036
|
-
if (parsed.needs_gender_confirmation) required.push("gender");
|
|
1037
|
-
if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
|
|
1038
|
-
if (parsed.needs_criteria_confirmation) required.push("criteria");
|
|
1039
|
-
if (parsed.needs_target_count_confirmation) required.push("target_count");
|
|
1040
|
-
if (parsed.needs_post_action_confirmation) required.push("post_action");
|
|
1041
|
-
if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
|
|
1042
|
-
if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
|
|
1043
|
-
|
|
1044
|
-
const confirmation = args.confirmation || {};
|
|
1045
|
-
const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
|
|
1046
|
-
if (confirmation.job_confirmed !== true || !jobValue) required.push("job");
|
|
1047
|
-
if (confirmation.final_confirmed !== true) required.push("final_review");
|
|
1048
|
-
return Array.from(new Set(required));
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function buildJobPendingQuestion(args = {}) {
|
|
1052
|
-
const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
|
|
1053
|
-
return {
|
|
1054
|
-
field: "job",
|
|
1055
|
-
question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
|
|
1056
|
-
value: value || null
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function buildFinalReviewQuestion(parsed) {
|
|
1061
|
-
return {
|
|
1062
|
-
field: "final_review",
|
|
1063
|
-
question: "请最终确认本次推荐页筛选参数无误,并明确 final_confirmed=true 后再启动。",
|
|
1064
|
-
value: {
|
|
1065
|
-
page_scope: parsed.page_scope,
|
|
1066
|
-
search_params: parsed.searchParams,
|
|
1067
|
-
screen_params: parsed.screenParams
|
|
1068
|
-
}
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function buildNeedInputResponse(parsed) {
|
|
1073
|
-
return {
|
|
1074
|
-
status: "NEED_INPUT",
|
|
1075
|
-
missing_fields: parsed.missing_fields,
|
|
1076
|
-
required_confirmations: buildRequiredConfirmations(parsed),
|
|
1077
|
-
search_params: parsed.searchParams,
|
|
1078
|
-
screen_params: parsed.screenParams,
|
|
1079
|
-
pending_questions: parsed.pending_questions,
|
|
1080
|
-
review: parsed.review,
|
|
1081
|
-
error: {
|
|
1082
|
-
code: "MISSING_REQUIRED_FIELDS",
|
|
1083
|
-
message: "缺少必要字段。请补齐推荐页 criteria 等必填字段后再启动 CDP-only recommend run。",
|
|
1084
|
-
retryable: true
|
|
1085
|
-
}
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
|
|
1090
|
-
const pending = [...(parsed.pending_questions || [])];
|
|
1091
|
-
if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
|
|
1092
|
-
pending.push(buildJobPendingQuestion(args));
|
|
1093
|
-
}
|
|
1094
|
-
if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
|
|
1095
|
-
pending.push(buildFinalReviewQuestion(parsed));
|
|
1096
|
-
}
|
|
1097
|
-
return {
|
|
1098
|
-
status: "NEED_CONFIRMATION",
|
|
1099
|
-
required_confirmations: requiredConfirmations,
|
|
1100
|
-
page_scope: parsed.page_scope,
|
|
1101
|
-
search_params: parsed.searchParams,
|
|
1102
|
-
screen_params: parsed.screenParams,
|
|
1103
|
-
pending_questions: pending,
|
|
1104
|
-
review: {
|
|
1105
|
-
...(parsed.review || {}),
|
|
1106
|
-
required_confirmations: requiredConfirmations
|
|
1107
|
-
}
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function evaluateRecommendPipelineGate(parsed, args = {}) {
|
|
1112
|
-
if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed);
|
|
1113
|
-
const requiredConfirmations = buildRequiredConfirmations(parsed, args);
|
|
1114
|
-
if (requiredConfirmations.length) {
|
|
1115
|
-
return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
if (args.follow_up?.chat || args.overrides?.follow_up?.chat) {
|
|
1119
|
-
return {
|
|
1120
|
-
status: "FAILED",
|
|
1121
|
-
error: {
|
|
1122
|
-
code: "FOLLOW_UP_CHAT_NOT_CDP_REWRITTEN",
|
|
1123
|
-
message: "recommend -> chat follow-up orchestration is legacy-only and intentionally fenced from the CDP-only MCP route. Run recommend first, then use the direct chat MCP route separately, or keep the old chained behavior in the archived legacy lane.",
|
|
1124
|
-
retryable: true
|
|
1125
|
-
},
|
|
1126
|
-
review: parsed.review
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
return null;
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
function toArray(value) {
|
|
1134
|
-
if (Array.isArray(value)) return value;
|
|
1135
|
-
if (value === undefined || value === null) return [];
|
|
1136
|
-
return [value];
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
function withoutUnlimited(values = []) {
|
|
1140
|
-
return toArray(values)
|
|
1141
|
-
.map((value) => normalizeText(value))
|
|
1142
|
-
.filter((value) => value && value !== "不限" && value.toLowerCase() !== "all" && value !== "全部");
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function buildRecommendFilter(parsed, args = {}) {
|
|
1146
|
-
if (args.no_filter === true || args.filter_enabled === false) {
|
|
1147
|
-
return { enabled: false };
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const groups = [];
|
|
1151
|
-
const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
|
|
1152
|
-
if (recentNotView.length) {
|
|
1153
|
-
groups.push({
|
|
1154
|
-
group: "recentNotView",
|
|
1155
|
-
labels: recentNotView,
|
|
1156
|
-
selectAllLabels: true
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
const degree = withoutUnlimited(parsed.searchParams?.degree);
|
|
1161
|
-
if (degree.length) {
|
|
1162
|
-
groups.push({
|
|
1163
|
-
group: "degree",
|
|
1164
|
-
labels: degree,
|
|
1165
|
-
selectAllLabels: true
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const gender = withoutUnlimited(parsed.searchParams?.gender);
|
|
1170
|
-
if (gender.length) {
|
|
1171
|
-
groups.push({
|
|
1172
|
-
group: "gender",
|
|
1173
|
-
labels: gender,
|
|
1174
|
-
selectAllLabels: true
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const school = withoutUnlimited(parsed.searchParams?.school_tag);
|
|
1179
|
-
if (school.length) {
|
|
1180
|
-
groups.push({
|
|
1181
|
-
group: "school",
|
|
1182
|
-
labels: school,
|
|
1183
|
-
selectAllLabels: true
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
return groups.length ? { filterGroups: groups } : { enabled: false };
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
|
|
1191
|
-
const confirmation = args.confirmation || {};
|
|
1192
|
-
const overrides = args.overrides || {};
|
|
1193
|
-
const slowLive = args.slow_live === true;
|
|
1194
|
-
const targetCount = parsePositiveInteger(
|
|
1195
|
-
args.max_candidates,
|
|
1196
|
-
parsed.screenParams?.target_count || parsePositiveInteger(confirmation.target_count_value, 5)
|
|
1197
|
-
);
|
|
1198
|
-
return {
|
|
1199
|
-
host: normalizeText(args.host) || DEFAULT_RECOMMEND_HOST,
|
|
1200
|
-
port: parsePositiveInteger(
|
|
1201
|
-
args.port,
|
|
1202
|
-
configResolution?.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
|
|
1203
|
-
),
|
|
1204
|
-
targetUrlIncludes: normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL,
|
|
1205
|
-
allowNavigate: args.allow_navigate !== false,
|
|
1206
|
-
slowLive,
|
|
1207
|
-
criteria: parsed.screenParams?.criteria || normalizeText(overrides.criteria),
|
|
1208
|
-
targetCount,
|
|
1209
|
-
job: normalizeText(confirmation.job_value || overrides.job || ""),
|
|
1210
|
-
pageScope: parsed.page_scope || "recommend",
|
|
1211
|
-
filter: buildRecommendFilter(parsed, args),
|
|
1212
|
-
postAction: parsed.screenParams?.post_action || "none",
|
|
1213
|
-
maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
|
|
1214
|
-
? parsed.screenParams.max_greet_count
|
|
1215
|
-
: null,
|
|
1216
|
-
screeningMode: normalizeScreeningModeArg(args)
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
|
|
52
|
+
import { DEFAULT_MAX_IMAGE_PAGES } from "./core/cv-acquisition/index.js";
|
|
53
|
+
|
|
54
|
+
const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
|
|
55
|
+
const DEFAULT_RECOMMEND_PORT = 9222;
|
|
56
|
+
const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
|
|
57
|
+
const TARGET_COUNT_SEMANTICS = "target_count means candidates that pass screening; scan continues until that many candidates pass or the list ends";
|
|
58
|
+
const RUN_MODE_ASYNC = "async";
|
|
59
|
+
|
|
60
|
+
const TERMINAL_STATUSES = new Set([
|
|
61
|
+
RUN_STATUS_COMPLETED,
|
|
62
|
+
RUN_STATUS_FAILED,
|
|
63
|
+
RUN_STATUS_CANCELED
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
let recommendWorkflowImpl = runRecommendWorkflow;
|
|
67
|
+
let recommendConnectorImpl = connectRecommendChromeSession;
|
|
68
|
+
let recommendJobReaderImpl = readRecommendJobOptionsFromSession;
|
|
69
|
+
let recommendRunService = createRecommendRunService({
|
|
70
|
+
idPrefix: "mcp_recommend",
|
|
71
|
+
workflow: (...args) => recommendWorkflowImpl(...args),
|
|
72
|
+
onSnapshot: persistRecommendLifecycleSnapshot
|
|
73
|
+
});
|
|
74
|
+
const recommendRunMeta = new Map();
|
|
75
|
+
|
|
76
|
+
function normalizeText(value) {
|
|
77
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parsePositiveInteger(raw, fallback) {
|
|
81
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
82
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseNonNegativeInteger(raw, fallback) {
|
|
86
|
+
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
87
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isDebugTestMode(args = {}) {
|
|
91
|
+
return args.debug_test_mode === true || args.allow_debug_test_mode === true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeScreeningModeArg(args = {}) {
|
|
95
|
+
const raw = normalizeText(args.screening_mode || args.screeningMode || "");
|
|
96
|
+
if (args.use_llm === false) return "deterministic";
|
|
97
|
+
return ["deterministic", "local", "local_scorer"].includes(raw.toLowerCase())
|
|
98
|
+
? "deterministic"
|
|
99
|
+
: "llm";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectRecommendDebugTestOptions(args = {}, normalized = {}) {
|
|
103
|
+
const reasons = [];
|
|
104
|
+
if (normalizeScreeningModeArg(args) === "deterministic") reasons.push("deterministic_screening");
|
|
105
|
+
if (args.allow_card_only_screening === true) reasons.push("allow_card_only_screening");
|
|
106
|
+
if (parseNonNegativeInteger(args.detail_limit, null) === 0) reasons.push("detail_limit=0");
|
|
107
|
+
if (args.no_filter === true) reasons.push("no_filter");
|
|
108
|
+
if (args.filter_enabled === false) reasons.push("filter_enabled=false");
|
|
109
|
+
if (args.dry_run_post_action === true) reasons.push("dry_run_post_action");
|
|
110
|
+
if (args.execute_post_action === false && normalized.postAction && normalized.postAction !== "none") {
|
|
111
|
+
reasons.push("execute_post_action=false");
|
|
112
|
+
}
|
|
113
|
+
return reasons;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveRecommendDetailLimit(args = {}, normalized = {}) {
|
|
117
|
+
const fallback = parsePositiveInteger(normalized.targetCount, 5);
|
|
118
|
+
const requested = parseNonNegativeInteger(args.detail_limit, fallback);
|
|
119
|
+
if (requested === 0 && !isDebugTestMode(args)) {
|
|
120
|
+
return fallback;
|
|
121
|
+
}
|
|
122
|
+
if (requested === 0 && args.allow_card_only_screening !== true) {
|
|
123
|
+
return fallback;
|
|
124
|
+
}
|
|
125
|
+
return requested;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function methodSummary(methodLog = []) {
|
|
129
|
+
const summary = {};
|
|
130
|
+
for (const entry of methodLog || []) {
|
|
131
|
+
summary[entry.method] = (summary[entry.method] || 0) + 1;
|
|
132
|
+
}
|
|
133
|
+
return summary;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clonePlain(value, fallback = null) {
|
|
137
|
+
try {
|
|
138
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
139
|
+
} catch {
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function plainRecord(value) {
|
|
145
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nonEmptyRecord(value) {
|
|
149
|
+
const record = plainRecord(value);
|
|
150
|
+
return Object.keys(record).length ? record : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeRunId(runId) {
|
|
154
|
+
const normalized = normalizeText(runId);
|
|
155
|
+
if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getRecommendRunArtifacts(runId) {
|
|
160
|
+
const normalized = normalizeRunId(runId);
|
|
161
|
+
if (!normalized) return null;
|
|
162
|
+
const runsDir = getRunsDir();
|
|
163
|
+
const outputDir = resolveBossConfiguredOutputDir("", runsDir);
|
|
164
|
+
return {
|
|
165
|
+
runs_dir: runsDir,
|
|
166
|
+
output_dir: outputDir,
|
|
167
|
+
run_state_path: path.join(runsDir, `${normalized}.json`),
|
|
168
|
+
checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
|
|
169
|
+
worker_stdout_path: path.join(runsDir, `${normalized}.worker.stdout.log`),
|
|
170
|
+
worker_stderr_path: path.join(runsDir, `${normalized}.worker.stderr.log`),
|
|
171
|
+
output_csv: path.join(outputDir, `${normalized}.results.csv`),
|
|
172
|
+
report_json: path.join(outputDir, `${normalized}.report.json`)
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ensureDirectory(dirPath) {
|
|
177
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function writeJsonAtomic(filePath, payload) {
|
|
181
|
+
ensureDirectory(path.dirname(filePath));
|
|
182
|
+
const tempPath = `${filePath}.tmp`;
|
|
183
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
184
|
+
fs.renameSync(tempPath, filePath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readJsonFile(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
if (!fs.existsSync(filePath)) return null;
|
|
190
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
191
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recommendSearchParamsForCsv(searchParams = {}) {
|
|
198
|
+
return {
|
|
199
|
+
school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
|
|
200
|
+
degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
|
|
201
|
+
gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
|
|
202
|
+
recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getSnapshotRequestContext(snapshot = {}) {
|
|
207
|
+
const context = plainRecord(snapshot?.context);
|
|
208
|
+
const shared = plainRecord(context.shared_run_context);
|
|
209
|
+
return {
|
|
210
|
+
context,
|
|
211
|
+
confirmation: nonEmptyRecord(context.confirmation) || plainRecord(shared.confirmation),
|
|
212
|
+
overrides: nonEmptyRecord(context.overrides) || plainRecord(shared.overrides),
|
|
213
|
+
followUp: context.follow_up ?? shared.follow_up ?? null,
|
|
214
|
+
shared
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function selectedRecommendJobForCsv(meta = {}, snapshot = {}) {
|
|
219
|
+
const { confirmation, overrides, shared } = getSnapshotRequestContext(snapshot);
|
|
220
|
+
const value = normalizeText(
|
|
221
|
+
meta.args?.confirmation?.job_value
|
|
222
|
+
|| meta.normalized?.job
|
|
223
|
+
|| meta.args?.overrides?.job
|
|
224
|
+
|| confirmation.job_value
|
|
225
|
+
|| overrides.job
|
|
226
|
+
|| shared.confirmation?.job_value
|
|
227
|
+
|| shared.overrides?.job
|
|
228
|
+
|| shared.job_label
|
|
229
|
+
|| ""
|
|
230
|
+
);
|
|
231
|
+
return {
|
|
232
|
+
value,
|
|
233
|
+
title: value,
|
|
234
|
+
label: value
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
|
|
239
|
+
const { context, confirmation, overrides, followUp, shared } = getSnapshotRequestContext(snapshot);
|
|
240
|
+
const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {
|
|
241
|
+
school_tag: overrides.school_tag ?? confirmation.school_tag_value,
|
|
242
|
+
degree: overrides.degree ?? confirmation.degree_value,
|
|
243
|
+
gender: overrides.gender ?? confirmation.gender_value,
|
|
244
|
+
recent_not_view: overrides.recent_not_view ?? confirmation.recent_not_view_value
|
|
245
|
+
});
|
|
246
|
+
const parsedScreenParams = meta.parsed?.screenParams || {};
|
|
247
|
+
const screenParams = {
|
|
248
|
+
criteria: parsedScreenParams.criteria || meta.normalized?.criteria || overrides.criteria || "",
|
|
249
|
+
target_count: parsedScreenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || overrides.target_count || confirmation.target_count_value || shared.max_candidates || "",
|
|
250
|
+
post_action: parsedScreenParams.post_action || overrides.post_action || confirmation.post_action_value || shared.post_action || "none",
|
|
251
|
+
max_greet_count: parsedScreenParams.max_greet_count ?? overrides.max_greet_count ?? confirmation.max_greet_count_value ?? shared.max_greet_count ?? ""
|
|
252
|
+
};
|
|
253
|
+
return buildLegacyScreenInputRows({
|
|
254
|
+
instruction: meta.args?.instruction || context.instruction || shared.instruction || "",
|
|
255
|
+
selectedPage: "recommend",
|
|
256
|
+
selectedJob: selectedRecommendJobForCsv(meta, snapshot),
|
|
257
|
+
userSearchParams: cloneReportInput(searchParams, {}),
|
|
258
|
+
effectiveSearchParams: cloneReportInput(searchParams, {}),
|
|
259
|
+
screenParams,
|
|
260
|
+
followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || followUp || overrides.follow_up || null
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeRecommendLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
|
|
265
|
+
writeLegacyScreenCsv(filePath, {
|
|
266
|
+
inputRows: buildRecommendCsvInputRows(snapshot, meta),
|
|
267
|
+
results: rows
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readRecommendRunState(runId) {
|
|
272
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
273
|
+
if (!artifacts) return null;
|
|
274
|
+
return readJsonFile(artifacts.run_state_path);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isProcessAlive(pid) {
|
|
278
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
279
|
+
try {
|
|
280
|
+
process.kill(pid, 0);
|
|
281
|
+
return true;
|
|
282
|
+
} catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getRecommendRunMeta(runId) {
|
|
288
|
+
return recommendRunMeta.get(runId) || {};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toIsoOrNull(value) {
|
|
292
|
+
const normalized = normalizeText(value);
|
|
293
|
+
return normalized || null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function secondsBetween(startedAt, endedAt) {
|
|
297
|
+
const startMs = Date.parse(startedAt || "");
|
|
298
|
+
const endMs = Date.parse(endedAt || "") || Date.now();
|
|
299
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
|
|
300
|
+
return Math.max(1, Math.round((endMs - startMs) / 1000));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeLegacyProgress(progress = {}, summary = null) {
|
|
304
|
+
const processed = Number.isInteger(progress.processed)
|
|
305
|
+
? progress.processed
|
|
306
|
+
: Number.isInteger(summary?.processed)
|
|
307
|
+
? summary.processed
|
|
308
|
+
: 0;
|
|
309
|
+
const screened = Number.isInteger(progress.screened)
|
|
310
|
+
? progress.screened
|
|
311
|
+
: Number.isInteger(summary?.screened)
|
|
312
|
+
? summary.screened
|
|
313
|
+
: processed;
|
|
314
|
+
const passed = Number.isInteger(progress.passed)
|
|
315
|
+
? progress.passed
|
|
316
|
+
: Number.isInteger(summary?.passed)
|
|
317
|
+
? summary.passed
|
|
318
|
+
: 0;
|
|
319
|
+
return {
|
|
320
|
+
...progress,
|
|
321
|
+
processed,
|
|
322
|
+
inspected: processed,
|
|
323
|
+
screened,
|
|
324
|
+
passed,
|
|
325
|
+
skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
|
|
326
|
+
greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
|
|
327
|
+
post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function completionReason(status) {
|
|
332
|
+
if (status === RUN_STATUS_COMPLETED) return "completed";
|
|
333
|
+
if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
|
|
334
|
+
if (status === RUN_STATUS_FAILED) return "failed";
|
|
335
|
+
if (status === RUN_STATUS_PAUSED) return "paused";
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeErrorText(error = {}) {
|
|
340
|
+
return normalizeText([
|
|
341
|
+
error?.code || "",
|
|
342
|
+
error?.message || error || ""
|
|
343
|
+
].join(" "));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function classifyRecommendRecovery(error = {}) {
|
|
347
|
+
const text = normalizeErrorText(error);
|
|
348
|
+
if (!text) return null;
|
|
349
|
+
if (/BOSS_LOGIN_REQUIRED/i.test(text)) return "login_required";
|
|
350
|
+
if (/Could not find node with given id|No node with given id|Node is detached|Cannot find node|DETAIL_STALE_NODE|IMAGE_CAPTURE_STALE_NODE/i.test(text)) {
|
|
351
|
+
return "transient_stale_dom";
|
|
352
|
+
}
|
|
353
|
+
if (/IMAGE_CAPTURE_TIMEOUT|IMAGE_CAPTURE_TOTAL_TIMEOUT|Image fallback capture timed out/i.test(text)) {
|
|
354
|
+
return "transient_image_capture";
|
|
355
|
+
}
|
|
356
|
+
if (/(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i.test(text)) {
|
|
357
|
+
return "transient_network_or_llm";
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function buildConstrainedAgentRecovery(snapshot = {}, meta = {}, artifacts = null) {
|
|
363
|
+
const error = snapshot?.error || snapshot?.result?.error || null;
|
|
364
|
+
const classification = classifyRecommendRecovery(error);
|
|
365
|
+
if (!classification) return null;
|
|
366
|
+
const canRestartSameRequest = classification !== "login_required";
|
|
367
|
+
return {
|
|
368
|
+
policy_version: 1,
|
|
369
|
+
classification,
|
|
370
|
+
safe_for_outer_ai_agent: true,
|
|
371
|
+
recommended_action: canRestartSameRequest
|
|
372
|
+
? "restart_same_recommend_request_only"
|
|
373
|
+
: "ask_user_to_login_then_retry_same_recommend_request",
|
|
374
|
+
package_requirement: "@reconcrap/boss-recommend-mcp@>=2.0.30",
|
|
375
|
+
run_id: snapshot?.runId || snapshot?.run_id || null,
|
|
376
|
+
retryable: true,
|
|
377
|
+
same_request_sources: {
|
|
378
|
+
instruction: "run.context.instruction",
|
|
379
|
+
confirmation: "run.context.confirmation",
|
|
380
|
+
overrides: "run.context.overrides",
|
|
381
|
+
follow_up: "run.context.follow_up"
|
|
382
|
+
},
|
|
383
|
+
constraints: [
|
|
384
|
+
"Do not change instruction, criteria, filters, job, page_scope, target_count, post_action, or max_greet_count.",
|
|
385
|
+
"Do not switch to search/recruit/chat and do not add follow_up.chat.",
|
|
386
|
+
"Do not summarize, translate, or rewrite criteria.",
|
|
387
|
+
"Do not ask the user to reconfirm business choices unless Boss login is required or the stored context is missing.",
|
|
388
|
+
"Use the same Chrome debug port and recommend page route."
|
|
389
|
+
],
|
|
390
|
+
artifacts: artifacts ? {
|
|
391
|
+
run_state_path: artifacts.run_state_path || null,
|
|
392
|
+
checkpoint_path: artifacts.checkpoint_path || null,
|
|
393
|
+
report_json: artifacts.report_json || null,
|
|
394
|
+
output_csv: artifacts.output_csv || null
|
|
395
|
+
} : null
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function ensureRecommendRunArtifacts(snapshot) {
|
|
400
|
+
const artifacts = getRecommendRunArtifacts(snapshot?.runId || snapshot?.run_id);
|
|
401
|
+
if (!artifacts) return null;
|
|
402
|
+
|
|
403
|
+
const meta = getRecommendRunMeta(snapshot?.runId || snapshot?.run_id);
|
|
404
|
+
const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
|
|
405
|
+
? snapshot.checkpoint
|
|
406
|
+
: {};
|
|
407
|
+
writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
|
|
408
|
+
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
409
|
+
|
|
410
|
+
const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
411
|
+
const checkpointResults = Array.isArray(checkpoint.results) ? checkpoint.results : [];
|
|
412
|
+
const artifactSummary = summary || (checkpointResults.length ? {
|
|
413
|
+
domain: "recommend",
|
|
414
|
+
partial: true,
|
|
415
|
+
partial_reason: snapshot?.status || snapshot?.state || "non_terminal",
|
|
416
|
+
results: checkpointResults
|
|
417
|
+
} : null);
|
|
418
|
+
if (artifactSummary) {
|
|
419
|
+
const rows = Array.isArray(artifactSummary.results) ? artifactSummary.results : [];
|
|
420
|
+
writeRecommendLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
|
|
421
|
+
writeJsonAtomic(artifacts.report_json, {
|
|
422
|
+
run_id: snapshot.runId || snapshot.run_id,
|
|
423
|
+
status: snapshot.status || snapshot.state,
|
|
424
|
+
phase: snapshot.phase || snapshot.stage,
|
|
425
|
+
progress: snapshot.progress || {},
|
|
426
|
+
context: snapshot.context || {},
|
|
427
|
+
checkpoint,
|
|
428
|
+
error: snapshot.error || null,
|
|
429
|
+
last_message: snapshot.error?.message || snapshot.phase || snapshot.stage || null,
|
|
430
|
+
recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
|
|
431
|
+
summary: artifactSummary,
|
|
432
|
+
generated_at: new Date().toISOString()
|
|
433
|
+
});
|
|
434
|
+
if (meta) {
|
|
435
|
+
meta.outputCsvPath = artifacts.output_csv;
|
|
436
|
+
meta.reportJsonPath = artifacts.report_json;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return artifacts;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function persistRecommendCheckpointSnapshot(normalized) {
|
|
444
|
+
const artifacts = getRecommendRunArtifacts(normalized?.run_id || normalized?.runId);
|
|
445
|
+
if (!artifacts) return;
|
|
446
|
+
const checkpoint = normalized?.checkpoint && typeof normalized.checkpoint === "object"
|
|
447
|
+
? normalized.checkpoint
|
|
448
|
+
: {};
|
|
449
|
+
writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
|
|
450
|
+
const meta = getRecommendRunMeta(normalized?.run_id || normalized?.runId);
|
|
451
|
+
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildLegacyRecommendResult(snapshot) {
|
|
455
|
+
if (!snapshot) return null;
|
|
456
|
+
const artifacts = ensureRecommendRunArtifacts(snapshot);
|
|
457
|
+
const meta = getRecommendRunMeta(snapshot.runId);
|
|
458
|
+
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
459
|
+
const checkpoint = snapshot.checkpoint && typeof snapshot.checkpoint === "object" ? snapshot.checkpoint : {};
|
|
460
|
+
const resultRows = Array.isArray(summary?.results)
|
|
461
|
+
? summary.results
|
|
462
|
+
: Array.isArray(checkpoint.results)
|
|
463
|
+
? checkpoint.results
|
|
464
|
+
: [];
|
|
465
|
+
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
466
|
+
const targetCount = Number.isInteger(progress.target_count)
|
|
467
|
+
? progress.target_count
|
|
468
|
+
: Number.isInteger(snapshot.context?.max_candidates)
|
|
469
|
+
? snapshot.context.max_candidates
|
|
470
|
+
: meta.parsed?.screenParams?.target_count || null;
|
|
471
|
+
return {
|
|
472
|
+
status: snapshot.status === RUN_STATUS_COMPLETED
|
|
473
|
+
? "COMPLETED"
|
|
474
|
+
: snapshot.status === RUN_STATUS_CANCELED
|
|
475
|
+
? "CANCELED"
|
|
476
|
+
: snapshot.status === RUN_STATUS_PAUSED
|
|
477
|
+
? "PAUSED"
|
|
478
|
+
: snapshot.status === RUN_STATUS_FAILED
|
|
479
|
+
? "FAILED"
|
|
480
|
+
: snapshot.status,
|
|
481
|
+
run_id: snapshot.runId,
|
|
482
|
+
completion_reason: completionReason(snapshot.status),
|
|
483
|
+
requested_count: targetCount,
|
|
484
|
+
processed_count: progress.processed,
|
|
485
|
+
inspected_count: progress.processed,
|
|
486
|
+
screened_count: progress.screened,
|
|
487
|
+
passed_count: progress.passed,
|
|
488
|
+
skipped_count: progress.skipped,
|
|
489
|
+
detail_opened: progress.detail_opened || summary?.detail_opened || 0,
|
|
490
|
+
greet_count: progress.greet_count || 0,
|
|
491
|
+
post_action_clicked: progress.post_action_clicked || summary?.post_action_clicked || 0,
|
|
492
|
+
output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
|
|
493
|
+
report_json: artifacts?.report_json || meta.reportJsonPath || null,
|
|
494
|
+
checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
|
|
495
|
+
started_at: snapshot.startedAt,
|
|
496
|
+
completed_at: snapshot.completedAt || null,
|
|
497
|
+
duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
|
|
498
|
+
selected_job: {
|
|
499
|
+
title: meta.normalized?.job || meta.args?.confirmation?.job_value || meta.args?.overrides?.job || ""
|
|
500
|
+
},
|
|
501
|
+
selected_page_scope: summary?.page_scope || {
|
|
502
|
+
requested_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend",
|
|
503
|
+
effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
|
|
504
|
+
},
|
|
505
|
+
search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
|
|
506
|
+
screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
|
|
507
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
508
|
+
error: snapshot.error || null,
|
|
509
|
+
recovery: buildConstrainedAgentRecovery(snapshot, meta, artifacts),
|
|
510
|
+
results: resultRows
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function normalizeRunSnapshot(snapshot) {
|
|
515
|
+
if (!snapshot) return null;
|
|
516
|
+
const meta = getRecommendRunMeta(snapshot.runId);
|
|
517
|
+
const artifacts = getRecommendRunArtifacts(snapshot.runId);
|
|
518
|
+
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
519
|
+
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
520
|
+
const legacyResult = (
|
|
521
|
+
TERMINAL_STATUSES.has(snapshot.status)
|
|
522
|
+
|| snapshot.status === RUN_STATUS_PAUSED
|
|
523
|
+
) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
|
|
524
|
+
const recovery = buildConstrainedAgentRecovery(snapshot, meta, artifacts);
|
|
525
|
+
const snapshotContext = plainRecord(snapshot.context);
|
|
526
|
+
const metaArgs = plainRecord(meta.args);
|
|
527
|
+
const oldContext = {
|
|
528
|
+
workspace_root: meta.workspaceRoot || snapshotContext.workspace_root || null,
|
|
529
|
+
instruction: metaArgs.instruction || snapshotContext.instruction || "",
|
|
530
|
+
confirmation: clonePlain(metaArgs.confirmation ?? snapshotContext.confirmation ?? {}, {}),
|
|
531
|
+
overrides: clonePlain(metaArgs.overrides ?? snapshotContext.overrides ?? {}, {}),
|
|
532
|
+
follow_up: clonePlain(metaArgs.follow_up ?? snapshotContext.follow_up ?? null, null),
|
|
533
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
...snapshot,
|
|
537
|
+
progress,
|
|
538
|
+
run_id: snapshot.runId,
|
|
539
|
+
mode: RUN_MODE_ASYNC,
|
|
540
|
+
state: snapshot.status,
|
|
541
|
+
stage: snapshot.phase,
|
|
542
|
+
started_at: snapshot.startedAt,
|
|
543
|
+
updated_at: snapshot.updatedAt,
|
|
544
|
+
completed_at: toIsoOrNull(snapshot.completedAt),
|
|
545
|
+
heartbeat_at: snapshot.updatedAt,
|
|
546
|
+
pid: Number.isInteger(snapshot.pid) && snapshot.pid > 0 ? snapshot.pid : process.pid || null,
|
|
547
|
+
last_message: snapshot.error?.message || snapshot.phase || null,
|
|
548
|
+
context: {
|
|
549
|
+
...snapshotContext,
|
|
550
|
+
...oldContext,
|
|
551
|
+
shared_run_context: snapshotContext
|
|
552
|
+
},
|
|
553
|
+
control: {
|
|
554
|
+
pause_requested: snapshot.status === RUN_STATUS_PAUSED,
|
|
555
|
+
pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
|
|
556
|
+
pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_recommend_pipeline_run" : null,
|
|
557
|
+
cancel_requested: snapshot.status === RUN_STATUS_CANCELING
|
|
558
|
+
},
|
|
559
|
+
resume: {
|
|
560
|
+
checkpoint_path: legacyResult?.checkpoint_path || meta.checkpointPath || artifacts?.checkpoint_path || null,
|
|
561
|
+
pause_control_path: artifacts?.run_state_path || null,
|
|
562
|
+
output_csv: legacyResult?.output_csv || null,
|
|
563
|
+
worker_stdout_path: artifacts?.worker_stdout_path || null,
|
|
564
|
+
worker_stderr_path: artifacts?.worker_stderr_path || null,
|
|
565
|
+
resume_count: meta.resumeCount || 0,
|
|
566
|
+
last_resumed_at: meta.lastResumedAt || null,
|
|
567
|
+
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
568
|
+
},
|
|
569
|
+
recovery,
|
|
570
|
+
result: legacyResult,
|
|
571
|
+
artifacts
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function mergePersistedControlRequest(normalized, existing) {
|
|
576
|
+
const control = {
|
|
577
|
+
...(normalized?.control || {})
|
|
578
|
+
};
|
|
579
|
+
if (!normalized || TERMINAL_STATUSES.has(normalized.state)) return control;
|
|
580
|
+
const existingControl = plainRecord(existing?.control);
|
|
581
|
+
if (existingControl.cancel_requested === true) {
|
|
582
|
+
return {
|
|
583
|
+
...control,
|
|
584
|
+
pause_requested: true,
|
|
585
|
+
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
586
|
+
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "cancel_recommend_pipeline_run",
|
|
587
|
+
cancel_requested: true
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (existingControl.pause_requested === true && normalized.state !== RUN_STATUS_PAUSED) {
|
|
591
|
+
return {
|
|
592
|
+
...control,
|
|
593
|
+
pause_requested: true,
|
|
594
|
+
pause_requested_at: existingControl.pause_requested_at || control.pause_requested_at || new Date().toISOString(),
|
|
595
|
+
pause_requested_by: existingControl.pause_requested_by || control.pause_requested_by || "pause_recommend_pipeline_run"
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (existingControl.pause_requested === false && normalized.state === RUN_STATUS_PAUSED) {
|
|
599
|
+
return {
|
|
600
|
+
...control,
|
|
601
|
+
pause_requested: false,
|
|
602
|
+
pause_requested_at: null,
|
|
603
|
+
pause_requested_by: null,
|
|
604
|
+
cancel_requested: false
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return control;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function persistRecommendRunSnapshot(snapshot, {
|
|
611
|
+
persistActiveCheckpoint = false
|
|
612
|
+
} = {}) {
|
|
613
|
+
const normalized = normalizeRunSnapshot(snapshot);
|
|
614
|
+
if (!normalized?.run_id) return normalized;
|
|
615
|
+
const artifacts = getRecommendRunArtifacts(normalized.run_id);
|
|
616
|
+
if (!artifacts) return normalized;
|
|
617
|
+
const existing = readJsonFile(artifacts.run_state_path);
|
|
618
|
+
normalized.control = mergePersistedControlRequest(normalized, existing);
|
|
619
|
+
if (persistActiveCheckpoint) {
|
|
620
|
+
persistRecommendCheckpointSnapshot(normalized);
|
|
621
|
+
}
|
|
622
|
+
const payload = {
|
|
623
|
+
run_id: normalized.run_id,
|
|
624
|
+
mode: normalized.mode,
|
|
625
|
+
state: normalized.state,
|
|
626
|
+
status: normalized.status,
|
|
627
|
+
stage: normalized.stage,
|
|
628
|
+
started_at: normalized.started_at,
|
|
629
|
+
updated_at: normalized.updated_at,
|
|
630
|
+
heartbeat_at: normalized.heartbeat_at,
|
|
631
|
+
completed_at: normalized.completed_at,
|
|
632
|
+
pid: normalized.pid,
|
|
633
|
+
progress: normalized.progress,
|
|
634
|
+
last_message: normalized.last_message,
|
|
635
|
+
context: normalized.context,
|
|
636
|
+
control: normalized.control,
|
|
637
|
+
resume: normalized.resume,
|
|
638
|
+
error: normalized.error,
|
|
639
|
+
recovery: normalized.recovery,
|
|
640
|
+
result: normalized.result,
|
|
641
|
+
summary: normalized.summary,
|
|
642
|
+
artifacts: normalized.artifacts
|
|
643
|
+
};
|
|
644
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
645
|
+
return normalized;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function reconcilePersistedRecommendRunIfNeeded(persisted) {
|
|
649
|
+
if (!persisted || typeof persisted !== "object") return persisted;
|
|
650
|
+
const persistedState = normalizeText(persisted.state || persisted.status);
|
|
651
|
+
if (TERMINAL_STATUSES.has(persistedState)) return persisted;
|
|
652
|
+
if (isProcessAlive(persisted.pid)) return persisted;
|
|
653
|
+
|
|
654
|
+
const runId = normalizeRunId(persisted.run_id || persisted.runId);
|
|
655
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
656
|
+
const checkpoint = artifacts?.checkpoint_path ? readJsonFile(artifacts.checkpoint_path) : null;
|
|
657
|
+
const now = new Date().toISOString();
|
|
658
|
+
const error = {
|
|
659
|
+
code: "RUN_PROCESS_EXITED",
|
|
660
|
+
message: `检测到推荐任务进程已退出(pid=${persisted.pid || "unknown"}),已自动标记为失败。`,
|
|
661
|
+
retryable: true
|
|
662
|
+
};
|
|
663
|
+
return persistRecommendRunSnapshot({
|
|
664
|
+
runId,
|
|
665
|
+
name: persisted.name || runId,
|
|
666
|
+
status: RUN_STATUS_FAILED,
|
|
667
|
+
phase: persisted.stage || persisted.phase || "recommend:orphaned",
|
|
668
|
+
progress: persisted.progress || {},
|
|
669
|
+
context: persisted.context || {},
|
|
670
|
+
checkpoint: checkpoint || persisted.checkpoint || {},
|
|
671
|
+
startedAt: persisted.started_at || persisted.startedAt || now,
|
|
672
|
+
updatedAt: now,
|
|
673
|
+
completedAt: now,
|
|
674
|
+
pid: Number.isInteger(persisted.pid) && persisted.pid > 0 ? persisted.pid : null,
|
|
675
|
+
error,
|
|
676
|
+
summary: persisted.summary || null
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function persistRecommendLifecycleSnapshot(snapshot, event = {}) {
|
|
681
|
+
return persistRecommendRunSnapshot(snapshot, {
|
|
682
|
+
persistActiveCheckpoint: event?.type === "checkpoint"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function attachMethodEvidence(payload, runId) {
|
|
687
|
+
const meta = getRecommendRunMeta(runId);
|
|
688
|
+
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
689
|
+
return {
|
|
690
|
+
...payload,
|
|
691
|
+
runtime_evaluate_used: false,
|
|
692
|
+
method_summary: methodSummary(meta.methodLog || []),
|
|
693
|
+
method_log: meta.methodLog || [],
|
|
694
|
+
chrome: meta.chrome || null
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function compactRecommendJobListOption(option, index) {
|
|
699
|
+
const label = normalizeText(option?.label);
|
|
700
|
+
const name = normalizeText(option?.label_without_salary || label);
|
|
701
|
+
return {
|
|
702
|
+
index,
|
|
703
|
+
name,
|
|
704
|
+
label,
|
|
705
|
+
label_without_salary: name,
|
|
706
|
+
current: Boolean(option?.current),
|
|
707
|
+
visible: Boolean(option?.visible)
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function readRecommendJobOptionsFromSession(session) {
|
|
712
|
+
const client = session?.client;
|
|
713
|
+
if (!client) throw new Error("Recommend Chrome session is missing a CDP client");
|
|
714
|
+
const rootState = await getRecommendRoots(client);
|
|
715
|
+
const frameNodeId = rootState?.iframe?.documentNodeId;
|
|
716
|
+
if (!frameNodeId) throw new Error("recommendFrame iframe document was not found");
|
|
717
|
+
|
|
718
|
+
let options = [];
|
|
719
|
+
try {
|
|
720
|
+
options = await listRecommendJobOptions(client, frameNodeId, {
|
|
721
|
+
openDropdown: true
|
|
722
|
+
});
|
|
723
|
+
} finally {
|
|
724
|
+
await closeRecommendJobDropdown(client).catch(() => {});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const compacted = [];
|
|
728
|
+
const seen = new Set();
|
|
729
|
+
for (const option of options) {
|
|
730
|
+
const compact = compactRecommendJobListOption(option, compacted.length);
|
|
731
|
+
if (!compact.name && !compact.label) continue;
|
|
732
|
+
const key = `${compact.name}\n${compact.label}`;
|
|
733
|
+
if (seen.has(key)) continue;
|
|
734
|
+
seen.add(key);
|
|
735
|
+
compacted.push(compact);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
source: "recommend_job_dropdown",
|
|
740
|
+
selector: "recommend job selection dropdown",
|
|
741
|
+
job_options: compacted,
|
|
742
|
+
selected_job: compacted.find((option) => option.current) || null
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
|
|
747
|
+
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
748
|
+
const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
|
|
749
|
+
const port = parsePositiveInteger(
|
|
750
|
+
args.port,
|
|
751
|
+
configResolution.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
|
|
752
|
+
);
|
|
753
|
+
const targetUrlIncludes = normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL;
|
|
754
|
+
const allowNavigate = args.allow_navigate !== false;
|
|
755
|
+
const slowLive = args.slow_live === true;
|
|
756
|
+
let session;
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
session = await recommendConnectorImpl({
|
|
760
|
+
host,
|
|
761
|
+
port,
|
|
762
|
+
targetUrlIncludes,
|
|
763
|
+
allowNavigate,
|
|
764
|
+
slowLive
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const jobs = await recommendJobReaderImpl(session, {
|
|
768
|
+
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
769
|
+
args: clonePlain(args, {}),
|
|
770
|
+
normalized: {
|
|
771
|
+
host,
|
|
772
|
+
port,
|
|
773
|
+
targetUrlIncludes,
|
|
774
|
+
allowNavigate,
|
|
775
|
+
slowLive
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
|
|
779
|
+
assertNoForbiddenCdpCalls(session.methodLog || []);
|
|
780
|
+
return {
|
|
781
|
+
status: "OK",
|
|
782
|
+
stage: "recommend_job_list",
|
|
783
|
+
cdp_only: true,
|
|
784
|
+
runtime_evaluate_used: false,
|
|
785
|
+
page_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
786
|
+
job_count: jobOptions.length,
|
|
787
|
+
job_names: jobOptions.map((option) => option.name || option.label).filter(Boolean),
|
|
788
|
+
job_full_labels: jobOptions.map((option) => option.label || option.name).filter(Boolean),
|
|
789
|
+
job_options: jobOptions,
|
|
790
|
+
selected_job: jobs?.selected_job || jobOptions.find((option) => option.current) || null,
|
|
791
|
+
source: jobs?.source || "recommend_job_dropdown",
|
|
792
|
+
selector: jobs?.selector || "",
|
|
793
|
+
message: "已通过 CDP-only 从推荐页岗位下拉框读取可用岗位。Cron/一次性任务里的 job 参数优先使用 job_names 中的完整岗位名。",
|
|
794
|
+
chrome: {
|
|
795
|
+
host,
|
|
796
|
+
port,
|
|
797
|
+
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
798
|
+
target_id: session.target?.id || null,
|
|
799
|
+
auto_launch: session.chrome || null
|
|
800
|
+
},
|
|
801
|
+
method_summary: methodSummary(session.methodLog || []),
|
|
802
|
+
method_log: session.methodLog || []
|
|
803
|
+
};
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const methodLog = session?.methodLog || [];
|
|
806
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
807
|
+
return {
|
|
808
|
+
status: "FAILED",
|
|
809
|
+
stage: "recommend_job_list",
|
|
810
|
+
cdp_only: true,
|
|
811
|
+
runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
|
|
812
|
+
error: {
|
|
813
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "RECOMMEND_JOB_LIST_FAILED",
|
|
814
|
+
message: error?.message || "Failed to read recommend job list",
|
|
815
|
+
requires_login: Boolean(error?.requires_login),
|
|
816
|
+
login_url: error?.login_url || null,
|
|
817
|
+
login_detection: error?.login_detection || null,
|
|
818
|
+
current_url: error?.current_url || null,
|
|
819
|
+
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
820
|
+
chrome: error?.chrome || null,
|
|
821
|
+
retryable: true
|
|
822
|
+
},
|
|
823
|
+
chrome: {
|
|
824
|
+
host,
|
|
825
|
+
port,
|
|
826
|
+
target_url: targetUrlIncludes,
|
|
827
|
+
auto_launch: error?.chrome || session?.chrome || null
|
|
828
|
+
},
|
|
829
|
+
method_summary: methodSummary(methodLog),
|
|
830
|
+
method_log: methodLog
|
|
831
|
+
};
|
|
832
|
+
} finally {
|
|
833
|
+
if (session) {
|
|
834
|
+
try {
|
|
835
|
+
await session.close?.();
|
|
836
|
+
} catch {
|
|
837
|
+
// Best-effort cleanup after a read-only helper.
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function compactHealth(check) {
|
|
844
|
+
if (!check) return null;
|
|
845
|
+
return {
|
|
846
|
+
status: check.status,
|
|
847
|
+
summary: check.summary,
|
|
848
|
+
drift_report: check.drift_report,
|
|
849
|
+
probes: (check.probes || []).map((probe) => ({
|
|
850
|
+
id: probe.id,
|
|
851
|
+
type: probe.type,
|
|
852
|
+
status: probe.status,
|
|
853
|
+
count: probe.count,
|
|
854
|
+
required: probe.required
|
|
855
|
+
}))
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function waitForHealthyRecommend(client, config, {
|
|
860
|
+
timeoutMs = 90000,
|
|
861
|
+
intervalMs = 1000
|
|
862
|
+
} = {}) {
|
|
863
|
+
const started = Date.now();
|
|
864
|
+
let lastCheck = null;
|
|
865
|
+
while (Date.now() - started <= timeoutMs) {
|
|
866
|
+
const loginDetection = await detectBossLoginState(client).catch(() => null);
|
|
867
|
+
if (loginDetection?.requires_login) {
|
|
868
|
+
return {
|
|
869
|
+
status: "login_required",
|
|
870
|
+
summary: "Boss login is required",
|
|
871
|
+
loginDetection
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const roots = await resolveRecommendSelfHealRoots(client, config);
|
|
875
|
+
lastCheck = await runSelfHealCheck({
|
|
876
|
+
client,
|
|
877
|
+
domain: "recommend",
|
|
878
|
+
roots: roots.roots,
|
|
879
|
+
selectorProbes: config.selectorProbes,
|
|
880
|
+
accessibilityProbes: config.accessibilityProbes,
|
|
881
|
+
viewportProbes: config.viewportProbes
|
|
882
|
+
});
|
|
883
|
+
if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
|
|
884
|
+
await sleep(intervalMs);
|
|
885
|
+
}
|
|
886
|
+
return lastCheck;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function shouldNavigateToRecommend(url) {
|
|
890
|
+
return !String(url || "").includes("/web/chat/recommend");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function connectRecommendChromeSession({
|
|
894
|
+
host = DEFAULT_RECOMMEND_HOST,
|
|
895
|
+
port = DEFAULT_RECOMMEND_PORT,
|
|
896
|
+
targetUrlIncludes = RECOMMEND_TARGET_URL,
|
|
897
|
+
allowNavigate = true,
|
|
898
|
+
slowLive = false
|
|
899
|
+
} = {}) {
|
|
900
|
+
const session = await connectToChromeTargetOrOpen({
|
|
901
|
+
host,
|
|
902
|
+
port,
|
|
903
|
+
targetUrlIncludes,
|
|
904
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
905
|
+
allowNavigate,
|
|
906
|
+
slowLive,
|
|
907
|
+
fallbackTargetPredicate: (target) => (
|
|
908
|
+
target?.type === "page"
|
|
909
|
+
&& String(target?.url || "").includes("zhipin.com")
|
|
910
|
+
)
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const { client, target } = session;
|
|
914
|
+
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
915
|
+
if (typeof client?.Network?.setCacheDisabled === "function") {
|
|
916
|
+
await client.Network.setCacheDisabled({ cacheDisabled: true });
|
|
917
|
+
}
|
|
918
|
+
await bringPageToFront(client);
|
|
919
|
+
|
|
920
|
+
const targetUrl = String(target?.url || "");
|
|
921
|
+
let navigation = {
|
|
922
|
+
navigated: false,
|
|
923
|
+
url: targetUrl
|
|
924
|
+
};
|
|
925
|
+
if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
|
|
926
|
+
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
927
|
+
const settleMs = slowLive ? 12000 : 5000;
|
|
928
|
+
const waited = await waitForMainFrameUrl(
|
|
929
|
+
client,
|
|
930
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
931
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
932
|
+
);
|
|
933
|
+
navigation = {
|
|
934
|
+
navigated: true,
|
|
935
|
+
url: RECOMMEND_TARGET_URL,
|
|
936
|
+
settle_ms: settleMs,
|
|
937
|
+
observed_url: waited.url || null,
|
|
938
|
+
observed_url_ok: waited.ok
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
let currentUrl = await getMainFrameUrl(client).catch(() => navigation.url || targetUrl);
|
|
942
|
+
if (allowNavigate && shouldNavigateToRecommend(currentUrl) && !isBossLoginUrl(currentUrl)) {
|
|
943
|
+
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
944
|
+
const settleMs = slowLive ? 12000 : 5000;
|
|
945
|
+
const waited = await waitForMainFrameUrl(
|
|
946
|
+
client,
|
|
947
|
+
(url) => isBossLoginUrl(url) || !shouldNavigateToRecommend(url),
|
|
948
|
+
{ timeoutMs: settleMs, intervalMs: 500 }
|
|
949
|
+
);
|
|
950
|
+
navigation = {
|
|
951
|
+
navigated: true,
|
|
952
|
+
url: RECOMMEND_TARGET_URL,
|
|
953
|
+
settle_ms: settleMs,
|
|
954
|
+
observed_url: waited.url || null,
|
|
955
|
+
observed_url_ok: waited.ok,
|
|
956
|
+
reason: "observed_url_mismatch"
|
|
957
|
+
};
|
|
958
|
+
currentUrl = await getMainFrameUrl(client).catch(() => waited.url || currentUrl);
|
|
959
|
+
}
|
|
960
|
+
const loginDetection = await detectBossLoginState(client, { currentUrl }).catch(() => ({
|
|
961
|
+
requires_login: isBossLoginUrl(currentUrl),
|
|
962
|
+
reason: "login_detection_failed",
|
|
963
|
+
current_url: currentUrl
|
|
964
|
+
}));
|
|
965
|
+
if (loginDetection.requires_login) {
|
|
966
|
+
await session.close?.();
|
|
967
|
+
throw createBossLoginRequiredError({
|
|
968
|
+
domain: "recommend",
|
|
969
|
+
currentUrl: loginDetection.current_url || currentUrl,
|
|
970
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
971
|
+
loginDetection,
|
|
972
|
+
chrome: session.chrome || null
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
if (shouldNavigateToRecommend(currentUrl)) {
|
|
976
|
+
await session.close?.();
|
|
977
|
+
throw new Error(`Boss recommend page did not navigate to ${RECOMMEND_TARGET_URL}; current URL: ${currentUrl || "unknown"}`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const selfHealConfig = buildRecommendSelfHealConfig();
|
|
981
|
+
const health = await waitForHealthyRecommend(client, selfHealConfig, {
|
|
982
|
+
timeoutMs: slowLive ? 180000 : 90000,
|
|
983
|
+
intervalMs: slowLive ? 1200 : 800
|
|
984
|
+
});
|
|
985
|
+
if (health?.loginDetection?.requires_login) {
|
|
986
|
+
await session.close?.();
|
|
987
|
+
throw createBossLoginRequiredError({
|
|
988
|
+
domain: "recommend",
|
|
989
|
+
currentUrl: health.loginDetection.current_url || currentUrl,
|
|
990
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
991
|
+
loginDetection: health.loginDetection,
|
|
992
|
+
chrome: session.chrome || null
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
|
|
996
|
+
const latestUrl = await getMainFrameUrl(client).catch(() => currentUrl);
|
|
997
|
+
const latestLoginDetection = await detectBossLoginState(client, { currentUrl: latestUrl }).catch(() => ({
|
|
998
|
+
requires_login: isBossLoginUrl(latestUrl),
|
|
999
|
+
reason: "login_detection_failed",
|
|
1000
|
+
current_url: latestUrl
|
|
1001
|
+
}));
|
|
1002
|
+
if (latestLoginDetection.requires_login) {
|
|
1003
|
+
await session.close?.();
|
|
1004
|
+
throw createBossLoginRequiredError({
|
|
1005
|
+
domain: "recommend",
|
|
1006
|
+
currentUrl: latestLoginDetection.current_url || latestUrl,
|
|
1007
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
1008
|
+
loginDetection: latestLoginDetection,
|
|
1009
|
+
chrome: session.chrome || null
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return {
|
|
1016
|
+
...session,
|
|
1017
|
+
navigation,
|
|
1018
|
+
health
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function parseRecommendPipelineRequest(args = {}) {
|
|
1023
|
+
return parseRecommendInstruction({
|
|
1024
|
+
instruction: args.instruction,
|
|
1025
|
+
confirmation: args.confirmation,
|
|
1026
|
+
overrides: args.overrides
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function buildRequiredConfirmations(parsed, args = {}) {
|
|
1031
|
+
const required = [];
|
|
1032
|
+
if (parsed.needs_page_confirmation) required.push("page_scope");
|
|
1033
|
+
if (parsed.needs_filters_confirmation) required.push("filters");
|
|
1034
|
+
if (parsed.needs_school_tag_confirmation) required.push("school_tag");
|
|
1035
|
+
if (parsed.needs_degree_confirmation) required.push("degree");
|
|
1036
|
+
if (parsed.needs_gender_confirmation) required.push("gender");
|
|
1037
|
+
if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
|
|
1038
|
+
if (parsed.needs_criteria_confirmation) required.push("criteria");
|
|
1039
|
+
if (parsed.needs_target_count_confirmation) required.push("target_count");
|
|
1040
|
+
if (parsed.needs_post_action_confirmation) required.push("post_action");
|
|
1041
|
+
if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
|
|
1042
|
+
if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
|
|
1043
|
+
|
|
1044
|
+
const confirmation = args.confirmation || {};
|
|
1045
|
+
const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
|
|
1046
|
+
if (confirmation.job_confirmed !== true || !jobValue) required.push("job");
|
|
1047
|
+
if (confirmation.final_confirmed !== true) required.push("final_review");
|
|
1048
|
+
return Array.from(new Set(required));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function buildJobPendingQuestion(args = {}) {
|
|
1052
|
+
const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
|
|
1053
|
+
return {
|
|
1054
|
+
field: "job",
|
|
1055
|
+
question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
|
|
1056
|
+
value: value || null
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function buildFinalReviewQuestion(parsed) {
|
|
1061
|
+
return {
|
|
1062
|
+
field: "final_review",
|
|
1063
|
+
question: "请最终确认本次推荐页筛选参数无误,并明确 final_confirmed=true 后再启动。",
|
|
1064
|
+
value: {
|
|
1065
|
+
page_scope: parsed.page_scope,
|
|
1066
|
+
search_params: parsed.searchParams,
|
|
1067
|
+
screen_params: parsed.screenParams
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function buildNeedInputResponse(parsed) {
|
|
1073
|
+
return {
|
|
1074
|
+
status: "NEED_INPUT",
|
|
1075
|
+
missing_fields: parsed.missing_fields,
|
|
1076
|
+
required_confirmations: buildRequiredConfirmations(parsed),
|
|
1077
|
+
search_params: parsed.searchParams,
|
|
1078
|
+
screen_params: parsed.screenParams,
|
|
1079
|
+
pending_questions: parsed.pending_questions,
|
|
1080
|
+
review: parsed.review,
|
|
1081
|
+
error: {
|
|
1082
|
+
code: "MISSING_REQUIRED_FIELDS",
|
|
1083
|
+
message: "缺少必要字段。请补齐推荐页 criteria 等必填字段后再启动 CDP-only recommend run。",
|
|
1084
|
+
retryable: true
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
|
|
1090
|
+
const pending = [...(parsed.pending_questions || [])];
|
|
1091
|
+
if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
|
|
1092
|
+
pending.push(buildJobPendingQuestion(args));
|
|
1093
|
+
}
|
|
1094
|
+
if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
|
|
1095
|
+
pending.push(buildFinalReviewQuestion(parsed));
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
status: "NEED_CONFIRMATION",
|
|
1099
|
+
required_confirmations: requiredConfirmations,
|
|
1100
|
+
page_scope: parsed.page_scope,
|
|
1101
|
+
search_params: parsed.searchParams,
|
|
1102
|
+
screen_params: parsed.screenParams,
|
|
1103
|
+
pending_questions: pending,
|
|
1104
|
+
review: {
|
|
1105
|
+
...(parsed.review || {}),
|
|
1106
|
+
required_confirmations: requiredConfirmations
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function evaluateRecommendPipelineGate(parsed, args = {}) {
|
|
1112
|
+
if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed);
|
|
1113
|
+
const requiredConfirmations = buildRequiredConfirmations(parsed, args);
|
|
1114
|
+
if (requiredConfirmations.length) {
|
|
1115
|
+
return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (args.follow_up?.chat || args.overrides?.follow_up?.chat) {
|
|
1119
|
+
return {
|
|
1120
|
+
status: "FAILED",
|
|
1121
|
+
error: {
|
|
1122
|
+
code: "FOLLOW_UP_CHAT_NOT_CDP_REWRITTEN",
|
|
1123
|
+
message: "recommend -> chat follow-up orchestration is legacy-only and intentionally fenced from the CDP-only MCP route. Run recommend first, then use the direct chat MCP route separately, or keep the old chained behavior in the archived legacy lane.",
|
|
1124
|
+
retryable: true
|
|
1125
|
+
},
|
|
1126
|
+
review: parsed.review
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function toArray(value) {
|
|
1134
|
+
if (Array.isArray(value)) return value;
|
|
1135
|
+
if (value === undefined || value === null) return [];
|
|
1136
|
+
return [value];
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function withoutUnlimited(values = []) {
|
|
1140
|
+
return toArray(values)
|
|
1141
|
+
.map((value) => normalizeText(value))
|
|
1142
|
+
.filter((value) => value && value !== "不限" && value.toLowerCase() !== "all" && value !== "全部");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function buildRecommendFilter(parsed, args = {}) {
|
|
1146
|
+
if (args.no_filter === true || args.filter_enabled === false) {
|
|
1147
|
+
return { enabled: false };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const groups = [];
|
|
1151
|
+
const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
|
|
1152
|
+
if (recentNotView.length) {
|
|
1153
|
+
groups.push({
|
|
1154
|
+
group: "recentNotView",
|
|
1155
|
+
labels: recentNotView,
|
|
1156
|
+
selectAllLabels: true
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const degree = withoutUnlimited(parsed.searchParams?.degree);
|
|
1161
|
+
if (degree.length) {
|
|
1162
|
+
groups.push({
|
|
1163
|
+
group: "degree",
|
|
1164
|
+
labels: degree,
|
|
1165
|
+
selectAllLabels: true
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const gender = withoutUnlimited(parsed.searchParams?.gender);
|
|
1170
|
+
if (gender.length) {
|
|
1171
|
+
groups.push({
|
|
1172
|
+
group: "gender",
|
|
1173
|
+
labels: gender,
|
|
1174
|
+
selectAllLabels: true
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const school = withoutUnlimited(parsed.searchParams?.school_tag);
|
|
1179
|
+
if (school.length) {
|
|
1180
|
+
groups.push({
|
|
1181
|
+
group: "school",
|
|
1182
|
+
labels: school,
|
|
1183
|
+
selectAllLabels: true
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return groups.length ? { filterGroups: groups } : { enabled: false };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function normalizeRecommendStartInput(args = {}, parsed, configResolution = null) {
|
|
1191
|
+
const confirmation = args.confirmation || {};
|
|
1192
|
+
const overrides = args.overrides || {};
|
|
1193
|
+
const slowLive = args.slow_live === true;
|
|
1194
|
+
const targetCount = parsePositiveInteger(
|
|
1195
|
+
args.max_candidates,
|
|
1196
|
+
parsed.screenParams?.target_count || parsePositiveInteger(confirmation.target_count_value, 5)
|
|
1197
|
+
);
|
|
1198
|
+
return {
|
|
1199
|
+
host: normalizeText(args.host) || DEFAULT_RECOMMEND_HOST,
|
|
1200
|
+
port: parsePositiveInteger(
|
|
1201
|
+
args.port,
|
|
1202
|
+
configResolution?.ok ? configResolution.config.debugPort : DEFAULT_RECOMMEND_PORT
|
|
1203
|
+
),
|
|
1204
|
+
targetUrlIncludes: normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL,
|
|
1205
|
+
allowNavigate: args.allow_navigate !== false,
|
|
1206
|
+
slowLive,
|
|
1207
|
+
criteria: parsed.screenParams?.criteria || normalizeText(overrides.criteria),
|
|
1208
|
+
targetCount,
|
|
1209
|
+
job: normalizeText(confirmation.job_value || overrides.job || ""),
|
|
1210
|
+
pageScope: parsed.page_scope || "recommend",
|
|
1211
|
+
filter: buildRecommendFilter(parsed, args),
|
|
1212
|
+
postAction: parsed.screenParams?.post_action || "none",
|
|
1213
|
+
maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
|
|
1214
|
+
? parsed.screenParams.max_greet_count
|
|
1215
|
+
: null,
|
|
1216
|
+
screeningMode: normalizeScreeningModeArg(args)
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
1220
|
function getRunOptions(args, parsed, normalized, session, configResolution = null) {
|
|
1221
1221
|
const slowLive = args.slow_live === true;
|
|
1222
1222
|
const executePostAction = args.dry_run_post_action === true
|
|
@@ -1224,48 +1224,48 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
|
|
|
1224
1224
|
: args.execute_post_action !== false;
|
|
1225
1225
|
const humanBehavior = resolveHumanBehaviorForRun(args, configResolution?.config || {});
|
|
1226
1226
|
return {
|
|
1227
|
-
client: session.client,
|
|
1228
|
-
targetUrl: RECOMMEND_TARGET_URL,
|
|
1229
|
-
criteria: normalized.criteria,
|
|
1230
|
-
jobLabel: normalized.job,
|
|
1231
|
-
pageScope: normalized.pageScope,
|
|
1232
|
-
fallbackPageScope: "recommend",
|
|
1233
|
-
filter: normalized.filter,
|
|
1234
|
-
maxCandidates: normalized.targetCount,
|
|
1235
|
-
detailLimit: resolveRecommendDetailLimit(args, normalized),
|
|
1236
|
-
closeDetail: true,
|
|
1237
|
-
delayMs: parseNonNegativeInteger(args.delay_ms, 0),
|
|
1238
|
-
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
1239
|
-
maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
|
|
1240
|
-
imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
|
|
1241
|
-
cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
|
|
1242
|
-
listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
|
|
1243
|
-
listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
|
|
1244
|
-
listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
|
|
1245
|
-
listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
|
|
1246
|
-
listFallbackPoint: null,
|
|
1247
|
-
refreshOnEnd: args.refresh_on_end !== false,
|
|
1248
|
-
maxRefreshRounds: parseNonNegativeInteger(args.max_refresh_rounds, 2),
|
|
1249
|
-
refreshButtonSettleMs: parsePositiveInteger(args.refresh_button_settle_ms, slowLive ? 10000 : 8000),
|
|
1250
|
-
refreshReloadSettleMs: parsePositiveInteger(args.refresh_reload_settle_ms, slowLive ? 12000 : 8000),
|
|
1251
|
-
postAction: normalized.postAction,
|
|
1252
|
-
maxGreetCount: normalized.maxGreetCount,
|
|
1253
|
-
executePostAction,
|
|
1254
|
-
actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
|
|
1255
|
-
actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
|
|
1256
|
-
actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
|
|
1257
|
-
screeningMode: normalized.screeningMode,
|
|
1258
|
-
llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
|
|
1259
|
-
...configResolution.config
|
|
1260
|
-
} : null,
|
|
1261
|
-
llmTimeoutMs: parsePositiveInteger(
|
|
1262
|
-
args.llm_timeout_ms,
|
|
1263
|
-
parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
|
|
1264
|
-
),
|
|
1265
|
-
llmImageLimit: parsePositiveInteger(
|
|
1266
|
-
args.llm_image_limit,
|
|
1267
|
-
parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
|
|
1268
|
-
),
|
|
1227
|
+
client: session.client,
|
|
1228
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
1229
|
+
criteria: normalized.criteria,
|
|
1230
|
+
jobLabel: normalized.job,
|
|
1231
|
+
pageScope: normalized.pageScope,
|
|
1232
|
+
fallbackPageScope: "recommend",
|
|
1233
|
+
filter: normalized.filter,
|
|
1234
|
+
maxCandidates: normalized.targetCount,
|
|
1235
|
+
detailLimit: resolveRecommendDetailLimit(args, normalized),
|
|
1236
|
+
closeDetail: true,
|
|
1237
|
+
delayMs: parseNonNegativeInteger(args.delay_ms, 0),
|
|
1238
|
+
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
1239
|
+
maxImagePages: parsePositiveInteger(args.max_image_pages, DEFAULT_MAX_IMAGE_PAGES),
|
|
1240
|
+
imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
|
|
1241
|
+
cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
|
|
1242
|
+
listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
|
|
1243
|
+
listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
|
|
1244
|
+
listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
|
|
1245
|
+
listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
|
|
1246
|
+
listFallbackPoint: null,
|
|
1247
|
+
refreshOnEnd: args.refresh_on_end !== false,
|
|
1248
|
+
maxRefreshRounds: parseNonNegativeInteger(args.max_refresh_rounds, 2),
|
|
1249
|
+
refreshButtonSettleMs: parsePositiveInteger(args.refresh_button_settle_ms, slowLive ? 10000 : 8000),
|
|
1250
|
+
refreshReloadSettleMs: parsePositiveInteger(args.refresh_reload_settle_ms, slowLive ? 12000 : 8000),
|
|
1251
|
+
postAction: normalized.postAction,
|
|
1252
|
+
maxGreetCount: normalized.maxGreetCount,
|
|
1253
|
+
executePostAction,
|
|
1254
|
+
actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
|
|
1255
|
+
actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
|
|
1256
|
+
actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
|
|
1257
|
+
screeningMode: normalized.screeningMode,
|
|
1258
|
+
llmConfig: normalized.screeningMode === "llm" && configResolution?.ok ? {
|
|
1259
|
+
...configResolution.config
|
|
1260
|
+
} : null,
|
|
1261
|
+
llmTimeoutMs: parsePositiveInteger(
|
|
1262
|
+
args.llm_timeout_ms,
|
|
1263
|
+
parsePositiveInteger(configResolution?.config?.llmTimeoutMs || configResolution?.config?.timeoutMs, slowLive ? 180000 : 120000)
|
|
1264
|
+
),
|
|
1265
|
+
llmImageLimit: parsePositiveInteger(
|
|
1266
|
+
args.llm_image_limit,
|
|
1267
|
+
parsePositiveInteger(configResolution?.config?.llmImageLimit || configResolution?.config?.imageLimit, 8)
|
|
1268
|
+
),
|
|
1269
1269
|
llmImageDetail: normalizeText(
|
|
1270
1270
|
args.llm_image_detail || configResolution?.config?.llmImageDetail || configResolution?.config?.imageDetail
|
|
1271
1271
|
) || "low",
|
|
@@ -1276,448 +1276,448 @@ function getRunOptions(args, parsed, normalized, session, configResolution = nul
|
|
|
1276
1276
|
parsed
|
|
1277
1277
|
};
|
|
1278
1278
|
}
|
|
1279
|
-
|
|
1280
|
-
function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
|
|
1281
|
-
const parsed = parseRecommendPipelineRequest(args);
|
|
1282
|
-
const gate = evaluateRecommendPipelineGate(parsed, args);
|
|
1283
|
-
if (gate) return { response: gate };
|
|
1284
|
-
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1285
|
-
const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
|
|
1286
|
-
const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
|
|
1287
|
-
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1288
|
-
return {
|
|
1289
|
-
response: {
|
|
1290
|
-
status: "FAILED",
|
|
1291
|
-
error: {
|
|
1292
|
-
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1293
|
-
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1294
|
-
retryable: false
|
|
1295
|
-
},
|
|
1296
|
-
debug_test_options: debugTestOptions
|
|
1297
|
-
}
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
|
-
if (normalized.screeningMode === "llm" && !configResolution.ok) {
|
|
1301
|
-
return {
|
|
1302
|
-
response: {
|
|
1303
|
-
status: "FAILED",
|
|
1304
|
-
error: {
|
|
1305
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
1306
|
-
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
1307
|
-
retryable: true
|
|
1308
|
-
},
|
|
1309
|
-
config_path: configResolution.config_path || null,
|
|
1310
|
-
candidate_paths: configResolution.candidate_paths || []
|
|
1311
|
-
}
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
return {
|
|
1315
|
-
parsed,
|
|
1316
|
-
configResolution,
|
|
1317
|
-
normalized
|
|
1318
|
-
};
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
async function closeRecommendRunSession(runId) {
|
|
1322
|
-
const meta = recommendRunMeta.get(runId);
|
|
1323
|
-
if (!meta || meta.closed) return;
|
|
1324
|
-
try {
|
|
1325
|
-
try {
|
|
1326
|
-
if (meta.session?.client) {
|
|
1327
|
-
await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
|
|
1328
|
-
}
|
|
1329
|
-
} catch {
|
|
1330
|
-
// Cleanup is best-effort once the run has settled.
|
|
1331
|
-
}
|
|
1332
|
-
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
1333
|
-
} finally {
|
|
1334
|
-
meta.closed = true;
|
|
1335
|
-
try {
|
|
1336
|
-
await meta.session?.close?.();
|
|
1337
|
-
} catch {
|
|
1338
|
-
// Nothing actionable for the caller once the run has settled.
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
async function waitForRecommendRunTerminal(runId) {
|
|
1344
|
-
while (true) {
|
|
1345
|
-
try {
|
|
1346
|
-
const snapshot = recommendRunService.getRecommendRun(runId);
|
|
1347
|
-
if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
|
|
1348
|
-
} catch {
|
|
1349
|
-
return null;
|
|
1350
|
-
}
|
|
1351
|
-
await sleep(1000);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function trackRecommendRun(runId) {
|
|
1356
|
-
waitForRecommendRunTerminal(runId)
|
|
1357
|
-
.then((terminal) => {
|
|
1358
|
-
if (terminal) persistRecommendRunSnapshot(terminal);
|
|
1359
|
-
})
|
|
1360
|
-
.catch(() => null)
|
|
1361
|
-
.finally(() => {
|
|
1362
|
-
closeRecommendRunSession(runId).catch(() => {});
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
1367
|
-
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1368
|
-
if (prepared.response) return prepared.response;
|
|
1369
|
-
const { parsed, configResolution, normalized } = prepared;
|
|
1370
|
-
const fixedRunId = normalizeRunId(runId);
|
|
1371
|
-
if (runId && !fixedRunId) {
|
|
1372
|
-
return {
|
|
1373
|
-
status: "FAILED",
|
|
1374
|
-
error: {
|
|
1375
|
-
code: "INVALID_RUN_ID",
|
|
1376
|
-
message: "run_id is invalid",
|
|
1377
|
-
retryable: false
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
let session;
|
|
1383
|
-
try {
|
|
1384
|
-
session = await recommendConnectorImpl({
|
|
1385
|
-
host: normalized.host,
|
|
1386
|
-
port: normalized.port,
|
|
1387
|
-
targetUrlIncludes: normalized.targetUrlIncludes,
|
|
1388
|
-
allowNavigate: normalized.allowNavigate,
|
|
1389
|
-
slowLive: normalized.slowLive
|
|
1390
|
-
});
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
1393
|
-
return {
|
|
1394
|
-
status: "FAILED",
|
|
1395
|
-
error: {
|
|
1396
|
-
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1397
|
-
message: error?.message || "Boss recommend page is not ready",
|
|
1398
|
-
requires_login: Boolean(error?.requires_login),
|
|
1399
|
-
login_url: error?.login_url || null,
|
|
1400
|
-
login_detection: error?.login_detection || null,
|
|
1401
|
-
chrome: error?.chrome || null,
|
|
1402
|
-
current_url: error?.current_url || null,
|
|
1403
|
-
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
1404
|
-
retryable: true
|
|
1405
|
-
},
|
|
1406
|
-
chrome: error?.chrome || null
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
let started;
|
|
1411
|
-
try {
|
|
1412
|
-
started = recommendRunService.startRecommendRun({
|
|
1413
|
-
...getRunOptions(args, parsed, normalized, session, configResolution),
|
|
1414
|
-
runId: fixedRunId || undefined,
|
|
1415
|
-
pid: process.pid
|
|
1416
|
-
});
|
|
1417
|
-
} catch (error) {
|
|
1418
|
-
await session.close?.();
|
|
1419
|
-
return {
|
|
1420
|
-
status: "FAILED",
|
|
1421
|
-
error: {
|
|
1422
|
-
code: "RECOMMEND_RUN_START_FAILED",
|
|
1423
|
-
message: error?.message || "Failed to start recommend run",
|
|
1424
|
-
retryable: true
|
|
1425
|
-
}
|
|
1426
|
-
};
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
recommendRunMeta.set(started.runId, {
|
|
1430
|
-
session,
|
|
1431
|
-
methodLog: session.methodLog || [],
|
|
1432
|
-
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
1433
|
-
args: clonePlain(args, {}),
|
|
1434
|
-
normalized,
|
|
1435
|
-
parsed,
|
|
1436
|
-
chrome: {
|
|
1437
|
-
host: normalized.host,
|
|
1438
|
-
port: normalized.port,
|
|
1439
|
-
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
1440
|
-
target_id: session.target?.id || null,
|
|
1441
|
-
auto_launch: session.chrome || null
|
|
1442
|
-
},
|
|
1443
|
-
health: session.health || null
|
|
1444
|
-
});
|
|
1445
|
-
trackRecommendRun(started.runId);
|
|
1446
|
-
const persistedStarted = persistRecommendRunSnapshot(started);
|
|
1447
|
-
|
|
1448
|
-
return {
|
|
1449
|
-
status: "ACCEPTED",
|
|
1450
|
-
run_id: persistedStarted.run_id,
|
|
1451
|
-
state: persistedStarted.state,
|
|
1452
|
-
run: persistedStarted,
|
|
1453
|
-
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1454
|
-
review: parsed.review,
|
|
1455
|
-
message: normalized.postAction === "none"
|
|
1456
|
-
? "Recommend pipeline run started through the shared CDP-only recommend service. No post-action was requested."
|
|
1457
|
-
: `Recommend pipeline run started through the shared CDP-only recommend service with post_action=${normalized.postAction}${args.dry_run_post_action === true ? " in dry-run mode" : ""}.`,
|
|
1458
|
-
post_action: {
|
|
1459
|
-
requested: normalized.postAction,
|
|
1460
|
-
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1461
|
-
max_greet_count: normalized.maxGreetCount
|
|
1462
|
-
},
|
|
1463
|
-
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1468
|
-
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1469
|
-
if (prepared.response) return prepared.response;
|
|
1470
|
-
const { parsed, normalized } = prepared;
|
|
1471
|
-
return {
|
|
1472
|
-
status: "READY",
|
|
1473
|
-
review: parsed.review,
|
|
1474
|
-
post_action: {
|
|
1475
|
-
requested: normalized.postAction,
|
|
1476
|
-
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1477
|
-
max_greet_count: normalized.maxGreetCount
|
|
1478
|
-
},
|
|
1479
|
-
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {}, runId = "" } = {}) {
|
|
1484
|
-
const started = await startRecommendPipelineRunInternal(args, { workspaceRoot, runId });
|
|
1485
|
-
if (started.status !== "ACCEPTED") return started;
|
|
1486
|
-
return attachMethodEvidence(started, started.run_id);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
export function getRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1490
|
-
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1491
|
-
if (!runId) {
|
|
1492
|
-
return {
|
|
1493
|
-
status: "FAILED",
|
|
1494
|
-
error: {
|
|
1495
|
-
code: "INVALID_RUN_ID",
|
|
1496
|
-
message: "run_id is required",
|
|
1497
|
-
retryable: false
|
|
1498
|
-
}
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
try {
|
|
1502
|
-
const run = recommendRunService.getRecommendRun(runId);
|
|
1503
|
-
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1504
|
-
return attachMethodEvidence({
|
|
1505
|
-
status: "RUN_STATUS",
|
|
1506
|
-
run: normalizedRun
|
|
1507
|
-
}, runId);
|
|
1508
|
-
} catch {
|
|
1509
|
-
const persisted = readRecommendRunState(runId);
|
|
1510
|
-
if (persisted) {
|
|
1511
|
-
const reconciled = reconcilePersistedRecommendRunIfNeeded(persisted);
|
|
1512
|
-
return {
|
|
1513
|
-
status: "RUN_STATUS",
|
|
1514
|
-
run: reconciled,
|
|
1515
|
-
persistence: {
|
|
1516
|
-
source: "disk",
|
|
1517
|
-
active_control_available: false,
|
|
1518
|
-
stale_process_reconciled: reconciled?.state !== persisted.state
|
|
1519
|
-
},
|
|
1520
|
-
runtime_evaluate_used: false,
|
|
1521
|
-
method_summary: {},
|
|
1522
|
-
method_log: [],
|
|
1523
|
-
chrome: null
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
return {
|
|
1527
|
-
status: "FAILED",
|
|
1528
|
-
error: {
|
|
1529
|
-
code: "RUN_NOT_FOUND",
|
|
1530
|
-
message: `No recommend run found for run_id=${runId}`,
|
|
1531
|
-
retryable: false
|
|
1532
|
-
}
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1538
|
-
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1539
|
-
try {
|
|
1540
|
-
const before = recommendRunService.getRecommendRun(runId);
|
|
1541
|
-
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1542
|
-
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1543
|
-
return attachMethodEvidence({
|
|
1544
|
-
status: "PAUSE_IGNORED",
|
|
1545
|
-
run: normalizedBefore,
|
|
1546
|
-
message: "目标任务已结束,无需暂停。"
|
|
1547
|
-
}, runId);
|
|
1548
|
-
}
|
|
1549
|
-
if (before.status === RUN_STATUS_PAUSED) {
|
|
1550
|
-
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1551
|
-
return attachMethodEvidence({
|
|
1552
|
-
status: "PAUSE_IGNORED",
|
|
1553
|
-
run: normalizedBefore,
|
|
1554
|
-
message: "目标任务已经处于 paused 状态。"
|
|
1555
|
-
}, runId);
|
|
1556
|
-
}
|
|
1557
|
-
const run = recommendRunService.pauseRecommendRun(runId);
|
|
1558
|
-
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1559
|
-
return attachMethodEvidence({
|
|
1560
|
-
status: "PAUSE_REQUESTED",
|
|
1561
|
-
run: normalizedRun,
|
|
1562
|
-
message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
|
|
1563
|
-
}, runId);
|
|
1564
|
-
} catch {
|
|
1565
|
-
const persisted = readRecommendRunState(runId);
|
|
1566
|
-
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1567
|
-
return {
|
|
1568
|
-
status: "PAUSE_IGNORED",
|
|
1569
|
-
run: persisted,
|
|
1570
|
-
message: "目标任务已结束,无需暂停。",
|
|
1571
|
-
runtime_evaluate_used: false,
|
|
1572
|
-
method_summary: {},
|
|
1573
|
-
method_log: [],
|
|
1574
|
-
chrome: null
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
return getRecommendPipelineRunTool({ args });
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1582
|
-
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1583
|
-
try {
|
|
1584
|
-
const before = recommendRunService.getRecommendRun(runId);
|
|
1585
|
-
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1586
|
-
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1587
|
-
return attachMethodEvidence({
|
|
1588
|
-
status: "FAILED",
|
|
1589
|
-
error: {
|
|
1590
|
-
code: "RUN_ALREADY_TERMINATED",
|
|
1591
|
-
message: "目标任务已结束,无法继续。",
|
|
1592
|
-
retryable: false
|
|
1593
|
-
},
|
|
1594
|
-
run: normalizedBefore
|
|
1595
|
-
}, runId);
|
|
1596
|
-
}
|
|
1597
|
-
if (before.status !== RUN_STATUS_PAUSED) {
|
|
1598
|
-
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1599
|
-
return attachMethodEvidence({
|
|
1600
|
-
status: "FAILED",
|
|
1601
|
-
error: {
|
|
1602
|
-
code: "RUN_NOT_PAUSED",
|
|
1603
|
-
message: "仅 paused 状态的 run 才能继续。",
|
|
1604
|
-
retryable: true
|
|
1605
|
-
},
|
|
1606
|
-
run: normalizedBefore
|
|
1607
|
-
}, runId);
|
|
1608
|
-
}
|
|
1609
|
-
const run = recommendRunService.resumeRecommendRun(runId);
|
|
1610
|
-
const meta = getRecommendRunMeta(runId);
|
|
1611
|
-
if (meta) {
|
|
1612
|
-
meta.resumeCount = (meta.resumeCount || 0) + 1;
|
|
1613
|
-
meta.lastResumedAt = new Date().toISOString();
|
|
1614
|
-
}
|
|
1615
|
-
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1616
|
-
return attachMethodEvidence({
|
|
1617
|
-
status: "RESUME_REQUESTED",
|
|
1618
|
-
run: normalizedRun,
|
|
1619
|
-
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1620
|
-
message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
|
|
1621
|
-
}, runId);
|
|
1622
|
-
} catch {
|
|
1623
|
-
const persisted = readRecommendRunState(runId);
|
|
1624
|
-
if (persisted) {
|
|
1625
|
-
return {
|
|
1626
|
-
status: "FAILED",
|
|
1627
|
-
error: {
|
|
1628
|
-
code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
|
|
1629
|
-
message: TERMINAL_STATUSES.has(persisted.state)
|
|
1630
|
-
? "目标任务已结束,无法继续。"
|
|
1631
|
-
: "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
|
|
1632
|
-
retryable: !TERMINAL_STATUSES.has(persisted.state)
|
|
1633
|
-
},
|
|
1634
|
-
run: persisted,
|
|
1635
|
-
persistence: {
|
|
1636
|
-
source: "disk",
|
|
1637
|
-
active_control_available: false
|
|
1638
|
-
},
|
|
1639
|
-
runtime_evaluate_used: false,
|
|
1640
|
-
method_summary: {},
|
|
1641
|
-
method_log: [],
|
|
1642
|
-
chrome: null
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
return getRecommendPipelineRunTool({ args });
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1650
|
-
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1651
|
-
try {
|
|
1652
|
-
const before = recommendRunService.getRecommendRun(runId);
|
|
1653
|
-
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1654
|
-
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1655
|
-
return attachMethodEvidence({
|
|
1656
|
-
status: "CANCEL_IGNORED",
|
|
1657
|
-
run: normalizedBefore,
|
|
1658
|
-
message: "目标任务已结束,无需取消。"
|
|
1659
|
-
}, runId);
|
|
1660
|
-
}
|
|
1661
|
-
const run = recommendRunService.cancelRecommendRun(runId);
|
|
1662
|
-
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1663
|
-
return attachMethodEvidence({
|
|
1664
|
-
status: "CANCEL_REQUESTED",
|
|
1665
|
-
run: normalizedRun,
|
|
1666
|
-
message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
|
|
1667
|
-
}, runId);
|
|
1668
|
-
} catch {
|
|
1669
|
-
const persisted = readRecommendRunState(runId);
|
|
1670
|
-
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1671
|
-
return {
|
|
1672
|
-
status: "CANCEL_IGNORED",
|
|
1673
|
-
run: persisted,
|
|
1674
|
-
message: "目标任务已结束,无需取消。",
|
|
1675
|
-
runtime_evaluate_used: false,
|
|
1676
|
-
method_summary: {},
|
|
1677
|
-
method_log: [],
|
|
1678
|
-
chrome: null
|
|
1679
|
-
};
|
|
1680
|
-
}
|
|
1681
|
-
return getRecommendPipelineRunTool({ args });
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
export function getRecommendMcpHealthSnapshot(runId) {
|
|
1686
|
-
const meta = getRecommendRunMeta(runId);
|
|
1687
|
-
return {
|
|
1688
|
-
health: compactHealth(meta.health || null),
|
|
1689
|
-
chrome: meta.chrome || null,
|
|
1690
|
-
method_summary: methodSummary(meta.methodLog || [])
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
export function __setRecommendMcpConnectorForTests(nextConnector) {
|
|
1695
|
-
recommendConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectRecommendChromeSession;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
export function __setRecommendMcpJobReaderForTests(nextReader) {
|
|
1699
|
-
recommendJobReaderImpl = typeof nextReader === "function" ? nextReader : readRecommendJobOptionsFromSession;
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
export function __setRecommendMcpWorkflowForTests(nextWorkflow) {
|
|
1703
|
-
recommendWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecommendWorkflow;
|
|
1704
|
-
recommendRunService = createRecommendRunService({
|
|
1705
|
-
idPrefix: "mcp_recommend",
|
|
1706
|
-
workflow: (...args) => recommendWorkflowImpl(...args),
|
|
1707
|
-
onSnapshot: persistRecommendLifecycleSnapshot
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
export function __resetRecommendMcpStateForTests() {
|
|
1712
|
-
for (const meta of recommendRunMeta.values()) {
|
|
1713
|
-
try {
|
|
1714
|
-
meta.session?.close?.();
|
|
1715
|
-
} catch {
|
|
1716
|
-
// Best-effort test cleanup.
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
recommendRunMeta.clear();
|
|
1720
|
-
__setRecommendMcpConnectorForTests(null);
|
|
1721
|
-
__setRecommendMcpJobReaderForTests(null);
|
|
1722
|
-
__setRecommendMcpWorkflowForTests(null);
|
|
1723
|
-
}
|
|
1279
|
+
|
|
1280
|
+
function prepareRecommendPipelineStart(args = {}, { workspaceRoot = "" } = {}) {
|
|
1281
|
+
const parsed = parseRecommendPipelineRequest(args);
|
|
1282
|
+
const gate = evaluateRecommendPipelineGate(parsed, args);
|
|
1283
|
+
if (gate) return { response: gate };
|
|
1284
|
+
const configResolution = resolveBossScreeningConfig(workspaceRoot);
|
|
1285
|
+
const normalized = normalizeRecommendStartInput(args, parsed, configResolution);
|
|
1286
|
+
const debugTestOptions = collectRecommendDebugTestOptions(args, normalized);
|
|
1287
|
+
if (debugTestOptions.length && !isDebugTestMode(args)) {
|
|
1288
|
+
return {
|
|
1289
|
+
response: {
|
|
1290
|
+
status: "FAILED",
|
|
1291
|
+
error: {
|
|
1292
|
+
code: "DEBUG_TEST_MODE_REQUIRED",
|
|
1293
|
+
message: `这些参数属于调试/测试路径,正式 live run 不会默认启用:${debugTestOptions.join(", ")}。如确需测试,请显式传 debug_test_mode=true。`,
|
|
1294
|
+
retryable: false
|
|
1295
|
+
},
|
|
1296
|
+
debug_test_options: debugTestOptions
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
if (normalized.screeningMode === "llm" && !configResolution.ok) {
|
|
1301
|
+
return {
|
|
1302
|
+
response: {
|
|
1303
|
+
status: "FAILED",
|
|
1304
|
+
error: {
|
|
1305
|
+
code: "SCREEN_CONFIG_ERROR",
|
|
1306
|
+
message: configResolution.error?.message || "screening-config.json is required for LLM screening.",
|
|
1307
|
+
retryable: true
|
|
1308
|
+
},
|
|
1309
|
+
config_path: configResolution.config_path || null,
|
|
1310
|
+
candidate_paths: configResolution.candidate_paths || []
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
parsed,
|
|
1316
|
+
configResolution,
|
|
1317
|
+
normalized
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function closeRecommendRunSession(runId) {
|
|
1322
|
+
const meta = recommendRunMeta.get(runId);
|
|
1323
|
+
if (!meta || meta.closed) return;
|
|
1324
|
+
try {
|
|
1325
|
+
try {
|
|
1326
|
+
if (meta.session?.client) {
|
|
1327
|
+
await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
|
|
1328
|
+
}
|
|
1329
|
+
} catch {
|
|
1330
|
+
// Cleanup is best-effort once the run has settled.
|
|
1331
|
+
}
|
|
1332
|
+
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
1333
|
+
} finally {
|
|
1334
|
+
meta.closed = true;
|
|
1335
|
+
try {
|
|
1336
|
+
await meta.session?.close?.();
|
|
1337
|
+
} catch {
|
|
1338
|
+
// Nothing actionable for the caller once the run has settled.
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
async function waitForRecommendRunTerminal(runId) {
|
|
1344
|
+
while (true) {
|
|
1345
|
+
try {
|
|
1346
|
+
const snapshot = recommendRunService.getRecommendRun(runId);
|
|
1347
|
+
if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
|
|
1348
|
+
} catch {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
await sleep(1000);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function trackRecommendRun(runId) {
|
|
1356
|
+
waitForRecommendRunTerminal(runId)
|
|
1357
|
+
.then((terminal) => {
|
|
1358
|
+
if (terminal) persistRecommendRunSnapshot(terminal);
|
|
1359
|
+
})
|
|
1360
|
+
.catch(() => null)
|
|
1361
|
+
.finally(() => {
|
|
1362
|
+
closeRecommendRunSession(runId).catch(() => {});
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "", runId = "" } = {}) {
|
|
1367
|
+
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1368
|
+
if (prepared.response) return prepared.response;
|
|
1369
|
+
const { parsed, configResolution, normalized } = prepared;
|
|
1370
|
+
const fixedRunId = normalizeRunId(runId);
|
|
1371
|
+
if (runId && !fixedRunId) {
|
|
1372
|
+
return {
|
|
1373
|
+
status: "FAILED",
|
|
1374
|
+
error: {
|
|
1375
|
+
code: "INVALID_RUN_ID",
|
|
1376
|
+
message: "run_id is invalid",
|
|
1377
|
+
retryable: false
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
let session;
|
|
1383
|
+
try {
|
|
1384
|
+
session = await recommendConnectorImpl({
|
|
1385
|
+
host: normalized.host,
|
|
1386
|
+
port: normalized.port,
|
|
1387
|
+
targetUrlIncludes: normalized.targetUrlIncludes,
|
|
1388
|
+
allowNavigate: normalized.allowNavigate,
|
|
1389
|
+
slowLive: normalized.slowLive
|
|
1390
|
+
});
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
const loginRequired = error?.code === "BOSS_LOGIN_REQUIRED";
|
|
1393
|
+
return {
|
|
1394
|
+
status: "FAILED",
|
|
1395
|
+
error: {
|
|
1396
|
+
code: loginRequired ? "BOSS_LOGIN_REQUIRED" : "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1397
|
+
message: error?.message || "Boss recommend page is not ready",
|
|
1398
|
+
requires_login: Boolean(error?.requires_login),
|
|
1399
|
+
login_url: error?.login_url || null,
|
|
1400
|
+
login_detection: error?.login_detection || null,
|
|
1401
|
+
chrome: error?.chrome || null,
|
|
1402
|
+
current_url: error?.current_url || null,
|
|
1403
|
+
target_url: error?.target_url || RECOMMEND_TARGET_URL,
|
|
1404
|
+
retryable: true
|
|
1405
|
+
},
|
|
1406
|
+
chrome: error?.chrome || null
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
let started;
|
|
1411
|
+
try {
|
|
1412
|
+
started = recommendRunService.startRecommendRun({
|
|
1413
|
+
...getRunOptions(args, parsed, normalized, session, configResolution),
|
|
1414
|
+
runId: fixedRunId || undefined,
|
|
1415
|
+
pid: process.pid
|
|
1416
|
+
});
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
await session.close?.();
|
|
1419
|
+
return {
|
|
1420
|
+
status: "FAILED",
|
|
1421
|
+
error: {
|
|
1422
|
+
code: "RECOMMEND_RUN_START_FAILED",
|
|
1423
|
+
message: error?.message || "Failed to start recommend run",
|
|
1424
|
+
retryable: true
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
recommendRunMeta.set(started.runId, {
|
|
1430
|
+
session,
|
|
1431
|
+
methodLog: session.methodLog || [],
|
|
1432
|
+
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
1433
|
+
args: clonePlain(args, {}),
|
|
1434
|
+
normalized,
|
|
1435
|
+
parsed,
|
|
1436
|
+
chrome: {
|
|
1437
|
+
host: normalized.host,
|
|
1438
|
+
port: normalized.port,
|
|
1439
|
+
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
1440
|
+
target_id: session.target?.id || null,
|
|
1441
|
+
auto_launch: session.chrome || null
|
|
1442
|
+
},
|
|
1443
|
+
health: session.health || null
|
|
1444
|
+
});
|
|
1445
|
+
trackRecommendRun(started.runId);
|
|
1446
|
+
const persistedStarted = persistRecommendRunSnapshot(started);
|
|
1447
|
+
|
|
1448
|
+
return {
|
|
1449
|
+
status: "ACCEPTED",
|
|
1450
|
+
run_id: persistedStarted.run_id,
|
|
1451
|
+
state: persistedStarted.state,
|
|
1452
|
+
run: persistedStarted,
|
|
1453
|
+
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1454
|
+
review: parsed.review,
|
|
1455
|
+
message: normalized.postAction === "none"
|
|
1456
|
+
? "Recommend pipeline run started through the shared CDP-only recommend service. No post-action was requested."
|
|
1457
|
+
: `Recommend pipeline run started through the shared CDP-only recommend service with post_action=${normalized.postAction}${args.dry_run_post_action === true ? " in dry-run mode" : ""}.`,
|
|
1458
|
+
post_action: {
|
|
1459
|
+
requested: normalized.postAction,
|
|
1460
|
+
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1461
|
+
max_greet_count: normalized.maxGreetCount
|
|
1462
|
+
},
|
|
1463
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
export function prepareRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1468
|
+
const prepared = prepareRecommendPipelineStart(args, { workspaceRoot });
|
|
1469
|
+
if (prepared.response) return prepared.response;
|
|
1470
|
+
const { parsed, normalized } = prepared;
|
|
1471
|
+
return {
|
|
1472
|
+
status: "READY",
|
|
1473
|
+
review: parsed.review,
|
|
1474
|
+
post_action: {
|
|
1475
|
+
requested: normalized.postAction,
|
|
1476
|
+
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1477
|
+
max_greet_count: normalized.maxGreetCount
|
|
1478
|
+
},
|
|
1479
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {}, runId = "" } = {}) {
|
|
1484
|
+
const started = await startRecommendPipelineRunInternal(args, { workspaceRoot, runId });
|
|
1485
|
+
if (started.status !== "ACCEPTED") return started;
|
|
1486
|
+
return attachMethodEvidence(started, started.run_id);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
export function getRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1490
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1491
|
+
if (!runId) {
|
|
1492
|
+
return {
|
|
1493
|
+
status: "FAILED",
|
|
1494
|
+
error: {
|
|
1495
|
+
code: "INVALID_RUN_ID",
|
|
1496
|
+
message: "run_id is required",
|
|
1497
|
+
retryable: false
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
try {
|
|
1502
|
+
const run = recommendRunService.getRecommendRun(runId);
|
|
1503
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1504
|
+
return attachMethodEvidence({
|
|
1505
|
+
status: "RUN_STATUS",
|
|
1506
|
+
run: normalizedRun
|
|
1507
|
+
}, runId);
|
|
1508
|
+
} catch {
|
|
1509
|
+
const persisted = readRecommendRunState(runId);
|
|
1510
|
+
if (persisted) {
|
|
1511
|
+
const reconciled = reconcilePersistedRecommendRunIfNeeded(persisted);
|
|
1512
|
+
return {
|
|
1513
|
+
status: "RUN_STATUS",
|
|
1514
|
+
run: reconciled,
|
|
1515
|
+
persistence: {
|
|
1516
|
+
source: "disk",
|
|
1517
|
+
active_control_available: false,
|
|
1518
|
+
stale_process_reconciled: reconciled?.state !== persisted.state
|
|
1519
|
+
},
|
|
1520
|
+
runtime_evaluate_used: false,
|
|
1521
|
+
method_summary: {},
|
|
1522
|
+
method_log: [],
|
|
1523
|
+
chrome: null
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
status: "FAILED",
|
|
1528
|
+
error: {
|
|
1529
|
+
code: "RUN_NOT_FOUND",
|
|
1530
|
+
message: `No recommend run found for run_id=${runId}`,
|
|
1531
|
+
retryable: false
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1538
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1539
|
+
try {
|
|
1540
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1541
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1542
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1543
|
+
return attachMethodEvidence({
|
|
1544
|
+
status: "PAUSE_IGNORED",
|
|
1545
|
+
run: normalizedBefore,
|
|
1546
|
+
message: "目标任务已结束,无需暂停。"
|
|
1547
|
+
}, runId);
|
|
1548
|
+
}
|
|
1549
|
+
if (before.status === RUN_STATUS_PAUSED) {
|
|
1550
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1551
|
+
return attachMethodEvidence({
|
|
1552
|
+
status: "PAUSE_IGNORED",
|
|
1553
|
+
run: normalizedBefore,
|
|
1554
|
+
message: "目标任务已经处于 paused 状态。"
|
|
1555
|
+
}, runId);
|
|
1556
|
+
}
|
|
1557
|
+
const run = recommendRunService.pauseRecommendRun(runId);
|
|
1558
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1559
|
+
return attachMethodEvidence({
|
|
1560
|
+
status: "PAUSE_REQUESTED",
|
|
1561
|
+
run: normalizedRun,
|
|
1562
|
+
message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
|
|
1563
|
+
}, runId);
|
|
1564
|
+
} catch {
|
|
1565
|
+
const persisted = readRecommendRunState(runId);
|
|
1566
|
+
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1567
|
+
return {
|
|
1568
|
+
status: "PAUSE_IGNORED",
|
|
1569
|
+
run: persisted,
|
|
1570
|
+
message: "目标任务已结束,无需暂停。",
|
|
1571
|
+
runtime_evaluate_used: false,
|
|
1572
|
+
method_summary: {},
|
|
1573
|
+
method_log: [],
|
|
1574
|
+
chrome: null
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
return getRecommendPipelineRunTool({ args });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1582
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1583
|
+
try {
|
|
1584
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1585
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1586
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1587
|
+
return attachMethodEvidence({
|
|
1588
|
+
status: "FAILED",
|
|
1589
|
+
error: {
|
|
1590
|
+
code: "RUN_ALREADY_TERMINATED",
|
|
1591
|
+
message: "目标任务已结束,无法继续。",
|
|
1592
|
+
retryable: false
|
|
1593
|
+
},
|
|
1594
|
+
run: normalizedBefore
|
|
1595
|
+
}, runId);
|
|
1596
|
+
}
|
|
1597
|
+
if (before.status !== RUN_STATUS_PAUSED) {
|
|
1598
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1599
|
+
return attachMethodEvidence({
|
|
1600
|
+
status: "FAILED",
|
|
1601
|
+
error: {
|
|
1602
|
+
code: "RUN_NOT_PAUSED",
|
|
1603
|
+
message: "仅 paused 状态的 run 才能继续。",
|
|
1604
|
+
retryable: true
|
|
1605
|
+
},
|
|
1606
|
+
run: normalizedBefore
|
|
1607
|
+
}, runId);
|
|
1608
|
+
}
|
|
1609
|
+
const run = recommendRunService.resumeRecommendRun(runId);
|
|
1610
|
+
const meta = getRecommendRunMeta(runId);
|
|
1611
|
+
if (meta) {
|
|
1612
|
+
meta.resumeCount = (meta.resumeCount || 0) + 1;
|
|
1613
|
+
meta.lastResumedAt = new Date().toISOString();
|
|
1614
|
+
}
|
|
1615
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1616
|
+
return attachMethodEvidence({
|
|
1617
|
+
status: "RESUME_REQUESTED",
|
|
1618
|
+
run: normalizedRun,
|
|
1619
|
+
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1620
|
+
message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
|
|
1621
|
+
}, runId);
|
|
1622
|
+
} catch {
|
|
1623
|
+
const persisted = readRecommendRunState(runId);
|
|
1624
|
+
if (persisted) {
|
|
1625
|
+
return {
|
|
1626
|
+
status: "FAILED",
|
|
1627
|
+
error: {
|
|
1628
|
+
code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
|
|
1629
|
+
message: TERMINAL_STATUSES.has(persisted.state)
|
|
1630
|
+
? "目标任务已结束,无法继续。"
|
|
1631
|
+
: "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
|
|
1632
|
+
retryable: !TERMINAL_STATUSES.has(persisted.state)
|
|
1633
|
+
},
|
|
1634
|
+
run: persisted,
|
|
1635
|
+
persistence: {
|
|
1636
|
+
source: "disk",
|
|
1637
|
+
active_control_available: false
|
|
1638
|
+
},
|
|
1639
|
+
runtime_evaluate_used: false,
|
|
1640
|
+
method_summary: {},
|
|
1641
|
+
method_log: [],
|
|
1642
|
+
chrome: null
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
return getRecommendPipelineRunTool({ args });
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1650
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1651
|
+
try {
|
|
1652
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1653
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1654
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1655
|
+
return attachMethodEvidence({
|
|
1656
|
+
status: "CANCEL_IGNORED",
|
|
1657
|
+
run: normalizedBefore,
|
|
1658
|
+
message: "目标任务已结束,无需取消。"
|
|
1659
|
+
}, runId);
|
|
1660
|
+
}
|
|
1661
|
+
const run = recommendRunService.cancelRecommendRun(runId);
|
|
1662
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1663
|
+
return attachMethodEvidence({
|
|
1664
|
+
status: "CANCEL_REQUESTED",
|
|
1665
|
+
run: normalizedRun,
|
|
1666
|
+
message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
|
|
1667
|
+
}, runId);
|
|
1668
|
+
} catch {
|
|
1669
|
+
const persisted = readRecommendRunState(runId);
|
|
1670
|
+
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1671
|
+
return {
|
|
1672
|
+
status: "CANCEL_IGNORED",
|
|
1673
|
+
run: persisted,
|
|
1674
|
+
message: "目标任务已结束,无需取消。",
|
|
1675
|
+
runtime_evaluate_used: false,
|
|
1676
|
+
method_summary: {},
|
|
1677
|
+
method_log: [],
|
|
1678
|
+
chrome: null
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
return getRecommendPipelineRunTool({ args });
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
export function getRecommendMcpHealthSnapshot(runId) {
|
|
1686
|
+
const meta = getRecommendRunMeta(runId);
|
|
1687
|
+
return {
|
|
1688
|
+
health: compactHealth(meta.health || null),
|
|
1689
|
+
chrome: meta.chrome || null,
|
|
1690
|
+
method_summary: methodSummary(meta.methodLog || [])
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
export function __setRecommendMcpConnectorForTests(nextConnector) {
|
|
1695
|
+
recommendConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectRecommendChromeSession;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
export function __setRecommendMcpJobReaderForTests(nextReader) {
|
|
1699
|
+
recommendJobReaderImpl = typeof nextReader === "function" ? nextReader : readRecommendJobOptionsFromSession;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
export function __setRecommendMcpWorkflowForTests(nextWorkflow) {
|
|
1703
|
+
recommendWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecommendWorkflow;
|
|
1704
|
+
recommendRunService = createRecommendRunService({
|
|
1705
|
+
idPrefix: "mcp_recommend",
|
|
1706
|
+
workflow: (...args) => recommendWorkflowImpl(...args),
|
|
1707
|
+
onSnapshot: persistRecommendLifecycleSnapshot
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
export function __resetRecommendMcpStateForTests() {
|
|
1712
|
+
for (const meta of recommendRunMeta.values()) {
|
|
1713
|
+
try {
|
|
1714
|
+
meta.session?.close?.();
|
|
1715
|
+
} catch {
|
|
1716
|
+
// Best-effort test cleanup.
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
recommendRunMeta.clear();
|
|
1720
|
+
__setRecommendMcpConnectorForTests(null);
|
|
1721
|
+
__setRecommendMcpJobReaderForTests(null);
|
|
1722
|
+
__setRecommendMcpWorkflowForTests(null);
|
|
1723
|
+
}
|