@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
package/src/pipeline.js
DELETED
|
@@ -1,2249 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { parseRecommendInstruction } from "./parser.js";
|
|
3
|
-
import {
|
|
4
|
-
attemptPipelineAutoRepair,
|
|
5
|
-
ensureBossRecommendPageReady,
|
|
6
|
-
ensureFeaturedCalibrationReady,
|
|
7
|
-
listRecommendJobs,
|
|
8
|
-
readRecommendTabState,
|
|
9
|
-
refreshBossRecommendList,
|
|
10
|
-
reloadBossRecommendPage,
|
|
11
|
-
runPipelinePreflight,
|
|
12
|
-
runRecommendSearchCli,
|
|
13
|
-
runRecommendScreenCli,
|
|
14
|
-
switchRecommendTab
|
|
15
|
-
} from "./adapters.js";
|
|
16
|
-
import {
|
|
17
|
-
buildTargetCountCompatibilityHints,
|
|
18
|
-
cancelBossChatRun,
|
|
19
|
-
getBossChatRun,
|
|
20
|
-
normalizeTargetCountInput,
|
|
21
|
-
pauseBossChatRun,
|
|
22
|
-
resumeBossChatRun,
|
|
23
|
-
startBossChatRun
|
|
24
|
-
} from "./boss-chat.js";
|
|
25
|
-
|
|
26
|
-
const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
|
|
27
|
-
const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
|
|
28
|
-
const MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS = 1;
|
|
29
|
-
const SEARCH_NO_IFRAME_RETRY_DELAY_MS = 1200;
|
|
30
|
-
const MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS = 2;
|
|
31
|
-
const SEARCH_FILTER_AUTO_RETRY_DELAY_MS = 1200;
|
|
32
|
-
const BOSS_CHAT_FOLLOW_UP_POLL_MS = 1500;
|
|
33
|
-
const FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN = "passed_count";
|
|
34
|
-
const FOLLOW_UP_TARGET_COUNT_PASSED_LABEL = "通过筛选数";
|
|
35
|
-
const FOLLOW_UP_TARGET_COUNT_PASSED_ALIASES = new Set([
|
|
36
|
-
FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
|
|
37
|
-
"passed",
|
|
38
|
-
"通过筛选数",
|
|
39
|
-
"筛选通过数",
|
|
40
|
-
"通过数"
|
|
41
|
-
]);
|
|
42
|
-
const SEARCH_FILTER_RETRY_TOKENS = [
|
|
43
|
-
"FILTER_CONFIRM_FAILED",
|
|
44
|
-
"FILTER_DOM_CLASS_VERIFY_FAILED",
|
|
45
|
-
"RECOMMEND_FILTER_PANEL_UNAVAILABLE",
|
|
46
|
-
"RECOMMEND_FILTER_PANEL_NOT_READY",
|
|
47
|
-
"FILTER_PANEL_NOT_FOUND",
|
|
48
|
-
"FILTER_TRIGGER_NOT_FOUND",
|
|
49
|
-
"FILTER_PANEL_OPEN_FAILED"
|
|
50
|
-
];
|
|
51
|
-
const PAGE_SCOPE_TO_TAB_STATUS = {
|
|
52
|
-
recommend: "0",
|
|
53
|
-
latest: "1",
|
|
54
|
-
featured: "3"
|
|
55
|
-
};
|
|
56
|
-
const TAB_STATUS_TO_PAGE_SCOPE = {
|
|
57
|
-
"0": "recommend",
|
|
58
|
-
"1": "latest",
|
|
59
|
-
"3": "featured"
|
|
60
|
-
};
|
|
61
|
-
const PAGE_SCOPE_LABELS = {
|
|
62
|
-
recommend: "推荐",
|
|
63
|
-
latest: "最新",
|
|
64
|
-
featured: "精选"
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function dedupe(values = []) {
|
|
68
|
-
return [...new Set(values.filter(Boolean))];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function normalizeText(value) {
|
|
72
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parsePositiveIntegerValue(value) {
|
|
76
|
-
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
77
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function normalizePipelineTargetCountValue(value) {
|
|
81
|
-
return normalizeTargetCountInput(value).publicValue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isFollowUpPassedTargetCountToken(value) {
|
|
85
|
-
const normalized = normalizeText(value).toLowerCase().replace(/\s+/g, "");
|
|
86
|
-
if (!normalized) return false;
|
|
87
|
-
return FOLLOW_UP_TARGET_COUNT_PASSED_ALIASES.has(normalized);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function normalizeFollowUpTargetCountInput(value) {
|
|
91
|
-
const normalized = normalizeTargetCountInput(value);
|
|
92
|
-
if (normalized.provided) {
|
|
93
|
-
return {
|
|
94
|
-
...normalized,
|
|
95
|
-
launchValue: normalized.publicValue,
|
|
96
|
-
passedCountMode: false
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
if (isFollowUpPassedTargetCountToken(value)) {
|
|
100
|
-
return {
|
|
101
|
-
provided: true,
|
|
102
|
-
targetCount: null,
|
|
103
|
-
cliArg: null,
|
|
104
|
-
publicValue: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
|
|
105
|
-
rawValue: value,
|
|
106
|
-
parseError: null,
|
|
107
|
-
launchValue: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
|
|
108
|
-
passedCountMode: true
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
...normalized,
|
|
113
|
-
launchValue: null,
|
|
114
|
-
passedCountMode: false
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function resolveFollowUpChatTargetCountForLaunch(targetCount, recommendSummary = null) {
|
|
119
|
-
if (isFollowUpPassedTargetCountToken(targetCount)) {
|
|
120
|
-
const passedCount = parsePositiveIntegerValue(recommendSummary?.passed_count);
|
|
121
|
-
if (passedCount) {
|
|
122
|
-
return {
|
|
123
|
-
ok: true,
|
|
124
|
-
target_count: passedCount,
|
|
125
|
-
resolved_from: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
|
|
126
|
-
passed_count: recommendSummary?.passed_count ?? null
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
code: "FOLLOW_UP_TARGET_COUNT_PASSED_UNAVAILABLE",
|
|
132
|
-
message: "boss-chat follow-up 选择了“通过筛选数”,但本次通过人数为空或 0。请改为正整数,或填写 all(扫到底)。",
|
|
133
|
-
passed_count: recommendSummary?.passed_count ?? null
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const normalized = normalizeTargetCountInput(targetCount);
|
|
137
|
-
if (normalized.provided) {
|
|
138
|
-
return {
|
|
139
|
-
ok: true,
|
|
140
|
-
target_count: normalized.publicValue,
|
|
141
|
-
resolved_from: "explicit",
|
|
142
|
-
passed_count: recommendSummary?.passed_count ?? null
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
ok: false,
|
|
147
|
-
code: "FOLLOW_UP_TARGET_COUNT_INVALID",
|
|
148
|
-
message: normalized.parseError || "boss-chat follow-up target_count 无效。",
|
|
149
|
-
passed_count: recommendSummary?.passed_count ?? null
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function sleep(ms) {
|
|
154
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function shouldAutoRetrySearchFilterFailure(errorCode, errorMessage) {
|
|
158
|
-
const normalizedCode = normalizeText(errorCode).toUpperCase();
|
|
159
|
-
const normalizedMessage = normalizeText(errorMessage).toUpperCase();
|
|
160
|
-
const combined = `${normalizedCode} ${normalizedMessage}`.trim();
|
|
161
|
-
if (!combined) return false;
|
|
162
|
-
if (combined.includes("LOGIN_REQUIRED") || combined.includes("NO_RECOMMEND_IFRAME")) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
if (SEARCH_FILTER_RETRY_TOKENS.some((token) => combined.includes(token))) {
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
return /^(RECOMMEND_)?FILTER_/.test(normalizedCode);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function normalizePageScope(value) {
|
|
172
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
173
|
-
if (!normalized) return null;
|
|
174
|
-
if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
|
|
175
|
-
if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
|
|
176
|
-
if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function resolvePipelinePageScope(parsed, confirmation, overrides) {
|
|
181
|
-
const parsedResolved = normalizePageScope(parsed?.page_scope);
|
|
182
|
-
if (parsedResolved) return parsedResolved;
|
|
183
|
-
const fromConfirmation = normalizePageScope(confirmation?.page_value);
|
|
184
|
-
if (fromConfirmation) return fromConfirmation;
|
|
185
|
-
return "recommend";
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function pageScopeToTabStatus(scope) {
|
|
189
|
-
return PAGE_SCOPE_TO_TAB_STATUS[scope] || "0";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function tabStatusToPageScope(status) {
|
|
193
|
-
return TAB_STATUS_TO_PAGE_SCOPE[String(status || "")] || "recommend";
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function normalizeJobTitle(value) {
|
|
197
|
-
const text = normalizeText(value);
|
|
198
|
-
if (!text) return "";
|
|
199
|
-
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
200
|
-
const strippedRange = byGap
|
|
201
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
202
|
-
.trim();
|
|
203
|
-
const strippedSingle = strippedRange
|
|
204
|
-
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
205
|
-
.trim();
|
|
206
|
-
return strippedSingle || byGap;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function normalizeJobOptions(jobOptions = []) {
|
|
210
|
-
const normalized = [];
|
|
211
|
-
const seen = new Set();
|
|
212
|
-
for (const item of jobOptions) {
|
|
213
|
-
if (!item || typeof item !== "object") continue;
|
|
214
|
-
const value = normalizeText(item.value);
|
|
215
|
-
const label = normalizeText(item.label);
|
|
216
|
-
const title = normalizeJobTitle(item.title || label);
|
|
217
|
-
const optionKey = value || title || label;
|
|
218
|
-
if (!optionKey || seen.has(optionKey)) continue;
|
|
219
|
-
seen.add(optionKey);
|
|
220
|
-
normalized.push({
|
|
221
|
-
value: value || null,
|
|
222
|
-
title: title || label || null,
|
|
223
|
-
label: label || title || null,
|
|
224
|
-
current: item.current === true
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
return normalized;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function resolveSelectedJob(jobOptions = [], requestedRaw) {
|
|
231
|
-
const requested = normalizeText(requestedRaw);
|
|
232
|
-
if (!requested) {
|
|
233
|
-
return { job: null, ambiguous: false, candidates: [] };
|
|
234
|
-
}
|
|
235
|
-
const requestedTitle = normalizeJobTitle(requested).toLowerCase();
|
|
236
|
-
const requestedLower = requested.toLowerCase();
|
|
237
|
-
const byValue = jobOptions.find((item) => normalizeText(item.value || "").toLowerCase() === requestedLower);
|
|
238
|
-
if (byValue) return { job: byValue, ambiguous: false, candidates: [] };
|
|
239
|
-
const byTitle = jobOptions.find((item) => normalizeJobTitle(item.title || "").toLowerCase() === requestedTitle);
|
|
240
|
-
if (byTitle) return { job: byTitle, ambiguous: false, candidates: [] };
|
|
241
|
-
const byLabel = jobOptions.find((item) => normalizeText(item.label || "").toLowerCase() === requestedLower);
|
|
242
|
-
if (byLabel) return { job: byLabel, ambiguous: false, candidates: [] };
|
|
243
|
-
const partialMatches = jobOptions.filter((item) => {
|
|
244
|
-
const title = normalizeJobTitle(item.title || "").toLowerCase();
|
|
245
|
-
const label = normalizeText(item.label || "").toLowerCase();
|
|
246
|
-
return (
|
|
247
|
-
(title && (title.includes(requestedTitle) || requestedTitle.includes(title)))
|
|
248
|
-
|| (label && (label.includes(requestedLower) || requestedLower.includes(label)))
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
if (partialMatches.length === 1) {
|
|
252
|
-
return { job: partialMatches[0], ambiguous: false, candidates: [] };
|
|
253
|
-
}
|
|
254
|
-
if (partialMatches.length > 1) {
|
|
255
|
-
return {
|
|
256
|
-
job: null,
|
|
257
|
-
ambiguous: true,
|
|
258
|
-
candidates: partialMatches.map((item) => item.title || item.label || "").filter(Boolean)
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
return { job: null, ambiguous: false, candidates: [] };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function buildJobPendingQuestion(jobOptions = [], selectedHint = null, reason = null) {
|
|
265
|
-
const options = jobOptions.map((item) => ({
|
|
266
|
-
label: item.title || item.label || item.value,
|
|
267
|
-
value: item.value || item.title || item.label
|
|
268
|
-
}));
|
|
269
|
-
return {
|
|
270
|
-
field: "job",
|
|
271
|
-
question: reason
|
|
272
|
-
|| "已识别当前推荐页岗位列表,请确认本次要执行的岗位。确认后会先点击该岗位,再开始 search 和 screen。",
|
|
273
|
-
value: normalizeText(selectedHint) || null,
|
|
274
|
-
options
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function failedCheckSet(checks = []) {
|
|
279
|
-
const failed = checks
|
|
280
|
-
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
281
|
-
.map((item) => item.key);
|
|
282
|
-
return new Set(failed);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function collectNpmInstallDirs(checks = [], workspaceRoot) {
|
|
286
|
-
const npmCheckKeys = new Set([
|
|
287
|
-
"npm_dep_chrome_remote_interface_search",
|
|
288
|
-
"npm_dep_chrome_remote_interface_screen",
|
|
289
|
-
"npm_dep_ws",
|
|
290
|
-
"npm_dep_sharp"
|
|
291
|
-
]);
|
|
292
|
-
const dirs = checks
|
|
293
|
-
.filter((item) => item && item.ok === false && npmCheckKeys.has(item.key))
|
|
294
|
-
.map((item) => item.install_cwd)
|
|
295
|
-
.filter((value) => typeof value === "string" && value.trim());
|
|
296
|
-
if (dirs.length > 0) return dedupe(dirs);
|
|
297
|
-
return workspaceRoot ? [workspaceRoot] : [];
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function quoteForCommand(value) {
|
|
301
|
-
return JSON.stringify(String(value));
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function buildNpmInstallCommands(checks = [], workspaceRoot) {
|
|
305
|
-
const dirs = collectNpmInstallDirs(checks, workspaceRoot);
|
|
306
|
-
const commands = [];
|
|
307
|
-
for (const dir of dirs) {
|
|
308
|
-
commands.push(`npm install --prefix ${quoteForCommand(dir)}`);
|
|
309
|
-
}
|
|
310
|
-
return commands;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function getNodeInstallCommands() {
|
|
314
|
-
if (process.platform === "win32") {
|
|
315
|
-
return [
|
|
316
|
-
"winget install OpenJS.NodeJS.LTS",
|
|
317
|
-
"node --version"
|
|
318
|
-
];
|
|
319
|
-
}
|
|
320
|
-
if (process.platform === "darwin") {
|
|
321
|
-
return [
|
|
322
|
-
"brew install node",
|
|
323
|
-
"node --version"
|
|
324
|
-
];
|
|
325
|
-
}
|
|
326
|
-
return [
|
|
327
|
-
"使用系统包管理器安装 Node.js >= 18(例如 apt / yum / brew)",
|
|
328
|
-
"node --version"
|
|
329
|
-
];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function formatCommandBlock(commands = []) {
|
|
333
|
-
return commands.map((command) => `- ${command}`).join("\n");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function buildPreflightRecovery(checks = [], workspaceRoot) {
|
|
337
|
-
const failed = failedCheckSet(checks);
|
|
338
|
-
if (failed.size === 0) return null;
|
|
339
|
-
|
|
340
|
-
const needScreenConfig = failed.has("screen_config");
|
|
341
|
-
const needNode = failed.has("node_cli");
|
|
342
|
-
const needNpm = (
|
|
343
|
-
failed.has("npm_dep_chrome_remote_interface_search")
|
|
344
|
-
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
345
|
-
|| failed.has("npm_dep_ws")
|
|
346
|
-
|| failed.has("npm_dep_sharp")
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
const ordered_steps = [];
|
|
350
|
-
if (needScreenConfig) {
|
|
351
|
-
const configCheck = checks.find((item) => item?.key === "screen_config");
|
|
352
|
-
ordered_steps.push({
|
|
353
|
-
id: "fill_screening_config",
|
|
354
|
-
title: "填写 screening-config.json(baseUrl / apiKey / model)",
|
|
355
|
-
blocked_by: [],
|
|
356
|
-
commands: [
|
|
357
|
-
`打开并填写:${configCheck?.path || "~/.boss-recommend-mcp/screening-config.json"}`,
|
|
358
|
-
"确认 baseUrl、apiKey、model 都是可用值(不要保留模板占位符)。"
|
|
359
|
-
]
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
if (needNode) {
|
|
363
|
-
ordered_steps.push({
|
|
364
|
-
id: "install_nodejs",
|
|
365
|
-
title: "安装 Node.js >= 18",
|
|
366
|
-
blocked_by: [],
|
|
367
|
-
commands: getNodeInstallCommands()
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
if (needNpm) {
|
|
371
|
-
ordered_steps.push({
|
|
372
|
-
id: "install_npm_dependencies",
|
|
373
|
-
title: "安装 npm 依赖(chrome-remote-interface / ws / sharp)",
|
|
374
|
-
blocked_by: needNode ? ["install_nodejs"] : [],
|
|
375
|
-
commands: buildNpmInstallCommands(checks, workspaceRoot)
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const promptLines = [
|
|
380
|
-
"你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
|
|
381
|
-
"1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
|
|
382
|
-
"2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws / sharp)。",
|
|
383
|
-
"每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
|
|
384
|
-
];
|
|
385
|
-
if (needScreenConfig) {
|
|
386
|
-
promptLines.splice(
|
|
387
|
-
1,
|
|
388
|
-
0,
|
|
389
|
-
"0) 若 screen_config 失败:先让用户提供并填写 baseUrl、apiKey、model(不得使用模板占位符)。"
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (needNpm) {
|
|
394
|
-
const npmCommands = buildNpmInstallCommands(checks, workspaceRoot);
|
|
395
|
-
if (npmCommands.length > 0) {
|
|
396
|
-
promptLines.push("建议执行的 npm 命令:");
|
|
397
|
-
promptLines.push(formatCommandBlock(npmCommands));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
failed_check_keys: [...failed],
|
|
403
|
-
ordered_steps,
|
|
404
|
-
agent_prompt: promptLines.join("\n")
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function buildRequiredConfirmations(parsedResult) {
|
|
409
|
-
const confirmations = [];
|
|
410
|
-
if (parsedResult.needs_page_confirmation) confirmations.push("page_scope");
|
|
411
|
-
if (parsedResult.needs_filters_confirmation) confirmations.push("filters");
|
|
412
|
-
if (parsedResult.needs_school_tag_confirmation) confirmations.push("school_tag");
|
|
413
|
-
if (parsedResult.needs_degree_confirmation) confirmations.push("degree");
|
|
414
|
-
if (parsedResult.needs_gender_confirmation) confirmations.push("gender");
|
|
415
|
-
if (parsedResult.needs_recent_not_view_confirmation) confirmations.push("recent_not_view");
|
|
416
|
-
if (parsedResult.needs_criteria_confirmation) confirmations.push("criteria");
|
|
417
|
-
if (parsedResult.needs_target_count_confirmation) confirmations.push("target_count");
|
|
418
|
-
if (parsedResult.needs_post_action_confirmation) confirmations.push("post_action");
|
|
419
|
-
if (parsedResult.needs_max_greet_count_confirmation) confirmations.push("max_greet_count");
|
|
420
|
-
return confirmations;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function buildNeedInputResponse(parsedResult) {
|
|
424
|
-
return {
|
|
425
|
-
status: "NEED_INPUT",
|
|
426
|
-
missing_fields: parsedResult.missing_fields,
|
|
427
|
-
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
428
|
-
selected_page: parsedResult.proposed_page_scope || parsedResult.page_scope || "recommend",
|
|
429
|
-
search_params: parsedResult.searchParams,
|
|
430
|
-
screen_params: parsedResult.screenParams,
|
|
431
|
-
follow_up: parsedResult.follow_up || null,
|
|
432
|
-
pending_questions: parsedResult.pending_questions,
|
|
433
|
-
review: parsedResult.review,
|
|
434
|
-
error: {
|
|
435
|
-
code: "MISSING_REQUIRED_FIELDS",
|
|
436
|
-
message: buildNeedInputMessage(parsedResult.missing_fields),
|
|
437
|
-
retryable: true
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function buildNeedConfirmationResponse(parsedResult) {
|
|
443
|
-
return {
|
|
444
|
-
status: "NEED_CONFIRMATION",
|
|
445
|
-
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
446
|
-
selected_page: parsedResult.proposed_page_scope || parsedResult.page_scope || "recommend",
|
|
447
|
-
search_params: parsedResult.searchParams,
|
|
448
|
-
screen_params: {
|
|
449
|
-
...parsedResult.screenParams,
|
|
450
|
-
target_count: parsedResult.proposed_target_count ?? parsedResult.screenParams.target_count,
|
|
451
|
-
post_action: parsedResult.proposed_post_action || parsedResult.screenParams.post_action,
|
|
452
|
-
max_greet_count: parsedResult.proposed_max_greet_count || parsedResult.screenParams.max_greet_count
|
|
453
|
-
},
|
|
454
|
-
follow_up: parsedResult.follow_up || null,
|
|
455
|
-
pending_questions: parsedResult.pending_questions,
|
|
456
|
-
review: parsedResult.review
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function normalizeFollowUpChatInput(followUp = null, defaults = null) {
|
|
461
|
-
const raw = followUp?.chat;
|
|
462
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
463
|
-
return {
|
|
464
|
-
requested: false,
|
|
465
|
-
missing_fields: [],
|
|
466
|
-
pending_questions: [],
|
|
467
|
-
summary: null,
|
|
468
|
-
input: null
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const defaultCriteria = normalizeText(defaults?.criteria || "");
|
|
473
|
-
const defaultStartFromRaw = normalizeText(defaults?.start_from || "").toLowerCase();
|
|
474
|
-
const defaultStartFrom = defaultStartFromRaw === "all" ? "all" : "unread";
|
|
475
|
-
|
|
476
|
-
const explicitCriteria = normalizeText(raw.criteria);
|
|
477
|
-
const explicitStartFromRaw = normalizeText(raw.start_from).toLowerCase();
|
|
478
|
-
const explicitStartFrom = explicitStartFromRaw === "all" ? "all" : explicitStartFromRaw === "unread" ? "unread" : "";
|
|
479
|
-
const explicitGreetingText = normalizeText(raw.greeting_text || raw.greetingText);
|
|
480
|
-
const explicitTarget = normalizeFollowUpTargetCountInput(raw.target_count);
|
|
481
|
-
const explicitTargetCount = explicitTarget.launchValue;
|
|
482
|
-
|
|
483
|
-
const hasExplicitCriteria = Boolean(explicitCriteria);
|
|
484
|
-
const hasExplicitStartFrom = Boolean(explicitStartFrom);
|
|
485
|
-
const hasExplicitTargetCount = explicitTarget.provided;
|
|
486
|
-
|
|
487
|
-
const criteria = explicitCriteria || defaultCriteria;
|
|
488
|
-
const startFrom = explicitStartFrom || defaultStartFrom;
|
|
489
|
-
const targetCount = explicitTargetCount || FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN;
|
|
490
|
-
const targetCountSummaryValue = explicitTarget.publicValue || FOLLOW_UP_TARGET_COUNT_PASSED_LABEL;
|
|
491
|
-
|
|
492
|
-
const profile = normalizeText(raw.profile) || "default";
|
|
493
|
-
const summary = {
|
|
494
|
-
profile,
|
|
495
|
-
criteria: criteria || null,
|
|
496
|
-
start_from: startFrom || null,
|
|
497
|
-
greeting_text: explicitGreetingText || null,
|
|
498
|
-
target_count: targetCountSummaryValue,
|
|
499
|
-
dry_run: raw.dry_run === true,
|
|
500
|
-
no_state: raw.no_state === true,
|
|
501
|
-
safe_pacing: typeof raw.safe_pacing === "boolean" ? raw.safe_pacing : null,
|
|
502
|
-
batch_rest_enabled: typeof raw.batch_rest_enabled === "boolean" ? raw.batch_rest_enabled : null
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
const missing_fields = [];
|
|
506
|
-
const pending_questions = [];
|
|
507
|
-
|
|
508
|
-
if (!hasExplicitCriteria) {
|
|
509
|
-
missing_fields.push("follow_up.chat.criteria");
|
|
510
|
-
pending_questions.push({
|
|
511
|
-
field: "follow_up.chat.criteria",
|
|
512
|
-
question: "请填写 boss-chat follow-up 的筛选 criteria(自然语言,必填)。",
|
|
513
|
-
value: criteria || null
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
if (!hasExplicitStartFrom) {
|
|
517
|
-
missing_fields.push("follow_up.chat.start_from");
|
|
518
|
-
pending_questions.push({
|
|
519
|
-
field: "follow_up.chat.start_from",
|
|
520
|
-
question: "请确认 boss-chat follow-up 从未读还是全部聊天列表开始。",
|
|
521
|
-
value: summary.start_from,
|
|
522
|
-
options: [
|
|
523
|
-
{ label: "未读", value: "unread" },
|
|
524
|
-
{ label: "全部", value: "all" }
|
|
525
|
-
]
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
if (!hasExplicitTargetCount) {
|
|
529
|
-
const recommendedTargetCountPatch = {
|
|
530
|
-
follow_up: {
|
|
531
|
-
chat: {
|
|
532
|
-
target_count: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
const targetCountHints = buildTargetCountCompatibilityHints({
|
|
537
|
-
argumentName: "follow_up.chat.target_count",
|
|
538
|
-
recommendedArgumentPatch: recommendedTargetCountPatch
|
|
539
|
-
});
|
|
540
|
-
missing_fields.push("follow_up.chat.target_count");
|
|
541
|
-
pending_questions.push({
|
|
542
|
-
...targetCountHints,
|
|
543
|
-
answer_format: `follow_up.chat.target_count = "${FOLLOW_UP_TARGET_COUNT_PASSED_LABEL}" | 正整数 | "all"`,
|
|
544
|
-
recommended_value: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
|
|
545
|
-
recommended_argument_patch: recommendedTargetCountPatch,
|
|
546
|
-
canonical_passed_count_value: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
|
|
547
|
-
accepted_examples: [
|
|
548
|
-
FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
|
|
549
|
-
...targetCountHints.accepted_examples
|
|
550
|
-
],
|
|
551
|
-
options: [
|
|
552
|
-
{
|
|
553
|
-
label: `通过筛选数(推荐)`,
|
|
554
|
-
value: FOLLOW_UP_TARGET_COUNT_PASSED_LABEL,
|
|
555
|
-
canonical_value: FOLLOW_UP_TARGET_COUNT_PASSED_TOKEN,
|
|
556
|
-
argument_patch: recommendedTargetCountPatch
|
|
557
|
-
},
|
|
558
|
-
...(Array.isArray(targetCountHints.options) ? targetCountHints.options : [])
|
|
559
|
-
],
|
|
560
|
-
field: "follow_up.chat.target_count",
|
|
561
|
-
question: "请填写 boss-chat follow-up 目标人数。默认建议填写“通过筛选数”;若要扫到底,请在 follow_up.chat.target_count 里字面填写 \"all\"。",
|
|
562
|
-
value: summary.target_count,
|
|
563
|
-
...(explicitTarget.rawValue !== undefined ? { received_target_count: explicitTarget.rawValue } : {}),
|
|
564
|
-
...(explicitTarget.parseError ? { target_count_parse_error: explicitTarget.parseError } : {})
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return {
|
|
569
|
-
requested: true,
|
|
570
|
-
missing_fields,
|
|
571
|
-
pending_questions,
|
|
572
|
-
summary,
|
|
573
|
-
input: {
|
|
574
|
-
profile,
|
|
575
|
-
criteria: criteria || null,
|
|
576
|
-
start_from: startFrom || null,
|
|
577
|
-
greeting_text: explicitGreetingText || undefined,
|
|
578
|
-
target_count: targetCount,
|
|
579
|
-
dry_run: raw.dry_run === true,
|
|
580
|
-
no_state: raw.no_state === true,
|
|
581
|
-
safe_pacing: typeof raw.safe_pacing === "boolean" ? raw.safe_pacing : undefined,
|
|
582
|
-
batch_rest_enabled: typeof raw.batch_rest_enabled === "boolean" ? raw.batch_rest_enabled : undefined
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function mergeParsedFollowUp(parsedResult, followUpChat) {
|
|
588
|
-
if (!followUpChat?.requested) {
|
|
589
|
-
return {
|
|
590
|
-
...parsedResult,
|
|
591
|
-
follow_up: null,
|
|
592
|
-
follow_up_chat: followUpChat || null
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
const pending_questions = [
|
|
596
|
-
...(Array.isArray(parsedResult?.pending_questions) ? parsedResult.pending_questions : []),
|
|
597
|
-
...followUpChat.pending_questions
|
|
598
|
-
];
|
|
599
|
-
return {
|
|
600
|
-
...parsedResult,
|
|
601
|
-
missing_fields: dedupe([
|
|
602
|
-
...(Array.isArray(parsedResult?.missing_fields) ? parsedResult.missing_fields : []),
|
|
603
|
-
...followUpChat.missing_fields
|
|
604
|
-
]),
|
|
605
|
-
pending_questions,
|
|
606
|
-
review: {
|
|
607
|
-
...(parsedResult?.review || {}),
|
|
608
|
-
follow_up: {
|
|
609
|
-
chat: followUpChat.summary
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
follow_up: {
|
|
613
|
-
chat: followUpChat.summary
|
|
614
|
-
},
|
|
615
|
-
follow_up_chat: followUpChat
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function buildResolvedFollowUpChatInput(followUpChat, { selectedJob, debugPort }) {
|
|
620
|
-
return {
|
|
621
|
-
profile: followUpChat?.input?.profile || "default",
|
|
622
|
-
job: selectedJob?.title || selectedJob?.label || selectedJob?.value || null,
|
|
623
|
-
start_from: followUpChat?.input?.start_from || null,
|
|
624
|
-
criteria: followUpChat?.input?.criteria || null,
|
|
625
|
-
greeting_text: followUpChat?.input?.greeting_text || null,
|
|
626
|
-
target_count: followUpChat?.input?.target_count || null,
|
|
627
|
-
port: Number.isFinite(debugPort) ? debugPort : null,
|
|
628
|
-
dry_run: followUpChat?.input?.dry_run === true,
|
|
629
|
-
no_state: followUpChat?.input?.no_state === true,
|
|
630
|
-
safe_pacing: followUpChat?.input?.safe_pacing,
|
|
631
|
-
batch_rest_enabled: followUpChat?.input?.batch_rest_enabled
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function buildBossChatFollowUpStatus({ payload, runId, fallbackInput = null, startMessage = null }) {
|
|
636
|
-
const run = payload?.run || null;
|
|
637
|
-
const progress = run?.progress && typeof run.progress === "object" ? run.progress : {};
|
|
638
|
-
return {
|
|
639
|
-
enabled: true,
|
|
640
|
-
run_id: normalizeText(payload?.run_id || run?.runId || runId) || null,
|
|
641
|
-
state: normalizeText(run?.state || payload?.state || payload?.status).toLowerCase() || null,
|
|
642
|
-
profile: normalizeText(fallbackInput?.profile) || "default",
|
|
643
|
-
job: normalizeText(fallbackInput?.job) || null,
|
|
644
|
-
start_from: normalizeText(fallbackInput?.start_from) || null,
|
|
645
|
-
criteria: normalizeText(fallbackInput?.criteria) || null,
|
|
646
|
-
greeting_text: normalizeText(fallbackInput?.greeting_text) || null,
|
|
647
|
-
target_count: normalizePipelineTargetCountValue(fallbackInput?.target_count),
|
|
648
|
-
port: parsePositiveIntegerValue(fallbackInput?.port),
|
|
649
|
-
progress: {
|
|
650
|
-
inspected: Number.isInteger(progress.inspected) ? progress.inspected : 0,
|
|
651
|
-
passed: Number.isInteger(progress.passed) ? progress.passed : 0,
|
|
652
|
-
requested: Number.isInteger(progress.requested) ? progress.requested : 0,
|
|
653
|
-
skipped: Number.isInteger(progress.skipped) ? progress.skipped : 0,
|
|
654
|
-
errors: Number.isInteger(progress.errors) ? progress.errors : 0
|
|
655
|
-
},
|
|
656
|
-
last_message: normalizeText(run?.lastMessage || payload?.message || startMessage) || null,
|
|
657
|
-
error: run?.error || payload?.error || null,
|
|
658
|
-
result: run?.result || null
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function buildNeedInputMessage(missingFields = []) {
|
|
663
|
-
if (!Array.isArray(missingFields) || missingFields.length === 0) {
|
|
664
|
-
return "缺少必要字段,请先补充后再继续。";
|
|
665
|
-
}
|
|
666
|
-
if (missingFields.length === 1 && missingFields[0] === "criteria") {
|
|
667
|
-
return "缺少必要的筛选 criteria,请先补充或通过 overrides.criteria 明确传入。";
|
|
668
|
-
}
|
|
669
|
-
return `缺少必要字段:${missingFields.join(", ")}。请先补充后再继续。`;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function buildFinalReviewQuestion({ searchParams, screenParams, selectedJob, selectedPage, followUpChat }) {
|
|
673
|
-
return {
|
|
674
|
-
field: "final_review",
|
|
675
|
-
question: followUpChat
|
|
676
|
-
? "开始执行前,请最后确认全部参数(岗位/页面/筛选条件/筛选 criteria/目标通过人数/post_action/max_greet_count/boss-chat follow-up)无误。"
|
|
677
|
-
: "开始执行搜索和筛选前,请最后确认全部参数(岗位/页面/筛选条件/筛选 criteria/目标通过人数/post_action/max_greet_count)无误。",
|
|
678
|
-
value: {
|
|
679
|
-
job: selectedJob?.title || selectedJob?.label || selectedJob?.value || null,
|
|
680
|
-
page_scope: selectedPage || "recommend",
|
|
681
|
-
search_params: searchParams,
|
|
682
|
-
screen_params: screenParams,
|
|
683
|
-
follow_up: followUpChat
|
|
684
|
-
? {
|
|
685
|
-
chat: followUpChat
|
|
686
|
-
}
|
|
687
|
-
: null
|
|
688
|
-
},
|
|
689
|
-
options: [
|
|
690
|
-
{ label: "参数无误,开始执行", value: "confirm" },
|
|
691
|
-
{ label: "参数需要调整", value: "revise" }
|
|
692
|
-
]
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function cloneJsonSafe(value) {
|
|
697
|
-
if (value === null || value === undefined) return null;
|
|
698
|
-
try {
|
|
699
|
-
return JSON.parse(JSON.stringify(value));
|
|
700
|
-
} catch {
|
|
701
|
-
return null;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function buildScreenInputSummary({
|
|
706
|
-
instruction,
|
|
707
|
-
selectedPage,
|
|
708
|
-
selectedJob,
|
|
709
|
-
userSearchParams,
|
|
710
|
-
effectiveSearchParams,
|
|
711
|
-
screenParams,
|
|
712
|
-
followUp
|
|
713
|
-
}) {
|
|
714
|
-
return {
|
|
715
|
-
instruction: normalizeText(instruction || "") || null,
|
|
716
|
-
selected_page: selectedPage || "recommend",
|
|
717
|
-
selected_job: selectedJob
|
|
718
|
-
? {
|
|
719
|
-
value: normalizeText(selectedJob.value || "") || null,
|
|
720
|
-
title: normalizeText(selectedJob.title || "") || null,
|
|
721
|
-
label: normalizeText(selectedJob.label || "") || null
|
|
722
|
-
}
|
|
723
|
-
: null,
|
|
724
|
-
user_search_params: cloneJsonSafe(userSearchParams),
|
|
725
|
-
effective_search_params: cloneJsonSafe(effectiveSearchParams),
|
|
726
|
-
screen_params: cloneJsonSafe(screenParams),
|
|
727
|
-
follow_up: cloneJsonSafe(followUp)
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function buildFailedResponse(code, message, extra = {}) {
|
|
732
|
-
return {
|
|
733
|
-
status: "FAILED",
|
|
734
|
-
error: {
|
|
735
|
-
code,
|
|
736
|
-
message,
|
|
737
|
-
retryable: true
|
|
738
|
-
},
|
|
739
|
-
...extra
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function buildPausedResponse(message, extra = {}) {
|
|
744
|
-
return {
|
|
745
|
-
status: "PAUSED",
|
|
746
|
-
message: normalizeText(message || "") || "Recommend 流水线已暂停。",
|
|
747
|
-
...extra
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function buildRecommendFollowUpEnvelope(recommendResult, chatPayload = null) {
|
|
752
|
-
return {
|
|
753
|
-
search_params: recommendResult?.search_params || null,
|
|
754
|
-
screen_params: recommendResult?.screen_params || null,
|
|
755
|
-
result: recommendResult?.result || null,
|
|
756
|
-
partial_result: recommendResult?.result || null,
|
|
757
|
-
follow_up: chatPayload
|
|
758
|
-
? {
|
|
759
|
-
chat: chatPayload
|
|
760
|
-
}
|
|
761
|
-
: null
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function buildFollowUpFailedResponse(code, message, recommendResult, chatPayload) {
|
|
766
|
-
return {
|
|
767
|
-
status: "FAILED",
|
|
768
|
-
error: {
|
|
769
|
-
code,
|
|
770
|
-
message,
|
|
771
|
-
retryable: true
|
|
772
|
-
},
|
|
773
|
-
...buildRecommendFollowUpEnvelope(recommendResult, chatPayload)
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
function buildFollowUpPausedResponse(message, recommendResult, chatPayload) {
|
|
778
|
-
return {
|
|
779
|
-
status: "PAUSED",
|
|
780
|
-
message: normalizeText(message || "") || "Recommend 流水线已暂停。",
|
|
781
|
-
...buildRecommendFollowUpEnvelope(recommendResult, chatPayload)
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
class PipelineAbortError extends Error {
|
|
786
|
-
constructor(message = "Pipeline execution aborted") {
|
|
787
|
-
super(message);
|
|
788
|
-
this.name = "PipelineAbortError";
|
|
789
|
-
this.code = "PIPELINE_ABORTED";
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function isAbortSignalTriggered(signal) {
|
|
794
|
-
return Boolean(signal && signal.aborted);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function ensurePipelineNotAborted(signal) {
|
|
798
|
-
if (isAbortSignalTriggered(signal)) {
|
|
799
|
-
throw new PipelineAbortError("Pipeline execution aborted by caller.");
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function safeInvokeRuntimeCallback(callback, payload) {
|
|
804
|
-
if (typeof callback !== "function") return;
|
|
805
|
-
try {
|
|
806
|
-
callback(payload);
|
|
807
|
-
} catch {
|
|
808
|
-
// Keep pipeline stable even if runtime callback fails.
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function createPipelineRuntime(runtime = null) {
|
|
813
|
-
const signal = runtime?.signal;
|
|
814
|
-
const heartbeatIntervalMs = Number.isFinite(runtime?.heartbeatIntervalMs) && runtime.heartbeatIntervalMs > 0
|
|
815
|
-
? runtime.heartbeatIntervalMs
|
|
816
|
-
: 10_000;
|
|
817
|
-
const isPauseRequested = typeof runtime?.isPauseRequested === "function"
|
|
818
|
-
? runtime.isPauseRequested
|
|
819
|
-
: () => false;
|
|
820
|
-
const isCancelRequested = typeof runtime?.isCancelRequested === "function"
|
|
821
|
-
? runtime.isCancelRequested
|
|
822
|
-
: () => false;
|
|
823
|
-
|
|
824
|
-
function setStage(stage, message = null) {
|
|
825
|
-
safeInvokeRuntimeCallback(runtime?.onStage, {
|
|
826
|
-
stage,
|
|
827
|
-
message: normalizeText(message || "") || null,
|
|
828
|
-
at: new Date().toISOString()
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function heartbeat(stage, details = null) {
|
|
833
|
-
safeInvokeRuntimeCallback(runtime?.onHeartbeat, {
|
|
834
|
-
stage,
|
|
835
|
-
details: details || null,
|
|
836
|
-
at: new Date().toISOString()
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function output(stage, event) {
|
|
841
|
-
safeInvokeRuntimeCallback(runtime?.onOutput, {
|
|
842
|
-
stage,
|
|
843
|
-
...(event || {}),
|
|
844
|
-
at: new Date().toISOString()
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function progress(stage, payload) {
|
|
849
|
-
safeInvokeRuntimeCallback(runtime?.onProgress, {
|
|
850
|
-
stage,
|
|
851
|
-
...(payload || {}),
|
|
852
|
-
at: new Date().toISOString()
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function followUp(payload) {
|
|
857
|
-
safeInvokeRuntimeCallback(runtime?.onFollowUp, {
|
|
858
|
-
...(payload || {}),
|
|
859
|
-
at: new Date().toISOString()
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function adapterRuntime(stage) {
|
|
864
|
-
return {
|
|
865
|
-
signal,
|
|
866
|
-
heartbeatIntervalMs,
|
|
867
|
-
onOutput: (event) => output(stage, event),
|
|
868
|
-
onHeartbeat: (event) => heartbeat(stage, event),
|
|
869
|
-
onProgress: (payload) => progress(stage, payload)
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return {
|
|
874
|
-
signal,
|
|
875
|
-
heartbeatIntervalMs,
|
|
876
|
-
isPauseRequested,
|
|
877
|
-
isCancelRequested,
|
|
878
|
-
setStage,
|
|
879
|
-
heartbeat,
|
|
880
|
-
output,
|
|
881
|
-
progress,
|
|
882
|
-
followUp,
|
|
883
|
-
adapterRuntime
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function isProcessAbortError(errorLike) {
|
|
888
|
-
const code = normalizeText(errorLike?.code || "").toUpperCase();
|
|
889
|
-
return code === "PROCESS_ABORTED" || code === "ABORTED";
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function isPauseRequested(runtimeHooks) {
|
|
893
|
-
try {
|
|
894
|
-
return runtimeHooks?.isPauseRequested?.() === true;
|
|
895
|
-
} catch {
|
|
896
|
-
return false;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function isCancelRequested(runtimeHooks) {
|
|
901
|
-
try {
|
|
902
|
-
return runtimeHooks?.isCancelRequested?.() === true;
|
|
903
|
-
} catch {
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
909
|
-
const expectedUrl = pageState?.expected_url || "https://www.zhipin.com/web/chat/recommend";
|
|
910
|
-
const loginUrl = pageState?.login_url || "https://www.zhipin.com/web/user/?ka=bticket";
|
|
911
|
-
const currentUrl = pageState?.current_url || null;
|
|
912
|
-
const state = pageState?.state || "UNKNOWN";
|
|
913
|
-
const isPortIssue = state === "DEBUG_PORT_UNREACHABLE";
|
|
914
|
-
const needsLogin = state === "LOGIN_REQUIRED" || state === "LOGIN_REQUIRED_AFTER_REDIRECT";
|
|
915
|
-
const launchAttempt = pageState?.launch_attempt || null;
|
|
916
|
-
const launchLine = launchAttempt?.ok
|
|
917
|
-
? `已自动启动 Chrome(--remote-debugging-port=${debugPort},--user-data-dir=${launchAttempt.user_data_dir || "auto"})。`
|
|
918
|
-
: null;
|
|
919
|
-
const launchExample = process.platform === "win32"
|
|
920
|
-
? `chrome.exe --remote-debugging-port=${debugPort} --user-data-dir=<profile-dir>`
|
|
921
|
-
: process.platform === "darwin"
|
|
922
|
-
? `'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --remote-debugging-port=${debugPort} --user-data-dir=<profile-dir>`
|
|
923
|
-
: `google-chrome --remote-debugging-port=${debugPort} --user-data-dir=<profile-dir>`;
|
|
924
|
-
const steps = [
|
|
925
|
-
`请先在可连接到 DevTools 端口 ${debugPort} 的 Chrome 实例中完成以下操作:`,
|
|
926
|
-
...(launchLine ? [launchLine] : []),
|
|
927
|
-
"1) 确认当前 Chrome 与本次运行使用同一个远程调试端口。",
|
|
928
|
-
isPortIssue
|
|
929
|
-
? `2) 若端口不可连接,请用远程调试方式启动 Chrome(示例:${launchExample})。`
|
|
930
|
-
: "2) 确认端口可连接且浏览器窗口保持打开。",
|
|
931
|
-
needsLogin
|
|
932
|
-
? `3) 当前检测到 Boss 未登录,请先打开并完成登录:${loginUrl}`
|
|
933
|
-
: "3) 如 Boss 登录态失效,请先重新登录。",
|
|
934
|
-
`4) 登录完成后先导航并停留在推荐页:${expectedUrl}`,
|
|
935
|
-
"5) 完成后回复“已就绪”,我会继续执行并优先自动导航到推荐页。"
|
|
936
|
-
];
|
|
937
|
-
return {
|
|
938
|
-
debug_port: debugPort,
|
|
939
|
-
expected_url: expectedUrl,
|
|
940
|
-
current_url: currentUrl,
|
|
941
|
-
page_state: state,
|
|
942
|
-
agent_prompt: steps.join("\n")
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
async function runBossChatFollowUpPhase({
|
|
947
|
-
workspaceRoot,
|
|
948
|
-
followUpChat,
|
|
949
|
-
selectedJob,
|
|
950
|
-
debugPort,
|
|
951
|
-
recommendResult,
|
|
952
|
-
resume,
|
|
953
|
-
runtimeHooks,
|
|
954
|
-
startChatRun,
|
|
955
|
-
getChatRun,
|
|
956
|
-
pauseChatRun,
|
|
957
|
-
resumeChatRun,
|
|
958
|
-
cancelChatRun
|
|
959
|
-
}) {
|
|
960
|
-
const recommendSummary = recommendResult?.result || null;
|
|
961
|
-
const requestedChatInput = buildResolvedFollowUpChatInput(followUpChat, {
|
|
962
|
-
selectedJob,
|
|
963
|
-
debugPort
|
|
964
|
-
});
|
|
965
|
-
const targetCountResolution = resolveFollowUpChatTargetCountForLaunch(
|
|
966
|
-
requestedChatInput.target_count,
|
|
967
|
-
recommendSummary
|
|
968
|
-
);
|
|
969
|
-
if (!targetCountResolution.ok) {
|
|
970
|
-
return buildFollowUpFailedResponse(
|
|
971
|
-
targetCountResolution.code || "BOSS_CHAT_FOLLOW_UP_TARGET_COUNT_INVALID",
|
|
972
|
-
targetCountResolution.message || "boss-chat follow-up target_count 无法解析。",
|
|
973
|
-
recommendResult,
|
|
974
|
-
{
|
|
975
|
-
enabled: true,
|
|
976
|
-
input: requestedChatInput,
|
|
977
|
-
target_count_resolution: targetCountResolution
|
|
978
|
-
}
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
const resolvedChatInput = {
|
|
982
|
-
...requestedChatInput,
|
|
983
|
-
target_count: targetCountResolution.target_count,
|
|
984
|
-
target_count_source: targetCountResolution.resolved_from,
|
|
985
|
-
target_count_requested: requestedChatInput.target_count
|
|
986
|
-
};
|
|
987
|
-
let chatRunId = normalizeText(resume?.chat_run_id || "");
|
|
988
|
-
const resumeFromChatPhase = resume?.resume === true && normalizeText(resume?.follow_up_phase) === "chat_followup";
|
|
989
|
-
let pauseRequested = false;
|
|
990
|
-
let cancelRequested = false;
|
|
991
|
-
let resumeIssued = false;
|
|
992
|
-
|
|
993
|
-
runtimeHooks.setStage(
|
|
994
|
-
"chat_followup",
|
|
995
|
-
chatRunId
|
|
996
|
-
? "Recommend 流水线已完成,准备恢复 boss-chat follow-up。"
|
|
997
|
-
: "Recommend 流水线已完成,开始执行 boss-chat follow-up。"
|
|
998
|
-
);
|
|
999
|
-
runtimeHooks.heartbeat("chat_followup", {
|
|
1000
|
-
profile: resolvedChatInput.profile,
|
|
1001
|
-
job: resolvedChatInput.job,
|
|
1002
|
-
start_from: resolvedChatInput.start_from,
|
|
1003
|
-
greeting_text: resolvedChatInput.greeting_text,
|
|
1004
|
-
target_count: resolvedChatInput.target_count
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
if (!chatRunId) {
|
|
1008
|
-
const startResult = await startChatRun({
|
|
1009
|
-
workspaceRoot,
|
|
1010
|
-
input: {
|
|
1011
|
-
profile: resolvedChatInput.profile,
|
|
1012
|
-
job: resolvedChatInput.job,
|
|
1013
|
-
start_from: resolvedChatInput.start_from,
|
|
1014
|
-
criteria: resolvedChatInput.criteria,
|
|
1015
|
-
greeting_text: resolvedChatInput.greeting_text,
|
|
1016
|
-
target_count: resolvedChatInput.target_count,
|
|
1017
|
-
port: resolvedChatInput.port,
|
|
1018
|
-
dry_run: resolvedChatInput.dry_run,
|
|
1019
|
-
no_state: resolvedChatInput.no_state,
|
|
1020
|
-
safe_pacing: resolvedChatInput.safe_pacing,
|
|
1021
|
-
batch_rest_enabled: resolvedChatInput.batch_rest_enabled
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
if (startResult?.status !== "ACCEPTED" || !normalizeText(startResult?.run_id)) {
|
|
1025
|
-
return buildFollowUpFailedResponse(
|
|
1026
|
-
startResult?.error?.code || "BOSS_CHAT_FOLLOW_UP_LAUNCH_FAILED",
|
|
1027
|
-
startResult?.error?.message || "boss-chat follow-up 启动失败。",
|
|
1028
|
-
recommendResult,
|
|
1029
|
-
{
|
|
1030
|
-
enabled: true,
|
|
1031
|
-
input: resolvedChatInput,
|
|
1032
|
-
launch_result: startResult || null
|
|
1033
|
-
}
|
|
1034
|
-
);
|
|
1035
|
-
}
|
|
1036
|
-
chatRunId = normalizeText(startResult.run_id);
|
|
1037
|
-
runtimeHooks.followUp({
|
|
1038
|
-
stage: "chat_followup",
|
|
1039
|
-
last_message: startResult.message || "boss-chat follow-up 已启动。",
|
|
1040
|
-
recommend_payload: recommendResult,
|
|
1041
|
-
recommend_result: recommendSummary,
|
|
1042
|
-
follow_up: {
|
|
1043
|
-
chat: {
|
|
1044
|
-
...buildBossChatFollowUpStatus({
|
|
1045
|
-
payload: startResult,
|
|
1046
|
-
runId: chatRunId,
|
|
1047
|
-
fallbackInput: resolvedChatInput,
|
|
1048
|
-
startMessage: startResult.message || "boss-chat follow-up 已启动。"
|
|
1049
|
-
}),
|
|
1050
|
-
input: resolvedChatInput
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
while (true) {
|
|
1057
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1058
|
-
|
|
1059
|
-
const chatStatusPayload = await getChatRun({
|
|
1060
|
-
workspaceRoot,
|
|
1061
|
-
input: {
|
|
1062
|
-
profile: resolvedChatInput.profile,
|
|
1063
|
-
runId: chatRunId
|
|
1064
|
-
}
|
|
1065
|
-
});
|
|
1066
|
-
const chatStatus = buildBossChatFollowUpStatus({
|
|
1067
|
-
payload: chatStatusPayload,
|
|
1068
|
-
runId: chatRunId,
|
|
1069
|
-
fallbackInput: resolvedChatInput
|
|
1070
|
-
});
|
|
1071
|
-
const chatState = normalizeText(chatStatus.state).toLowerCase();
|
|
1072
|
-
|
|
1073
|
-
runtimeHooks.followUp({
|
|
1074
|
-
stage: "chat_followup",
|
|
1075
|
-
last_message: chatStatus.last_message || "boss-chat follow-up 进行中。",
|
|
1076
|
-
recommend_payload: recommendResult,
|
|
1077
|
-
recommend_result: recommendSummary,
|
|
1078
|
-
follow_up: {
|
|
1079
|
-
chat: {
|
|
1080
|
-
...chatStatus,
|
|
1081
|
-
input: resolvedChatInput
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
if (isCancelRequested(runtimeHooks) && !cancelRequested && !["completed", "failed", "canceled"].includes(chatState)) {
|
|
1087
|
-
cancelRequested = true;
|
|
1088
|
-
await cancelChatRun({
|
|
1089
|
-
workspaceRoot,
|
|
1090
|
-
input: {
|
|
1091
|
-
profile: resolvedChatInput.profile,
|
|
1092
|
-
runId: chatRunId
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
await sleep(500);
|
|
1096
|
-
continue;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (
|
|
1100
|
-
isPauseRequested(runtimeHooks)
|
|
1101
|
-
&& !pauseRequested
|
|
1102
|
-
&& !cancelRequested
|
|
1103
|
-
&& !["paused", "completed", "failed", "canceled"].includes(chatState)
|
|
1104
|
-
) {
|
|
1105
|
-
pauseRequested = true;
|
|
1106
|
-
await pauseChatRun({
|
|
1107
|
-
workspaceRoot,
|
|
1108
|
-
input: {
|
|
1109
|
-
profile: resolvedChatInput.profile,
|
|
1110
|
-
runId: chatRunId
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
await sleep(500);
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
if (resumeFromChatPhase && !isPauseRequested(runtimeHooks) && !isCancelRequested(runtimeHooks) && !resumeIssued) {
|
|
1118
|
-
if (chatState === "paused") {
|
|
1119
|
-
resumeIssued = true;
|
|
1120
|
-
await resumeChatRun({
|
|
1121
|
-
workspaceRoot,
|
|
1122
|
-
input: {
|
|
1123
|
-
profile: resolvedChatInput.profile,
|
|
1124
|
-
runId: chatRunId
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
await sleep(500);
|
|
1128
|
-
continue;
|
|
1129
|
-
}
|
|
1130
|
-
if (chatState === "running" || chatState === "queued") {
|
|
1131
|
-
resumeIssued = true;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
if (chatState === "completed") {
|
|
1136
|
-
return {
|
|
1137
|
-
...recommendResult,
|
|
1138
|
-
follow_up: {
|
|
1139
|
-
chat: {
|
|
1140
|
-
...chatStatus,
|
|
1141
|
-
input: resolvedChatInput
|
|
1142
|
-
}
|
|
1143
|
-
},
|
|
1144
|
-
message: "Recommend 流水线已完成,boss-chat follow-up 也已执行完成。"
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
if (chatState === "failed") {
|
|
1149
|
-
return buildFollowUpFailedResponse(
|
|
1150
|
-
chatStatus.error?.code || "BOSS_CHAT_FOLLOW_UP_FAILED",
|
|
1151
|
-
chatStatus.error?.message || "boss-chat follow-up 执行失败。",
|
|
1152
|
-
recommendResult,
|
|
1153
|
-
{
|
|
1154
|
-
...chatStatus,
|
|
1155
|
-
input: resolvedChatInput
|
|
1156
|
-
}
|
|
1157
|
-
);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
if (chatState === "canceled") {
|
|
1161
|
-
if (isCancelRequested(runtimeHooks)) {
|
|
1162
|
-
return buildFollowUpPausedResponse(
|
|
1163
|
-
"Recommend 流水线已取消,boss-chat follow-up 已停止。",
|
|
1164
|
-
recommendResult,
|
|
1165
|
-
{
|
|
1166
|
-
...chatStatus,
|
|
1167
|
-
input: resolvedChatInput
|
|
1168
|
-
}
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
return buildFollowUpFailedResponse(
|
|
1172
|
-
"BOSS_CHAT_FOLLOW_UP_CANCELED",
|
|
1173
|
-
"boss-chat follow-up 已取消。",
|
|
1174
|
-
recommendResult,
|
|
1175
|
-
{
|
|
1176
|
-
...chatStatus,
|
|
1177
|
-
input: resolvedChatInput
|
|
1178
|
-
}
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
if (chatState === "paused" && isPauseRequested(runtimeHooks)) {
|
|
1183
|
-
return buildFollowUpPausedResponse(
|
|
1184
|
-
"Recommend 流水线已暂停,可使用 resume 继续 boss-chat follow-up。",
|
|
1185
|
-
recommendResult,
|
|
1186
|
-
{
|
|
1187
|
-
...chatStatus,
|
|
1188
|
-
input: resolvedChatInput
|
|
1189
|
-
}
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
await sleep(BOSS_CHAT_FOLLOW_UP_POLL_MS);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const defaultDependencies = {
|
|
1198
|
-
attemptPipelineAutoRepair,
|
|
1199
|
-
parseRecommendInstruction,
|
|
1200
|
-
ensureBossRecommendPageReady,
|
|
1201
|
-
ensureFeaturedCalibrationReady,
|
|
1202
|
-
listRecommendJobs,
|
|
1203
|
-
readRecommendTabState,
|
|
1204
|
-
refreshBossRecommendList,
|
|
1205
|
-
reloadBossRecommendPage,
|
|
1206
|
-
runPipelinePreflight,
|
|
1207
|
-
runRecommendSearchCli,
|
|
1208
|
-
runRecommendScreenCli,
|
|
1209
|
-
startBossChatRun,
|
|
1210
|
-
getBossChatRun,
|
|
1211
|
-
pauseBossChatRun,
|
|
1212
|
-
resumeBossChatRun,
|
|
1213
|
-
cancelBossChatRun,
|
|
1214
|
-
switchRecommendTab
|
|
1215
|
-
};
|
|
1216
|
-
|
|
1217
|
-
export async function runRecommendPipeline(
|
|
1218
|
-
{ workspaceRoot, instruction, confirmation, overrides, followUp = null, resume = null },
|
|
1219
|
-
dependencies = defaultDependencies,
|
|
1220
|
-
runtime = null
|
|
1221
|
-
) {
|
|
1222
|
-
const injectedDependencies = dependencies || {};
|
|
1223
|
-
const resolvedDependencies = { ...defaultDependencies, ...(dependencies || {}) };
|
|
1224
|
-
const {
|
|
1225
|
-
attemptPipelineAutoRepair: attemptAutoRepair,
|
|
1226
|
-
parseRecommendInstruction: parseInstruction,
|
|
1227
|
-
ensureBossRecommendPageReady: ensureRecommendPageReady,
|
|
1228
|
-
ensureFeaturedCalibrationReady: ensureCalibrationReady,
|
|
1229
|
-
listRecommendJobs: listJobs,
|
|
1230
|
-
readRecommendTabState: readTabState,
|
|
1231
|
-
refreshBossRecommendList: refreshRecommendList,
|
|
1232
|
-
reloadBossRecommendPage: reloadRecommendPage,
|
|
1233
|
-
runPipelinePreflight: runPreflight,
|
|
1234
|
-
runRecommendSearchCli: searchCli,
|
|
1235
|
-
runRecommendScreenCli: screenCli,
|
|
1236
|
-
startBossChatRun: startChatRun,
|
|
1237
|
-
getBossChatRun: getChatRun,
|
|
1238
|
-
pauseBossChatRun: pauseChatRun,
|
|
1239
|
-
resumeBossChatRun: resumeChatRun,
|
|
1240
|
-
cancelBossChatRun: cancelChatRun,
|
|
1241
|
-
switchRecommendTab: switchTab
|
|
1242
|
-
} = resolvedDependencies;
|
|
1243
|
-
const runtimeHooks = createPipelineRuntime(runtime);
|
|
1244
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1245
|
-
|
|
1246
|
-
const startedAt = Date.now();
|
|
1247
|
-
const instructionParsed = parseInstruction({ instruction, confirmation, overrides });
|
|
1248
|
-
const parsed = mergeParsedFollowUp(
|
|
1249
|
-
instructionParsed,
|
|
1250
|
-
normalizeFollowUpChatInput(followUp, {
|
|
1251
|
-
criteria: instructionParsed?.screenParams?.criteria || null,
|
|
1252
|
-
target_count: instructionParsed?.screenParams?.target_count || null,
|
|
1253
|
-
start_from: "unread"
|
|
1254
|
-
})
|
|
1255
|
-
);
|
|
1256
|
-
const selectedPage = resolvePipelinePageScope(parsed, confirmation, overrides);
|
|
1257
|
-
|
|
1258
|
-
if (parsed.missing_fields.length > 0) {
|
|
1259
|
-
return buildNeedInputResponse(parsed);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
if (
|
|
1263
|
-
parsed.needs_page_confirmation
|
|
1264
|
-
|| parsed.needs_filters_confirmation
|
|
1265
|
-
|| parsed.needs_school_tag_confirmation
|
|
1266
|
-
|| parsed.needs_degree_confirmation
|
|
1267
|
-
|| parsed.needs_gender_confirmation
|
|
1268
|
-
|| parsed.needs_recent_not_view_confirmation
|
|
1269
|
-
|| parsed.needs_criteria_confirmation
|
|
1270
|
-
|| parsed.needs_target_count_confirmation
|
|
1271
|
-
|| parsed.needs_post_action_confirmation
|
|
1272
|
-
|| parsed.needs_max_greet_count_confirmation
|
|
1273
|
-
) {
|
|
1274
|
-
return buildNeedConfirmationResponse(parsed);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
const resumeFromChatPhase = (
|
|
1278
|
-
resume?.resume === true
|
|
1279
|
-
&& normalizeText(resume?.follow_up_phase) === "chat_followup"
|
|
1280
|
-
&& normalizeText(resume?.chat_run_id)
|
|
1281
|
-
);
|
|
1282
|
-
if (resumeFromChatPhase) {
|
|
1283
|
-
if (!parsed.follow_up_chat?.requested || !resume?.recommend_result) {
|
|
1284
|
-
return buildFailedResponse(
|
|
1285
|
-
"BOSS_CHAT_FOLLOW_UP_RESUME_CONTEXT_MISSING",
|
|
1286
|
-
"缺少 boss-chat follow-up 恢复上下文,无法继续。"
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
const preflight = runPreflight(workspaceRoot, { pageScope: selectedPage });
|
|
1290
|
-
return runBossChatFollowUpPhase({
|
|
1291
|
-
workspaceRoot,
|
|
1292
|
-
followUpChat: parsed.follow_up_chat,
|
|
1293
|
-
selectedJob: resume.recommend_result?.selected_job || null,
|
|
1294
|
-
debugPort: preflight.debug_port,
|
|
1295
|
-
recommendResult: {
|
|
1296
|
-
status: "COMPLETED",
|
|
1297
|
-
search_params: resume.recommend_search_params || parsed.searchParams,
|
|
1298
|
-
screen_params: resume.recommend_screen_params || parsed.screenParams,
|
|
1299
|
-
result: resume.recommend_result,
|
|
1300
|
-
message: "Recommend 流水线已完成,正在恢复 boss-chat follow-up。"
|
|
1301
|
-
},
|
|
1302
|
-
resume,
|
|
1303
|
-
runtimeHooks,
|
|
1304
|
-
startChatRun,
|
|
1305
|
-
getChatRun,
|
|
1306
|
-
pauseChatRun,
|
|
1307
|
-
resumeChatRun,
|
|
1308
|
-
cancelChatRun
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1313
|
-
runtimeHooks.setStage("preflight", "开始执行 preflight 检查。");
|
|
1314
|
-
runtimeHooks.heartbeat("preflight");
|
|
1315
|
-
|
|
1316
|
-
let preflight = runPreflight(workspaceRoot, { pageScope: selectedPage });
|
|
1317
|
-
let autoRepair = null;
|
|
1318
|
-
const shouldAttemptAutoRepair = (
|
|
1319
|
-
dependencies === defaultDependencies
|
|
1320
|
-
|| Object.prototype.hasOwnProperty.call(injectedDependencies, "attemptPipelineAutoRepair")
|
|
1321
|
-
);
|
|
1322
|
-
if (!preflight.ok) {
|
|
1323
|
-
if (shouldAttemptAutoRepair && typeof attemptAutoRepair === "function") {
|
|
1324
|
-
autoRepair = attemptAutoRepair(workspaceRoot, preflight);
|
|
1325
|
-
if (autoRepair?.preflight) {
|
|
1326
|
-
preflight = autoRepair.preflight;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
const shouldCheckFeaturedCalibration = (
|
|
1332
|
-
dependencies === defaultDependencies
|
|
1333
|
-
|| Object.prototype.hasOwnProperty.call(injectedDependencies, "ensureFeaturedCalibrationReady")
|
|
1334
|
-
);
|
|
1335
|
-
const featuredCalibrationCheck = preflight.checks?.find((item) => item?.key === "favorite_calibration");
|
|
1336
|
-
if (
|
|
1337
|
-
selectedPage === "featured"
|
|
1338
|
-
&& shouldCheckFeaturedCalibration
|
|
1339
|
-
&& featuredCalibrationCheck
|
|
1340
|
-
&& featuredCalibrationCheck.ok === false
|
|
1341
|
-
) {
|
|
1342
|
-
runtimeHooks.setStage("calibration", "检测到精选页缺少可用收藏校准文件,开始自动执行校准。");
|
|
1343
|
-
runtimeHooks.heartbeat("calibration");
|
|
1344
|
-
const calibrationResult = await ensureCalibrationReady(workspaceRoot, {
|
|
1345
|
-
port: preflight.debug_port,
|
|
1346
|
-
timeoutMs: 60000,
|
|
1347
|
-
autoCalibrate: true,
|
|
1348
|
-
runtime: runtimeHooks.adapterRuntime("calibration")
|
|
1349
|
-
});
|
|
1350
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1351
|
-
if (!calibrationResult?.ok) {
|
|
1352
|
-
return buildFailedResponse(
|
|
1353
|
-
"CALIBRATION_REQUIRED",
|
|
1354
|
-
calibrationResult?.error?.message || "精选页收藏校准失败,请先完成校准后重试。",
|
|
1355
|
-
{
|
|
1356
|
-
selected_page: selectedPage,
|
|
1357
|
-
search_params: parsed.searchParams,
|
|
1358
|
-
screen_params: parsed.screenParams,
|
|
1359
|
-
required_user_action: "run_featured_calibration",
|
|
1360
|
-
guidance: {
|
|
1361
|
-
calibration_path: calibrationResult?.calibration_path || featuredCalibrationCheck.path || null,
|
|
1362
|
-
debug_port: calibrationResult?.debug_port || preflight.debug_port,
|
|
1363
|
-
calibration_script_path: calibrationResult?.calibration_script_path || null,
|
|
1364
|
-
tip: "请在 Boss 推荐页切换到精选 tab 后打开候选人详情,按提示先收藏再取消收藏完成校准后重试。"
|
|
1365
|
-
},
|
|
1366
|
-
diagnostics: {
|
|
1367
|
-
checks: preflight.checks,
|
|
1368
|
-
calibration: calibrationResult || null
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
preflight = runPreflight(workspaceRoot, { pageScope: selectedPage });
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
if (!preflight.ok) {
|
|
1377
|
-
runtimeHooks.heartbeat("preflight", {
|
|
1378
|
-
status: "failed"
|
|
1379
|
-
});
|
|
1380
|
-
const screenConfigCheck = preflight.checks?.find((item) => item?.key === "screen_config" && item?.ok === false);
|
|
1381
|
-
const screenConfigPath = String(screenConfigCheck?.path || "");
|
|
1382
|
-
const screenConfigDir = screenConfigPath ? path.dirname(screenConfigPath) : null;
|
|
1383
|
-
const screenConfigReason = String(screenConfigCheck?.reason || "").trim().toUpperCase();
|
|
1384
|
-
const screenConfigMessage = String(screenConfigCheck?.message || "");
|
|
1385
|
-
const screenConfigHasPlaceholder = (
|
|
1386
|
-
screenConfigReason.includes("PLACEHOLDER")
|
|
1387
|
-
|| /占位符|默认模板值|replace-with-openai-api-key/i.test(screenConfigMessage)
|
|
1388
|
-
);
|
|
1389
|
-
const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
|
|
1390
|
-
return buildFailedResponse(
|
|
1391
|
-
"PIPELINE_PREFLIGHT_FAILED",
|
|
1392
|
-
"Recommend 流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
|
|
1393
|
-
{
|
|
1394
|
-
search_params: parsed.searchParams,
|
|
1395
|
-
screen_params: parsed.screenParams,
|
|
1396
|
-
required_user_action: screenConfigCheck
|
|
1397
|
-
? (screenConfigHasPlaceholder ? "confirm_screening_config_updated" : "provide_screening_config")
|
|
1398
|
-
: undefined,
|
|
1399
|
-
guidance: screenConfigCheck
|
|
1400
|
-
? {
|
|
1401
|
-
config_path: screenConfigCheck.path,
|
|
1402
|
-
config_dir: screenConfigDir,
|
|
1403
|
-
agent_prompt: [
|
|
1404
|
-
...(screenConfigHasPlaceholder
|
|
1405
|
-
? [
|
|
1406
|
-
"检测到 screening-config.json 仍包含默认占位词,当前禁止继续执行。",
|
|
1407
|
-
`请引导用户在以下目录修改配置文件:${screenConfigDir || "(unknown)"}`,
|
|
1408
|
-
`配置文件路径:${screenConfigCheck.path}`,
|
|
1409
|
-
"必须替换为真实可用值:baseUrl、apiKey、model(不要保留任何模板占位符)。",
|
|
1410
|
-
"修改完成后,必须先让用户明确回复“已修改完成”,再继续下一步。"
|
|
1411
|
-
]
|
|
1412
|
-
: [
|
|
1413
|
-
"请先让用户填写 screening-config.json 的以下字段:",
|
|
1414
|
-
"1) baseUrl",
|
|
1415
|
-
"2) apiKey",
|
|
1416
|
-
"3) model",
|
|
1417
|
-
`配置文件路径:${screenConfigCheck.path}`,
|
|
1418
|
-
"注意:不要使用模板占位符(例如 replace-with-openai-api-key),也不要由 agent 自行猜测或代填示例值。必须向用户逐项确认真实可用值后再重试。"
|
|
1419
|
-
])
|
|
1420
|
-
].join("\n")
|
|
1421
|
-
}
|
|
1422
|
-
: undefined,
|
|
1423
|
-
diagnostics: {
|
|
1424
|
-
checks: preflight.checks,
|
|
1425
|
-
debug_port: preflight.debug_port,
|
|
1426
|
-
config_resolution: preflight.config_resolution,
|
|
1427
|
-
auto_repair: autoRepair,
|
|
1428
|
-
recovery
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1435
|
-
runtimeHooks.setStage("page_ready", "preflight 完成,开始检查 recommend 页面就绪状态。");
|
|
1436
|
-
runtimeHooks.heartbeat("page_ready");
|
|
1437
|
-
|
|
1438
|
-
const pageCheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
1439
|
-
port: preflight.debug_port
|
|
1440
|
-
});
|
|
1441
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1442
|
-
if (!pageCheck.ok) {
|
|
1443
|
-
const loginRelated = new Set(["LOGIN_REQUIRED", "LOGIN_REQUIRED_AFTER_REDIRECT"]);
|
|
1444
|
-
const connectivityRelated = new Set(["DEBUG_PORT_UNREACHABLE"]);
|
|
1445
|
-
const guidance = buildChromeSetupGuidance({
|
|
1446
|
-
debugPort: preflight.debug_port,
|
|
1447
|
-
pageState: pageCheck.page_state
|
|
1448
|
-
});
|
|
1449
|
-
return buildFailedResponse(
|
|
1450
|
-
connectivityRelated.has(pageCheck.state)
|
|
1451
|
-
? "BOSS_CHROME_NOT_CONNECTED"
|
|
1452
|
-
: loginRelated.has(pageCheck.state)
|
|
1453
|
-
? "BOSS_LOGIN_REQUIRED"
|
|
1454
|
-
: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1455
|
-
loginRelated.has(pageCheck.state)
|
|
1456
|
-
? `开始执行搜索和筛选前,请先在端口 ${preflight.debug_port} 的 Chrome 完成 Boss 登录并停留在 recommend 页面。`
|
|
1457
|
-
: connectivityRelated.has(pageCheck.state)
|
|
1458
|
-
? `开始执行搜索和筛选前,需要先连接到端口 ${preflight.debug_port} 的 Chrome 远程调试实例。`
|
|
1459
|
-
: `开始执行搜索和筛选前,请先在端口 ${preflight.debug_port} 的 Chrome 停留在 Boss recommend 页面。`,
|
|
1460
|
-
{
|
|
1461
|
-
search_params: parsed.searchParams,
|
|
1462
|
-
screen_params: parsed.screenParams,
|
|
1463
|
-
required_user_action: "prepare_boss_recommend_page",
|
|
1464
|
-
guidance,
|
|
1465
|
-
diagnostics: {
|
|
1466
|
-
debug_port: preflight.debug_port,
|
|
1467
|
-
page_state: pageCheck.page_state
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
runtimeHooks.setStage("job_list", "页面就绪,开始读取岗位列表。");
|
|
1474
|
-
runtimeHooks.heartbeat("job_list");
|
|
1475
|
-
const jobListResult = await listJobs({
|
|
1476
|
-
workspaceRoot,
|
|
1477
|
-
port: preflight.debug_port,
|
|
1478
|
-
runtime: runtimeHooks.adapterRuntime("job_list")
|
|
1479
|
-
});
|
|
1480
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1481
|
-
if (isProcessAbortError(jobListResult.error)) {
|
|
1482
|
-
throw new PipelineAbortError(jobListResult.error?.message || "岗位列表读取已取消。");
|
|
1483
|
-
}
|
|
1484
|
-
if (!jobListResult.ok) {
|
|
1485
|
-
const jobListErrorCode = String(jobListResult.error?.code || "");
|
|
1486
|
-
const jobListErrorMessage = String(jobListResult.error?.message || "");
|
|
1487
|
-
const pageReadinessFailure = (
|
|
1488
|
-
jobListErrorCode === "JOB_TRIGGER_NOT_FOUND"
|
|
1489
|
-
|| jobListErrorCode === "NO_RECOMMEND_IFRAME"
|
|
1490
|
-
|| jobListErrorCode === "LOGIN_REQUIRED"
|
|
1491
|
-
|| jobListErrorMessage.includes("JOB_TRIGGER_NOT_FOUND")
|
|
1492
|
-
|| jobListErrorMessage.includes("NO_RECOMMEND_IFRAME")
|
|
1493
|
-
|| jobListErrorMessage.includes("LOGIN_REQUIRED")
|
|
1494
|
-
);
|
|
1495
|
-
if (pageReadinessFailure) {
|
|
1496
|
-
const recheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
1497
|
-
port: preflight.debug_port
|
|
1498
|
-
});
|
|
1499
|
-
const loginRelated = new Set(["LOGIN_REQUIRED", "LOGIN_REQUIRED_AFTER_REDIRECT"]);
|
|
1500
|
-
const connectivityRelated = new Set(["DEBUG_PORT_UNREACHABLE"]);
|
|
1501
|
-
const guidance = buildChromeSetupGuidance({
|
|
1502
|
-
debugPort: preflight.debug_port,
|
|
1503
|
-
pageState: recheck.page_state
|
|
1504
|
-
});
|
|
1505
|
-
if (!recheck.ok || loginRelated.has(recheck.state) || connectivityRelated.has(recheck.state)) {
|
|
1506
|
-
return buildFailedResponse(
|
|
1507
|
-
connectivityRelated.has(recheck.state)
|
|
1508
|
-
? "BOSS_CHROME_NOT_CONNECTED"
|
|
1509
|
-
: loginRelated.has(recheck.state)
|
|
1510
|
-
? "BOSS_LOGIN_REQUIRED"
|
|
1511
|
-
: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
1512
|
-
loginRelated.has(recheck.state)
|
|
1513
|
-
? `检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket`
|
|
1514
|
-
: connectivityRelated.has(recheck.state)
|
|
1515
|
-
? `读取岗位列表前需要先连接到端口 ${preflight.debug_port} 的 Chrome 远程调试实例。`
|
|
1516
|
-
: `读取岗位列表前,请先在端口 ${preflight.debug_port} 的 Chrome 停留在 Boss recommend 页面。`,
|
|
1517
|
-
{
|
|
1518
|
-
search_params: parsed.searchParams,
|
|
1519
|
-
screen_params: parsed.screenParams,
|
|
1520
|
-
required_user_action: "prepare_boss_recommend_page",
|
|
1521
|
-
guidance,
|
|
1522
|
-
diagnostics: {
|
|
1523
|
-
debug_port: preflight.debug_port,
|
|
1524
|
-
page_state: recheck.page_state,
|
|
1525
|
-
stdout: jobListResult.stdout?.slice(-1000),
|
|
1526
|
-
stderr: jobListResult.stderr?.slice(-1000),
|
|
1527
|
-
result: jobListResult.structured || null
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return buildFailedResponse(
|
|
1534
|
-
jobListResult.error?.code || "RECOMMEND_JOB_LIST_FAILED",
|
|
1535
|
-
jobListResult.error?.message || "读取推荐岗位列表失败,无法开始筛选。",
|
|
1536
|
-
{
|
|
1537
|
-
search_params: parsed.searchParams,
|
|
1538
|
-
screen_params: parsed.screenParams,
|
|
1539
|
-
diagnostics: {
|
|
1540
|
-
debug_port: preflight.debug_port,
|
|
1541
|
-
stdout: jobListResult.stdout?.slice(-1000),
|
|
1542
|
-
stderr: jobListResult.stderr?.slice(-1000),
|
|
1543
|
-
result: jobListResult.structured || null
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
const jobOptions = normalizeJobOptions(jobListResult.jobs);
|
|
1549
|
-
if (jobOptions.length === 0) {
|
|
1550
|
-
return buildFailedResponse(
|
|
1551
|
-
"RECOMMEND_JOB_LIST_EMPTY",
|
|
1552
|
-
"未识别到可选岗位,暂时无法开始筛选。",
|
|
1553
|
-
{
|
|
1554
|
-
search_params: parsed.searchParams,
|
|
1555
|
-
screen_params: parsed.screenParams,
|
|
1556
|
-
diagnostics: {
|
|
1557
|
-
debug_port: preflight.debug_port,
|
|
1558
|
-
result: jobListResult.structured || null
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
const selectedJobHint = normalizeText(confirmation?.job_value || parsed.job_selection_hint || "");
|
|
1565
|
-
const selectedJobResolution = resolveSelectedJob(jobOptions, selectedJobHint);
|
|
1566
|
-
const jobConfirmed = confirmation?.job_confirmed === true;
|
|
1567
|
-
const selectedTabStatus = pageScopeToTabStatus(selectedPage);
|
|
1568
|
-
if (!jobConfirmed || !selectedJobResolution.job) {
|
|
1569
|
-
const reason = selectedJobResolution.ambiguous
|
|
1570
|
-
? `你提供的岗位“${selectedJobHint}”匹配到多个选项:${selectedJobResolution.candidates.join(" / ")}。请明确选择其中一个岗位。`
|
|
1571
|
-
: selectedJobHint
|
|
1572
|
-
? `未在当前岗位列表中找到“${selectedJobHint}”,请从以下岗位中重新确认一个。`
|
|
1573
|
-
: "已识别当前推荐页岗位列表,请先确认本次要执行的岗位;确认后会先点击该岗位,再开始 search 和 screen。";
|
|
1574
|
-
const pendingQuestions = (parsed.pending_questions || []).filter((item) => item?.field !== "job");
|
|
1575
|
-
pendingQuestions.push(buildJobPendingQuestion(jobOptions, selectedJobHint, reason));
|
|
1576
|
-
const requiredConfirmations = dedupe([...buildRequiredConfirmations(parsed), "job"]);
|
|
1577
|
-
return {
|
|
1578
|
-
status: "NEED_CONFIRMATION",
|
|
1579
|
-
required_confirmations: requiredConfirmations,
|
|
1580
|
-
selected_page: selectedPage,
|
|
1581
|
-
search_params: parsed.searchParams,
|
|
1582
|
-
screen_params: {
|
|
1583
|
-
...parsed.screenParams,
|
|
1584
|
-
target_count: parsed.proposed_target_count ?? parsed.screenParams.target_count,
|
|
1585
|
-
post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
|
|
1586
|
-
max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
|
|
1587
|
-
},
|
|
1588
|
-
follow_up: parsed.follow_up || null,
|
|
1589
|
-
pending_questions: pendingQuestions,
|
|
1590
|
-
review: parsed.review,
|
|
1591
|
-
job_options: jobOptions
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
const selectedJob = selectedJobResolution.job;
|
|
1595
|
-
const selectedJobToken = selectedJob.value || selectedJob.title || selectedJob.label;
|
|
1596
|
-
if (confirmation?.final_confirmed !== true) {
|
|
1597
|
-
const pendingQuestions = (parsed.pending_questions || []).filter((item) => item?.field !== "final_review");
|
|
1598
|
-
pendingQuestions.push(buildFinalReviewQuestion({
|
|
1599
|
-
searchParams: parsed.searchParams,
|
|
1600
|
-
screenParams: {
|
|
1601
|
-
...parsed.screenParams,
|
|
1602
|
-
target_count: parsed.proposed_target_count ?? parsed.screenParams.target_count,
|
|
1603
|
-
post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
|
|
1604
|
-
max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
|
|
1605
|
-
},
|
|
1606
|
-
selectedJob,
|
|
1607
|
-
selectedPage,
|
|
1608
|
-
followUpChat: parsed.follow_up?.chat || null
|
|
1609
|
-
}));
|
|
1610
|
-
return {
|
|
1611
|
-
status: "NEED_CONFIRMATION",
|
|
1612
|
-
required_confirmations: dedupe([...buildRequiredConfirmations(parsed), "final_review"]),
|
|
1613
|
-
selected_page: selectedPage,
|
|
1614
|
-
search_params: parsed.searchParams,
|
|
1615
|
-
screen_params: {
|
|
1616
|
-
...parsed.screenParams,
|
|
1617
|
-
target_count: parsed.proposed_target_count ?? parsed.screenParams.target_count,
|
|
1618
|
-
post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
|
|
1619
|
-
max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
|
|
1620
|
-
},
|
|
1621
|
-
follow_up: parsed.follow_up || null,
|
|
1622
|
-
selected_job: selectedJob,
|
|
1623
|
-
pending_questions: pendingQuestions,
|
|
1624
|
-
review: parsed.review,
|
|
1625
|
-
job_options: jobOptions
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
const resumeCompletionReason = normalizeText(resume?.previous_completion_reason || "").toLowerCase();
|
|
1630
|
-
const isResumeRun = resume?.resume === true;
|
|
1631
|
-
const resumeFromPausedBeforeScreen = isResumeRun && resumeCompletionReason === "paused_before_screen";
|
|
1632
|
-
const skipSearchOnResume = isResumeRun && !resumeFromPausedBeforeScreen;
|
|
1633
|
-
let effectiveSearchParams = { ...parsed.searchParams };
|
|
1634
|
-
let searchSummary = null;
|
|
1635
|
-
let shouldRunSearch = !skipSearchOnResume;
|
|
1636
|
-
let screenAutoRecoveryCount = 0;
|
|
1637
|
-
let lastAutoRecovery = null;
|
|
1638
|
-
let searchNoIframeRetryCount = 0;
|
|
1639
|
-
let searchFilterRetryCount = 0;
|
|
1640
|
-
let activeTabStatus = null;
|
|
1641
|
-
let currentResumeConfig = {
|
|
1642
|
-
checkpoint_path: resume?.checkpoint_path || null,
|
|
1643
|
-
pause_control_path: resume?.pause_control_path || null,
|
|
1644
|
-
output_csv: resume?.output_csv || null,
|
|
1645
|
-
resume: resume?.resume === true,
|
|
1646
|
-
require_checkpoint: skipSearchOnResume
|
|
1647
|
-
};
|
|
1648
|
-
|
|
1649
|
-
const ensureSelectedPageTab = async () => {
|
|
1650
|
-
const expectedStatus = selectedTabStatus;
|
|
1651
|
-
let beforeState = null;
|
|
1652
|
-
if (typeof readTabState === "function") {
|
|
1653
|
-
beforeState = await readTabState(workspaceRoot, { port: preflight.debug_port });
|
|
1654
|
-
if (beforeState?.ok && normalizeText(beforeState.active_status)) {
|
|
1655
|
-
activeTabStatus = normalizeText(beforeState.active_status);
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
if (String(activeTabStatus || "") === String(expectedStatus)) {
|
|
1659
|
-
return {
|
|
1660
|
-
ok: true,
|
|
1661
|
-
switched: false,
|
|
1662
|
-
before_state: beforeState || null,
|
|
1663
|
-
after_state: beforeState || null
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
if (typeof switchTab !== "function") {
|
|
1667
|
-
return {
|
|
1668
|
-
ok: false,
|
|
1669
|
-
error: {
|
|
1670
|
-
code: "RECOMMEND_TAB_SWITCH_ADAPTER_MISSING",
|
|
1671
|
-
message: "缺少 recommend tab 切换适配器。"
|
|
1672
|
-
},
|
|
1673
|
-
before_state: beforeState || null,
|
|
1674
|
-
after_state: null
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
const switchResult = await switchTab(workspaceRoot, {
|
|
1678
|
-
port: preflight.debug_port,
|
|
1679
|
-
target_status: expectedStatus
|
|
1680
|
-
});
|
|
1681
|
-
if (!switchResult?.ok) {
|
|
1682
|
-
return {
|
|
1683
|
-
ok: false,
|
|
1684
|
-
error: {
|
|
1685
|
-
code: switchResult?.state || "RECOMMEND_TAB_SWITCH_FAILED",
|
|
1686
|
-
message: switchResult?.message || `切换到${PAGE_SCOPE_LABELS[selectedPage]} tab 失败。`
|
|
1687
|
-
},
|
|
1688
|
-
before_state: beforeState || null,
|
|
1689
|
-
after_state: switchResult?.tab_state || null
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
activeTabStatus = normalizeText(switchResult.active_status || switchResult.tab_state?.active_status || expectedStatus);
|
|
1693
|
-
return {
|
|
1694
|
-
ok: true,
|
|
1695
|
-
switched: true,
|
|
1696
|
-
before_state: beforeState || null,
|
|
1697
|
-
after_state: switchResult.tab_state || null
|
|
1698
|
-
};
|
|
1699
|
-
};
|
|
1700
|
-
|
|
1701
|
-
while (true) {
|
|
1702
|
-
if (shouldRunSearch) {
|
|
1703
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1704
|
-
runtimeHooks.setStage(
|
|
1705
|
-
"search",
|
|
1706
|
-
screenAutoRecoveryCount > 0
|
|
1707
|
-
? `自动恢复第 ${screenAutoRecoveryCount} 次:重新执行 recommend search(强制 recent_not_view=${FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY})。`
|
|
1708
|
-
: "岗位已确认,开始执行 recommend search。"
|
|
1709
|
-
);
|
|
1710
|
-
runtimeHooks.heartbeat("search", lastAutoRecovery);
|
|
1711
|
-
const searchResult = await searchCli({
|
|
1712
|
-
workspaceRoot,
|
|
1713
|
-
searchParams: effectiveSearchParams,
|
|
1714
|
-
selectedJob: selectedJobToken,
|
|
1715
|
-
pageScope: selectedPage,
|
|
1716
|
-
runtime: runtimeHooks.adapterRuntime("search")
|
|
1717
|
-
});
|
|
1718
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1719
|
-
if (isProcessAbortError(searchResult.error)) {
|
|
1720
|
-
throw new PipelineAbortError(searchResult.error?.message || "推荐筛选已取消。");
|
|
1721
|
-
}
|
|
1722
|
-
if (!searchResult.ok) {
|
|
1723
|
-
const searchErrorCode = String(searchResult.error?.code || "");
|
|
1724
|
-
const searchErrorMessage = String(searchResult.error?.message || "");
|
|
1725
|
-
const isNoIframeSearchFailure = (
|
|
1726
|
-
searchErrorCode === "NO_RECOMMEND_IFRAME"
|
|
1727
|
-
|| searchErrorMessage.includes("NO_RECOMMEND_IFRAME")
|
|
1728
|
-
);
|
|
1729
|
-
const loginRelatedSearchFailure = (
|
|
1730
|
-
searchErrorCode === "LOGIN_REQUIRED"
|
|
1731
|
-
|| isNoIframeSearchFailure
|
|
1732
|
-
|| searchErrorMessage.includes("LOGIN_REQUIRED")
|
|
1733
|
-
);
|
|
1734
|
-
if (loginRelatedSearchFailure) {
|
|
1735
|
-
const recheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
1736
|
-
port: preflight.debug_port
|
|
1737
|
-
});
|
|
1738
|
-
if (recheck.state === "LOGIN_REQUIRED" || recheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1739
|
-
const guidance = buildChromeSetupGuidance({
|
|
1740
|
-
debugPort: preflight.debug_port,
|
|
1741
|
-
pageState: recheck.page_state
|
|
1742
|
-
});
|
|
1743
|
-
return buildFailedResponse(
|
|
1744
|
-
"BOSS_LOGIN_REQUIRED",
|
|
1745
|
-
"检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket",
|
|
1746
|
-
{
|
|
1747
|
-
search_params: effectiveSearchParams,
|
|
1748
|
-
screen_params: parsed.screenParams,
|
|
1749
|
-
selected_job: selectedJob,
|
|
1750
|
-
required_user_action: "prepare_boss_recommend_page",
|
|
1751
|
-
guidance,
|
|
1752
|
-
diagnostics: {
|
|
1753
|
-
debug_port: preflight.debug_port,
|
|
1754
|
-
page_state: recheck.page_state,
|
|
1755
|
-
stdout: searchResult.stdout?.slice(-1000),
|
|
1756
|
-
stderr: searchResult.stderr?.slice(-1000),
|
|
1757
|
-
result: searchResult.structured || null,
|
|
1758
|
-
auto_recovery: lastAutoRecovery
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
|
-
if (
|
|
1764
|
-
isNoIframeSearchFailure
|
|
1765
|
-
&& recheck.state === "RECOMMEND_READY"
|
|
1766
|
-
&& searchNoIframeRetryCount < MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS
|
|
1767
|
-
) {
|
|
1768
|
-
searchNoIframeRetryCount += 1;
|
|
1769
|
-
const retryDelayMs = SEARCH_NO_IFRAME_RETRY_DELAY_MS;
|
|
1770
|
-
const retryDiagnostics = {
|
|
1771
|
-
trigger: "NO_RECOMMEND_IFRAME",
|
|
1772
|
-
attempt: searchNoIframeRetryCount,
|
|
1773
|
-
max_attempts: MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS,
|
|
1774
|
-
delay_ms: retryDelayMs,
|
|
1775
|
-
page_state: recheck.page_state || null
|
|
1776
|
-
};
|
|
1777
|
-
runtimeHooks.setStage(
|
|
1778
|
-
"search_recovery",
|
|
1779
|
-
`检测到 recommend iframe 暂未就绪,等待 ${Math.round(retryDelayMs / 1000)} 秒后重试 search(第 ${searchNoIframeRetryCount}/${MAX_SEARCH_NO_IFRAME_RETRY_ATTEMPTS} 次)。`
|
|
1780
|
-
);
|
|
1781
|
-
runtimeHooks.heartbeat("search_recovery", retryDiagnostics);
|
|
1782
|
-
await sleep(retryDelayMs);
|
|
1783
|
-
continue;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
if (
|
|
1787
|
-
shouldAutoRetrySearchFilterFailure(searchErrorCode, searchErrorMessage)
|
|
1788
|
-
&& searchFilterRetryCount < MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS
|
|
1789
|
-
) {
|
|
1790
|
-
searchFilterRetryCount += 1;
|
|
1791
|
-
const retryDelayMs = SEARCH_FILTER_AUTO_RETRY_DELAY_MS;
|
|
1792
|
-
lastAutoRecovery = {
|
|
1793
|
-
trigger: "SEARCH_FILTER_RETRY",
|
|
1794
|
-
attempt: searchFilterRetryCount,
|
|
1795
|
-
max_attempts: MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS,
|
|
1796
|
-
delay_ms: retryDelayMs,
|
|
1797
|
-
error_code: searchErrorCode || null,
|
|
1798
|
-
error_message: searchErrorMessage || null,
|
|
1799
|
-
action: "retry_search"
|
|
1800
|
-
};
|
|
1801
|
-
runtimeHooks.setStage(
|
|
1802
|
-
"search_recovery",
|
|
1803
|
-
`检测到筛选控件状态异常(${searchErrorCode || "UNKNOWN"}),等待 ${Math.round(retryDelayMs / 1000)} 秒后重试 search(第 ${searchFilterRetryCount}/${MAX_SEARCH_FILTER_AUTO_RETRY_ATTEMPTS} 次)。`
|
|
1804
|
-
);
|
|
1805
|
-
runtimeHooks.heartbeat("search_recovery", lastAutoRecovery);
|
|
1806
|
-
await sleep(retryDelayMs);
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
return buildFailedResponse(
|
|
1810
|
-
searchResult.error?.code || "RECOMMEND_SEARCH_FAILED",
|
|
1811
|
-
searchResult.error?.message || "推荐页筛选执行失败。",
|
|
1812
|
-
{
|
|
1813
|
-
search_params: effectiveSearchParams,
|
|
1814
|
-
screen_params: parsed.screenParams,
|
|
1815
|
-
selected_job: selectedJob,
|
|
1816
|
-
diagnostics: {
|
|
1817
|
-
debug_port: preflight.debug_port,
|
|
1818
|
-
stdout: searchResult.stdout?.slice(-1000),
|
|
1819
|
-
stderr: searchResult.stderr?.slice(-1000),
|
|
1820
|
-
result: searchResult.structured || null,
|
|
1821
|
-
auto_recovery: lastAutoRecovery
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
);
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
searchFilterRetryCount = 0;
|
|
1828
|
-
searchSummary = searchResult.summary || {};
|
|
1829
|
-
if (isPauseRequested(runtimeHooks)) {
|
|
1830
|
-
return buildPausedResponse("已在 screen 阶段开始前暂停 Recommend 流水线。", {
|
|
1831
|
-
selected_page: selectedPage,
|
|
1832
|
-
active_tab_status: activeTabStatus || null,
|
|
1833
|
-
search_params: effectiveSearchParams,
|
|
1834
|
-
screen_params: parsed.screenParams,
|
|
1835
|
-
selected_job: selectedJob,
|
|
1836
|
-
partial_result: {
|
|
1837
|
-
candidate_count: searchSummary.candidate_count ?? null,
|
|
1838
|
-
applied_filters: searchSummary.applied_filters || effectiveSearchParams,
|
|
1839
|
-
output_csv: currentResumeConfig.output_csv || null,
|
|
1840
|
-
completion_reason: "paused_before_screen"
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
const tabSwitchResult = await ensureSelectedPageTab();
|
|
1845
|
-
if (!tabSwitchResult.ok) {
|
|
1846
|
-
return buildFailedResponse(
|
|
1847
|
-
tabSwitchResult.error?.code || "RECOMMEND_TAB_SWITCH_FAILED",
|
|
1848
|
-
tabSwitchResult.error?.message || `切换到${PAGE_SCOPE_LABELS[selectedPage]} tab 失败。`,
|
|
1849
|
-
{
|
|
1850
|
-
selected_page: selectedPage,
|
|
1851
|
-
active_tab_status: activeTabStatus || null,
|
|
1852
|
-
search_params: effectiveSearchParams,
|
|
1853
|
-
screen_params: parsed.screenParams,
|
|
1854
|
-
selected_job: selectedJob,
|
|
1855
|
-
required_user_action: "retry_switch_recommend_tab",
|
|
1856
|
-
guidance: {
|
|
1857
|
-
expected_tab_status: selectedTabStatus,
|
|
1858
|
-
expected_page_scope: selectedPage,
|
|
1859
|
-
expected_page_label: PAGE_SCOPE_LABELS[selectedPage]
|
|
1860
|
-
},
|
|
1861
|
-
diagnostics: {
|
|
1862
|
-
debug_port: preflight.debug_port,
|
|
1863
|
-
tab_switch: tabSwitchResult
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
);
|
|
1867
|
-
}
|
|
1868
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1869
|
-
runtimeHooks.setStage(
|
|
1870
|
-
"screen",
|
|
1871
|
-
selectedPage !== "recommend"
|
|
1872
|
-
? `search 完成,已切换到${PAGE_SCOPE_LABELS[selectedPage]} tab,开始执行 recommend screen。`
|
|
1873
|
-
: "search 完成,开始执行 recommend screen。"
|
|
1874
|
-
);
|
|
1875
|
-
} else {
|
|
1876
|
-
const tabSwitchResult = await ensureSelectedPageTab();
|
|
1877
|
-
if (!tabSwitchResult.ok) {
|
|
1878
|
-
return buildFailedResponse(
|
|
1879
|
-
tabSwitchResult.error?.code || "RECOMMEND_TAB_SWITCH_FAILED",
|
|
1880
|
-
tabSwitchResult.error?.message || `切换到${PAGE_SCOPE_LABELS[selectedPage]} tab 失败。`,
|
|
1881
|
-
{
|
|
1882
|
-
selected_page: selectedPage,
|
|
1883
|
-
active_tab_status: activeTabStatus || null,
|
|
1884
|
-
search_params: effectiveSearchParams,
|
|
1885
|
-
screen_params: parsed.screenParams,
|
|
1886
|
-
selected_job: selectedJob,
|
|
1887
|
-
required_user_action: "retry_switch_recommend_tab",
|
|
1888
|
-
guidance: {
|
|
1889
|
-
expected_tab_status: selectedTabStatus,
|
|
1890
|
-
expected_page_scope: selectedPage,
|
|
1891
|
-
expected_page_label: PAGE_SCOPE_LABELS[selectedPage]
|
|
1892
|
-
},
|
|
1893
|
-
diagnostics: {
|
|
1894
|
-
debug_port: preflight.debug_port,
|
|
1895
|
-
tab_switch: tabSwitchResult
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1901
|
-
runtimeHooks.setStage("screen", "检测到可续跑 checkpoint,跳过 search,直接恢复 recommend screen。");
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
runtimeHooks.heartbeat("screen", lastAutoRecovery);
|
|
1905
|
-
const screenResult = await screenCli({
|
|
1906
|
-
workspaceRoot,
|
|
1907
|
-
screenParams: parsed.screenParams,
|
|
1908
|
-
pageScope: selectedPage,
|
|
1909
|
-
inputSummary: buildScreenInputSummary({
|
|
1910
|
-
instruction,
|
|
1911
|
-
selectedPage,
|
|
1912
|
-
selectedJob,
|
|
1913
|
-
userSearchParams: parsed.searchParams,
|
|
1914
|
-
effectiveSearchParams,
|
|
1915
|
-
screenParams: parsed.screenParams,
|
|
1916
|
-
followUp: parsed.follow_up || null
|
|
1917
|
-
}),
|
|
1918
|
-
resume: currentResumeConfig,
|
|
1919
|
-
runtime: runtimeHooks.adapterRuntime("screen")
|
|
1920
|
-
});
|
|
1921
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
1922
|
-
if (isProcessAbortError(screenResult.error)) {
|
|
1923
|
-
throw new PipelineAbortError(screenResult.error?.message || "推荐筛选已取消。");
|
|
1924
|
-
}
|
|
1925
|
-
if (screenResult.paused) {
|
|
1926
|
-
return buildPausedResponse("Recommend 流水线已暂停,可使用 resume 继续。", {
|
|
1927
|
-
selected_page: selectedPage,
|
|
1928
|
-
active_tab_status: activeTabStatus || null,
|
|
1929
|
-
search_params: effectiveSearchParams,
|
|
1930
|
-
screen_params: parsed.screenParams,
|
|
1931
|
-
selected_job: selectedJob,
|
|
1932
|
-
partial_result: screenResult.summary || screenResult.structured?.result || null
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
if (!screenResult.ok) {
|
|
1936
|
-
const screenErrorCode = String(screenResult.error?.code || "");
|
|
1937
|
-
const partialScreenResult = screenResult.summary || screenResult.structured?.result || null;
|
|
1938
|
-
const resumeOutputCsv = normalizeText(partialScreenResult?.output_csv || currentResumeConfig.output_csv || "");
|
|
1939
|
-
const hasCheckpointForRecovery = Boolean(normalizeText(currentResumeConfig.checkpoint_path || ""));
|
|
1940
|
-
const screenPartialForRecovery = partialScreenResult
|
|
1941
|
-
? {
|
|
1942
|
-
processed_count: partialScreenResult.processed_count ?? null,
|
|
1943
|
-
passed_count: partialScreenResult.passed_count ?? null,
|
|
1944
|
-
skipped_count: partialScreenResult.skipped_count ?? null,
|
|
1945
|
-
output_csv: partialScreenResult.output_csv || currentResumeConfig.output_csv || null,
|
|
1946
|
-
checkpoint_path: partialScreenResult.checkpoint_path || currentResumeConfig.checkpoint_path || null,
|
|
1947
|
-
completion_reason: partialScreenResult.completion_reason || null
|
|
1948
|
-
}
|
|
1949
|
-
: null;
|
|
1950
|
-
const isResumeCaptureRecovery = screenErrorCode === "RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT";
|
|
1951
|
-
const isPageExhaustedRecovery = screenErrorCode === "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED";
|
|
1952
|
-
const isRecoverableScreenFailure = isResumeCaptureRecovery || isPageExhaustedRecovery;
|
|
1953
|
-
const canRecoverSafely = hasCheckpointForRecovery && Boolean(resumeOutputCsv);
|
|
1954
|
-
const hasRecoveryAttemptsRemaining = screenAutoRecoveryCount < MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS;
|
|
1955
|
-
|
|
1956
|
-
if (isRecoverableScreenFailure && !canRecoverSafely) {
|
|
1957
|
-
return buildFailedResponse(
|
|
1958
|
-
"SCREEN_AUTO_RECOVERY_UNSAFE",
|
|
1959
|
-
"检测到 recommend 自动恢复触发,但缺少 checkpoint 或 output_csv,无法安全续跑。",
|
|
1960
|
-
{
|
|
1961
|
-
search_params: effectiveSearchParams,
|
|
1962
|
-
screen_params: parsed.screenParams,
|
|
1963
|
-
selected_job: selectedJob,
|
|
1964
|
-
partial_result: partialScreenResult,
|
|
1965
|
-
diagnostics: {
|
|
1966
|
-
debug_port: preflight.debug_port,
|
|
1967
|
-
stdout: screenResult.stdout?.slice(-1000),
|
|
1968
|
-
stderr: screenResult.stderr?.slice(-1000),
|
|
1969
|
-
result: screenResult.structured || null,
|
|
1970
|
-
auto_recovery: {
|
|
1971
|
-
trigger: screenErrorCode,
|
|
1972
|
-
attempt: screenAutoRecoveryCount,
|
|
1973
|
-
max_attempts: MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS,
|
|
1974
|
-
original_recent_not_view: parsed.searchParams.recent_not_view,
|
|
1975
|
-
effective_recent_not_view: effectiveSearchParams.recent_not_view,
|
|
1976
|
-
partial_result: screenPartialForRecovery
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
);
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
if (isRecoverableScreenFailure && !hasRecoveryAttemptsRemaining) {
|
|
1984
|
-
return buildFailedResponse(
|
|
1985
|
-
screenResult.error?.code || "RECOMMEND_SCREEN_FAILED",
|
|
1986
|
-
`${screenResult.error?.message || "推荐页筛选执行失败。"} 已达到自动恢复上限 ${MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS} 次。`,
|
|
1987
|
-
{
|
|
1988
|
-
search_params: effectiveSearchParams,
|
|
1989
|
-
screen_params: parsed.screenParams,
|
|
1990
|
-
selected_job: selectedJob,
|
|
1991
|
-
partial_result: partialScreenResult,
|
|
1992
|
-
diagnostics: {
|
|
1993
|
-
debug_port: preflight.debug_port,
|
|
1994
|
-
stdout: screenResult.stdout?.slice(-1000),
|
|
1995
|
-
stderr: screenResult.stderr?.slice(-1000),
|
|
1996
|
-
result: screenResult.structured || null,
|
|
1997
|
-
auto_recovery: lastAutoRecovery
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
if (isRecoverableScreenFailure && canRecoverSafely && hasRecoveryAttemptsRemaining) {
|
|
2004
|
-
screenAutoRecoveryCount += 1;
|
|
2005
|
-
lastAutoRecovery = {
|
|
2006
|
-
trigger: screenErrorCode,
|
|
2007
|
-
attempt: screenAutoRecoveryCount,
|
|
2008
|
-
max_attempts: MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS,
|
|
2009
|
-
original_recent_not_view: parsed.searchParams.recent_not_view,
|
|
2010
|
-
effective_recent_not_view: effectiveSearchParams.recent_not_view,
|
|
2011
|
-
partial_result: screenPartialForRecovery,
|
|
2012
|
-
page_exhaustion: screenResult.error?.page_exhaustion || null
|
|
2013
|
-
};
|
|
2014
|
-
|
|
2015
|
-
if (isPageExhaustedRecovery) {
|
|
2016
|
-
runtimeHooks.setStage(
|
|
2017
|
-
"screen_recovery",
|
|
2018
|
-
`推荐列表已到底但未达目标,开始自动补货(第 ${screenAutoRecoveryCount} 次):优先尝试页内刷新。`
|
|
2019
|
-
);
|
|
2020
|
-
runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
|
|
2021
|
-
|
|
2022
|
-
const refreshResult = typeof refreshRecommendList === "function"
|
|
2023
|
-
? await refreshRecommendList(workspaceRoot, {
|
|
2024
|
-
port: preflight.debug_port
|
|
2025
|
-
})
|
|
2026
|
-
: {
|
|
2027
|
-
ok: false,
|
|
2028
|
-
action: "in_page_refresh",
|
|
2029
|
-
state: "REFRESH_ADAPTER_MISSING",
|
|
2030
|
-
message: "缺少页内刷新适配器。"
|
|
2031
|
-
};
|
|
2032
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
2033
|
-
|
|
2034
|
-
lastAutoRecovery = {
|
|
2035
|
-
...lastAutoRecovery,
|
|
2036
|
-
refresh: refreshResult
|
|
2037
|
-
? {
|
|
2038
|
-
ok: refreshResult.ok,
|
|
2039
|
-
state: refreshResult.state || null,
|
|
2040
|
-
message: refreshResult.message || null,
|
|
2041
|
-
before_state: refreshResult.before_state || null,
|
|
2042
|
-
after_state: refreshResult.after_state || null
|
|
2043
|
-
}
|
|
2044
|
-
: null
|
|
2045
|
-
};
|
|
2046
|
-
|
|
2047
|
-
if (refreshResult?.ok) {
|
|
2048
|
-
lastAutoRecovery = {
|
|
2049
|
-
...lastAutoRecovery,
|
|
2050
|
-
action: "in_page_refresh"
|
|
2051
|
-
};
|
|
2052
|
-
currentResumeConfig = {
|
|
2053
|
-
checkpoint_path: currentResumeConfig.checkpoint_path || null,
|
|
2054
|
-
pause_control_path: currentResumeConfig.pause_control_path || null,
|
|
2055
|
-
output_csv: resumeOutputCsv || null,
|
|
2056
|
-
resume: true,
|
|
2057
|
-
require_checkpoint: true
|
|
2058
|
-
};
|
|
2059
|
-
shouldRunSearch = false;
|
|
2060
|
-
searchSummary = null;
|
|
2061
|
-
continue;
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
runtimeHooks.setStage(
|
|
2065
|
-
"screen_recovery",
|
|
2066
|
-
`页内刷新不可用(${refreshResult?.state || "unknown"}),改为刷新 recommend 页面并重跑 search。`
|
|
2067
|
-
);
|
|
2068
|
-
runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
|
|
2069
|
-
} else {
|
|
2070
|
-
const recoveryFailureText = "简历获取失败(network + 截图)";
|
|
2071
|
-
runtimeHooks.setStage(
|
|
2072
|
-
"screen_recovery",
|
|
2073
|
-
`screen 连续${recoveryFailureText},开始自动恢复(第 ${screenAutoRecoveryCount} 次):刷新 recommend 页面并重跑 search。`
|
|
2074
|
-
);
|
|
2075
|
-
runtimeHooks.heartbeat("screen_recovery", lastAutoRecovery);
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
effectiveSearchParams = {
|
|
2079
|
-
...effectiveSearchParams,
|
|
2080
|
-
recent_not_view: FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY
|
|
2081
|
-
};
|
|
2082
|
-
lastAutoRecovery = {
|
|
2083
|
-
...lastAutoRecovery,
|
|
2084
|
-
action: "reload_page_and_rerun_search",
|
|
2085
|
-
effective_recent_not_view: effectiveSearchParams.recent_not_view
|
|
2086
|
-
};
|
|
2087
|
-
|
|
2088
|
-
const reloadResult = typeof reloadRecommendPage === "function"
|
|
2089
|
-
? await reloadRecommendPage(workspaceRoot, {
|
|
2090
|
-
port: preflight.debug_port
|
|
2091
|
-
})
|
|
2092
|
-
: null;
|
|
2093
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
2094
|
-
|
|
2095
|
-
lastAutoRecovery = {
|
|
2096
|
-
...lastAutoRecovery,
|
|
2097
|
-
reload: reloadResult
|
|
2098
|
-
? {
|
|
2099
|
-
ok: reloadResult.ok,
|
|
2100
|
-
state: reloadResult.state || null,
|
|
2101
|
-
message: reloadResult.message || null,
|
|
2102
|
-
reloaded_url: reloadResult.reloaded_url || null
|
|
2103
|
-
}
|
|
2104
|
-
: null
|
|
2105
|
-
};
|
|
2106
|
-
|
|
2107
|
-
const recoveryPageCheck = await ensureRecommendPageReady(workspaceRoot, {
|
|
2108
|
-
port: preflight.debug_port
|
|
2109
|
-
});
|
|
2110
|
-
ensurePipelineNotAborted(runtimeHooks.signal);
|
|
2111
|
-
if (!recoveryPageCheck.ok) {
|
|
2112
|
-
const guidance = buildChromeSetupGuidance({
|
|
2113
|
-
debugPort: preflight.debug_port,
|
|
2114
|
-
pageState: recoveryPageCheck.page_state
|
|
2115
|
-
});
|
|
2116
|
-
return buildFailedResponse(
|
|
2117
|
-
recoveryPageCheck.state === "LOGIN_REQUIRED" || recoveryPageCheck.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
2118
|
-
? "BOSS_LOGIN_REQUIRED"
|
|
2119
|
-
: recoveryPageCheck.state === "DEBUG_PORT_UNREACHABLE"
|
|
2120
|
-
? "BOSS_CHROME_NOT_CONNECTED"
|
|
2121
|
-
: "BOSS_RECOMMEND_PAGE_NOT_READY",
|
|
2122
|
-
"自动恢复时无法重新就绪 recommend 页面,请先处理页面状态后再继续。",
|
|
2123
|
-
{
|
|
2124
|
-
search_params: effectiveSearchParams,
|
|
2125
|
-
screen_params: parsed.screenParams,
|
|
2126
|
-
selected_job: selectedJob,
|
|
2127
|
-
partial_result: partialScreenResult,
|
|
2128
|
-
required_user_action: "prepare_boss_recommend_page",
|
|
2129
|
-
guidance,
|
|
2130
|
-
diagnostics: {
|
|
2131
|
-
debug_port: preflight.debug_port,
|
|
2132
|
-
page_state: recoveryPageCheck.page_state,
|
|
2133
|
-
stdout: screenResult.stdout?.slice(-1000),
|
|
2134
|
-
stderr: screenResult.stderr?.slice(-1000),
|
|
2135
|
-
result: screenResult.structured || null,
|
|
2136
|
-
auto_recovery: lastAutoRecovery
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
currentResumeConfig = {
|
|
2143
|
-
checkpoint_path: currentResumeConfig.checkpoint_path || null,
|
|
2144
|
-
pause_control_path: currentResumeConfig.pause_control_path || null,
|
|
2145
|
-
output_csv: resumeOutputCsv || null,
|
|
2146
|
-
resume: true,
|
|
2147
|
-
require_checkpoint: true
|
|
2148
|
-
};
|
|
2149
|
-
shouldRunSearch = true;
|
|
2150
|
-
searchSummary = null;
|
|
2151
|
-
continue;
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
return buildFailedResponse(
|
|
2155
|
-
screenResult.error?.code || "RECOMMEND_SCREEN_FAILED",
|
|
2156
|
-
screenResult.error?.message || "推荐页筛选执行失败。",
|
|
2157
|
-
{
|
|
2158
|
-
search_params: effectiveSearchParams,
|
|
2159
|
-
screen_params: parsed.screenParams,
|
|
2160
|
-
selected_job: selectedJob,
|
|
2161
|
-
partial_result: partialScreenResult,
|
|
2162
|
-
diagnostics: {
|
|
2163
|
-
debug_port: preflight.debug_port,
|
|
2164
|
-
stdout: screenResult.stdout?.slice(-1000),
|
|
2165
|
-
stderr: screenResult.stderr?.slice(-1000),
|
|
2166
|
-
result: screenResult.structured || null,
|
|
2167
|
-
auto_recovery: lastAutoRecovery
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
runtimeHooks.setStage("finalize", "screen 完成,正在汇总结果。");
|
|
2174
|
-
runtimeHooks.heartbeat("finalize");
|
|
2175
|
-
const durationSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
2176
|
-
const finalSearchSummary = searchSummary || {};
|
|
2177
|
-
const screenSummary = screenResult.summary || {};
|
|
2178
|
-
const resolvedActiveTabStatus = normalizeText(
|
|
2179
|
-
screenSummary.active_tab_status
|
|
2180
|
-
|| finalSearchSummary.active_tab_status
|
|
2181
|
-
|| activeTabStatus
|
|
2182
|
-
|| selectedTabStatus
|
|
2183
|
-
) || selectedTabStatus;
|
|
2184
|
-
const resolvedSelectedPage = normalizePageScope(
|
|
2185
|
-
screenSummary.selected_page
|
|
2186
|
-
|| finalSearchSummary.selected_page
|
|
2187
|
-
|| selectedPage
|
|
2188
|
-
|| tabStatusToPageScope(resolvedActiveTabStatus)
|
|
2189
|
-
) || selectedPage;
|
|
2190
|
-
const resolvedResumeSourceRaw = normalizeText(screenSummary.resume_source || "").toLowerCase();
|
|
2191
|
-
const resolvedResumeSource = ["network", "image_fallback"].includes(resolvedResumeSourceRaw)
|
|
2192
|
-
? resolvedResumeSourceRaw
|
|
2193
|
-
: "network";
|
|
2194
|
-
runtimeHooks.progress("finalize", {
|
|
2195
|
-
processed: screenSummary.processed_count ?? 0,
|
|
2196
|
-
passed: screenSummary.passed_count ?? 0,
|
|
2197
|
-
skipped: screenSummary.skipped_count ?? 0,
|
|
2198
|
-
greet_count: screenSummary.greet_count ?? 0
|
|
2199
|
-
});
|
|
2200
|
-
|
|
2201
|
-
const recommendResult = {
|
|
2202
|
-
status: "COMPLETED",
|
|
2203
|
-
search_params: effectiveSearchParams,
|
|
2204
|
-
screen_params: parsed.screenParams,
|
|
2205
|
-
result: {
|
|
2206
|
-
candidate_count: finalSearchSummary.candidate_count ?? null,
|
|
2207
|
-
applied_filters: finalSearchSummary.applied_filters || effectiveSearchParams,
|
|
2208
|
-
processed_count: screenSummary.processed_count ?? 0,
|
|
2209
|
-
passed_count: screenSummary.passed_count ?? 0,
|
|
2210
|
-
skipped_count: screenSummary.skipped_count ?? 0,
|
|
2211
|
-
duration_sec: durationSec,
|
|
2212
|
-
output_csv: screenSummary.output_csv || null,
|
|
2213
|
-
completion_reason: screenSummary.completion_reason || "screen_completed",
|
|
2214
|
-
page_state: finalSearchSummary.page_state || pageCheck.page_state,
|
|
2215
|
-
selected_job: finalSearchSummary.selected_job || selectedJob,
|
|
2216
|
-
selected_page: resolvedSelectedPage,
|
|
2217
|
-
active_tab_status: resolvedActiveTabStatus,
|
|
2218
|
-
resume_source: resolvedResumeSource,
|
|
2219
|
-
post_action: parsed.screenParams.post_action,
|
|
2220
|
-
max_greet_count: parsed.screenParams.max_greet_count,
|
|
2221
|
-
greet_count: screenSummary.greet_count ?? 0,
|
|
2222
|
-
greet_limit_fallback_count: screenSummary.greet_limit_fallback_count ?? 0,
|
|
2223
|
-
auto_recovery: lastAutoRecovery
|
|
2224
|
-
},
|
|
2225
|
-
message: parsed.screenParams.post_action === "none"
|
|
2226
|
-
? "Recommend 流水线已完成。本次 post_action=none:符合条件的人选仅记录到 CSV,不执行收藏或打招呼。"
|
|
2227
|
-
: "Recommend 流水线已完成。post_action 在运行开始时已一次性确认;若选择打招呼并设置上限,超出上限后会自动改为收藏。"
|
|
2228
|
-
};
|
|
2229
|
-
|
|
2230
|
-
if (!parsed.follow_up_chat?.requested) {
|
|
2231
|
-
return recommendResult;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
return runBossChatFollowUpPhase({
|
|
2235
|
-
workspaceRoot,
|
|
2236
|
-
followUpChat: parsed.follow_up_chat,
|
|
2237
|
-
selectedJob,
|
|
2238
|
-
debugPort: preflight.debug_port,
|
|
2239
|
-
recommendResult,
|
|
2240
|
-
resume,
|
|
2241
|
-
runtimeHooks,
|
|
2242
|
-
startChatRun,
|
|
2243
|
-
getChatRun,
|
|
2244
|
-
pauseChatRun,
|
|
2245
|
-
resumeChatRun,
|
|
2246
|
-
cancelChatRun
|
|
2247
|
-
});
|
|
2248
|
-
}
|
|
2249
|
-
}
|