@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1
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 +86 -33
- package/package.json +62 -9
- package/skills/boss-chat/SKILL.md +5 -4
- package/skills/boss-recommend-pipeline/SKILL.md +21 -31
- package/skills/boss-recruit-pipeline/README.md +17 -0
- package/skills/boss-recruit-pipeline/SKILL.md +55 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1254 -225
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +66 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +67 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -0,0 +1,1257 @@
|
|
|
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
|
+
connectToChromeTarget,
|
|
8
|
+
enableDomains,
|
|
9
|
+
sleep
|
|
10
|
+
} from "./core/browser/index.js";
|
|
11
|
+
import {
|
|
12
|
+
RUN_STATUS_CANCELING,
|
|
13
|
+
RUN_STATUS_CANCELED,
|
|
14
|
+
RUN_STATUS_COMPLETED,
|
|
15
|
+
RUN_STATUS_FAILED,
|
|
16
|
+
RUN_STATUS_PAUSED
|
|
17
|
+
} from "./core/run/index.js";
|
|
18
|
+
import {
|
|
19
|
+
buildLegacyScreenInputRows,
|
|
20
|
+
cloneReportInput,
|
|
21
|
+
writeLegacyScreenCsv
|
|
22
|
+
} from "./core/reporting/legacy-csv.js";
|
|
23
|
+
import {
|
|
24
|
+
buildRecommendSelfHealConfig,
|
|
25
|
+
HEALTH_STATUS,
|
|
26
|
+
resolveRecommendSelfHealRoots,
|
|
27
|
+
runSelfHealCheck
|
|
28
|
+
} from "./core/self-heal/index.js";
|
|
29
|
+
import {
|
|
30
|
+
closeRecommendJobDropdown,
|
|
31
|
+
closeRecommendDetail,
|
|
32
|
+
createRecommendRunService,
|
|
33
|
+
getRecommendRoots,
|
|
34
|
+
listRecommendJobOptions,
|
|
35
|
+
RECOMMEND_TARGET_URL,
|
|
36
|
+
runRecommendWorkflow
|
|
37
|
+
} from "./domains/recommend/index.js";
|
|
38
|
+
import {
|
|
39
|
+
parseRecommendInstruction
|
|
40
|
+
} from "./parser.js";
|
|
41
|
+
import { getRunsDir } from "./run-state.js";
|
|
42
|
+
|
|
43
|
+
const DEFAULT_RECOMMEND_HOST = "127.0.0.1";
|
|
44
|
+
const DEFAULT_RECOMMEND_PORT = 9222;
|
|
45
|
+
const DEFAULT_RECOMMEND_POLL_AFTER_SEC = 10;
|
|
46
|
+
const TARGET_COUNT_SEMANTICS = "target_count means processed recommend candidates, not passed candidates";
|
|
47
|
+
const RUN_MODE_ASYNC = "async";
|
|
48
|
+
|
|
49
|
+
const TERMINAL_STATUSES = new Set([
|
|
50
|
+
RUN_STATUS_COMPLETED,
|
|
51
|
+
RUN_STATUS_FAILED,
|
|
52
|
+
RUN_STATUS_CANCELED
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
let recommendWorkflowImpl = runRecommendWorkflow;
|
|
56
|
+
let recommendConnectorImpl = connectRecommendChromeSession;
|
|
57
|
+
let recommendJobReaderImpl = readRecommendJobOptionsFromSession;
|
|
58
|
+
let recommendRunService = createRecommendRunService({
|
|
59
|
+
idPrefix: "mcp_recommend",
|
|
60
|
+
workflow: (...args) => recommendWorkflowImpl(...args)
|
|
61
|
+
});
|
|
62
|
+
const recommendRunMeta = new Map();
|
|
63
|
+
|
|
64
|
+
function normalizeText(value) {
|
|
65
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parsePositiveInteger(raw, fallback) {
|
|
69
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
70
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseNonNegativeInteger(raw, fallback) {
|
|
74
|
+
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
75
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function methodSummary(methodLog = []) {
|
|
79
|
+
const summary = {};
|
|
80
|
+
for (const entry of methodLog || []) {
|
|
81
|
+
summary[entry.method] = (summary[entry.method] || 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
return summary;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clonePlain(value, fallback = null) {
|
|
87
|
+
try {
|
|
88
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
89
|
+
} catch {
|
|
90
|
+
return fallback;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeRunId(runId) {
|
|
95
|
+
const normalized = normalizeText(runId);
|
|
96
|
+
if (!normalized || normalized.includes("/") || normalized.includes("\\")) return "";
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getRecommendRunArtifacts(runId) {
|
|
101
|
+
const normalized = normalizeRunId(runId);
|
|
102
|
+
if (!normalized) return null;
|
|
103
|
+
const runsDir = getRunsDir();
|
|
104
|
+
return {
|
|
105
|
+
runs_dir: runsDir,
|
|
106
|
+
run_state_path: path.join(runsDir, `${normalized}.json`),
|
|
107
|
+
checkpoint_path: path.join(runsDir, `${normalized}.checkpoint.json`),
|
|
108
|
+
output_csv: path.join(runsDir, `${normalized}.results.csv`),
|
|
109
|
+
report_json: path.join(runsDir, `${normalized}.report.json`)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureDirectory(dirPath) {
|
|
114
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeJsonAtomic(filePath, payload) {
|
|
118
|
+
ensureDirectory(path.dirname(filePath));
|
|
119
|
+
const tempPath = `${filePath}.tmp`;
|
|
120
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
121
|
+
fs.renameSync(tempPath, filePath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readJsonFile(filePath) {
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(filePath)) return null;
|
|
127
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
128
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function recommendSearchParamsForCsv(searchParams = {}) {
|
|
135
|
+
return {
|
|
136
|
+
school_tag: Object.prototype.hasOwnProperty.call(searchParams, "school_tag") ? searchParams.school_tag : "不限",
|
|
137
|
+
degree: Object.prototype.hasOwnProperty.call(searchParams, "degree") ? searchParams.degree : "不限",
|
|
138
|
+
gender: Object.prototype.hasOwnProperty.call(searchParams, "gender") ? searchParams.gender : "不限",
|
|
139
|
+
recent_not_view: Object.prototype.hasOwnProperty.call(searchParams, "recent_not_view") ? searchParams.recent_not_view : "不限"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectedRecommendJobForCsv(meta = {}) {
|
|
144
|
+
const value = normalizeText(
|
|
145
|
+
meta.args?.confirmation?.job_value
|
|
146
|
+
|| meta.normalized?.job
|
|
147
|
+
|| meta.args?.overrides?.job
|
|
148
|
+
|| ""
|
|
149
|
+
);
|
|
150
|
+
return {
|
|
151
|
+
value,
|
|
152
|
+
title: value,
|
|
153
|
+
label: value
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildRecommendCsvInputRows(snapshot = {}, meta = {}) {
|
|
158
|
+
const searchParams = recommendSearchParamsForCsv(meta.parsed?.searchParams || {});
|
|
159
|
+
const screenParams = meta.parsed?.screenParams || {};
|
|
160
|
+
return buildLegacyScreenInputRows({
|
|
161
|
+
instruction: meta.args?.instruction || "",
|
|
162
|
+
selectedPage: "recommend",
|
|
163
|
+
selectedJob: selectedRecommendJobForCsv(meta),
|
|
164
|
+
userSearchParams: cloneReportInput(searchParams, {}),
|
|
165
|
+
effectiveSearchParams: cloneReportInput(searchParams, {}),
|
|
166
|
+
screenParams: {
|
|
167
|
+
criteria: screenParams.criteria || meta.normalized?.criteria || "",
|
|
168
|
+
target_count: screenParams.target_count || snapshot.progress?.target_count || meta.normalized?.targetCount || "",
|
|
169
|
+
post_action: screenParams.post_action || "none",
|
|
170
|
+
max_greet_count: screenParams.max_greet_count ?? ""
|
|
171
|
+
},
|
|
172
|
+
followUp: meta.args?.follow_up || meta.args?.overrides?.follow_up || null
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeRecommendLegacyCsvAtomic(filePath, rows = [], snapshot = {}, meta = {}) {
|
|
177
|
+
writeLegacyScreenCsv(filePath, {
|
|
178
|
+
inputRows: buildRecommendCsvInputRows(snapshot, meta),
|
|
179
|
+
results: rows
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readRecommendRunState(runId) {
|
|
184
|
+
const artifacts = getRecommendRunArtifacts(runId);
|
|
185
|
+
if (!artifacts) return null;
|
|
186
|
+
return readJsonFile(artifacts.run_state_path);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getRecommendRunMeta(runId) {
|
|
190
|
+
return recommendRunMeta.get(runId) || {};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function toIsoOrNull(value) {
|
|
194
|
+
const normalized = normalizeText(value);
|
|
195
|
+
return normalized || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function secondsBetween(startedAt, endedAt) {
|
|
199
|
+
const startMs = Date.parse(startedAt || "");
|
|
200
|
+
const endMs = Date.parse(endedAt || "") || Date.now();
|
|
201
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
|
|
202
|
+
return Math.max(1, Math.round((endMs - startMs) / 1000));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeLegacyProgress(progress = {}, summary = null) {
|
|
206
|
+
const processed = Number.isInteger(progress.processed)
|
|
207
|
+
? progress.processed
|
|
208
|
+
: Number.isInteger(summary?.processed)
|
|
209
|
+
? summary.processed
|
|
210
|
+
: 0;
|
|
211
|
+
const screened = Number.isInteger(progress.screened)
|
|
212
|
+
? progress.screened
|
|
213
|
+
: Number.isInteger(summary?.screened)
|
|
214
|
+
? summary.screened
|
|
215
|
+
: processed;
|
|
216
|
+
const passed = Number.isInteger(progress.passed)
|
|
217
|
+
? progress.passed
|
|
218
|
+
: Number.isInteger(summary?.passed)
|
|
219
|
+
? summary.passed
|
|
220
|
+
: 0;
|
|
221
|
+
return {
|
|
222
|
+
...progress,
|
|
223
|
+
processed,
|
|
224
|
+
inspected: processed,
|
|
225
|
+
screened,
|
|
226
|
+
passed,
|
|
227
|
+
skipped: Number.isInteger(progress.skipped) ? progress.skipped : Math.max(processed - passed, 0),
|
|
228
|
+
greet_count: Number.isInteger(progress.greet_count) ? progress.greet_count : 0,
|
|
229
|
+
post_action_clicked: Number.isInteger(progress.post_action_clicked) ? progress.post_action_clicked : 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function completionReason(status) {
|
|
234
|
+
if (status === RUN_STATUS_COMPLETED) return "completed";
|
|
235
|
+
if (status === RUN_STATUS_CANCELED) return "canceled_by_user";
|
|
236
|
+
if (status === RUN_STATUS_FAILED) return "failed";
|
|
237
|
+
if (status === RUN_STATUS_PAUSED) return "paused";
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ensureRecommendRunArtifacts(snapshot) {
|
|
242
|
+
const artifacts = getRecommendRunArtifacts(snapshot?.runId || snapshot?.run_id);
|
|
243
|
+
if (!artifacts) return null;
|
|
244
|
+
|
|
245
|
+
const meta = getRecommendRunMeta(snapshot?.runId || snapshot?.run_id);
|
|
246
|
+
const checkpoint = snapshot?.checkpoint && typeof snapshot.checkpoint === "object"
|
|
247
|
+
? snapshot.checkpoint
|
|
248
|
+
: {};
|
|
249
|
+
writeJsonAtomic(artifacts.checkpoint_path, checkpoint);
|
|
250
|
+
if (meta) meta.checkpointPath = artifacts.checkpoint_path;
|
|
251
|
+
|
|
252
|
+
const summary = snapshot?.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
253
|
+
if (summary) {
|
|
254
|
+
const rows = Array.isArray(summary.results) ? summary.results : [];
|
|
255
|
+
writeRecommendLegacyCsvAtomic(artifacts.output_csv, rows, snapshot, meta);
|
|
256
|
+
writeJsonAtomic(artifacts.report_json, {
|
|
257
|
+
run_id: snapshot.runId || snapshot.run_id,
|
|
258
|
+
status: snapshot.status || snapshot.state,
|
|
259
|
+
phase: snapshot.phase || snapshot.stage,
|
|
260
|
+
progress: snapshot.progress || {},
|
|
261
|
+
context: snapshot.context || {},
|
|
262
|
+
checkpoint,
|
|
263
|
+
summary,
|
|
264
|
+
generated_at: new Date().toISOString()
|
|
265
|
+
});
|
|
266
|
+
if (meta) {
|
|
267
|
+
meta.outputCsvPath = artifacts.output_csv;
|
|
268
|
+
meta.reportJsonPath = artifacts.report_json;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return artifacts;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildLegacyRecommendResult(snapshot) {
|
|
276
|
+
if (!snapshot) return null;
|
|
277
|
+
const artifacts = ensureRecommendRunArtifacts(snapshot);
|
|
278
|
+
const meta = getRecommendRunMeta(snapshot.runId);
|
|
279
|
+
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
280
|
+
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
281
|
+
const targetCount = Number.isInteger(progress.target_count)
|
|
282
|
+
? progress.target_count
|
|
283
|
+
: Number.isInteger(snapshot.context?.max_candidates)
|
|
284
|
+
? snapshot.context.max_candidates
|
|
285
|
+
: meta.parsed?.screenParams?.target_count || null;
|
|
286
|
+
return {
|
|
287
|
+
status: snapshot.status === RUN_STATUS_COMPLETED
|
|
288
|
+
? "COMPLETED"
|
|
289
|
+
: snapshot.status === RUN_STATUS_CANCELED
|
|
290
|
+
? "CANCELED"
|
|
291
|
+
: snapshot.status === RUN_STATUS_PAUSED
|
|
292
|
+
? "PAUSED"
|
|
293
|
+
: snapshot.status === RUN_STATUS_FAILED
|
|
294
|
+
? "FAILED"
|
|
295
|
+
: snapshot.status,
|
|
296
|
+
run_id: snapshot.runId,
|
|
297
|
+
completion_reason: completionReason(snapshot.status),
|
|
298
|
+
requested_count: targetCount,
|
|
299
|
+
processed_count: progress.processed,
|
|
300
|
+
inspected_count: progress.processed,
|
|
301
|
+
screened_count: progress.screened,
|
|
302
|
+
passed_count: progress.passed,
|
|
303
|
+
skipped_count: progress.skipped,
|
|
304
|
+
detail_opened: progress.detail_opened || summary?.detail_opened || 0,
|
|
305
|
+
greet_count: progress.greet_count || 0,
|
|
306
|
+
post_action_clicked: progress.post_action_clicked || summary?.post_action_clicked || 0,
|
|
307
|
+
output_csv: artifacts?.output_csv || meta.outputCsvPath || null,
|
|
308
|
+
report_json: artifacts?.report_json || meta.reportJsonPath || null,
|
|
309
|
+
checkpoint_path: artifacts?.checkpoint_path || meta.checkpointPath || null,
|
|
310
|
+
started_at: snapshot.startedAt,
|
|
311
|
+
completed_at: snapshot.completedAt || null,
|
|
312
|
+
duration_sec: secondsBetween(snapshot.startedAt, snapshot.completedAt),
|
|
313
|
+
selected_job: {
|
|
314
|
+
title: meta.normalized?.job || meta.args?.confirmation?.job_value || meta.args?.overrides?.job || ""
|
|
315
|
+
},
|
|
316
|
+
selected_page_scope: summary?.page_scope || {
|
|
317
|
+
requested_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend",
|
|
318
|
+
effective_scope: meta.normalized?.pageScope || meta.parsed?.page_scope || "recommend"
|
|
319
|
+
},
|
|
320
|
+
search_params: clonePlain(meta.parsed?.searchParams || {}, {}),
|
|
321
|
+
screen_params: clonePlain(meta.parsed?.screenParams || {}, {}),
|
|
322
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS,
|
|
323
|
+
error: snapshot.error || null,
|
|
324
|
+
results: Array.isArray(summary?.results) ? summary.results : []
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeRunSnapshot(snapshot) {
|
|
329
|
+
if (!snapshot) return null;
|
|
330
|
+
const meta = getRecommendRunMeta(snapshot.runId);
|
|
331
|
+
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : null;
|
|
332
|
+
const progress = normalizeLegacyProgress(snapshot.progress, summary);
|
|
333
|
+
const legacyResult = (
|
|
334
|
+
TERMINAL_STATUSES.has(snapshot.status)
|
|
335
|
+
|| snapshot.status === RUN_STATUS_PAUSED
|
|
336
|
+
) ? buildLegacyRecommendResult({ ...snapshot, progress }) : null;
|
|
337
|
+
const oldContext = {
|
|
338
|
+
workspace_root: meta.workspaceRoot || null,
|
|
339
|
+
instruction: meta.args?.instruction || "",
|
|
340
|
+
confirmation: clonePlain(meta.args?.confirmation || {}, {}),
|
|
341
|
+
overrides: clonePlain(meta.args?.overrides || {}, {}),
|
|
342
|
+
follow_up: clonePlain(meta.args?.follow_up || {}, {}),
|
|
343
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
...snapshot,
|
|
347
|
+
progress,
|
|
348
|
+
run_id: snapshot.runId,
|
|
349
|
+
mode: RUN_MODE_ASYNC,
|
|
350
|
+
state: snapshot.status,
|
|
351
|
+
stage: snapshot.phase,
|
|
352
|
+
started_at: snapshot.startedAt,
|
|
353
|
+
updated_at: snapshot.updatedAt,
|
|
354
|
+
completed_at: toIsoOrNull(snapshot.completedAt),
|
|
355
|
+
heartbeat_at: snapshot.updatedAt,
|
|
356
|
+
pid: process.pid || null,
|
|
357
|
+
last_message: snapshot.error?.message || snapshot.phase || null,
|
|
358
|
+
context: {
|
|
359
|
+
...(snapshot.context || {}),
|
|
360
|
+
...oldContext,
|
|
361
|
+
shared_run_context: snapshot.context || {}
|
|
362
|
+
},
|
|
363
|
+
control: {
|
|
364
|
+
pause_requested: snapshot.status === RUN_STATUS_PAUSED,
|
|
365
|
+
pause_requested_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null,
|
|
366
|
+
pause_requested_by: snapshot.status === RUN_STATUS_PAUSED ? "pause_recommend_pipeline_run" : null,
|
|
367
|
+
cancel_requested: snapshot.status === RUN_STATUS_CANCELING
|
|
368
|
+
},
|
|
369
|
+
resume: {
|
|
370
|
+
checkpoint_path: legacyResult?.checkpoint_path || null,
|
|
371
|
+
pause_control_path: getRecommendRunArtifacts(snapshot.runId)?.run_state_path || null,
|
|
372
|
+
output_csv: legacyResult?.output_csv || null,
|
|
373
|
+
resume_count: meta.resumeCount || 0,
|
|
374
|
+
last_resumed_at: meta.lastResumedAt || null,
|
|
375
|
+
last_paused_at: snapshot.status === RUN_STATUS_PAUSED ? snapshot.updatedAt : null
|
|
376
|
+
},
|
|
377
|
+
result: legacyResult,
|
|
378
|
+
artifacts: getRecommendRunArtifacts(snapshot.runId)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function persistRecommendRunSnapshot(snapshot) {
|
|
383
|
+
const normalized = normalizeRunSnapshot(snapshot);
|
|
384
|
+
if (!normalized?.run_id) return normalized;
|
|
385
|
+
const artifacts = getRecommendRunArtifacts(normalized.run_id);
|
|
386
|
+
if (!artifacts) return normalized;
|
|
387
|
+
const payload = {
|
|
388
|
+
run_id: normalized.run_id,
|
|
389
|
+
mode: normalized.mode,
|
|
390
|
+
state: normalized.state,
|
|
391
|
+
status: normalized.status,
|
|
392
|
+
stage: normalized.stage,
|
|
393
|
+
started_at: normalized.started_at,
|
|
394
|
+
updated_at: normalized.updated_at,
|
|
395
|
+
heartbeat_at: normalized.heartbeat_at,
|
|
396
|
+
completed_at: normalized.completed_at,
|
|
397
|
+
pid: normalized.pid,
|
|
398
|
+
progress: normalized.progress,
|
|
399
|
+
last_message: normalized.last_message,
|
|
400
|
+
context: normalized.context,
|
|
401
|
+
control: normalized.control,
|
|
402
|
+
resume: normalized.resume,
|
|
403
|
+
error: normalized.error,
|
|
404
|
+
result: normalized.result,
|
|
405
|
+
summary: normalized.summary,
|
|
406
|
+
artifacts: normalized.artifacts
|
|
407
|
+
};
|
|
408
|
+
writeJsonAtomic(artifacts.run_state_path, payload);
|
|
409
|
+
return normalized;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function attachMethodEvidence(payload, runId) {
|
|
413
|
+
const meta = getRecommendRunMeta(runId);
|
|
414
|
+
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
415
|
+
return {
|
|
416
|
+
...payload,
|
|
417
|
+
runtime_evaluate_used: false,
|
|
418
|
+
method_summary: methodSummary(meta.methodLog || []),
|
|
419
|
+
method_log: meta.methodLog || [],
|
|
420
|
+
chrome: meta.chrome || null
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function compactRecommendJobListOption(option, index) {
|
|
425
|
+
const label = normalizeText(option?.label);
|
|
426
|
+
const name = normalizeText(option?.label_without_salary || label);
|
|
427
|
+
return {
|
|
428
|
+
index,
|
|
429
|
+
name,
|
|
430
|
+
label,
|
|
431
|
+
label_without_salary: name,
|
|
432
|
+
current: Boolean(option?.current),
|
|
433
|
+
visible: Boolean(option?.visible)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function readRecommendJobOptionsFromSession(session) {
|
|
438
|
+
const client = session?.client;
|
|
439
|
+
if (!client) throw new Error("Recommend Chrome session is missing a CDP client");
|
|
440
|
+
const rootState = await getRecommendRoots(client);
|
|
441
|
+
const frameNodeId = rootState?.iframe?.documentNodeId;
|
|
442
|
+
if (!frameNodeId) throw new Error("recommendFrame iframe document was not found");
|
|
443
|
+
|
|
444
|
+
let options = [];
|
|
445
|
+
try {
|
|
446
|
+
options = await listRecommendJobOptions(client, frameNodeId, {
|
|
447
|
+
openDropdown: true
|
|
448
|
+
});
|
|
449
|
+
} finally {
|
|
450
|
+
await closeRecommendJobDropdown(client).catch(() => {});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const compacted = [];
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
for (const option of options) {
|
|
456
|
+
const compact = compactRecommendJobListOption(option, compacted.length);
|
|
457
|
+
if (!compact.name && !compact.label) continue;
|
|
458
|
+
const key = `${compact.name}\n${compact.label}`;
|
|
459
|
+
if (seen.has(key)) continue;
|
|
460
|
+
seen.add(key);
|
|
461
|
+
compacted.push(compact);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
source: "recommend_job_dropdown",
|
|
466
|
+
selector: "recommend job selection dropdown",
|
|
467
|
+
job_options: compacted,
|
|
468
|
+
selected_job: compacted.find((option) => option.current) || null
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function listRecommendJobsTool({ workspaceRoot = "", args = {} } = {}) {
|
|
473
|
+
const host = normalizeText(args.host) || DEFAULT_RECOMMEND_HOST;
|
|
474
|
+
const port = parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT);
|
|
475
|
+
const targetUrlIncludes = normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL;
|
|
476
|
+
const allowNavigate = args.allow_navigate !== false;
|
|
477
|
+
const slowLive = args.slow_live === true;
|
|
478
|
+
let session;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
session = await recommendConnectorImpl({
|
|
482
|
+
host,
|
|
483
|
+
port,
|
|
484
|
+
targetUrlIncludes,
|
|
485
|
+
allowNavigate,
|
|
486
|
+
slowLive
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const jobs = await recommendJobReaderImpl(session, {
|
|
490
|
+
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
491
|
+
args: clonePlain(args, {}),
|
|
492
|
+
normalized: {
|
|
493
|
+
host,
|
|
494
|
+
port,
|
|
495
|
+
targetUrlIncludes,
|
|
496
|
+
allowNavigate,
|
|
497
|
+
slowLive
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
const jobOptions = Array.isArray(jobs?.job_options) ? jobs.job_options : [];
|
|
501
|
+
assertNoForbiddenCdpCalls(session.methodLog || []);
|
|
502
|
+
return {
|
|
503
|
+
status: "OK",
|
|
504
|
+
stage: "recommend_job_list",
|
|
505
|
+
cdp_only: true,
|
|
506
|
+
runtime_evaluate_used: false,
|
|
507
|
+
page_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
508
|
+
job_count: jobOptions.length,
|
|
509
|
+
job_names: jobOptions.map((option) => option.name || option.label).filter(Boolean),
|
|
510
|
+
job_full_labels: jobOptions.map((option) => option.label || option.name).filter(Boolean),
|
|
511
|
+
job_options: jobOptions,
|
|
512
|
+
selected_job: jobs?.selected_job || jobOptions.find((option) => option.current) || null,
|
|
513
|
+
source: jobs?.source || "recommend_job_dropdown",
|
|
514
|
+
selector: jobs?.selector || "",
|
|
515
|
+
message: "已通过 CDP-only 从推荐页岗位下拉框读取可用岗位。Cron/一次性任务里的 job 参数优先使用 job_names 中的完整岗位名。",
|
|
516
|
+
chrome: {
|
|
517
|
+
host,
|
|
518
|
+
port,
|
|
519
|
+
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
520
|
+
target_id: session.target?.id || null
|
|
521
|
+
},
|
|
522
|
+
method_summary: methodSummary(session.methodLog || []),
|
|
523
|
+
method_log: session.methodLog || []
|
|
524
|
+
};
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const methodLog = session?.methodLog || [];
|
|
527
|
+
return {
|
|
528
|
+
status: "FAILED",
|
|
529
|
+
stage: "recommend_job_list",
|
|
530
|
+
cdp_only: true,
|
|
531
|
+
runtime_evaluate_used: methodLog.some((entry) => String(entry?.method || entry).startsWith("Runtime.")),
|
|
532
|
+
error: {
|
|
533
|
+
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
534
|
+
message: error?.message || "Failed to read recommend job list",
|
|
535
|
+
retryable: true
|
|
536
|
+
},
|
|
537
|
+
chrome: {
|
|
538
|
+
host,
|
|
539
|
+
port,
|
|
540
|
+
target_url: targetUrlIncludes
|
|
541
|
+
},
|
|
542
|
+
method_summary: methodSummary(methodLog),
|
|
543
|
+
method_log: methodLog
|
|
544
|
+
};
|
|
545
|
+
} finally {
|
|
546
|
+
if (session) {
|
|
547
|
+
try {
|
|
548
|
+
await session.close?.();
|
|
549
|
+
} catch {
|
|
550
|
+
// Best-effort cleanup after a read-only helper.
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function compactHealth(check) {
|
|
557
|
+
if (!check) return null;
|
|
558
|
+
return {
|
|
559
|
+
status: check.status,
|
|
560
|
+
summary: check.summary,
|
|
561
|
+
drift_report: check.drift_report,
|
|
562
|
+
probes: (check.probes || []).map((probe) => ({
|
|
563
|
+
id: probe.id,
|
|
564
|
+
type: probe.type,
|
|
565
|
+
status: probe.status,
|
|
566
|
+
count: probe.count,
|
|
567
|
+
required: probe.required
|
|
568
|
+
}))
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function waitForHealthyRecommend(client, config, {
|
|
573
|
+
timeoutMs = 90000,
|
|
574
|
+
intervalMs = 1000
|
|
575
|
+
} = {}) {
|
|
576
|
+
const started = Date.now();
|
|
577
|
+
let lastCheck = null;
|
|
578
|
+
while (Date.now() - started <= timeoutMs) {
|
|
579
|
+
const roots = await resolveRecommendSelfHealRoots(client, config);
|
|
580
|
+
lastCheck = await runSelfHealCheck({
|
|
581
|
+
client,
|
|
582
|
+
domain: "recommend",
|
|
583
|
+
roots: roots.roots,
|
|
584
|
+
selectorProbes: config.selectorProbes,
|
|
585
|
+
accessibilityProbes: config.accessibilityProbes
|
|
586
|
+
});
|
|
587
|
+
if (lastCheck.status === HEALTH_STATUS.HEALTHY) return lastCheck;
|
|
588
|
+
await sleep(intervalMs);
|
|
589
|
+
}
|
|
590
|
+
return lastCheck;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function shouldNavigateToRecommend(url) {
|
|
594
|
+
return !String(url || "").includes("/web/chat/recommend");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function connectRecommendChromeSession({
|
|
598
|
+
host = DEFAULT_RECOMMEND_HOST,
|
|
599
|
+
port = DEFAULT_RECOMMEND_PORT,
|
|
600
|
+
targetUrlIncludes = RECOMMEND_TARGET_URL,
|
|
601
|
+
allowNavigate = true,
|
|
602
|
+
slowLive = false
|
|
603
|
+
} = {}) {
|
|
604
|
+
let session;
|
|
605
|
+
try {
|
|
606
|
+
session = await connectToChromeTarget({
|
|
607
|
+
host,
|
|
608
|
+
port,
|
|
609
|
+
targetUrlIncludes
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (!allowNavigate) throw error;
|
|
613
|
+
session = await connectToChromeTarget({
|
|
614
|
+
host,
|
|
615
|
+
port,
|
|
616
|
+
targetPredicate: (target) => (
|
|
617
|
+
target?.type === "page"
|
|
618
|
+
&& String(target?.url || "").includes("zhipin.com/web/chat")
|
|
619
|
+
)
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const { client, target } = session;
|
|
624
|
+
await enableDomains(client, ["Page", "DOM", "Input", "Network", "Accessibility"]);
|
|
625
|
+
if (typeof client?.Network?.setCacheDisabled === "function") {
|
|
626
|
+
await client.Network.setCacheDisabled({ cacheDisabled: true });
|
|
627
|
+
}
|
|
628
|
+
await bringPageToFront(client);
|
|
629
|
+
|
|
630
|
+
const targetUrl = String(target?.url || "");
|
|
631
|
+
let navigation = {
|
|
632
|
+
navigated: false,
|
|
633
|
+
url: targetUrl
|
|
634
|
+
};
|
|
635
|
+
if (allowNavigate && shouldNavigateToRecommend(targetUrl)) {
|
|
636
|
+
await client.Page.navigate({ url: RECOMMEND_TARGET_URL });
|
|
637
|
+
const settleMs = slowLive ? 12000 : 5000;
|
|
638
|
+
await sleep(settleMs);
|
|
639
|
+
navigation = {
|
|
640
|
+
navigated: true,
|
|
641
|
+
url: RECOMMEND_TARGET_URL,
|
|
642
|
+
settle_ms: settleMs
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const selfHealConfig = buildRecommendSelfHealConfig();
|
|
647
|
+
const health = await waitForHealthyRecommend(client, selfHealConfig, {
|
|
648
|
+
timeoutMs: slowLive ? 180000 : 90000,
|
|
649
|
+
intervalMs: slowLive ? 1200 : 800
|
|
650
|
+
});
|
|
651
|
+
if (!health || health.status !== HEALTH_STATUS.HEALTHY) {
|
|
652
|
+
throw new Error(`Boss recommend page is not healthy: ${health?.status || "missing"}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
...session,
|
|
657
|
+
navigation,
|
|
658
|
+
health
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function parseRecommendPipelineRequest(args = {}) {
|
|
663
|
+
return parseRecommendInstruction({
|
|
664
|
+
instruction: args.instruction,
|
|
665
|
+
confirmation: args.confirmation,
|
|
666
|
+
overrides: args.overrides
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function buildRequiredConfirmations(parsed, args = {}) {
|
|
671
|
+
const required = [];
|
|
672
|
+
if (parsed.needs_page_confirmation) required.push("page_scope");
|
|
673
|
+
if (parsed.needs_filters_confirmation) required.push("filters");
|
|
674
|
+
if (parsed.needs_school_tag_confirmation) required.push("school_tag");
|
|
675
|
+
if (parsed.needs_degree_confirmation) required.push("degree");
|
|
676
|
+
if (parsed.needs_gender_confirmation) required.push("gender");
|
|
677
|
+
if (parsed.needs_recent_not_view_confirmation) required.push("recent_not_view");
|
|
678
|
+
if (parsed.needs_criteria_confirmation) required.push("criteria");
|
|
679
|
+
if (parsed.needs_target_count_confirmation) required.push("target_count");
|
|
680
|
+
if (parsed.needs_post_action_confirmation) required.push("post_action");
|
|
681
|
+
if (parsed.needs_max_greet_count_confirmation) required.push("max_greet_count");
|
|
682
|
+
if ((parsed.suspicious_fields || []).length) required.push("suspicious_fields");
|
|
683
|
+
|
|
684
|
+
const confirmation = args.confirmation || {};
|
|
685
|
+
const jobValue = normalizeText(confirmation.job_value || args.overrides?.job || "");
|
|
686
|
+
if (confirmation.job_confirmed !== true || !jobValue) required.push("job");
|
|
687
|
+
if (confirmation.final_confirmed !== true) required.push("final_review");
|
|
688
|
+
return Array.from(new Set(required));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function buildJobPendingQuestion(args = {}) {
|
|
692
|
+
const value = normalizeText(args.confirmation?.job_value || args.overrides?.job || "");
|
|
693
|
+
return {
|
|
694
|
+
field: "job",
|
|
695
|
+
question: "请确认推荐页岗位。CDP-only rewrite 会先切换到该岗位,再按所选页面范围执行筛选。",
|
|
696
|
+
value: value || null
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function buildFinalReviewQuestion(parsed) {
|
|
701
|
+
return {
|
|
702
|
+
field: "final_review",
|
|
703
|
+
question: "请最终确认本次推荐页筛选参数无误,并明确 final_confirmed=true 后再启动。",
|
|
704
|
+
value: {
|
|
705
|
+
page_scope: parsed.page_scope,
|
|
706
|
+
search_params: parsed.searchParams,
|
|
707
|
+
screen_params: parsed.screenParams
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function buildNeedInputResponse(parsed) {
|
|
713
|
+
return {
|
|
714
|
+
status: "NEED_INPUT",
|
|
715
|
+
missing_fields: parsed.missing_fields,
|
|
716
|
+
required_confirmations: buildRequiredConfirmations(parsed),
|
|
717
|
+
search_params: parsed.searchParams,
|
|
718
|
+
screen_params: parsed.screenParams,
|
|
719
|
+
pending_questions: parsed.pending_questions,
|
|
720
|
+
review: parsed.review,
|
|
721
|
+
error: {
|
|
722
|
+
code: "MISSING_REQUIRED_FIELDS",
|
|
723
|
+
message: "缺少必要字段。请补齐推荐页 criteria 等必填字段后再启动 CDP-only recommend run。",
|
|
724
|
+
retryable: true
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function buildNeedConfirmationResponse(parsed, args, requiredConfirmations) {
|
|
730
|
+
const pending = [...(parsed.pending_questions || [])];
|
|
731
|
+
if (requiredConfirmations.includes("job") && !pending.some((item) => item.field === "job")) {
|
|
732
|
+
pending.push(buildJobPendingQuestion(args));
|
|
733
|
+
}
|
|
734
|
+
if (requiredConfirmations.includes("final_review") && !pending.some((item) => item.field === "final_review")) {
|
|
735
|
+
pending.push(buildFinalReviewQuestion(parsed));
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
status: "NEED_CONFIRMATION",
|
|
739
|
+
required_confirmations: requiredConfirmations,
|
|
740
|
+
page_scope: parsed.page_scope,
|
|
741
|
+
search_params: parsed.searchParams,
|
|
742
|
+
screen_params: parsed.screenParams,
|
|
743
|
+
pending_questions: pending,
|
|
744
|
+
review: {
|
|
745
|
+
...(parsed.review || {}),
|
|
746
|
+
required_confirmations: requiredConfirmations
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function evaluateRecommendPipelineGate(parsed, args = {}) {
|
|
752
|
+
if (parsed.missing_fields?.length) return buildNeedInputResponse(parsed);
|
|
753
|
+
const requiredConfirmations = buildRequiredConfirmations(parsed, args);
|
|
754
|
+
if (requiredConfirmations.length) {
|
|
755
|
+
return buildNeedConfirmationResponse(parsed, args, requiredConfirmations);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (args.follow_up?.chat || args.overrides?.follow_up?.chat) {
|
|
759
|
+
return {
|
|
760
|
+
status: "FAILED",
|
|
761
|
+
error: {
|
|
762
|
+
code: "FOLLOW_UP_CHAT_NOT_CDP_REWRITTEN",
|
|
763
|
+
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.",
|
|
764
|
+
retryable: true
|
|
765
|
+
},
|
|
766
|
+
review: parsed.review
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function toArray(value) {
|
|
774
|
+
if (Array.isArray(value)) return value;
|
|
775
|
+
if (value === undefined || value === null) return [];
|
|
776
|
+
return [value];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function withoutUnlimited(values = []) {
|
|
780
|
+
return toArray(values)
|
|
781
|
+
.map((value) => normalizeText(value))
|
|
782
|
+
.filter((value) => value && value !== "不限" && value.toLowerCase() !== "all" && value !== "全部");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function buildRecommendFilter(parsed, args = {}) {
|
|
786
|
+
if (args.no_filter === true || args.filter_enabled === false) {
|
|
787
|
+
return { enabled: false };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const groups = [];
|
|
791
|
+
const recentNotView = withoutUnlimited(parsed.searchParams?.recent_not_view);
|
|
792
|
+
if (recentNotView.length) {
|
|
793
|
+
groups.push({
|
|
794
|
+
group: "recentNotView",
|
|
795
|
+
labels: recentNotView,
|
|
796
|
+
selectAllLabels: true
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const degree = withoutUnlimited(parsed.searchParams?.degree);
|
|
801
|
+
if (degree.length) {
|
|
802
|
+
groups.push({
|
|
803
|
+
group: "degree",
|
|
804
|
+
labels: degree,
|
|
805
|
+
selectAllLabels: true
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const gender = withoutUnlimited(parsed.searchParams?.gender);
|
|
810
|
+
if (gender.length) {
|
|
811
|
+
groups.push({
|
|
812
|
+
group: "gender",
|
|
813
|
+
labels: gender,
|
|
814
|
+
selectAllLabels: true
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const school = withoutUnlimited(parsed.searchParams?.school_tag);
|
|
819
|
+
if (school.length) {
|
|
820
|
+
groups.push({
|
|
821
|
+
group: "school",
|
|
822
|
+
labels: school,
|
|
823
|
+
selectAllLabels: true
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return groups.length ? { filterGroups: groups } : { enabled: false };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function normalizeRecommendStartInput(args = {}, parsed) {
|
|
831
|
+
const confirmation = args.confirmation || {};
|
|
832
|
+
const overrides = args.overrides || {};
|
|
833
|
+
const slowLive = args.slow_live === true;
|
|
834
|
+
const targetCount = parsePositiveInteger(
|
|
835
|
+
args.max_candidates,
|
|
836
|
+
parsed.screenParams?.target_count || parsePositiveInteger(confirmation.target_count_value, 5)
|
|
837
|
+
);
|
|
838
|
+
return {
|
|
839
|
+
host: normalizeText(args.host) || DEFAULT_RECOMMEND_HOST,
|
|
840
|
+
port: parsePositiveInteger(args.port, DEFAULT_RECOMMEND_PORT),
|
|
841
|
+
targetUrlIncludes: normalizeText(args.target_url_includes) || RECOMMEND_TARGET_URL,
|
|
842
|
+
allowNavigate: args.allow_navigate !== false,
|
|
843
|
+
slowLive,
|
|
844
|
+
criteria: parsed.screenParams?.criteria || normalizeText(overrides.criteria),
|
|
845
|
+
targetCount,
|
|
846
|
+
job: normalizeText(confirmation.job_value || overrides.job || ""),
|
|
847
|
+
pageScope: parsed.page_scope || "recommend",
|
|
848
|
+
filter: buildRecommendFilter(parsed, args),
|
|
849
|
+
postAction: parsed.screenParams?.post_action || "none",
|
|
850
|
+
maxGreetCount: Number.isInteger(parsed.screenParams?.max_greet_count)
|
|
851
|
+
? parsed.screenParams.max_greet_count
|
|
852
|
+
: null
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function getRunOptions(args, parsed, normalized, session) {
|
|
857
|
+
const slowLive = args.slow_live === true;
|
|
858
|
+
const executePostAction = args.dry_run_post_action === true
|
|
859
|
+
? false
|
|
860
|
+
: args.execute_post_action !== false;
|
|
861
|
+
return {
|
|
862
|
+
client: session.client,
|
|
863
|
+
targetUrl: RECOMMEND_TARGET_URL,
|
|
864
|
+
criteria: normalized.criteria,
|
|
865
|
+
jobLabel: normalized.job,
|
|
866
|
+
pageScope: normalized.pageScope,
|
|
867
|
+
fallbackPageScope: "recommend",
|
|
868
|
+
filter: normalized.filter,
|
|
869
|
+
maxCandidates: normalized.targetCount,
|
|
870
|
+
detailLimit: parseNonNegativeInteger(args.detail_limit, 0),
|
|
871
|
+
closeDetail: true,
|
|
872
|
+
delayMs: parseNonNegativeInteger(args.delay_ms, 0),
|
|
873
|
+
cardTimeoutMs: slowLive ? 180000 : 90000,
|
|
874
|
+
maxImagePages: parsePositiveInteger(args.max_image_pages, 8),
|
|
875
|
+
imageWheelDeltaY: parsePositiveInteger(args.image_wheel_delta_y, 650),
|
|
876
|
+
cvAcquisitionMode: normalizeText(args.cv_acquisition_mode) || "unknown",
|
|
877
|
+
listMaxScrolls: parsePositiveInteger(args.list_max_scrolls, 20),
|
|
878
|
+
listStableSignatureLimit: parsePositiveInteger(args.list_stable_signature_limit, 2),
|
|
879
|
+
listWheelDeltaY: parsePositiveInteger(args.list_wheel_delta_y, 850),
|
|
880
|
+
listSettleMs: parsePositiveInteger(args.list_settle_ms, slowLive ? 1800 : 1200),
|
|
881
|
+
listFallbackPoint: null,
|
|
882
|
+
refreshOnEnd: args.refresh_on_end !== false,
|
|
883
|
+
maxRefreshRounds: parseNonNegativeInteger(args.max_refresh_rounds, 2),
|
|
884
|
+
refreshButtonSettleMs: parsePositiveInteger(args.refresh_button_settle_ms, slowLive ? 10000 : 8000),
|
|
885
|
+
refreshReloadSettleMs: parsePositiveInteger(args.refresh_reload_settle_ms, slowLive ? 12000 : 8000),
|
|
886
|
+
postAction: normalized.postAction,
|
|
887
|
+
maxGreetCount: normalized.maxGreetCount,
|
|
888
|
+
executePostAction,
|
|
889
|
+
actionTimeoutMs: parsePositiveInteger(args.action_timeout_ms, slowLive ? 12000 : 8000),
|
|
890
|
+
actionIntervalMs: parsePositiveInteger(args.action_interval_ms, 500),
|
|
891
|
+
actionAfterClickDelayMs: parseNonNegativeInteger(args.action_after_click_delay_ms, slowLive ? 1200 : 900),
|
|
892
|
+
name: "mcp-recommend-pipeline-run",
|
|
893
|
+
parsed
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async function closeRecommendRunSession(runId) {
|
|
898
|
+
const meta = recommendRunMeta.get(runId);
|
|
899
|
+
if (!meta || meta.closed) return;
|
|
900
|
+
try {
|
|
901
|
+
try {
|
|
902
|
+
if (meta.session?.client) {
|
|
903
|
+
await closeRecommendDetail(meta.session.client, { attemptsLimit: 2 });
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
// Cleanup is best-effort once the run has settled.
|
|
907
|
+
}
|
|
908
|
+
assertNoForbiddenCdpCalls(meta.methodLog || []);
|
|
909
|
+
} finally {
|
|
910
|
+
meta.closed = true;
|
|
911
|
+
try {
|
|
912
|
+
await meta.session?.close?.();
|
|
913
|
+
} catch {
|
|
914
|
+
// Nothing actionable for the caller once the run has settled.
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function waitForRecommendRunTerminal(runId) {
|
|
920
|
+
while (true) {
|
|
921
|
+
try {
|
|
922
|
+
const snapshot = recommendRunService.getRecommendRun(runId);
|
|
923
|
+
if (TERMINAL_STATUSES.has(snapshot.status)) return snapshot;
|
|
924
|
+
} catch {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
await sleep(1000);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function trackRecommendRun(runId) {
|
|
932
|
+
waitForRecommendRunTerminal(runId)
|
|
933
|
+
.then((terminal) => {
|
|
934
|
+
if (terminal) persistRecommendRunSnapshot(terminal);
|
|
935
|
+
})
|
|
936
|
+
.catch(() => null)
|
|
937
|
+
.finally(() => {
|
|
938
|
+
closeRecommendRunSession(runId).catch(() => {});
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function startRecommendPipelineRunInternal(args = {}, { workspaceRoot = "" } = {}) {
|
|
943
|
+
const parsed = parseRecommendPipelineRequest(args);
|
|
944
|
+
const gate = evaluateRecommendPipelineGate(parsed, args);
|
|
945
|
+
if (gate) return gate;
|
|
946
|
+
const normalized = normalizeRecommendStartInput(args, parsed);
|
|
947
|
+
|
|
948
|
+
let session;
|
|
949
|
+
try {
|
|
950
|
+
session = await recommendConnectorImpl({
|
|
951
|
+
host: normalized.host,
|
|
952
|
+
port: normalized.port,
|
|
953
|
+
targetUrlIncludes: normalized.targetUrlIncludes,
|
|
954
|
+
allowNavigate: normalized.allowNavigate,
|
|
955
|
+
slowLive: normalized.slowLive
|
|
956
|
+
});
|
|
957
|
+
} catch (error) {
|
|
958
|
+
return {
|
|
959
|
+
status: "FAILED",
|
|
960
|
+
error: {
|
|
961
|
+
code: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
962
|
+
message: error?.message || "Boss recommend page is not ready",
|
|
963
|
+
retryable: true
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let started;
|
|
969
|
+
try {
|
|
970
|
+
started = recommendRunService.startRecommendRun(getRunOptions(args, parsed, normalized, session));
|
|
971
|
+
} catch (error) {
|
|
972
|
+
await session.close?.();
|
|
973
|
+
return {
|
|
974
|
+
status: "FAILED",
|
|
975
|
+
error: {
|
|
976
|
+
code: "RECOMMEND_RUN_START_FAILED",
|
|
977
|
+
message: error?.message || "Failed to start recommend run",
|
|
978
|
+
retryable: true
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
recommendRunMeta.set(started.runId, {
|
|
984
|
+
session,
|
|
985
|
+
methodLog: session.methodLog || [],
|
|
986
|
+
workspaceRoot: normalizeText(workspaceRoot) || process.cwd(),
|
|
987
|
+
args: clonePlain(args, {}),
|
|
988
|
+
normalized,
|
|
989
|
+
parsed,
|
|
990
|
+
chrome: {
|
|
991
|
+
host: normalized.host,
|
|
992
|
+
port: normalized.port,
|
|
993
|
+
target_url: session.navigation?.url || session.target?.url || RECOMMEND_TARGET_URL,
|
|
994
|
+
target_id: session.target?.id || null
|
|
995
|
+
},
|
|
996
|
+
health: session.health || null
|
|
997
|
+
});
|
|
998
|
+
trackRecommendRun(started.runId);
|
|
999
|
+
const persistedStarted = persistRecommendRunSnapshot(started);
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
status: "ACCEPTED",
|
|
1003
|
+
run_id: persistedStarted.run_id,
|
|
1004
|
+
state: persistedStarted.state,
|
|
1005
|
+
run: persistedStarted,
|
|
1006
|
+
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1007
|
+
review: parsed.review,
|
|
1008
|
+
message: normalized.postAction === "none"
|
|
1009
|
+
? "Recommend pipeline run started through the shared CDP-only recommend service. No post-action was requested."
|
|
1010
|
+
: `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" : ""}.`,
|
|
1011
|
+
post_action: {
|
|
1012
|
+
requested: normalized.postAction,
|
|
1013
|
+
execute_post_action: args.dry_run_post_action === true ? false : args.execute_post_action !== false,
|
|
1014
|
+
max_greet_count: normalized.maxGreetCount
|
|
1015
|
+
},
|
|
1016
|
+
target_count_semantics: TARGET_COUNT_SEMANTICS
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export async function startRecommendPipelineRunTool({ workspaceRoot = "", args = {} } = {}) {
|
|
1021
|
+
const started = await startRecommendPipelineRunInternal(args, { workspaceRoot });
|
|
1022
|
+
if (started.status !== "ACCEPTED") return started;
|
|
1023
|
+
return attachMethodEvidence(started, started.run_id);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function getRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1027
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1028
|
+
if (!runId) {
|
|
1029
|
+
return {
|
|
1030
|
+
status: "FAILED",
|
|
1031
|
+
error: {
|
|
1032
|
+
code: "INVALID_RUN_ID",
|
|
1033
|
+
message: "run_id is required",
|
|
1034
|
+
retryable: false
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const run = recommendRunService.getRecommendRun(runId);
|
|
1040
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1041
|
+
return attachMethodEvidence({
|
|
1042
|
+
status: "RUN_STATUS",
|
|
1043
|
+
run: normalizedRun
|
|
1044
|
+
}, runId);
|
|
1045
|
+
} catch {
|
|
1046
|
+
const persisted = readRecommendRunState(runId);
|
|
1047
|
+
if (persisted) {
|
|
1048
|
+
return {
|
|
1049
|
+
status: "RUN_STATUS",
|
|
1050
|
+
run: persisted,
|
|
1051
|
+
persistence: {
|
|
1052
|
+
source: "disk",
|
|
1053
|
+
active_control_available: false
|
|
1054
|
+
},
|
|
1055
|
+
runtime_evaluate_used: false,
|
|
1056
|
+
method_summary: {},
|
|
1057
|
+
method_log: [],
|
|
1058
|
+
chrome: null
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
status: "FAILED",
|
|
1063
|
+
error: {
|
|
1064
|
+
code: "RUN_NOT_FOUND",
|
|
1065
|
+
message: `No recommend run found for run_id=${runId}`,
|
|
1066
|
+
retryable: false
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function pauseRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1073
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1074
|
+
try {
|
|
1075
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1076
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1077
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1078
|
+
return attachMethodEvidence({
|
|
1079
|
+
status: "PAUSE_IGNORED",
|
|
1080
|
+
run: normalizedBefore,
|
|
1081
|
+
message: "目标任务已结束,无需暂停。"
|
|
1082
|
+
}, runId);
|
|
1083
|
+
}
|
|
1084
|
+
if (before.status === RUN_STATUS_PAUSED) {
|
|
1085
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1086
|
+
return attachMethodEvidence({
|
|
1087
|
+
status: "PAUSE_IGNORED",
|
|
1088
|
+
run: normalizedBefore,
|
|
1089
|
+
message: "目标任务已经处于 paused 状态。"
|
|
1090
|
+
}, runId);
|
|
1091
|
+
}
|
|
1092
|
+
const run = recommendRunService.pauseRecommendRun(runId);
|
|
1093
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1094
|
+
return attachMethodEvidence({
|
|
1095
|
+
status: "PAUSE_REQUESTED",
|
|
1096
|
+
run: normalizedRun,
|
|
1097
|
+
message: "暂停请求已接收,将在当前候选人处理完成后进入 paused。"
|
|
1098
|
+
}, runId);
|
|
1099
|
+
} catch {
|
|
1100
|
+
const persisted = readRecommendRunState(runId);
|
|
1101
|
+
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1102
|
+
return {
|
|
1103
|
+
status: "PAUSE_IGNORED",
|
|
1104
|
+
run: persisted,
|
|
1105
|
+
message: "目标任务已结束,无需暂停。",
|
|
1106
|
+
runtime_evaluate_used: false,
|
|
1107
|
+
method_summary: {},
|
|
1108
|
+
method_log: [],
|
|
1109
|
+
chrome: null
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return getRecommendPipelineRunTool({ args });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function resumeRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1117
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1118
|
+
try {
|
|
1119
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1120
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1121
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1122
|
+
return attachMethodEvidence({
|
|
1123
|
+
status: "FAILED",
|
|
1124
|
+
error: {
|
|
1125
|
+
code: "RUN_ALREADY_TERMINATED",
|
|
1126
|
+
message: "目标任务已结束,无法继续。",
|
|
1127
|
+
retryable: false
|
|
1128
|
+
},
|
|
1129
|
+
run: normalizedBefore
|
|
1130
|
+
}, runId);
|
|
1131
|
+
}
|
|
1132
|
+
if (before.status !== RUN_STATUS_PAUSED) {
|
|
1133
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1134
|
+
return attachMethodEvidence({
|
|
1135
|
+
status: "FAILED",
|
|
1136
|
+
error: {
|
|
1137
|
+
code: "RUN_NOT_PAUSED",
|
|
1138
|
+
message: "仅 paused 状态的 run 才能继续。",
|
|
1139
|
+
retryable: true
|
|
1140
|
+
},
|
|
1141
|
+
run: normalizedBefore
|
|
1142
|
+
}, runId);
|
|
1143
|
+
}
|
|
1144
|
+
const run = recommendRunService.resumeRecommendRun(runId);
|
|
1145
|
+
const meta = getRecommendRunMeta(runId);
|
|
1146
|
+
if (meta) {
|
|
1147
|
+
meta.resumeCount = (meta.resumeCount || 0) + 1;
|
|
1148
|
+
meta.lastResumedAt = new Date().toISOString();
|
|
1149
|
+
}
|
|
1150
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1151
|
+
return attachMethodEvidence({
|
|
1152
|
+
status: "RESUME_REQUESTED",
|
|
1153
|
+
run: normalizedRun,
|
|
1154
|
+
poll_after_sec: DEFAULT_RECOMMEND_POLL_AFTER_SEC,
|
|
1155
|
+
message: "已恢复 Recommend run,请使用 get_recommend_pipeline_run 按需轮询。"
|
|
1156
|
+
}, runId);
|
|
1157
|
+
} catch {
|
|
1158
|
+
const persisted = readRecommendRunState(runId);
|
|
1159
|
+
if (persisted) {
|
|
1160
|
+
return {
|
|
1161
|
+
status: "FAILED",
|
|
1162
|
+
error: {
|
|
1163
|
+
code: TERMINAL_STATUSES.has(persisted.state) ? "RUN_ALREADY_TERMINATED" : "RUN_NOT_ACTIVE",
|
|
1164
|
+
message: TERMINAL_STATUSES.has(persisted.state)
|
|
1165
|
+
? "目标任务已结束,无法继续。"
|
|
1166
|
+
: "该 run 只有磁盘快照,没有当前进程内的活动 CDP 会话,无法安全继续。",
|
|
1167
|
+
retryable: !TERMINAL_STATUSES.has(persisted.state)
|
|
1168
|
+
},
|
|
1169
|
+
run: persisted,
|
|
1170
|
+
persistence: {
|
|
1171
|
+
source: "disk",
|
|
1172
|
+
active_control_available: false
|
|
1173
|
+
},
|
|
1174
|
+
runtime_evaluate_used: false,
|
|
1175
|
+
method_summary: {},
|
|
1176
|
+
method_log: [],
|
|
1177
|
+
chrome: null
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
return getRecommendPipelineRunTool({ args });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export function cancelRecommendPipelineRunTool({ args = {} } = {}) {
|
|
1185
|
+
const runId = normalizeRunId(args.run_id || args.runId);
|
|
1186
|
+
try {
|
|
1187
|
+
const before = recommendRunService.getRecommendRun(runId);
|
|
1188
|
+
if (TERMINAL_STATUSES.has(before.status)) {
|
|
1189
|
+
const normalizedBefore = persistRecommendRunSnapshot(before);
|
|
1190
|
+
return attachMethodEvidence({
|
|
1191
|
+
status: "CANCEL_IGNORED",
|
|
1192
|
+
run: normalizedBefore,
|
|
1193
|
+
message: "目标任务已结束,无需取消。"
|
|
1194
|
+
}, runId);
|
|
1195
|
+
}
|
|
1196
|
+
const run = recommendRunService.cancelRecommendRun(runId);
|
|
1197
|
+
const normalizedRun = persistRecommendRunSnapshot(run);
|
|
1198
|
+
return attachMethodEvidence({
|
|
1199
|
+
status: "CANCEL_REQUESTED",
|
|
1200
|
+
run: normalizedRun,
|
|
1201
|
+
message: "已收到取消请求,将在当前候选人处理完成后安全停止。"
|
|
1202
|
+
}, runId);
|
|
1203
|
+
} catch {
|
|
1204
|
+
const persisted = readRecommendRunState(runId);
|
|
1205
|
+
if (persisted && TERMINAL_STATUSES.has(persisted.state)) {
|
|
1206
|
+
return {
|
|
1207
|
+
status: "CANCEL_IGNORED",
|
|
1208
|
+
run: persisted,
|
|
1209
|
+
message: "目标任务已结束,无需取消。",
|
|
1210
|
+
runtime_evaluate_used: false,
|
|
1211
|
+
method_summary: {},
|
|
1212
|
+
method_log: [],
|
|
1213
|
+
chrome: null
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
return getRecommendPipelineRunTool({ args });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function getRecommendMcpHealthSnapshot(runId) {
|
|
1221
|
+
const meta = getRecommendRunMeta(runId);
|
|
1222
|
+
return {
|
|
1223
|
+
health: compactHealth(meta.health || null),
|
|
1224
|
+
chrome: meta.chrome || null,
|
|
1225
|
+
method_summary: methodSummary(meta.methodLog || [])
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
export function __setRecommendMcpConnectorForTests(nextConnector) {
|
|
1230
|
+
recommendConnectorImpl = typeof nextConnector === "function" ? nextConnector : connectRecommendChromeSession;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export function __setRecommendMcpJobReaderForTests(nextReader) {
|
|
1234
|
+
recommendJobReaderImpl = typeof nextReader === "function" ? nextReader : readRecommendJobOptionsFromSession;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export function __setRecommendMcpWorkflowForTests(nextWorkflow) {
|
|
1238
|
+
recommendWorkflowImpl = typeof nextWorkflow === "function" ? nextWorkflow : runRecommendWorkflow;
|
|
1239
|
+
recommendRunService = createRecommendRunService({
|
|
1240
|
+
idPrefix: "mcp_recommend",
|
|
1241
|
+
workflow: (...args) => recommendWorkflowImpl(...args)
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export function __resetRecommendMcpStateForTests() {
|
|
1246
|
+
for (const meta of recommendRunMeta.values()) {
|
|
1247
|
+
try {
|
|
1248
|
+
meta.session?.close?.();
|
|
1249
|
+
} catch {
|
|
1250
|
+
// Best-effort test cleanup.
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
recommendRunMeta.clear();
|
|
1254
|
+
__setRecommendMcpConnectorForTests(null);
|
|
1255
|
+
__setRecommendMcpJobReaderForTests(null);
|
|
1256
|
+
__setRecommendMcpWorkflowForTests(null);
|
|
1257
|
+
}
|