@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/adapters.js
DELETED
|
@@ -1,3079 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import CDP from "chrome-remote-interface";
|
|
7
|
-
import {
|
|
8
|
-
buildFirstSelectorLookupExpression,
|
|
9
|
-
getRecommendSelectorRule
|
|
10
|
-
} from "./recommend-healing-config.js";
|
|
11
|
-
|
|
12
|
-
const currentFilePath = fileURLToPath(import.meta.url);
|
|
13
|
-
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
14
|
-
const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
|
|
15
|
-
const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
16
|
-
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
17
|
-
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
18
|
-
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
19
|
-
const screenConfigTemplateDefaults = {
|
|
20
|
-
baseUrl: "https://api.openai.com/v1",
|
|
21
|
-
apiKey: "replace-with-openai-api-key",
|
|
22
|
-
model: "gpt-4.1-mini"
|
|
23
|
-
};
|
|
24
|
-
const LLM_THINKING_LEVEL_FIELDS = [
|
|
25
|
-
"llmThinkingLevel",
|
|
26
|
-
"thinkingLevel",
|
|
27
|
-
"reasoningEffort",
|
|
28
|
-
"reasoning_effort"
|
|
29
|
-
];
|
|
30
|
-
const DEFAULT_SHARED_LLM_TIMEOUT_MS = 60000;
|
|
31
|
-
const DEFAULT_SHARED_LLM_MAX_RETRIES = 3;
|
|
32
|
-
const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
33
|
-
const PAGE_SCOPE_TO_TAB_STATUS = {
|
|
34
|
-
recommend: "0",
|
|
35
|
-
latest: "1",
|
|
36
|
-
featured: "3"
|
|
37
|
-
};
|
|
38
|
-
const RECOMMEND_IFRAME_SELECTORS = getRecommendSelectorRule(
|
|
39
|
-
["top", "recommend_iframe"],
|
|
40
|
-
['iframe[name="recommendFrame"]', 'iframe[src*="/web/frame/recommend/"]', "iframe"]
|
|
41
|
-
);
|
|
42
|
-
const RECOMMEND_TAB_SELECTORS = getRecommendSelectorRule(
|
|
43
|
-
["frame", "tab_items"],
|
|
44
|
-
['li.tab-item[data-status]', 'li[data-status][class*="tab"]']
|
|
45
|
-
);
|
|
46
|
-
const RECOMMEND_CARD_SELECTORS = getRecommendSelectorRule(["frame", "recommend_cards"], ['ul.card-list > li.card-item']);
|
|
47
|
-
const FEATURED_CARD_SELECTORS = getRecommendSelectorRule(["frame", "featured_cards"], ['li.geek-info-card']);
|
|
48
|
-
const LATEST_CARD_SELECTORS = getRecommendSelectorRule(["frame", "latest_cards"], ['.candidate-card-wrap .card-inner[data-geek]', '.candidate-card-wrap [data-geek]']);
|
|
49
|
-
const DETAIL_POPUP_SELECTORS = getRecommendSelectorRule(
|
|
50
|
-
["detail", "popup"],
|
|
51
|
-
[
|
|
52
|
-
".dialog-wrap.active",
|
|
53
|
-
".boss-popup__wrapper",
|
|
54
|
-
".boss-popup_wrapper",
|
|
55
|
-
".boss-dialog_wrapper",
|
|
56
|
-
".boss-dialog",
|
|
57
|
-
".resume-item-detail",
|
|
58
|
-
".geek-detail-modal",
|
|
59
|
-
'[class*="popup"][class*="wrapper"]',
|
|
60
|
-
'[class*="dialog"][class*="wrapper"]'
|
|
61
|
-
]
|
|
62
|
-
);
|
|
63
|
-
const DETAIL_RESUME_IFRAME_SELECTORS = getRecommendSelectorRule(
|
|
64
|
-
["detail", "resume_iframe"],
|
|
65
|
-
['iframe[src*="/web/frame/c-resume/"]', 'iframe[name*="resume"]']
|
|
66
|
-
);
|
|
67
|
-
const REFRESH_FINISHED_WRAP_SELECTORS = getRecommendSelectorRule(["frame", "refresh_finished_wrap"], [".finished-wrap"]);
|
|
68
|
-
const REFRESH_BUTTON_SELECTORS = getRecommendSelectorRule(
|
|
69
|
-
["frame", "refresh_button"],
|
|
70
|
-
[".finished-wrap .btn.btn-refresh", ".finished-wrap .btn-refresh", ".no-data-refresh .btn-refresh"]
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
function buildRecommendIframeLookupExpression(rootExpr = "document") {
|
|
74
|
-
return buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS, rootExpr);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getCodexHome() {
|
|
78
|
-
return process.env.CODEX_HOME
|
|
79
|
-
? path.resolve(process.env.CODEX_HOME)
|
|
80
|
-
: path.join(os.homedir(), ".codex");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getStateHome() {
|
|
84
|
-
return process.env.BOSS_RECOMMEND_HOME
|
|
85
|
-
? path.resolve(process.env.BOSS_RECOMMEND_HOME)
|
|
86
|
-
: path.join(os.homedir(), ".boss-recommend-mcp");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getUserConfigPath() {
|
|
90
|
-
return path.join(getStateHome(), "screening-config.json");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function getLegacyUserConfigPath() {
|
|
94
|
-
return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function getUserCalibrationPath() {
|
|
98
|
-
return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getDesktopDir() {
|
|
102
|
-
return path.join(os.homedir(), "Desktop");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function ensureDir(targetPath) {
|
|
106
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function pathExists(targetPath) {
|
|
110
|
-
try {
|
|
111
|
-
return fs.existsSync(targetPath);
|
|
112
|
-
} catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function parsePositiveInteger(raw) {
|
|
118
|
-
const value = Number.parseInt(String(raw || ""), 10);
|
|
119
|
-
return Number.isFinite(value) && value > 0 ? value : null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function normalizeText(value) {
|
|
123
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function parseBooleanValue(value) {
|
|
127
|
-
if (typeof value === "boolean") return value;
|
|
128
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
129
|
-
if (!normalized) return null;
|
|
130
|
-
if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
|
|
131
|
-
if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function resolveHumanRestEnabled(config = {}) {
|
|
136
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
137
|
-
const candidates = [
|
|
138
|
-
config.humanRestEnabled,
|
|
139
|
-
config.human_rest_enabled,
|
|
140
|
-
config.humanLikeRestEnabled,
|
|
141
|
-
config.human_like_rest_enabled
|
|
142
|
-
];
|
|
143
|
-
for (const candidate of candidates) {
|
|
144
|
-
const parsed = parseBooleanValue(candidate);
|
|
145
|
-
if (typeof parsed === "boolean") return parsed;
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function serializeInputSummary(value) {
|
|
151
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
152
|
-
try {
|
|
153
|
-
return JSON.stringify(value);
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function normalizePageScope(value) {
|
|
160
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
161
|
-
if (!normalized) return null;
|
|
162
|
-
if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
|
|
163
|
-
if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
|
|
164
|
-
if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function shouldBringChromeToFront() {
|
|
169
|
-
const envValue = normalizeText(process.env.BOSS_RECOMMEND_BRING_TO_FRONT || "").toLowerCase();
|
|
170
|
-
if (envValue) {
|
|
171
|
-
if (["1", "true", "yes", "y", "on"].includes(envValue)) return true;
|
|
172
|
-
if (["0", "false", "no", "n", "off"].includes(envValue)) return false;
|
|
173
|
-
}
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function isRootDirectory(targetPath) {
|
|
178
|
-
const resolved = path.resolve(String(targetPath || ""));
|
|
179
|
-
const parsed = path.parse(resolved);
|
|
180
|
-
return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
|
|
184
|
-
const root = path.resolve(String(workspaceRoot || ""));
|
|
185
|
-
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
186
|
-
if (process.platform === "win32") {
|
|
187
|
-
return (
|
|
188
|
-
normalized.endsWith("/windows")
|
|
189
|
-
|| normalized.endsWith("/windows/system32")
|
|
190
|
-
|| normalized.endsWith("/windows/syswow64")
|
|
191
|
-
|| normalized.endsWith("/program files")
|
|
192
|
-
|| normalized.endsWith("/program files (x86)")
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
return (
|
|
196
|
-
normalized === "/system"
|
|
197
|
-
|| normalized.startsWith("/system/")
|
|
198
|
-
|| normalized === "/usr"
|
|
199
|
-
|| normalized.startsWith("/usr/")
|
|
200
|
-
|| normalized === "/bin"
|
|
201
|
-
|| normalized.startsWith("/bin/")
|
|
202
|
-
|| normalized === "/sbin"
|
|
203
|
-
|| normalized.startsWith("/sbin/")
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
|
|
208
|
-
const root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
209
|
-
const home = path.resolve(os.homedir());
|
|
210
|
-
return (
|
|
211
|
-
isEphemeralNpxWorkspaceRoot(root)
|
|
212
|
-
|| isRootDirectory(root)
|
|
213
|
-
|| root.toLowerCase() === home.toLowerCase()
|
|
214
|
-
|| isSystemDirectoryWorkspaceRoot(root)
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function resolveWorkspaceConfigCandidates(workspaceRoot) {
|
|
219
|
-
const root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
220
|
-
if (shouldIgnoreWorkspaceConfigRoot(root)) {
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
const directPath = path.join(root, "config", "screening-config.json");
|
|
224
|
-
const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
|
|
225
|
-
const candidates = [directPath];
|
|
226
|
-
if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
|
|
227
|
-
candidates.push(nestedPath);
|
|
228
|
-
}
|
|
229
|
-
return Array.from(new Set(candidates));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function serializeDegreeSelection(value) {
|
|
233
|
-
if (Array.isArray(value)) {
|
|
234
|
-
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
235
|
-
return normalized.length ? normalized.join(",") : "不限";
|
|
236
|
-
}
|
|
237
|
-
const normalized = String(value || "").trim();
|
|
238
|
-
return normalized || "不限";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function serializeSchoolTagSelection(value) {
|
|
242
|
-
if (Array.isArray(value)) {
|
|
243
|
-
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
244
|
-
if (!normalized.length) return "不限";
|
|
245
|
-
if (normalized.includes("不限")) {
|
|
246
|
-
return normalized.length === 1
|
|
247
|
-
? "不限"
|
|
248
|
-
: normalized.filter((item) => item !== "不限").join(",");
|
|
249
|
-
}
|
|
250
|
-
return normalized.join(",");
|
|
251
|
-
}
|
|
252
|
-
const normalized = String(value || "").trim();
|
|
253
|
-
return normalized || "不限";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
|
|
257
|
-
const root = path.resolve(String(workspaceRoot || ""));
|
|
258
|
-
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
259
|
-
return (
|
|
260
|
-
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
261
|
-
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function buildScreenConfigCandidateMap(workspaceRoot) {
|
|
266
|
-
return {
|
|
267
|
-
env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
|
|
268
|
-
? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
|
|
269
|
-
: null,
|
|
270
|
-
workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
|
|
271
|
-
user_path: getUserConfigPath(),
|
|
272
|
-
legacy_path: getLegacyUserConfigPath()
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function resolveScreenConfigCandidates(workspaceRoot) {
|
|
277
|
-
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
278
|
-
return [
|
|
279
|
-
candidateMap.env_path,
|
|
280
|
-
candidateMap.user_path,
|
|
281
|
-
...candidateMap.workspace_paths,
|
|
282
|
-
candidateMap.legacy_path
|
|
283
|
-
].filter(Boolean);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function canWriteDirectory(targetDir) {
|
|
287
|
-
try {
|
|
288
|
-
ensureDir(targetDir);
|
|
289
|
-
fs.accessSync(targetDir, fs.constants.W_OK);
|
|
290
|
-
return true;
|
|
291
|
-
} catch {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function resolveWritableScreenConfigPath(workspaceRoot) {
|
|
297
|
-
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
298
|
-
const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
|
|
299
|
-
if (candidateMap.env_path) {
|
|
300
|
-
return candidateMap.env_path;
|
|
301
|
-
}
|
|
302
|
-
if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
|
|
303
|
-
return candidateMap.user_path;
|
|
304
|
-
}
|
|
305
|
-
if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
|
|
306
|
-
return workspacePreferred;
|
|
307
|
-
}
|
|
308
|
-
if (workspacePreferred) {
|
|
309
|
-
return workspacePreferred;
|
|
310
|
-
}
|
|
311
|
-
return candidateMap.user_path || candidateMap.legacy_path;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function resolveScreenConfigPath(workspaceRoot) {
|
|
315
|
-
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
316
|
-
if (candidateMap.env_path) {
|
|
317
|
-
return candidateMap.env_path;
|
|
318
|
-
}
|
|
319
|
-
if (candidateMap.user_path && pathExists(candidateMap.user_path)) {
|
|
320
|
-
return candidateMap.user_path;
|
|
321
|
-
}
|
|
322
|
-
const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
|
|
323
|
-
if (existingWorkspacePath) {
|
|
324
|
-
return existingWorkspacePath;
|
|
325
|
-
}
|
|
326
|
-
const writablePath = resolveWritableScreenConfigPath(workspaceRoot);
|
|
327
|
-
if (writablePath) {
|
|
328
|
-
return writablePath;
|
|
329
|
-
}
|
|
330
|
-
return candidateMap.legacy_path;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function getScreenConfigResolution(workspaceRoot) {
|
|
334
|
-
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
335
|
-
const candidate_paths = resolveScreenConfigCandidates(workspaceRoot);
|
|
336
|
-
const resolved_path = resolveScreenConfigPath(workspaceRoot) || null;
|
|
337
|
-
const workspace_root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
338
|
-
return {
|
|
339
|
-
resolved_path,
|
|
340
|
-
candidate_paths,
|
|
341
|
-
workspace_root,
|
|
342
|
-
workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
|
|
343
|
-
workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspace_root),
|
|
344
|
-
writable_path: resolveWritableScreenConfigPath(workspaceRoot),
|
|
345
|
-
legacy_path: candidateMap.legacy_path
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function readJsonFile(filePath) {
|
|
350
|
-
if (!filePath || !pathExists(filePath)) return null;
|
|
351
|
-
try {
|
|
352
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
353
|
-
return JSON.parse(raw);
|
|
354
|
-
} catch {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function validateScreenConfig(config) {
|
|
360
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
361
|
-
return {
|
|
362
|
-
ok: false,
|
|
363
|
-
reason: "INVALID_OR_MISSING_CONFIG",
|
|
364
|
-
message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
const baseUrl = String(config.baseUrl || "").trim();
|
|
368
|
-
const apiKey = String(config.apiKey || "").trim();
|
|
369
|
-
const model = String(config.model || "").trim();
|
|
370
|
-
const missing = [];
|
|
371
|
-
if (!baseUrl) missing.push("baseUrl");
|
|
372
|
-
if (!apiKey) missing.push("apiKey");
|
|
373
|
-
if (!model) missing.push("model");
|
|
374
|
-
if (missing.length > 0) {
|
|
375
|
-
return {
|
|
376
|
-
ok: false,
|
|
377
|
-
reason: "MISSING_REQUIRED_FIELDS",
|
|
378
|
-
message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
|
|
382
|
-
return {
|
|
383
|
-
ok: false,
|
|
384
|
-
reason: "PLACEHOLDER_API_KEY",
|
|
385
|
-
message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
if (
|
|
389
|
-
baseUrl === screenConfigTemplateDefaults.baseUrl
|
|
390
|
-
&& apiKey === screenConfigTemplateDefaults.apiKey
|
|
391
|
-
&& model === screenConfigTemplateDefaults.model
|
|
392
|
-
) {
|
|
393
|
-
return {
|
|
394
|
-
ok: false,
|
|
395
|
-
reason: "PLACEHOLDER_TEMPLATE_VALUES",
|
|
396
|
-
message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function resolveLlmThinkingLevel(config = {}) {
|
|
403
|
-
if (!config || typeof config !== "object") return "";
|
|
404
|
-
for (const field of LLM_THINKING_LEVEL_FIELDS) {
|
|
405
|
-
const value = String(config[field] ?? "").trim();
|
|
406
|
-
if (value) return value;
|
|
407
|
-
}
|
|
408
|
-
return "";
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export function resolveSharedLlmTransportConfig(config = {}) {
|
|
412
|
-
const timeoutMs = parsePositiveInteger(config?.llmTimeoutMs)
|
|
413
|
-
|| parsePositiveInteger(config?.llm_timeout_ms)
|
|
414
|
-
|| DEFAULT_SHARED_LLM_TIMEOUT_MS;
|
|
415
|
-
const maxRetries = parsePositiveInteger(config?.llmMaxRetries)
|
|
416
|
-
|| parsePositiveInteger(config?.llm_max_retries)
|
|
417
|
-
|| DEFAULT_SHARED_LLM_MAX_RETRIES;
|
|
418
|
-
return {
|
|
419
|
-
llmTimeoutMs: timeoutMs,
|
|
420
|
-
llmMaxRetries: maxRetries
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function resolveWorkspaceDebugPort(workspaceRoot) {
|
|
425
|
-
const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
|
|
426
|
-
if (fromEnv) return fromEnv;
|
|
427
|
-
const config = readJsonFile(resolveScreenConfigPath(workspaceRoot));
|
|
428
|
-
return parsePositiveInteger(config?.debugPort) || 9222;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function getDefaultChromeExecutableCandidates() {
|
|
432
|
-
const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
|
|
433
|
-
if (process.platform === "win32") {
|
|
434
|
-
candidates.push(
|
|
435
|
-
path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
436
|
-
path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
|
|
437
|
-
path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
|
|
438
|
-
);
|
|
439
|
-
} else if (process.platform === "darwin") {
|
|
440
|
-
candidates.push(
|
|
441
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
442
|
-
path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
|
|
443
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
444
|
-
);
|
|
445
|
-
} else {
|
|
446
|
-
candidates.push(
|
|
447
|
-
"/usr/bin/google-chrome",
|
|
448
|
-
"/usr/bin/google-chrome-stable",
|
|
449
|
-
"/usr/bin/chromium-browser",
|
|
450
|
-
"/usr/bin/chromium",
|
|
451
|
-
"/snap/bin/chromium"
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
return Array.from(new Set(candidates.filter(Boolean)));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function getChromeExecutable() {
|
|
458
|
-
const candidates = getDefaultChromeExecutableCandidates();
|
|
459
|
-
return candidates.find((candidate) => pathExists(candidate)) || null;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function getChromeUserDataDir(port) {
|
|
463
|
-
const profileDir = resolveDefaultChromeUserDataDir(port);
|
|
464
|
-
ensureDir(profileDir);
|
|
465
|
-
return profileDir;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function getSharedChromeUserDataDir(port) {
|
|
469
|
-
return path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function getLegacyRecruitChromeUserDataDir(port) {
|
|
473
|
-
return path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function getLegacyRecommendChromeUserDataDir(port) {
|
|
477
|
-
return path.join(getStateHome(), `chrome-profile-${port}`);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function resolveDefaultChromeUserDataDir(port) {
|
|
481
|
-
const sharedPath = getSharedChromeUserDataDir(port);
|
|
482
|
-
if (pathExists(sharedPath)) {
|
|
483
|
-
return sharedPath;
|
|
484
|
-
}
|
|
485
|
-
const legacyPaths = [
|
|
486
|
-
getLegacyRecruitChromeUserDataDir(port),
|
|
487
|
-
getLegacyRecommendChromeUserDataDir(port)
|
|
488
|
-
];
|
|
489
|
-
const legacyExisting = legacyPaths.find((candidate) => pathExists(candidate));
|
|
490
|
-
return legacyExisting || sharedPath;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function launchChromeWithDebugPort(port) {
|
|
494
|
-
const chromePath = getChromeExecutable();
|
|
495
|
-
if (!chromePath) {
|
|
496
|
-
return {
|
|
497
|
-
ok: false,
|
|
498
|
-
code: "CHROME_EXECUTABLE_NOT_FOUND",
|
|
499
|
-
message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
const userDataDir = getChromeUserDataDir(port);
|
|
503
|
-
const args = [
|
|
504
|
-
`--remote-debugging-port=${port}`,
|
|
505
|
-
`--user-data-dir=${userDataDir}`,
|
|
506
|
-
"--no-first-run",
|
|
507
|
-
"--no-default-browser-check",
|
|
508
|
-
"--new-window",
|
|
509
|
-
bossRecommendUrl
|
|
510
|
-
];
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const child = spawn(chromePath, args, {
|
|
514
|
-
detached: true,
|
|
515
|
-
stdio: "ignore",
|
|
516
|
-
windowsHide: false
|
|
517
|
-
});
|
|
518
|
-
child.unref();
|
|
519
|
-
return {
|
|
520
|
-
ok: true,
|
|
521
|
-
code: "CHROME_LAUNCHED",
|
|
522
|
-
chrome_path: chromePath,
|
|
523
|
-
user_data_dir: userDataDir
|
|
524
|
-
};
|
|
525
|
-
} catch (error) {
|
|
526
|
-
return {
|
|
527
|
-
ok: false,
|
|
528
|
-
code: "CHROME_LAUNCH_FAILED",
|
|
529
|
-
message: error.message || "Chrome 启动失败。"
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function resolveRecommendSearchCliDir(workspaceRoot) {
|
|
535
|
-
const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
|
|
536
|
-
if (pathExists(localDir)) return localDir;
|
|
537
|
-
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-search-cli");
|
|
538
|
-
if (pathExists(vendoredDir)) return vendoredDir;
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function resolveRecommendScreenCliDir(workspaceRoot) {
|
|
543
|
-
const localDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
|
|
544
|
-
if (pathExists(localDir)) return localDir;
|
|
545
|
-
const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-screen-cli");
|
|
546
|
-
if (pathExists(vendoredDir)) return vendoredDir;
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function resolveRecommendScreenCliEntry(screenDir) {
|
|
551
|
-
const candidates = [
|
|
552
|
-
path.join(screenDir, "boss-recommend-screen-cli.cjs"),
|
|
553
|
-
path.join(screenDir, "boss-recommend-screen-cli.js")
|
|
554
|
-
];
|
|
555
|
-
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function resolveRecommendSearchCliEntry(searchDir) {
|
|
559
|
-
const candidates = [
|
|
560
|
-
path.join(searchDir, "src", "cli.js"),
|
|
561
|
-
path.join(searchDir, "src", "cli.cjs")
|
|
562
|
-
];
|
|
563
|
-
return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function parseKeyValueOutput(text) {
|
|
567
|
-
const result = {};
|
|
568
|
-
for (const line of String(text || "").split(/\r?\n/)) {
|
|
569
|
-
const trimmed = line.trim();
|
|
570
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
571
|
-
const sep = trimmed.indexOf("=");
|
|
572
|
-
if (sep <= 0) continue;
|
|
573
|
-
const key = trimmed.slice(0, sep).trim();
|
|
574
|
-
const value = trimmed.slice(sep + 1).trim();
|
|
575
|
-
if (!key) continue;
|
|
576
|
-
result[key] = value;
|
|
577
|
-
}
|
|
578
|
-
return result;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function runBossRecruitWhere() {
|
|
582
|
-
const direct = runProcessSync({
|
|
583
|
-
command: "boss-recruit-mcp",
|
|
584
|
-
args: ["where"]
|
|
585
|
-
});
|
|
586
|
-
if (direct.ok) {
|
|
587
|
-
return parseKeyValueOutput(direct.stdout);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (process.platform !== "win32") {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const fallback = runProcessSync({
|
|
595
|
-
command: "cmd.exe",
|
|
596
|
-
args: ["/d", "/s", "/c", "boss-recruit-mcp where"]
|
|
597
|
-
});
|
|
598
|
-
if (!fallback.ok) return null;
|
|
599
|
-
return parseKeyValueOutput(fallback.stdout);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function resolveRecruitCalibrationScriptPath(workspaceRoot) {
|
|
603
|
-
const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
|
|
604
|
-
const fromWhere = runBossRecruitWhere();
|
|
605
|
-
const packageRootFromWhere = normalizeText(fromWhere?.package_root || "");
|
|
606
|
-
const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
|
|
607
|
-
const candidates = [
|
|
608
|
-
fromEnv,
|
|
609
|
-
packageRootFromWhere
|
|
610
|
-
? path.join(packageRootFromWhere, "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
|
|
611
|
-
: null,
|
|
612
|
-
path.join(workspaceResolved, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
|
|
613
|
-
path.join(packagedMcpDir, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
|
|
614
|
-
].filter(Boolean).map((item) => path.resolve(item));
|
|
615
|
-
|
|
616
|
-
for (const candidate of new Set(candidates)) {
|
|
617
|
-
if (pathExists(candidate)) {
|
|
618
|
-
return candidate;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function getCalibrationTimeoutMs(raw) {
|
|
625
|
-
const parsed = parsePositiveInteger(raw);
|
|
626
|
-
if (!parsed) return 60000;
|
|
627
|
-
return Math.max(5000, parsed);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function safeInvokeCallback(callback, payload) {
|
|
631
|
-
if (typeof callback !== "function") return;
|
|
632
|
-
try {
|
|
633
|
-
callback(payload);
|
|
634
|
-
} catch {
|
|
635
|
-
// Ignore callback errors to keep pipeline runtime stable.
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function runProcess({
|
|
640
|
-
command,
|
|
641
|
-
args,
|
|
642
|
-
cwd,
|
|
643
|
-
timeoutMs,
|
|
644
|
-
onOutput,
|
|
645
|
-
onLine,
|
|
646
|
-
onHeartbeat,
|
|
647
|
-
heartbeatIntervalMs = 10_000,
|
|
648
|
-
signal
|
|
649
|
-
}) {
|
|
650
|
-
return new Promise((resolve) => {
|
|
651
|
-
let stdout = "";
|
|
652
|
-
let stderr = "";
|
|
653
|
-
let stdoutLineBuffer = "";
|
|
654
|
-
let stderrLineBuffer = "";
|
|
655
|
-
let settled = false;
|
|
656
|
-
let timer = null;
|
|
657
|
-
let heartbeatTimer = null;
|
|
658
|
-
let abortedBySignal = Boolean(signal?.aborted);
|
|
659
|
-
let abortListener = null;
|
|
660
|
-
|
|
661
|
-
function notifyHeartbeat(source) {
|
|
662
|
-
safeInvokeCallback(onHeartbeat, {
|
|
663
|
-
source,
|
|
664
|
-
command,
|
|
665
|
-
args,
|
|
666
|
-
cwd,
|
|
667
|
-
at: new Date().toISOString()
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function emitLine(stream, line) {
|
|
672
|
-
const normalized = String(line ?? "").replace(/\r$/, "");
|
|
673
|
-
if (!normalized) return;
|
|
674
|
-
safeInvokeCallback(onLine, {
|
|
675
|
-
stream,
|
|
676
|
-
line: normalized,
|
|
677
|
-
at: new Date().toISOString()
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function pushLineBuffer(stream, chunkText) {
|
|
682
|
-
if (stream === "stdout") {
|
|
683
|
-
stdoutLineBuffer += chunkText;
|
|
684
|
-
} else {
|
|
685
|
-
stderrLineBuffer += chunkText;
|
|
686
|
-
}
|
|
687
|
-
let buffer = stream === "stdout" ? stdoutLineBuffer : stderrLineBuffer;
|
|
688
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
689
|
-
while (newlineIndex !== -1) {
|
|
690
|
-
emitLine(stream, buffer.slice(0, newlineIndex));
|
|
691
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
692
|
-
newlineIndex = buffer.indexOf("\n");
|
|
693
|
-
}
|
|
694
|
-
if (stream === "stdout") {
|
|
695
|
-
stdoutLineBuffer = buffer;
|
|
696
|
-
} else {
|
|
697
|
-
stderrLineBuffer = buffer;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function finish(payload) {
|
|
702
|
-
if (settled) return;
|
|
703
|
-
settled = true;
|
|
704
|
-
if (timer) clearTimeout(timer);
|
|
705
|
-
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
706
|
-
if (signal && typeof signal.removeEventListener === "function" && abortListener) {
|
|
707
|
-
signal.removeEventListener("abort", abortListener);
|
|
708
|
-
}
|
|
709
|
-
emitLine("stdout", stdoutLineBuffer);
|
|
710
|
-
emitLine("stderr", stderrLineBuffer);
|
|
711
|
-
stdoutLineBuffer = "";
|
|
712
|
-
stderrLineBuffer = "";
|
|
713
|
-
resolve(payload);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (abortedBySignal) {
|
|
717
|
-
finish({
|
|
718
|
-
code: -1,
|
|
719
|
-
stdout,
|
|
720
|
-
stderr: "Process aborted before spawn",
|
|
721
|
-
error_code: "ABORTED"
|
|
722
|
-
});
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
let child;
|
|
727
|
-
try {
|
|
728
|
-
child = spawn(command, args, {
|
|
729
|
-
cwd,
|
|
730
|
-
windowsHide: true,
|
|
731
|
-
shell: false,
|
|
732
|
-
env: process.env
|
|
733
|
-
});
|
|
734
|
-
} catch (error) {
|
|
735
|
-
finish({
|
|
736
|
-
code: -1,
|
|
737
|
-
stdout,
|
|
738
|
-
stderr: error.message,
|
|
739
|
-
error_code: error.code || "SPAWN_FAILED"
|
|
740
|
-
});
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (signal && typeof signal.addEventListener === "function") {
|
|
745
|
-
abortListener = () => {
|
|
746
|
-
abortedBySignal = true;
|
|
747
|
-
try {
|
|
748
|
-
child.kill();
|
|
749
|
-
} catch {}
|
|
750
|
-
};
|
|
751
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
755
|
-
timer = setTimeout(() => {
|
|
756
|
-
try {
|
|
757
|
-
child.kill();
|
|
758
|
-
} catch {}
|
|
759
|
-
finish({
|
|
760
|
-
code: -1,
|
|
761
|
-
stdout,
|
|
762
|
-
stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
|
|
763
|
-
error_code: "TIMEOUT"
|
|
764
|
-
});
|
|
765
|
-
}, timeoutMs);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0) {
|
|
769
|
-
heartbeatTimer = setInterval(() => {
|
|
770
|
-
notifyHeartbeat("timer");
|
|
771
|
-
}, heartbeatIntervalMs);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
child.stdout.on("data", (chunk) => {
|
|
775
|
-
const text = chunk.toString();
|
|
776
|
-
stdout += text;
|
|
777
|
-
pushLineBuffer("stdout", text);
|
|
778
|
-
safeInvokeCallback(onOutput, {
|
|
779
|
-
stream: "stdout",
|
|
780
|
-
text,
|
|
781
|
-
at: new Date().toISOString()
|
|
782
|
-
});
|
|
783
|
-
notifyHeartbeat("stdout");
|
|
784
|
-
});
|
|
785
|
-
child.stderr.on("data", (chunk) => {
|
|
786
|
-
const text = chunk.toString();
|
|
787
|
-
stderr += text;
|
|
788
|
-
pushLineBuffer("stderr", text);
|
|
789
|
-
safeInvokeCallback(onOutput, {
|
|
790
|
-
stream: "stderr",
|
|
791
|
-
text,
|
|
792
|
-
at: new Date().toISOString()
|
|
793
|
-
});
|
|
794
|
-
notifyHeartbeat("stderr");
|
|
795
|
-
});
|
|
796
|
-
child.on("close", (code) => {
|
|
797
|
-
if (abortedBySignal) {
|
|
798
|
-
finish({
|
|
799
|
-
code: -1,
|
|
800
|
-
stdout,
|
|
801
|
-
stderr: `${stderr}\nProcess aborted by signal`.trim(),
|
|
802
|
-
error_code: "ABORTED"
|
|
803
|
-
});
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
finish({ code, stdout, stderr });
|
|
807
|
-
});
|
|
808
|
-
child.on("error", (error) => {
|
|
809
|
-
finish({
|
|
810
|
-
code: -1,
|
|
811
|
-
stdout,
|
|
812
|
-
stderr: `${stderr}\n${error.message}`.trim(),
|
|
813
|
-
error_code: error.code || "SPAWN_FAILED"
|
|
814
|
-
});
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function runProcessSync({ command, args, cwd }) {
|
|
820
|
-
try {
|
|
821
|
-
const result = spawnSync(command, args, {
|
|
822
|
-
cwd,
|
|
823
|
-
windowsHide: true,
|
|
824
|
-
shell: false,
|
|
825
|
-
env: process.env,
|
|
826
|
-
encoding: "utf8"
|
|
827
|
-
});
|
|
828
|
-
const stdout = String(result.stdout || "").trim();
|
|
829
|
-
const stderr = String(result.stderr || "").trim();
|
|
830
|
-
return {
|
|
831
|
-
ok: result.status === 0,
|
|
832
|
-
status: result.status,
|
|
833
|
-
stdout,
|
|
834
|
-
stderr,
|
|
835
|
-
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
836
|
-
error_code: result.error?.code || null,
|
|
837
|
-
error_message: result.error?.message || null
|
|
838
|
-
};
|
|
839
|
-
} catch (error) {
|
|
840
|
-
return {
|
|
841
|
-
ok: false,
|
|
842
|
-
status: -1,
|
|
843
|
-
stdout: "",
|
|
844
|
-
stderr: "",
|
|
845
|
-
output: "",
|
|
846
|
-
error_code: error.code || "SPAWN_FAILED",
|
|
847
|
-
error_message: error.message || String(error)
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function parseMajorVersion(raw) {
|
|
853
|
-
const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
|
|
854
|
-
if (!match) return null;
|
|
855
|
-
const major = Number.parseInt(match[1], 10);
|
|
856
|
-
return Number.isFinite(major) ? major : null;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function buildNodeCommandCheck() {
|
|
860
|
-
const probe = runProcessSync({
|
|
861
|
-
command: "node",
|
|
862
|
-
args: ["--version"]
|
|
863
|
-
});
|
|
864
|
-
const major = parseMajorVersion(probe.output);
|
|
865
|
-
const versionOk = Number.isInteger(major) && major >= 18;
|
|
866
|
-
return {
|
|
867
|
-
key: "node_cli",
|
|
868
|
-
ok: probe.ok && versionOk,
|
|
869
|
-
path: "node --version",
|
|
870
|
-
message: probe.ok
|
|
871
|
-
? (versionOk
|
|
872
|
-
? `Node 命令可用 (${probe.output || "unknown version"})`
|
|
873
|
-
: `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
|
|
874
|
-
: `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
|
|
879
|
-
if (!cwd || !pathExists(cwd)) {
|
|
880
|
-
return {
|
|
881
|
-
key,
|
|
882
|
-
ok: false,
|
|
883
|
-
path: moduleName,
|
|
884
|
-
module: moduleName,
|
|
885
|
-
install_cwd: null,
|
|
886
|
-
message: missingMessage
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
const probe = runProcessSync({
|
|
890
|
-
command: "node",
|
|
891
|
-
args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
|
|
892
|
-
cwd
|
|
893
|
-
});
|
|
894
|
-
return {
|
|
895
|
-
key,
|
|
896
|
-
ok: probe.ok,
|
|
897
|
-
path: moduleName,
|
|
898
|
-
module: moduleName,
|
|
899
|
-
install_cwd: cwd,
|
|
900
|
-
message: probe.ok
|
|
901
|
-
? `${moduleName} npm 依赖可用`
|
|
902
|
-
: `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
|
|
907
|
-
return [
|
|
908
|
-
buildNodeCommandCheck(),
|
|
909
|
-
buildNodePackageCheck({
|
|
910
|
-
key: "npm_dep_chrome_remote_interface_search",
|
|
911
|
-
moduleName: "chrome-remote-interface",
|
|
912
|
-
cwd: searchDir,
|
|
913
|
-
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-search-cli 目录不存在。"
|
|
914
|
-
}),
|
|
915
|
-
buildNodePackageCheck({
|
|
916
|
-
key: "npm_dep_chrome_remote_interface_screen",
|
|
917
|
-
moduleName: "chrome-remote-interface",
|
|
918
|
-
cwd: screenDir,
|
|
919
|
-
missingMessage: "无法校验 chrome-remote-interface:boss-recommend-screen-cli 目录不存在。"
|
|
920
|
-
}),
|
|
921
|
-
buildNodePackageCheck({
|
|
922
|
-
key: "npm_dep_ws",
|
|
923
|
-
moduleName: "ws",
|
|
924
|
-
cwd: screenDir,
|
|
925
|
-
missingMessage: "无法校验 ws:boss-recommend-screen-cli 目录不存在。"
|
|
926
|
-
}),
|
|
927
|
-
buildNodePackageCheck({
|
|
928
|
-
key: "npm_dep_sharp",
|
|
929
|
-
moduleName: "sharp",
|
|
930
|
-
cwd: screenDir,
|
|
931
|
-
missingMessage: "无法校验 sharp:boss-recommend-screen-cli 目录不存在。"
|
|
932
|
-
})
|
|
933
|
-
];
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function parseJsonOutput(text) {
|
|
937
|
-
const trimmed = String(text || "").trim();
|
|
938
|
-
if (!trimmed) return null;
|
|
939
|
-
try {
|
|
940
|
-
return JSON.parse(trimmed);
|
|
941
|
-
} catch {}
|
|
942
|
-
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
943
|
-
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
944
|
-
try {
|
|
945
|
-
return JSON.parse(lines[index]);
|
|
946
|
-
} catch {
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return null;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function createScreenProgressTracker(currentTracker = {}) {
|
|
954
|
-
const outcome = String(currentTracker.outcome || "").trim();
|
|
955
|
-
return {
|
|
956
|
-
candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
|
|
957
|
-
outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
|
|
958
|
-
action_failed: currentTracker.action_failed === true
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function finalizeCandidateProgress(progress, tracker) {
|
|
963
|
-
if (!Number.isInteger(tracker.candidate_index)) {
|
|
964
|
-
return false;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
let changed = false;
|
|
968
|
-
if (tracker.action_failed === true) {
|
|
969
|
-
progress.skipped += 1;
|
|
970
|
-
changed = true;
|
|
971
|
-
} else if (tracker.outcome === "pass") {
|
|
972
|
-
progress.passed += 1;
|
|
973
|
-
changed = true;
|
|
974
|
-
} else if (tracker.outcome === "skip") {
|
|
975
|
-
progress.skipped += 1;
|
|
976
|
-
changed = true;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
tracker.candidate_index = null;
|
|
980
|
-
tracker.outcome = null;
|
|
981
|
-
tracker.action_failed = false;
|
|
982
|
-
return changed;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
|
|
986
|
-
const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
|
|
987
|
-
if (!normalizedLine) return null;
|
|
988
|
-
|
|
989
|
-
const nextProgress = {
|
|
990
|
-
processed: Number.isInteger(currentProgress.processed) ? currentProgress.processed : 0,
|
|
991
|
-
passed: Number.isInteger(currentProgress.passed) ? currentProgress.passed : 0,
|
|
992
|
-
skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
|
|
993
|
-
greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
|
|
994
|
-
};
|
|
995
|
-
const nextTracker = createScreenProgressTracker(currentTracker);
|
|
996
|
-
|
|
997
|
-
let changed = false;
|
|
998
|
-
const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
|
|
999
|
-
if (processedMatch) {
|
|
1000
|
-
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
1001
|
-
changed = true;
|
|
1002
|
-
}
|
|
1003
|
-
const processed = Number.parseInt(processedMatch[1], 10);
|
|
1004
|
-
if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
|
|
1005
|
-
nextProgress.processed = processed;
|
|
1006
|
-
changed = true;
|
|
1007
|
-
}
|
|
1008
|
-
nextTracker.candidate_index = processed;
|
|
1009
|
-
nextTracker.outcome = null;
|
|
1010
|
-
nextTracker.action_failed = false;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (/筛选结果:\s*通过/u.test(normalizedLine)) {
|
|
1014
|
-
if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
|
|
1015
|
-
changed = true;
|
|
1016
|
-
}
|
|
1017
|
-
nextTracker.outcome = "pass";
|
|
1018
|
-
nextTracker.action_failed = false;
|
|
1019
|
-
} else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
|
|
1020
|
-
if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
|
|
1021
|
-
changed = true;
|
|
1022
|
-
}
|
|
1023
|
-
nextTracker.outcome = "skip";
|
|
1024
|
-
nextTracker.action_failed = false;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if (/候选人处理失败\s*:/u.test(normalizedLine)) {
|
|
1028
|
-
if (!nextTracker.action_failed) {
|
|
1029
|
-
changed = true;
|
|
1030
|
-
}
|
|
1031
|
-
nextTracker.action_failed = true;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
|
|
1035
|
-
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
1036
|
-
changed = true;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
|
|
1041
|
-
if (finalStateLine) {
|
|
1042
|
-
if (finalizeCandidateProgress(nextProgress, nextTracker)) {
|
|
1043
|
-
changed = true;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
|
|
1048
|
-
if (greetMatch) {
|
|
1049
|
-
const greetCount = Number.parseInt(greetMatch[1], 10);
|
|
1050
|
-
if (Number.isInteger(greetCount) && greetCount >= 0 && greetCount !== nextProgress.greet_count) {
|
|
1051
|
-
nextProgress.greet_count = greetCount;
|
|
1052
|
-
changed = true;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (!changed) return null;
|
|
1057
|
-
return {
|
|
1058
|
-
line: normalizedLine,
|
|
1059
|
-
progress: nextProgress,
|
|
1060
|
-
tracker: nextTracker
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function resolveRecommendScreenTimeoutMs(runtime = null) {
|
|
1065
|
-
const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
|
|
1066
|
-
const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
|
|
1067
|
-
return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
function buildRecommendScreenProcessError(result, screenTimeoutMs) {
|
|
1071
|
-
if (result.code === 0) return null;
|
|
1072
|
-
if (result.error_code === "TIMEOUT") {
|
|
1073
|
-
return {
|
|
1074
|
-
code: "TIMEOUT",
|
|
1075
|
-
message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
if (result.error_code === "ABORTED") {
|
|
1079
|
-
return {
|
|
1080
|
-
code: "PROCESS_ABORTED",
|
|
1081
|
-
message: "推荐页筛选命令已取消。"
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
return {
|
|
1085
|
-
code: "RECOMMEND_SCREEN_FAILED",
|
|
1086
|
-
message: "推荐页筛选命令执行失败。"
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function loadScreenConfig(configPath) {
|
|
1091
|
-
const parsed = readJsonFile(configPath);
|
|
1092
|
-
const validation = validateScreenConfig(parsed);
|
|
1093
|
-
if (!validation.ok) {
|
|
1094
|
-
return {
|
|
1095
|
-
ok: false,
|
|
1096
|
-
error: `${validation.message} (path: ${configPath})`
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
return { ok: true, config: parsed };
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function localDirHint(workspaceRoot, dirName) {
|
|
1103
|
-
return path.join(workspaceRoot, dirName);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
export function getFeaturedCalibrationResolution(workspaceRoot) {
|
|
1107
|
-
const calibration_path = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1108
|
-
const calibration_exists = pathExists(calibration_path);
|
|
1109
|
-
const calibration_usable = isUsableCalibrationFile(calibration_path);
|
|
1110
|
-
const calibration_script_path = resolveRecruitCalibrationScriptPath(workspaceRoot);
|
|
1111
|
-
return {
|
|
1112
|
-
calibration_path,
|
|
1113
|
-
calibration_exists,
|
|
1114
|
-
calibration_usable,
|
|
1115
|
-
calibration_script_path
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
export async function runRecommendCalibration(
|
|
1120
|
-
workspaceRoot,
|
|
1121
|
-
options = {}
|
|
1122
|
-
) {
|
|
1123
|
-
const debugPort = parsePositiveInteger(options.port) || resolveWorkspaceDebugPort(workspaceRoot);
|
|
1124
|
-
const calibrationPath = options.output
|
|
1125
|
-
? path.resolve(String(options.output))
|
|
1126
|
-
: resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1127
|
-
const timeoutMs = getCalibrationTimeoutMs(options.timeoutMs);
|
|
1128
|
-
const calibrationScriptPath = resolveRecruitCalibrationScriptPath(workspaceRoot);
|
|
1129
|
-
|
|
1130
|
-
if (!calibrationScriptPath) {
|
|
1131
|
-
return {
|
|
1132
|
-
ok: false,
|
|
1133
|
-
stdout: "",
|
|
1134
|
-
stderr: "",
|
|
1135
|
-
calibration_path: calibrationPath,
|
|
1136
|
-
calibration_script_path: null,
|
|
1137
|
-
debug_port: debugPort,
|
|
1138
|
-
error: {
|
|
1139
|
-
code: "CALIBRATION_SCRIPT_MISSING",
|
|
1140
|
-
message: "未找到 boss-recruit-mcp 校准脚本 calibrate-favorite-position-v2.cjs。"
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
ensureDir(path.dirname(calibrationPath));
|
|
1146
|
-
const result = await runProcess({
|
|
1147
|
-
command: "node",
|
|
1148
|
-
args: [
|
|
1149
|
-
calibrationScriptPath,
|
|
1150
|
-
"--port",
|
|
1151
|
-
String(debugPort),
|
|
1152
|
-
"--output",
|
|
1153
|
-
calibrationPath,
|
|
1154
|
-
"--timeout-ms",
|
|
1155
|
-
String(timeoutMs)
|
|
1156
|
-
],
|
|
1157
|
-
cwd: path.dirname(calibrationScriptPath),
|
|
1158
|
-
timeoutMs: timeoutMs + 15_000,
|
|
1159
|
-
heartbeatIntervalMs: options.runtime?.heartbeatIntervalMs,
|
|
1160
|
-
signal: options.runtime?.signal,
|
|
1161
|
-
onOutput: (event) => {
|
|
1162
|
-
safeInvokeCallback(options.runtime?.onOutput, event);
|
|
1163
|
-
},
|
|
1164
|
-
onHeartbeat: (event) => {
|
|
1165
|
-
safeInvokeCallback(options.runtime?.onHeartbeat, event);
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
const usable = isUsableCalibrationFile(calibrationPath);
|
|
1170
|
-
const ok = result.code === 0 && usable;
|
|
1171
|
-
return {
|
|
1172
|
-
ok,
|
|
1173
|
-
stdout: result.stdout,
|
|
1174
|
-
stderr: result.stderr,
|
|
1175
|
-
calibration_path: calibrationPath,
|
|
1176
|
-
calibration_script_path: calibrationScriptPath,
|
|
1177
|
-
debug_port: debugPort,
|
|
1178
|
-
auto_started: true,
|
|
1179
|
-
error: ok
|
|
1180
|
-
? null
|
|
1181
|
-
: {
|
|
1182
|
-
code: result.error_code === "ABORTED"
|
|
1183
|
-
? "CALIBRATION_ABORTED"
|
|
1184
|
-
: result.error_code === "TIMEOUT"
|
|
1185
|
-
? "CALIBRATION_TIMEOUT"
|
|
1186
|
-
: "CALIBRATION_FAILED",
|
|
1187
|
-
message: usable
|
|
1188
|
-
? "校准脚本执行异常。"
|
|
1189
|
-
: "校准脚本未生成可用的 favorite-calibration.json。"
|
|
1190
|
-
}
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
export async function ensureFeaturedCalibrationReady(
|
|
1195
|
-
workspaceRoot,
|
|
1196
|
-
options = {}
|
|
1197
|
-
) {
|
|
1198
|
-
const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1199
|
-
if (isUsableCalibrationFile(calibrationPath)) {
|
|
1200
|
-
return {
|
|
1201
|
-
ok: true,
|
|
1202
|
-
calibration_path: calibrationPath,
|
|
1203
|
-
auto_started: false
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
if (options.autoCalibrate === false) {
|
|
1207
|
-
return {
|
|
1208
|
-
ok: false,
|
|
1209
|
-
calibration_path: calibrationPath,
|
|
1210
|
-
auto_started: false,
|
|
1211
|
-
error: {
|
|
1212
|
-
code: "CALIBRATION_REQUIRED",
|
|
1213
|
-
message: "精选页收藏缺少可用校准文件,需先在推荐页精选 tab 完成校准。"
|
|
1214
|
-
}
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
const calibrationRun = await runRecommendCalibration(workspaceRoot, options);
|
|
1218
|
-
if (calibrationRun.ok) {
|
|
1219
|
-
return calibrationRun;
|
|
1220
|
-
}
|
|
1221
|
-
return {
|
|
1222
|
-
...calibrationRun,
|
|
1223
|
-
ok: false,
|
|
1224
|
-
error: {
|
|
1225
|
-
code: "CALIBRATION_REQUIRED",
|
|
1226
|
-
message: calibrationRun.error?.message || "精选页收藏校准失败,请在推荐页精选 tab 重试校准。"
|
|
1227
|
-
}
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
export function runPipelinePreflight(workspaceRoot, options = {}) {
|
|
1232
|
-
const pageScope = normalizePageScope(options.pageScope) || "recommend";
|
|
1233
|
-
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
1234
|
-
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
1235
|
-
const searchDirExists = Boolean(searchDir && pathExists(searchDir));
|
|
1236
|
-
const searchEntryPath = searchDir
|
|
1237
|
-
? resolveRecommendSearchCliEntry(searchDir)
|
|
1238
|
-
: path.join(localDirHint(workspaceRoot, "boss-recommend-search-cli"), "src", "cli.js");
|
|
1239
|
-
const searchEntryExists = Boolean(searchDir && pathExists(searchEntryPath));
|
|
1240
|
-
const screenDirExists = Boolean(screenDir && pathExists(screenDir));
|
|
1241
|
-
const screenEntryPath = screenDir
|
|
1242
|
-
? resolveRecommendScreenCliEntry(screenDir)
|
|
1243
|
-
: path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
|
|
1244
|
-
const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
|
|
1245
|
-
const configResolution = getScreenConfigResolution(workspaceRoot);
|
|
1246
|
-
const screenConfigPath = configResolution.resolved_path;
|
|
1247
|
-
const screenConfigParsed = readJsonFile(screenConfigPath);
|
|
1248
|
-
const screenConfigValidation = validateScreenConfig(screenConfigParsed);
|
|
1249
|
-
const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
|
|
1250
|
-
const calibrationUsable = isUsableCalibrationFile(calibrationPath);
|
|
1251
|
-
const checks = [
|
|
1252
|
-
{
|
|
1253
|
-
key: "recommend_search_cli_dir",
|
|
1254
|
-
ok: searchDirExists,
|
|
1255
|
-
path: searchDir || localDirHint(workspaceRoot, "boss-recommend-search-cli"),
|
|
1256
|
-
message: searchDirExists
|
|
1257
|
-
? "boss-recommend-search-cli 目录可用"
|
|
1258
|
-
: "boss-recommend-search-cli 目录不存在"
|
|
1259
|
-
},
|
|
1260
|
-
{
|
|
1261
|
-
key: "recommend_search_cli_entry",
|
|
1262
|
-
ok: searchEntryExists,
|
|
1263
|
-
path: searchEntryPath,
|
|
1264
|
-
message: searchEntryExists
|
|
1265
|
-
? "boss-recommend-search-cli 入口文件可用"
|
|
1266
|
-
: "boss-recommend-search-cli 入口文件缺失"
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
key: "recommend_screen_cli_dir",
|
|
1270
|
-
ok: screenDirExists,
|
|
1271
|
-
path: screenDir || localDirHint(workspaceRoot, "boss-recommend-screen-cli"),
|
|
1272
|
-
message: screenDirExists
|
|
1273
|
-
? "boss-recommend-screen-cli 目录可用"
|
|
1274
|
-
: "boss-recommend-screen-cli 目录不存在"
|
|
1275
|
-
},
|
|
1276
|
-
{
|
|
1277
|
-
key: "recommend_screen_cli_entry",
|
|
1278
|
-
ok: screenEntryExists,
|
|
1279
|
-
path: screenEntryPath,
|
|
1280
|
-
message: screenEntryExists
|
|
1281
|
-
? "boss-recommend-screen-cli 入口文件可用"
|
|
1282
|
-
: "boss-recommend-screen-cli 入口文件缺失"
|
|
1283
|
-
},
|
|
1284
|
-
{
|
|
1285
|
-
key: "screen_config",
|
|
1286
|
-
ok: screenConfigValidation.ok,
|
|
1287
|
-
path: screenConfigPath,
|
|
1288
|
-
reason: screenConfigValidation.reason || null,
|
|
1289
|
-
message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
|
|
1290
|
-
},
|
|
1291
|
-
{
|
|
1292
|
-
key: "favorite_calibration",
|
|
1293
|
-
ok: calibrationUsable,
|
|
1294
|
-
path: calibrationPath,
|
|
1295
|
-
optional: pageScope !== "featured",
|
|
1296
|
-
message: calibrationUsable
|
|
1297
|
-
? "favorite-calibration.json 可用"
|
|
1298
|
-
: "favorite-calibration.json 不存在或无效(精选页收藏仅支持校准坐标点击)"
|
|
1299
|
-
}
|
|
1300
|
-
];
|
|
1301
|
-
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
1302
|
-
|
|
1303
|
-
const requiredCheckKeys = new Set([
|
|
1304
|
-
"recommend_search_cli_dir",
|
|
1305
|
-
"recommend_search_cli_entry",
|
|
1306
|
-
"recommend_screen_cli_dir",
|
|
1307
|
-
"recommend_screen_cli_entry",
|
|
1308
|
-
"screen_config",
|
|
1309
|
-
"node_cli",
|
|
1310
|
-
"npm_dep_chrome_remote_interface_search",
|
|
1311
|
-
"npm_dep_chrome_remote_interface_screen",
|
|
1312
|
-
"npm_dep_ws",
|
|
1313
|
-
"npm_dep_sharp"
|
|
1314
|
-
]);
|
|
1315
|
-
if (pageScope === "featured") {
|
|
1316
|
-
requiredCheckKeys.add("favorite_calibration");
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
return {
|
|
1320
|
-
ok: checks.every((item) => !requiredCheckKeys.has(item.key) || item.ok),
|
|
1321
|
-
checks,
|
|
1322
|
-
debug_port: resolveWorkspaceDebugPort(workspaceRoot),
|
|
1323
|
-
config_resolution: configResolution,
|
|
1324
|
-
calibration_path: calibrationPath,
|
|
1325
|
-
page_scope: pageScope
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function collectFailedCheckKeys(checks = []) {
|
|
1330
|
-
return new Set(
|
|
1331
|
-
checks
|
|
1332
|
-
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
1333
|
-
.map((item) => item.key)
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
|
|
1338
|
-
const npmKeys = new Set([
|
|
1339
|
-
"npm_dep_chrome_remote_interface_search",
|
|
1340
|
-
"npm_dep_chrome_remote_interface_screen",
|
|
1341
|
-
"npm_dep_ws",
|
|
1342
|
-
"npm_dep_sharp"
|
|
1343
|
-
]);
|
|
1344
|
-
const dirs = checks
|
|
1345
|
-
.filter((item) => item && item.ok === false && npmKeys.has(item.key))
|
|
1346
|
-
.map((item) => item.install_cwd)
|
|
1347
|
-
.filter((item) => typeof item === "string" && item.trim())
|
|
1348
|
-
.map((item) => path.resolve(item));
|
|
1349
|
-
if (dirs.length > 0) {
|
|
1350
|
-
return [...new Set(dirs)];
|
|
1351
|
-
}
|
|
1352
|
-
return [path.resolve(workspaceRoot)];
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function installNpmDependencies(checks, workspaceRoot) {
|
|
1356
|
-
const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
|
|
1357
|
-
const commandResults = [];
|
|
1358
|
-
let allOk = true;
|
|
1359
|
-
for (const cwd of dirs) {
|
|
1360
|
-
const result = runProcessSync({
|
|
1361
|
-
command: "npm",
|
|
1362
|
-
args: ["install"],
|
|
1363
|
-
cwd
|
|
1364
|
-
});
|
|
1365
|
-
commandResults.push({
|
|
1366
|
-
cwd,
|
|
1367
|
-
ok: result.ok,
|
|
1368
|
-
output: result.output || result.error_message || ""
|
|
1369
|
-
});
|
|
1370
|
-
if (!result.ok) allOk = false;
|
|
1371
|
-
}
|
|
1372
|
-
return {
|
|
1373
|
-
ok: allOk,
|
|
1374
|
-
action: "install_npm_dependencies",
|
|
1375
|
-
changed: true,
|
|
1376
|
-
command_results: commandResults,
|
|
1377
|
-
message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
|
|
1382
|
-
const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
|
|
1383
|
-
const failed = collectFailedCheckKeys(checks);
|
|
1384
|
-
const actions = [];
|
|
1385
|
-
|
|
1386
|
-
if (
|
|
1387
|
-
failed.has("npm_dep_chrome_remote_interface_search")
|
|
1388
|
-
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
1389
|
-
|| failed.has("npm_dep_ws")
|
|
1390
|
-
|| failed.has("npm_dep_sharp")
|
|
1391
|
-
) {
|
|
1392
|
-
if (!failed.has("node_cli")) {
|
|
1393
|
-
actions.push(installNpmDependencies(checks, workspaceRoot));
|
|
1394
|
-
} else {
|
|
1395
|
-
actions.push({
|
|
1396
|
-
ok: false,
|
|
1397
|
-
action: "install_npm_dependencies",
|
|
1398
|
-
changed: false,
|
|
1399
|
-
message: "Node 命令不可用,跳过 npm 自动安装。"
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
const attempted = actions.length > 0;
|
|
1405
|
-
const nextPreflight = runPipelinePreflight(workspaceRoot, {
|
|
1406
|
-
pageScope: preflight?.page_scope
|
|
1407
|
-
});
|
|
1408
|
-
return {
|
|
1409
|
-
attempted,
|
|
1410
|
-
actions,
|
|
1411
|
-
preflight: nextPreflight
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function sleep(ms) {
|
|
1416
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
async function listChromeTabs(port) {
|
|
1420
|
-
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
1421
|
-
if (!response.ok) {
|
|
1422
|
-
throw new Error(`DevTools endpoint returned ${response.status}`);
|
|
1423
|
-
}
|
|
1424
|
-
const data = await response.json();
|
|
1425
|
-
return Array.isArray(data) ? data : [];
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
function buildBossPageState(payload) {
|
|
1429
|
-
return {
|
|
1430
|
-
key: "boss_page_state",
|
|
1431
|
-
...payload
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function extractSampleUrls(tabs, limit = 5) {
|
|
1436
|
-
return tabs
|
|
1437
|
-
.map((tab) => tab?.url)
|
|
1438
|
-
.filter(Boolean)
|
|
1439
|
-
.slice(0, limit);
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
function findChromeOnboardingUrl(tabs) {
|
|
1443
|
-
for (const tab of tabs) {
|
|
1444
|
-
if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
|
|
1445
|
-
return tab.url;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
return null;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
function isBossLoginTab(tab) {
|
|
1452
|
-
const url = String(tab?.url || "");
|
|
1453
|
-
const title = String(tab?.title || "");
|
|
1454
|
-
return (
|
|
1455
|
-
url === bossLoginUrl
|
|
1456
|
-
|| bossLoginUrlPattern.test(url)
|
|
1457
|
-
|| bossLoginTitlePattern.test(title)
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function findRecommendTab(tabs = []) {
|
|
1462
|
-
return tabs.find(
|
|
1463
|
-
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
1464
|
-
) || null;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
export async function inspectBossRecommendPageState(port, options = {}) {
|
|
1468
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
|
|
1469
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
|
|
1470
|
-
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1471
|
-
const deadline = Date.now() + timeoutMs;
|
|
1472
|
-
let lastError = null;
|
|
1473
|
-
let lastTabs = [];
|
|
1474
|
-
|
|
1475
|
-
while (Date.now() < deadline) {
|
|
1476
|
-
try {
|
|
1477
|
-
const tabs = await listChromeTabs(port);
|
|
1478
|
-
lastTabs = tabs;
|
|
1479
|
-
const exactTab = findRecommendTab(tabs);
|
|
1480
|
-
if (exactTab) {
|
|
1481
|
-
if (isBossLoginTab(exactTab)) {
|
|
1482
|
-
return buildBossPageState({
|
|
1483
|
-
ok: false,
|
|
1484
|
-
state: "LOGIN_REQUIRED",
|
|
1485
|
-
path: exactTab.url || bossLoginUrl,
|
|
1486
|
-
current_url: exactTab.url || bossLoginUrl,
|
|
1487
|
-
title: exactTab.title || null,
|
|
1488
|
-
requires_login: true,
|
|
1489
|
-
expected_url: expectedUrl,
|
|
1490
|
-
login_url: bossLoginUrl,
|
|
1491
|
-
message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
return buildBossPageState({
|
|
1495
|
-
ok: true,
|
|
1496
|
-
state: "RECOMMEND_READY",
|
|
1497
|
-
path: exactTab.url,
|
|
1498
|
-
current_url: exactTab.url,
|
|
1499
|
-
title: exactTab.title || null,
|
|
1500
|
-
requires_login: false,
|
|
1501
|
-
message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
const loginTab = tabs.find((tab) => isBossLoginTab(tab));
|
|
1506
|
-
if (loginTab) {
|
|
1507
|
-
return buildBossPageState({
|
|
1508
|
-
ok: false,
|
|
1509
|
-
state: "LOGIN_REQUIRED",
|
|
1510
|
-
path: loginTab.url || bossLoginUrl,
|
|
1511
|
-
current_url: loginTab.url || bossLoginUrl,
|
|
1512
|
-
title: loginTab.title || null,
|
|
1513
|
-
requires_login: true,
|
|
1514
|
-
expected_url: expectedUrl,
|
|
1515
|
-
login_url: bossLoginUrl,
|
|
1516
|
-
message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
const bossTab = tabs.find(
|
|
1521
|
-
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1522
|
-
);
|
|
1523
|
-
if (bossTab) {
|
|
1524
|
-
const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
|
|
1525
|
-
return buildBossPageState({
|
|
1526
|
-
ok: false,
|
|
1527
|
-
state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
|
|
1528
|
-
path: bossTab.url,
|
|
1529
|
-
current_url: bossTab.url,
|
|
1530
|
-
title: bossTab.title || null,
|
|
1531
|
-
requires_login: requiresLogin,
|
|
1532
|
-
expected_url: expectedUrl,
|
|
1533
|
-
message: requiresLogin
|
|
1534
|
-
? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1535
|
-
: "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
} catch (error) {
|
|
1539
|
-
lastError = error;
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
await sleep(pollMs);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
if (lastError) {
|
|
1546
|
-
return buildBossPageState({
|
|
1547
|
-
ok: false,
|
|
1548
|
-
state: "DEBUG_PORT_UNREACHABLE",
|
|
1549
|
-
path: `http://127.0.0.1:${port}`,
|
|
1550
|
-
current_url: null,
|
|
1551
|
-
title: null,
|
|
1552
|
-
requires_login: false,
|
|
1553
|
-
expected_url: expectedUrl,
|
|
1554
|
-
message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
|
|
1555
|
-
error: lastError.message
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const onboardingUrl = findChromeOnboardingUrl(lastTabs);
|
|
1560
|
-
if (onboardingUrl) {
|
|
1561
|
-
return buildBossPageState({
|
|
1562
|
-
ok: false,
|
|
1563
|
-
state: "CHROME_ONBOARDING_INTERCEPTED",
|
|
1564
|
-
path: onboardingUrl,
|
|
1565
|
-
current_url: onboardingUrl,
|
|
1566
|
-
title: null,
|
|
1567
|
-
requires_login: false,
|
|
1568
|
-
expected_url: expectedUrl,
|
|
1569
|
-
message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
|
|
1570
|
-
sample_urls: extractSampleUrls(lastTabs)
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
return buildBossPageState({
|
|
1575
|
-
ok: false,
|
|
1576
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
1577
|
-
path: expectedUrl,
|
|
1578
|
-
current_url: null,
|
|
1579
|
-
title: null,
|
|
1580
|
-
requires_login: false,
|
|
1581
|
-
expected_url,
|
|
1582
|
-
message: "未检测到 Boss 推荐页标签页。",
|
|
1583
|
-
sample_urls: extractSampleUrls(lastTabs)
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
async function openBossRecommendTab(port) {
|
|
1588
|
-
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossRecommendUrl)}`;
|
|
1589
|
-
const attempts = ["PUT", "GET"];
|
|
1590
|
-
let lastError = null;
|
|
1591
|
-
|
|
1592
|
-
for (const method of attempts) {
|
|
1593
|
-
try {
|
|
1594
|
-
const response = await fetch(endpoint, { method });
|
|
1595
|
-
if (response.ok) {
|
|
1596
|
-
return { ok: true, method };
|
|
1597
|
-
}
|
|
1598
|
-
lastError = new Error(`DevTools /json/new returned ${response.status}`);
|
|
1599
|
-
} catch (error) {
|
|
1600
|
-
lastError = error;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
return {
|
|
1605
|
-
ok: false,
|
|
1606
|
-
error: lastError?.message || "Failed to open Boss recommend tab via DevTools /json/new"
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
async function verifyRecommendPageStable(port, options = {}) {
|
|
1611
|
-
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
|
|
1612
|
-
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
|
|
1613
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1614
|
-
|
|
1615
|
-
await sleep(settleMs);
|
|
1616
|
-
const recheck = await inspectBossRecommendPageState(port, {
|
|
1617
|
-
timeoutMs: recheckTimeoutMs,
|
|
1618
|
-
pollMs
|
|
1619
|
-
});
|
|
1620
|
-
if (recheck.state === "RECOMMEND_READY") {
|
|
1621
|
-
const iframeState = await waitForRecommendIframeReady(port, {
|
|
1622
|
-
timeoutMs: recheckTimeoutMs,
|
|
1623
|
-
pollMs
|
|
1624
|
-
});
|
|
1625
|
-
if (iframeState.state === "RECOMMEND_IFRAME_READY") {
|
|
1626
|
-
return buildBossPageState({
|
|
1627
|
-
...recheck,
|
|
1628
|
-
ok: true,
|
|
1629
|
-
state: "RECOMMEND_READY",
|
|
1630
|
-
frame_url: iframeState.frame_url || null,
|
|
1631
|
-
iframe_state: iframeState
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
if (iframeState.state === "LOGIN_REQUIRED") {
|
|
1635
|
-
return buildBossPageState({
|
|
1636
|
-
...iframeState,
|
|
1637
|
-
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1638
|
-
message: "Boss 页面曾进入 recommend 但 iframe 不可用且出现登录态特征,通常表示登录态失效。"
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
if (iframeState.state === "NO_RECOMMEND_IFRAME") {
|
|
1642
|
-
return buildBossPageState({
|
|
1643
|
-
...recheck,
|
|
1644
|
-
ok: false,
|
|
1645
|
-
state: "NO_RECOMMEND_IFRAME",
|
|
1646
|
-
current_url: iframeState.current_url || recheck.current_url || null,
|
|
1647
|
-
title: iframeState.title || recheck.title || null,
|
|
1648
|
-
frame_url: iframeState.frame_url || null,
|
|
1649
|
-
iframe_state: iframeState,
|
|
1650
|
-
message: "Boss recommend 页面已打开,但 recommend iframe 尚未就绪。"
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
return iframeState;
|
|
1654
|
-
}
|
|
1655
|
-
if (recheck.state === "LOGIN_REQUIRED") {
|
|
1656
|
-
return buildBossPageState({
|
|
1657
|
-
...recheck,
|
|
1658
|
-
state: "LOGIN_REQUIRED_AFTER_REDIRECT",
|
|
1659
|
-
message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
return recheck;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function buildRecommendIframeProbeExpression() {
|
|
1666
|
-
return `(() => {
|
|
1667
|
-
const currentUrl = (() => {
|
|
1668
|
-
try { return String(window.location.href || ""); } catch { return ""; }
|
|
1669
|
-
})();
|
|
1670
|
-
const title = (() => {
|
|
1671
|
-
try { return String(document.title || ""); } catch { return ""; }
|
|
1672
|
-
})();
|
|
1673
|
-
const isLogin = ${bossLoginUrlPattern}.test(currentUrl)
|
|
1674
|
-
|| ${bossLoginTitlePattern}.test(title);
|
|
1675
|
-
if (isLogin) {
|
|
1676
|
-
return {
|
|
1677
|
-
ok: false,
|
|
1678
|
-
error: "LOGIN_REQUIRED",
|
|
1679
|
-
current_url: currentUrl || ${JSON.stringify(bossLoginUrl)},
|
|
1680
|
-
title
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1684
|
-
const frameUrl = (() => {
|
|
1685
|
-
try {
|
|
1686
|
-
return String(frame?.contentWindow?.location?.href || frame?.src || "");
|
|
1687
|
-
} catch {
|
|
1688
|
-
return String(frame?.src || "");
|
|
1689
|
-
}
|
|
1690
|
-
})();
|
|
1691
|
-
const iframeCount = document.querySelectorAll("iframe").length;
|
|
1692
|
-
if (!frame || !frame.contentDocument) {
|
|
1693
|
-
return {
|
|
1694
|
-
ok: false,
|
|
1695
|
-
error: "NO_RECOMMEND_IFRAME",
|
|
1696
|
-
current_url: currentUrl,
|
|
1697
|
-
title,
|
|
1698
|
-
frame_url: frameUrl,
|
|
1699
|
-
frame_present: Boolean(frame),
|
|
1700
|
-
iframe_count: iframeCount
|
|
1701
|
-
};
|
|
1702
|
-
}
|
|
1703
|
-
return {
|
|
1704
|
-
ok: true,
|
|
1705
|
-
current_url: currentUrl,
|
|
1706
|
-
title,
|
|
1707
|
-
frame_url: frameUrl,
|
|
1708
|
-
frame_present: true,
|
|
1709
|
-
iframe_count: iframeCount
|
|
1710
|
-
};
|
|
1711
|
-
})()`;
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
async function probeRecommendIframeState(port, options = {}) {
|
|
1715
|
-
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1716
|
-
let client = null;
|
|
1717
|
-
try {
|
|
1718
|
-
const tabs = await listChromeTabs(port);
|
|
1719
|
-
const exactTab = findRecommendTab(tabs);
|
|
1720
|
-
if (!exactTab) {
|
|
1721
|
-
const loginTab = tabs.find((tab) => isBossLoginTab(tab));
|
|
1722
|
-
if (loginTab) {
|
|
1723
|
-
return buildBossPageState({
|
|
1724
|
-
ok: false,
|
|
1725
|
-
state: "LOGIN_REQUIRED",
|
|
1726
|
-
path: loginTab.url || bossLoginUrl,
|
|
1727
|
-
current_url: loginTab.url || bossLoginUrl,
|
|
1728
|
-
title: loginTab.title || null,
|
|
1729
|
-
requires_login: true,
|
|
1730
|
-
expected_url: expectedUrl,
|
|
1731
|
-
login_url: bossLoginUrl,
|
|
1732
|
-
message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
const bossTab = tabs.find(
|
|
1736
|
-
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1737
|
-
);
|
|
1738
|
-
if (bossTab) {
|
|
1739
|
-
return buildBossPageState({
|
|
1740
|
-
ok: false,
|
|
1741
|
-
state: "BOSS_NOT_ON_RECOMMEND",
|
|
1742
|
-
path: bossTab.url,
|
|
1743
|
-
current_url: bossTab.url,
|
|
1744
|
-
title: bossTab.title || null,
|
|
1745
|
-
requires_login: false,
|
|
1746
|
-
expected_url: expectedUrl,
|
|
1747
|
-
message: "Boss 已登录但当前不在 recommend 页面。"
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
|
-
return buildBossPageState({
|
|
1751
|
-
ok: false,
|
|
1752
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
1753
|
-
path: expectedUrl,
|
|
1754
|
-
current_url: null,
|
|
1755
|
-
title: null,
|
|
1756
|
-
requires_login: false,
|
|
1757
|
-
expected_url: expectedUrl,
|
|
1758
|
-
message: "未检测到 Boss 推荐页标签页。",
|
|
1759
|
-
sample_urls: extractSampleUrls(tabs)
|
|
1760
|
-
});
|
|
1761
|
-
}
|
|
1762
|
-
if (isBossLoginTab(exactTab)) {
|
|
1763
|
-
return buildBossPageState({
|
|
1764
|
-
ok: false,
|
|
1765
|
-
state: "LOGIN_REQUIRED",
|
|
1766
|
-
path: exactTab.url || bossLoginUrl,
|
|
1767
|
-
current_url: exactTab.url || bossLoginUrl,
|
|
1768
|
-
title: exactTab.title || null,
|
|
1769
|
-
requires_login: true,
|
|
1770
|
-
expected_url: expectedUrl,
|
|
1771
|
-
login_url: bossLoginUrl,
|
|
1772
|
-
message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
client = await CDP({ port, target: exactTab });
|
|
1777
|
-
const { Runtime, Page } = client;
|
|
1778
|
-
if (Runtime && typeof Runtime.enable === "function") {
|
|
1779
|
-
await Runtime.enable();
|
|
1780
|
-
}
|
|
1781
|
-
if (Page && typeof Page.enable === "function") {
|
|
1782
|
-
await Page.enable();
|
|
1783
|
-
}
|
|
1784
|
-
const frameProbe = await evaluateCdpExpression(client, buildRecommendIframeProbeExpression());
|
|
1785
|
-
if (frameProbe?.ok) {
|
|
1786
|
-
return buildBossPageState({
|
|
1787
|
-
ok: true,
|
|
1788
|
-
state: "RECOMMEND_IFRAME_READY",
|
|
1789
|
-
path: frameProbe.current_url || exactTab.url || expectedUrl,
|
|
1790
|
-
current_url: frameProbe.current_url || exactTab.url || null,
|
|
1791
|
-
title: frameProbe.title || exactTab.title || null,
|
|
1792
|
-
expected_url: expectedUrl,
|
|
1793
|
-
frame_url: frameProbe.frame_url || null,
|
|
1794
|
-
frame_present: frameProbe.frame_present === true,
|
|
1795
|
-
iframe_count: Number.isFinite(Number(frameProbe.iframe_count))
|
|
1796
|
-
? Number(frameProbe.iframe_count)
|
|
1797
|
-
: null,
|
|
1798
|
-
message: "Boss 推荐页 iframe 已就绪。"
|
|
1799
|
-
});
|
|
1800
|
-
}
|
|
1801
|
-
if (frameProbe?.error === "LOGIN_REQUIRED") {
|
|
1802
|
-
return buildBossPageState({
|
|
1803
|
-
ok: false,
|
|
1804
|
-
state: "LOGIN_REQUIRED",
|
|
1805
|
-
path: frameProbe.current_url || exactTab.url || bossLoginUrl,
|
|
1806
|
-
current_url: frameProbe.current_url || exactTab.url || bossLoginUrl,
|
|
1807
|
-
title: frameProbe.title || exactTab.title || null,
|
|
1808
|
-
requires_login: true,
|
|
1809
|
-
expected_url: expectedUrl,
|
|
1810
|
-
login_url: bossLoginUrl,
|
|
1811
|
-
message: "页面检测到登录态特征,请先完成 Boss 登录。"
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
return buildBossPageState({
|
|
1815
|
-
ok: false,
|
|
1816
|
-
state: frameProbe?.error || "NO_RECOMMEND_IFRAME",
|
|
1817
|
-
path: frameProbe?.current_url || exactTab.url || expectedUrl,
|
|
1818
|
-
current_url: frameProbe?.current_url || exactTab.url || null,
|
|
1819
|
-
title: frameProbe?.title || exactTab.title || null,
|
|
1820
|
-
expected_url: expectedUrl,
|
|
1821
|
-
frame_url: frameProbe?.frame_url || null,
|
|
1822
|
-
frame_present: frameProbe?.frame_present === true,
|
|
1823
|
-
iframe_count: Number.isFinite(Number(frameProbe?.iframe_count))
|
|
1824
|
-
? Number(frameProbe.iframe_count)
|
|
1825
|
-
: null,
|
|
1826
|
-
message: "recommend iframe 暂不可用。"
|
|
1827
|
-
});
|
|
1828
|
-
} catch (error) {
|
|
1829
|
-
return buildBossPageState({
|
|
1830
|
-
ok: false,
|
|
1831
|
-
state: "DEBUG_PORT_UNREACHABLE",
|
|
1832
|
-
path: `http://127.0.0.1:${port}`,
|
|
1833
|
-
current_url: null,
|
|
1834
|
-
title: null,
|
|
1835
|
-
requires_login: false,
|
|
1836
|
-
expected_url: expectedUrl,
|
|
1837
|
-
message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
|
|
1838
|
-
error: error?.message || String(error)
|
|
1839
|
-
});
|
|
1840
|
-
} finally {
|
|
1841
|
-
if (client) {
|
|
1842
|
-
try {
|
|
1843
|
-
await client.close();
|
|
1844
|
-
} catch {}
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
async function waitForRecommendIframeReady(port, options = {}) {
|
|
1850
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 3000;
|
|
1851
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
1852
|
-
const expectedUrl = options.expectedUrl || bossRecommendUrl;
|
|
1853
|
-
const deadline = Date.now() + timeoutMs;
|
|
1854
|
-
let lastState = null;
|
|
1855
|
-
|
|
1856
|
-
while (Date.now() < deadline) {
|
|
1857
|
-
lastState = await probeRecommendIframeState(port, { expectedUrl });
|
|
1858
|
-
if (lastState?.state === "RECOMMEND_IFRAME_READY") {
|
|
1859
|
-
return lastState;
|
|
1860
|
-
}
|
|
1861
|
-
if (
|
|
1862
|
-
lastState?.state === "LOGIN_REQUIRED"
|
|
1863
|
-
|| lastState?.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
1864
|
-
|| lastState?.state === "DEBUG_PORT_UNREACHABLE"
|
|
1865
|
-
|| lastState?.state === "BOSS_TAB_NOT_FOUND"
|
|
1866
|
-
|| lastState?.state === "BOSS_NOT_ON_RECOMMEND"
|
|
1867
|
-
) {
|
|
1868
|
-
return lastState;
|
|
1869
|
-
}
|
|
1870
|
-
await sleep(pollMs);
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
return lastState || buildBossPageState({
|
|
1874
|
-
ok: false,
|
|
1875
|
-
state: "NO_RECOMMEND_IFRAME",
|
|
1876
|
-
path: expectedUrl,
|
|
1877
|
-
current_url: null,
|
|
1878
|
-
title: null,
|
|
1879
|
-
expected_url: expectedUrl,
|
|
1880
|
-
message: "recommend iframe 尚未就绪。"
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function pickBossRecommendReloadTarget(tabs = []) {
|
|
1885
|
-
return findRecommendTab(tabs) || tabs.find(
|
|
1886
|
-
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
1887
|
-
) || null;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
async function evaluateCdpExpression(client, expression) {
|
|
1891
|
-
const result = await client.Runtime.evaluate({
|
|
1892
|
-
expression,
|
|
1893
|
-
returnByValue: true,
|
|
1894
|
-
awaitPromise: true
|
|
1895
|
-
});
|
|
1896
|
-
if (result.exceptionDetails) {
|
|
1897
|
-
throw new Error(result.exceptionDetails.exception?.description || "Runtime.evaluate failed");
|
|
1898
|
-
}
|
|
1899
|
-
return result.result?.value;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
function buildRecommendTabStateExpression() {
|
|
1903
|
-
return `(() => {
|
|
1904
|
-
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1905
|
-
if (!frame || !frame.contentDocument) {
|
|
1906
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1907
|
-
}
|
|
1908
|
-
const doc = frame.contentDocument;
|
|
1909
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1910
|
-
const tabs = Array.from(new Set(${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1911
|
-
.flatMap((selector) => {
|
|
1912
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1913
|
-
}))).map((node) => {
|
|
1914
|
-
const status = normalize(node.getAttribute('data-status'));
|
|
1915
|
-
const className = normalize(node.className);
|
|
1916
|
-
const active = (
|
|
1917
|
-
/(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(className)
|
|
1918
|
-
|| normalize(node.getAttribute('aria-selected')) === 'true'
|
|
1919
|
-
|| normalize(node.getAttribute('data-selected')) === 'true'
|
|
1920
|
-
);
|
|
1921
|
-
return {
|
|
1922
|
-
status: status || null,
|
|
1923
|
-
title: normalize(node.getAttribute('title')) || null,
|
|
1924
|
-
label: normalize(node.textContent) || null,
|
|
1925
|
-
active,
|
|
1926
|
-
class_name: className || null
|
|
1927
|
-
};
|
|
1928
|
-
});
|
|
1929
|
-
const activeTab = tabs.find((item) => item.active && item.status) || null;
|
|
1930
|
-
const featuredCount = ${JSON.stringify(FEATURED_CARD_SELECTORS)}
|
|
1931
|
-
.reduce((sum, selector) => {
|
|
1932
|
-
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1933
|
-
}, 0);
|
|
1934
|
-
const recommendCount = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
1935
|
-
.reduce((sum, selector) => {
|
|
1936
|
-
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1937
|
-
}, 0);
|
|
1938
|
-
const latestCount = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
1939
|
-
.reduce((sum, selector) => {
|
|
1940
|
-
try { return sum + doc.querySelectorAll(selector).length; } catch { return sum; }
|
|
1941
|
-
}, 0);
|
|
1942
|
-
let inferredStatus = activeTab?.status || null;
|
|
1943
|
-
if (!inferredStatus) {
|
|
1944
|
-
if (featuredCount > 0 && recommendCount === 0 && latestCount === 0) inferredStatus = '3';
|
|
1945
|
-
else if (latestCount > 0 && featuredCount === 0 && recommendCount === 0) inferredStatus = '1';
|
|
1946
|
-
else if (recommendCount > 0 && featuredCount === 0 && latestCount === 0) inferredStatus = '0';
|
|
1947
|
-
}
|
|
1948
|
-
return {
|
|
1949
|
-
ok: true,
|
|
1950
|
-
active_status: inferredStatus,
|
|
1951
|
-
tabs,
|
|
1952
|
-
layout: {
|
|
1953
|
-
featured_count: featuredCount,
|
|
1954
|
-
recommend_count: recommendCount,
|
|
1955
|
-
latest_count: latestCount
|
|
1956
|
-
}
|
|
1957
|
-
};
|
|
1958
|
-
})()`;
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
function buildRecommendTabSwitchExpression(targetStatus) {
|
|
1962
|
-
return `((targetStatus) => {
|
|
1963
|
-
const frame = ${buildRecommendIframeLookupExpression()};
|
|
1964
|
-
if (!frame || !frame.contentDocument) {
|
|
1965
|
-
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
1966
|
-
}
|
|
1967
|
-
const doc = frame.contentDocument;
|
|
1968
|
-
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1969
|
-
const tabs = Array.from(new Set(${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
1970
|
-
.flatMap((selector) => {
|
|
1971
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
1972
|
-
})));
|
|
1973
|
-
const target = tabs.find((node) => normalize(node.getAttribute('data-status')) === String(targetStatus)) || null;
|
|
1974
|
-
if (!target) {
|
|
1975
|
-
return { ok: false, state: 'TAB_NOT_FOUND', target_status: String(targetStatus) };
|
|
1976
|
-
}
|
|
1977
|
-
try {
|
|
1978
|
-
target.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
|
|
1979
|
-
} catch {}
|
|
1980
|
-
try {
|
|
1981
|
-
target.click();
|
|
1982
|
-
} catch (error) {
|
|
1983
|
-
return {
|
|
1984
|
-
ok: false,
|
|
1985
|
-
state: 'TAB_CLICK_FAILED',
|
|
1986
|
-
message: error?.message || String(error),
|
|
1987
|
-
target_status: String(targetStatus)
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
return {
|
|
1991
|
-
ok: true,
|
|
1992
|
-
state: 'TAB_CLICKED',
|
|
1993
|
-
target_status: String(targetStatus)
|
|
1994
|
-
};
|
|
1995
|
-
})(${JSON.stringify(String(targetStatus || ""))})`;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
function buildRecommendDetailStateExpression() {
|
|
1999
|
-
return `(() => {
|
|
2000
|
-
const selectors = ${JSON.stringify([...DETAIL_POPUP_SELECTORS, ...DETAIL_RESUME_IFRAME_SELECTORS])};
|
|
2001
|
-
const isVisible = (node) => {
|
|
2002
|
-
if (!node || !node.getBoundingClientRect) return false;
|
|
2003
|
-
const rect = node.getBoundingClientRect();
|
|
2004
|
-
if (!rect || rect.width < 4 || rect.height < 4) return false;
|
|
2005
|
-
const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
2006
|
-
if (style) {
|
|
2007
|
-
if (style.display === 'none') return false;
|
|
2008
|
-
if (style.visibility === 'hidden') return false;
|
|
2009
|
-
if (Number(style.opacity || '1') <= 0) return false;
|
|
2010
|
-
}
|
|
2011
|
-
return true;
|
|
2012
|
-
};
|
|
2013
|
-
const findVisibleDetail = (doc, source) => {
|
|
2014
|
-
if (!doc) return null;
|
|
2015
|
-
for (const selector of selectors) {
|
|
2016
|
-
const nodes = doc.querySelectorAll(selector);
|
|
2017
|
-
for (const node of nodes) {
|
|
2018
|
-
if (isVisible(node)) {
|
|
2019
|
-
return {
|
|
2020
|
-
ok: true,
|
|
2021
|
-
open: true,
|
|
2022
|
-
source,
|
|
2023
|
-
selector
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
return null;
|
|
2029
|
-
};
|
|
2030
|
-
|
|
2031
|
-
const topDetail = findVisibleDetail(document, 'top');
|
|
2032
|
-
if (topDetail) return topDetail;
|
|
2033
|
-
|
|
2034
|
-
const frame = ${buildRecommendIframeLookupExpression()} || null;
|
|
2035
|
-
if (!frame || !frame.contentDocument) {
|
|
2036
|
-
return { ok: true, open: false, reason: 'NO_RECOMMEND_IFRAME' };
|
|
2037
|
-
}
|
|
2038
|
-
const frameDetail = findVisibleDetail(frame.contentDocument, 'recommendFrame');
|
|
2039
|
-
if (frameDetail) return frameDetail;
|
|
2040
|
-
|
|
2041
|
-
return { ok: true, open: false, reason: 'DETAIL_NOT_VISIBLE' };
|
|
2042
|
-
})()`;
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
export async function waitRecommendFeaturedDetailReady(workspaceRoot, options = {}) {
|
|
2046
|
-
const debugPort = Number.isFinite(options.port)
|
|
2047
|
-
? options.port
|
|
2048
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2049
|
-
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
2050
|
-
? Math.max(5000, options.timeoutMs)
|
|
2051
|
-
: 120000;
|
|
2052
|
-
const pollMs = Number.isFinite(options.pollMs)
|
|
2053
|
-
? Math.max(150, options.pollMs)
|
|
2054
|
-
: 400;
|
|
2055
|
-
|
|
2056
|
-
let client = null;
|
|
2057
|
-
try {
|
|
2058
|
-
const tabs = await listChromeTabs(debugPort);
|
|
2059
|
-
const target = pickBossRecommendReloadTarget(tabs);
|
|
2060
|
-
if (!target) {
|
|
2061
|
-
return {
|
|
2062
|
-
ok: false,
|
|
2063
|
-
debug_port: debugPort,
|
|
2064
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
2065
|
-
message: "未找到可检测详情页状态的 Boss recommend 标签页。"
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
client = await CDP({ port: debugPort, target });
|
|
2069
|
-
const { Runtime, Page } = client;
|
|
2070
|
-
if (Runtime && typeof Runtime.enable === "function") {
|
|
2071
|
-
await Runtime.enable();
|
|
2072
|
-
}
|
|
2073
|
-
if (Page && typeof Page.enable === "function") {
|
|
2074
|
-
await Page.enable();
|
|
2075
|
-
}
|
|
2076
|
-
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
2077
|
-
await Page.bringToFront();
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
const deadline = Date.now() + timeoutMs;
|
|
2081
|
-
let lastState = null;
|
|
2082
|
-
while (Date.now() < deadline) {
|
|
2083
|
-
lastState = await evaluateCdpExpression(client, buildRecommendDetailStateExpression());
|
|
2084
|
-
if (lastState?.ok && lastState.open) {
|
|
2085
|
-
return {
|
|
2086
|
-
ok: true,
|
|
2087
|
-
debug_port: debugPort,
|
|
2088
|
-
state: "DETAIL_READY",
|
|
2089
|
-
detail_state: lastState
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
await sleep(pollMs);
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
return {
|
|
2096
|
-
ok: false,
|
|
2097
|
-
debug_port: debugPort,
|
|
2098
|
-
state: "DETAIL_NOT_READY_TIMEOUT",
|
|
2099
|
-
message: "未在超时内检测到候选人详情页,请先打开精选候选人详情后重试。",
|
|
2100
|
-
detail_state: lastState || null
|
|
2101
|
-
};
|
|
2102
|
-
} catch (error) {
|
|
2103
|
-
return {
|
|
2104
|
-
ok: false,
|
|
2105
|
-
debug_port: debugPort,
|
|
2106
|
-
state: "DETAIL_STATE_CHECK_FAILED",
|
|
2107
|
-
message: error?.message || "检测候选人详情页状态失败。"
|
|
2108
|
-
};
|
|
2109
|
-
} finally {
|
|
2110
|
-
if (client) {
|
|
2111
|
-
try {
|
|
2112
|
-
await client.close();
|
|
2113
|
-
} catch {}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
export async function readRecommendTabState(workspaceRoot, options = {}) {
|
|
2119
|
-
const debugPort = Number.isFinite(options.port)
|
|
2120
|
-
? options.port
|
|
2121
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2122
|
-
|
|
2123
|
-
let client = null;
|
|
2124
|
-
try {
|
|
2125
|
-
const tabs = await listChromeTabs(debugPort);
|
|
2126
|
-
const target = pickBossRecommendReloadTarget(tabs);
|
|
2127
|
-
if (!target) {
|
|
2128
|
-
return {
|
|
2129
|
-
ok: false,
|
|
2130
|
-
debug_port: debugPort,
|
|
2131
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
2132
|
-
message: "未找到可读取 tab 状态的 Boss recommend 标签页。"
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
client = await CDP({ port: debugPort, target });
|
|
2136
|
-
const { Runtime } = client;
|
|
2137
|
-
if (Runtime && typeof Runtime.enable === "function") {
|
|
2138
|
-
await Runtime.enable();
|
|
2139
|
-
}
|
|
2140
|
-
const tabState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
2141
|
-
if (!tabState?.ok) {
|
|
2142
|
-
return {
|
|
2143
|
-
ok: false,
|
|
2144
|
-
debug_port: debugPort,
|
|
2145
|
-
state: tabState?.error || "RECOMMEND_TAB_STATE_FAILED",
|
|
2146
|
-
message: "读取 recommend tab 状态失败。",
|
|
2147
|
-
tab_state: tabState || null
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
return {
|
|
2151
|
-
ok: true,
|
|
2152
|
-
debug_port: debugPort,
|
|
2153
|
-
active_status: normalizeText(tabState.active_status),
|
|
2154
|
-
tab_state: tabState
|
|
2155
|
-
};
|
|
2156
|
-
} catch (error) {
|
|
2157
|
-
return {
|
|
2158
|
-
ok: false,
|
|
2159
|
-
debug_port: debugPort,
|
|
2160
|
-
state: "RECOMMEND_TAB_STATE_FAILED",
|
|
2161
|
-
message: error?.message || "读取 recommend tab 状态失败。"
|
|
2162
|
-
};
|
|
2163
|
-
} finally {
|
|
2164
|
-
if (client) {
|
|
2165
|
-
try {
|
|
2166
|
-
await client.close();
|
|
2167
|
-
} catch {}
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
function isUsableCalibrationFile(filePath) {
|
|
2173
|
-
if (!filePath || !pathExists(filePath)) return false;
|
|
2174
|
-
const parsed = readJsonFile(filePath);
|
|
2175
|
-
return Boolean(
|
|
2176
|
-
parsed
|
|
2177
|
-
&& parsed.favoritePosition
|
|
2178
|
-
&& Number.isFinite(parsed.favoritePosition.pageX)
|
|
2179
|
-
&& Number.isFinite(parsed.favoritePosition.pageY)
|
|
2180
|
-
);
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
function resolveFavoriteCalibrationPath(workspaceRoot) {
|
|
2184
|
-
const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
|
|
2185
|
-
if (fromEnv) return path.resolve(fromEnv);
|
|
2186
|
-
|
|
2187
|
-
const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
|
|
2188
|
-
const screenConfig = readJsonFile(screenConfigPath);
|
|
2189
|
-
const calibrationFile = normalizeText(screenConfig?.calibrationFile || "");
|
|
2190
|
-
if (calibrationFile) {
|
|
2191
|
-
return path.resolve(path.dirname(screenConfigPath), calibrationFile);
|
|
2192
|
-
}
|
|
2193
|
-
return getUserCalibrationPath();
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
export async function switchRecommendTab(workspaceRoot, options = {}) {
|
|
2197
|
-
const debugPort = Number.isFinite(options.port)
|
|
2198
|
-
? options.port
|
|
2199
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2200
|
-
const targetScope = normalizePageScope(options.page_scope);
|
|
2201
|
-
const targetStatus = normalizeText(options.target_status || PAGE_SCOPE_TO_TAB_STATUS[targetScope] || "");
|
|
2202
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
|
|
2203
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 350;
|
|
2204
|
-
if (!targetStatus) {
|
|
2205
|
-
return {
|
|
2206
|
-
ok: false,
|
|
2207
|
-
debug_port: debugPort,
|
|
2208
|
-
state: "TAB_STATUS_REQUIRED",
|
|
2209
|
-
message: "切换 recommend tab 失败:缺少 target_status。"
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
let client = null;
|
|
2214
|
-
try {
|
|
2215
|
-
const tabs = await listChromeTabs(debugPort);
|
|
2216
|
-
const target = pickBossRecommendReloadTarget(tabs);
|
|
2217
|
-
if (!target) {
|
|
2218
|
-
return {
|
|
2219
|
-
ok: false,
|
|
2220
|
-
debug_port: debugPort,
|
|
2221
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
2222
|
-
message: "未找到可操作的 Boss recommend 标签页。"
|
|
2223
|
-
};
|
|
2224
|
-
}
|
|
2225
|
-
client = await CDP({ port: debugPort, target });
|
|
2226
|
-
const { Runtime, Page } = client;
|
|
2227
|
-
if (Runtime && typeof Runtime.enable === "function") {
|
|
2228
|
-
await Runtime.enable();
|
|
2229
|
-
}
|
|
2230
|
-
if (Page && typeof Page.enable === "function") {
|
|
2231
|
-
await Page.enable();
|
|
2232
|
-
}
|
|
2233
|
-
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
2234
|
-
await Page.bringToFront();
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
const beforeState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
2238
|
-
if (beforeState?.ok && normalizeText(beforeState.active_status) === targetStatus) {
|
|
2239
|
-
return {
|
|
2240
|
-
ok: true,
|
|
2241
|
-
debug_port: debugPort,
|
|
2242
|
-
state: "TAB_ALREADY_ACTIVE",
|
|
2243
|
-
active_status: targetStatus,
|
|
2244
|
-
tab_state: beforeState
|
|
2245
|
-
};
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
const clickResult = await evaluateCdpExpression(client, buildRecommendTabSwitchExpression(targetStatus));
|
|
2249
|
-
if (!clickResult?.ok) {
|
|
2250
|
-
return {
|
|
2251
|
-
ok: false,
|
|
2252
|
-
debug_port: debugPort,
|
|
2253
|
-
state: clickResult?.state || "TAB_CLICK_FAILED",
|
|
2254
|
-
message: clickResult?.message || "点击 tab 失败。",
|
|
2255
|
-
tab_state: beforeState || null
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
const deadline = Date.now() + timeoutMs;
|
|
2260
|
-
let lastState = beforeState || null;
|
|
2261
|
-
while (Date.now() < deadline) {
|
|
2262
|
-
await sleep(pollMs);
|
|
2263
|
-
lastState = await evaluateCdpExpression(client, buildRecommendTabStateExpression());
|
|
2264
|
-
if (lastState?.ok && normalizeText(lastState.active_status) === targetStatus) {
|
|
2265
|
-
return {
|
|
2266
|
-
ok: true,
|
|
2267
|
-
debug_port: debugPort,
|
|
2268
|
-
state: "TAB_SWITCHED",
|
|
2269
|
-
active_status: targetStatus,
|
|
2270
|
-
tab_state: lastState
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
return {
|
|
2276
|
-
ok: false,
|
|
2277
|
-
debug_port: debugPort,
|
|
2278
|
-
state: "TAB_SWITCH_NOT_APPLIED",
|
|
2279
|
-
message: "点击 tab 后未在超时内确认激活状态。",
|
|
2280
|
-
tab_state: lastState || null
|
|
2281
|
-
};
|
|
2282
|
-
} catch (error) {
|
|
2283
|
-
return {
|
|
2284
|
-
ok: false,
|
|
2285
|
-
debug_port: debugPort,
|
|
2286
|
-
state: "RECOMMEND_TAB_SWITCH_FAILED",
|
|
2287
|
-
message: error?.message || "切换 recommend tab 失败。"
|
|
2288
|
-
};
|
|
2289
|
-
} finally {
|
|
2290
|
-
if (client) {
|
|
2291
|
-
try {
|
|
2292
|
-
await client.close();
|
|
2293
|
-
} catch {}
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
function buildRecommendRefreshStateExpression() {
|
|
2299
|
-
return `(() => {
|
|
2300
|
-
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2301
|
-
if (!frame || !frame.contentDocument) {
|
|
2302
|
-
return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
2303
|
-
}
|
|
2304
|
-
const doc = frame.contentDocument;
|
|
2305
|
-
const isVisible = (el) => {
|
|
2306
|
-
if (!el) return false;
|
|
2307
|
-
const win = doc.defaultView;
|
|
2308
|
-
if (!win) return el.offsetParent !== null;
|
|
2309
|
-
const style = win.getComputedStyle(el);
|
|
2310
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.02) {
|
|
2311
|
-
return false;
|
|
2312
|
-
}
|
|
2313
|
-
const rect = el.getBoundingClientRect();
|
|
2314
|
-
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
2315
|
-
};
|
|
2316
|
-
const finishedWrap = ${JSON.stringify(REFRESH_FINISHED_WRAP_SELECTORS)}
|
|
2317
|
-
.flatMap((selector) => {
|
|
2318
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2319
|
-
})
|
|
2320
|
-
.find((el) => isVisible(el)) || null;
|
|
2321
|
-
const refreshButton = ${JSON.stringify(REFRESH_BUTTON_SELECTORS)}
|
|
2322
|
-
.flatMap((selector) => {
|
|
2323
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2324
|
-
})
|
|
2325
|
-
.find((el) => isVisible(el)) || null;
|
|
2326
|
-
const cards = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
|
|
2327
|
-
.flatMap((selector) => {
|
|
2328
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2329
|
-
});
|
|
2330
|
-
const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
|
|
2331
|
-
const latestCards = ${JSON.stringify(LATEST_CARD_SELECTORS)}
|
|
2332
|
-
.flatMap((selector) => {
|
|
2333
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2334
|
-
});
|
|
2335
|
-
const tabs = ${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
|
|
2336
|
-
.flatMap((selector) => {
|
|
2337
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2338
|
-
});
|
|
2339
|
-
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
2340
|
-
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
2341
|
-
const inferredStatus = activeStatus
|
|
2342
|
-
|| (latestCards.length > 0 && candidateCards.length === 0 ? '1' : candidateCards.length > 0 ? '0' : '');
|
|
2343
|
-
const effectiveCandidates = inferredStatus === '1' ? latestCards : candidateCards;
|
|
2344
|
-
const finishedText = finishedWrap ? String(finishedWrap.textContent || '').replace(/\\s+/g, ' ').trim() : '';
|
|
2345
|
-
const buttonText = refreshButton ? String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() : '';
|
|
2346
|
-
return {
|
|
2347
|
-
ok: true,
|
|
2348
|
-
frame_url: (() => {
|
|
2349
|
-
try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
|
|
2350
|
-
})(),
|
|
2351
|
-
finished_wrap_visible: Boolean(finishedWrap),
|
|
2352
|
-
finished_wrap_text: finishedText || null,
|
|
2353
|
-
refresh_button_visible: Boolean(refreshButton),
|
|
2354
|
-
refresh_button_text: buttonText || null,
|
|
2355
|
-
candidate_count: effectiveCandidates.length,
|
|
2356
|
-
recommend_candidate_count: candidateCards.length,
|
|
2357
|
-
latest_candidate_count: latestCards.length,
|
|
2358
|
-
total_card_count: Math.max(cards.length, latestCards.length),
|
|
2359
|
-
list_ready: effectiveCandidates.length > 0
|
|
2360
|
-
};
|
|
2361
|
-
})()`;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
function buildRecommendRefreshClickExpression() {
|
|
2365
|
-
return `(() => {
|
|
2366
|
-
const frame = ${buildRecommendIframeLookupExpression()};
|
|
2367
|
-
if (!frame || !frame.contentDocument) {
|
|
2368
|
-
return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
|
|
2369
|
-
}
|
|
2370
|
-
const doc = frame.contentDocument;
|
|
2371
|
-
const isVisible = (el) => {
|
|
2372
|
-
if (!el) return false;
|
|
2373
|
-
const win = doc.defaultView;
|
|
2374
|
-
if (!win) return el.offsetParent !== null;
|
|
2375
|
-
const style = win.getComputedStyle(el);
|
|
2376
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.02) {
|
|
2377
|
-
return false;
|
|
2378
|
-
}
|
|
2379
|
-
const rect = el.getBoundingClientRect();
|
|
2380
|
-
return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
|
|
2381
|
-
};
|
|
2382
|
-
const refreshButton = ${JSON.stringify(REFRESH_BUTTON_SELECTORS)}
|
|
2383
|
-
.flatMap((selector) => {
|
|
2384
|
-
try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
|
|
2385
|
-
})
|
|
2386
|
-
.find((el) => isVisible(el)) || null;
|
|
2387
|
-
if (!refreshButton) {
|
|
2388
|
-
return { ok: false, state: 'REFRESH_BUTTON_NOT_FOUND' };
|
|
2389
|
-
}
|
|
2390
|
-
try {
|
|
2391
|
-
refreshButton.click();
|
|
2392
|
-
return {
|
|
2393
|
-
ok: true,
|
|
2394
|
-
state: 'REFRESH_BUTTON_CLICKED',
|
|
2395
|
-
refresh_button_text: String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() || null
|
|
2396
|
-
};
|
|
2397
|
-
} catch (error) {
|
|
2398
|
-
return {
|
|
2399
|
-
ok: false,
|
|
2400
|
-
state: 'REFRESH_BUTTON_CLICK_FAILED',
|
|
2401
|
-
message: error?.message || String(error)
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
2404
|
-
})()`;
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
export async function refreshBossRecommendList(workspaceRoot, options = {}) {
|
|
2408
|
-
const debugPort = Number.isFinite(options.port)
|
|
2409
|
-
? options.port
|
|
2410
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2411
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
2412
|
-
const reloadTimeoutMs = Number.isFinite(options.reloadTimeoutMs) ? options.reloadTimeoutMs : 10000;
|
|
2413
|
-
|
|
2414
|
-
let client = null;
|
|
2415
|
-
try {
|
|
2416
|
-
const tabs = await listChromeTabs(debugPort);
|
|
2417
|
-
const target = pickBossRecommendReloadTarget(tabs);
|
|
2418
|
-
if (!target) {
|
|
2419
|
-
return {
|
|
2420
|
-
ok: false,
|
|
2421
|
-
action: "in_page_refresh",
|
|
2422
|
-
debug_port: debugPort,
|
|
2423
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
2424
|
-
message: "未找到可操作的 Boss recommend 标签页。",
|
|
2425
|
-
before_state: null,
|
|
2426
|
-
after_state: null
|
|
2427
|
-
};
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
client = await CDP({ port: debugPort, target });
|
|
2431
|
-
const { Page, Runtime } = client;
|
|
2432
|
-
if (Runtime && typeof Runtime.enable === "function") {
|
|
2433
|
-
await Runtime.enable();
|
|
2434
|
-
}
|
|
2435
|
-
if (Page && typeof Page.enable === "function") {
|
|
2436
|
-
await Page.enable();
|
|
2437
|
-
}
|
|
2438
|
-
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
2439
|
-
await Page.bringToFront();
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
const beforeState = await evaluateCdpExpression(client, buildRecommendRefreshStateExpression());
|
|
2443
|
-
if (!beforeState?.ok) {
|
|
2444
|
-
return {
|
|
2445
|
-
ok: false,
|
|
2446
|
-
action: "in_page_refresh",
|
|
2447
|
-
debug_port: debugPort,
|
|
2448
|
-
state: beforeState?.error || "NO_RECOMMEND_IFRAME",
|
|
2449
|
-
message: "未能读取 recommend iframe,无法执行页内刷新。",
|
|
2450
|
-
before_state: beforeState || null,
|
|
2451
|
-
after_state: null
|
|
2452
|
-
};
|
|
2453
|
-
}
|
|
2454
|
-
if (!beforeState.refresh_button_visible) {
|
|
2455
|
-
return {
|
|
2456
|
-
ok: false,
|
|
2457
|
-
action: "in_page_refresh",
|
|
2458
|
-
debug_port: debugPort,
|
|
2459
|
-
state: "REFRESH_BUTTON_NOT_FOUND",
|
|
2460
|
-
message: "推荐列表到底后未发现可点击的刷新按钮。",
|
|
2461
|
-
before_state: beforeState,
|
|
2462
|
-
after_state: beforeState
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
const clickResult = await evaluateCdpExpression(client, buildRecommendRefreshClickExpression());
|
|
2467
|
-
if (!clickResult?.ok) {
|
|
2468
|
-
return {
|
|
2469
|
-
ok: false,
|
|
2470
|
-
action: "in_page_refresh",
|
|
2471
|
-
debug_port: debugPort,
|
|
2472
|
-
state: clickResult?.state || "REFRESH_BUTTON_CLICK_FAILED",
|
|
2473
|
-
message: clickResult?.message || "页内刷新按钮点击失败。",
|
|
2474
|
-
before_state: beforeState,
|
|
2475
|
-
after_state: null
|
|
2476
|
-
};
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
const deadline = Date.now() + reloadTimeoutMs;
|
|
2480
|
-
let lastState = beforeState;
|
|
2481
|
-
while (Date.now() < deadline) {
|
|
2482
|
-
await sleep(pollMs);
|
|
2483
|
-
lastState = await evaluateCdpExpression(client, buildRecommendRefreshStateExpression());
|
|
2484
|
-
if (lastState?.ok && lastState.finished_wrap_visible === false && lastState.list_ready === true) {
|
|
2485
|
-
return {
|
|
2486
|
-
ok: true,
|
|
2487
|
-
action: "in_page_refresh",
|
|
2488
|
-
debug_port: debugPort,
|
|
2489
|
-
state: "RECOMMEND_READY",
|
|
2490
|
-
message: "已点击页内刷新按钮并重新拿到候选人列表。",
|
|
2491
|
-
before_state: beforeState,
|
|
2492
|
-
after_state: lastState
|
|
2493
|
-
};
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
return {
|
|
2498
|
-
ok: false,
|
|
2499
|
-
action: "in_page_refresh",
|
|
2500
|
-
debug_port: debugPort,
|
|
2501
|
-
state: "LIST_NOT_RELOADED",
|
|
2502
|
-
message: "已点击页内刷新按钮,但候选人列表未在超时内重新就绪。",
|
|
2503
|
-
before_state: beforeState,
|
|
2504
|
-
after_state: lastState
|
|
2505
|
-
};
|
|
2506
|
-
} catch (error) {
|
|
2507
|
-
return {
|
|
2508
|
-
ok: false,
|
|
2509
|
-
action: "in_page_refresh",
|
|
2510
|
-
debug_port: debugPort,
|
|
2511
|
-
state: "REFRESH_BUTTON_CLICK_FAILED",
|
|
2512
|
-
message: error?.message || "页内刷新失败。",
|
|
2513
|
-
before_state: null,
|
|
2514
|
-
after_state: null
|
|
2515
|
-
};
|
|
2516
|
-
} finally {
|
|
2517
|
-
if (client) {
|
|
2518
|
-
try {
|
|
2519
|
-
await client.close();
|
|
2520
|
-
} catch {}
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
export async function reloadBossRecommendPage(workspaceRoot, options = {}) {
|
|
2526
|
-
const debugPort = Number.isFinite(options.port)
|
|
2527
|
-
? options.port
|
|
2528
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2529
|
-
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1200;
|
|
2530
|
-
const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 4000;
|
|
2531
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
|
|
2532
|
-
|
|
2533
|
-
let client = null;
|
|
2534
|
-
try {
|
|
2535
|
-
const tabs = await listChromeTabs(debugPort);
|
|
2536
|
-
const target = pickBossRecommendReloadTarget(tabs);
|
|
2537
|
-
if (!target) {
|
|
2538
|
-
return {
|
|
2539
|
-
ok: false,
|
|
2540
|
-
debug_port: debugPort,
|
|
2541
|
-
state: "BOSS_TAB_NOT_FOUND",
|
|
2542
|
-
page_state: null,
|
|
2543
|
-
message: "未找到可刷新的 Boss 标签页。"
|
|
2544
|
-
};
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
client = await CDP({ port: debugPort, target });
|
|
2548
|
-
const { Page } = client;
|
|
2549
|
-
if (Page && typeof Page.enable === "function") {
|
|
2550
|
-
await Page.enable();
|
|
2551
|
-
}
|
|
2552
|
-
if (shouldBringChromeToFront() && Page && typeof Page.bringToFront === "function") {
|
|
2553
|
-
await Page.bringToFront();
|
|
2554
|
-
}
|
|
2555
|
-
await Page.reload({ ignoreCache: true });
|
|
2556
|
-
|
|
2557
|
-
const stableState = await verifyRecommendPageStable(debugPort, {
|
|
2558
|
-
settleMs,
|
|
2559
|
-
recheckTimeoutMs,
|
|
2560
|
-
pollMs
|
|
2561
|
-
});
|
|
2562
|
-
return {
|
|
2563
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
2564
|
-
debug_port: debugPort,
|
|
2565
|
-
state: stableState.state,
|
|
2566
|
-
page_state: stableState,
|
|
2567
|
-
reloaded_url: target.url || null
|
|
2568
|
-
};
|
|
2569
|
-
} catch (error) {
|
|
2570
|
-
return {
|
|
2571
|
-
ok: false,
|
|
2572
|
-
debug_port: debugPort,
|
|
2573
|
-
state: "RELOAD_FAILED",
|
|
2574
|
-
page_state: null,
|
|
2575
|
-
message: error?.message || "刷新 Boss recommend 页面失败。"
|
|
2576
|
-
};
|
|
2577
|
-
} finally {
|
|
2578
|
-
if (client) {
|
|
2579
|
-
try {
|
|
2580
|
-
await client.close();
|
|
2581
|
-
} catch {}
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
export async function ensureBossRecommendPageReady(workspaceRoot, options = {}) {
|
|
2587
|
-
const debugPort = Number.isFinite(options.port)
|
|
2588
|
-
? options.port
|
|
2589
|
-
: resolveWorkspaceDebugPort(workspaceRoot);
|
|
2590
|
-
const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
|
|
2591
|
-
const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
|
|
2592
|
-
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
|
|
2593
|
-
const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
|
|
2594
|
-
|
|
2595
|
-
let pageState = await inspectBossRecommendPageState(debugPort, {
|
|
2596
|
-
timeoutMs: inspectTimeoutMs,
|
|
2597
|
-
pollMs
|
|
2598
|
-
});
|
|
2599
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
2600
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
2601
|
-
return {
|
|
2602
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
2603
|
-
debug_port: debugPort,
|
|
2604
|
-
state: stableState.state,
|
|
2605
|
-
page_state: stableState
|
|
2606
|
-
};
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
let launchAttempt = null;
|
|
2610
|
-
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
2611
|
-
return {
|
|
2612
|
-
ok: false,
|
|
2613
|
-
debug_port: debugPort,
|
|
2614
|
-
state: pageState.state,
|
|
2615
|
-
page_state: {
|
|
2616
|
-
...pageState,
|
|
2617
|
-
launch_attempt: launchAttempt
|
|
2618
|
-
}
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
2622
|
-
launchAttempt = launchChromeWithDebugPort(debugPort);
|
|
2623
|
-
if (launchAttempt.ok) {
|
|
2624
|
-
await sleep(settleMs + 1200);
|
|
2625
|
-
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
2626
|
-
timeoutMs: inspectTimeoutMs,
|
|
2627
|
-
pollMs
|
|
2628
|
-
});
|
|
2629
|
-
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
2630
|
-
return {
|
|
2631
|
-
ok: false,
|
|
2632
|
-
debug_port: debugPort,
|
|
2633
|
-
state: pageState.state,
|
|
2634
|
-
page_state: {
|
|
2635
|
-
...pageState,
|
|
2636
|
-
launch_attempt: launchAttempt
|
|
2637
|
-
}
|
|
2638
|
-
};
|
|
2639
|
-
}
|
|
2640
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
2641
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
2642
|
-
return {
|
|
2643
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
2644
|
-
debug_port: debugPort,
|
|
2645
|
-
state: stableState.state,
|
|
2646
|
-
page_state: {
|
|
2647
|
-
...stableState,
|
|
2648
|
-
launch_attempt: launchAttempt
|
|
2649
|
-
}
|
|
2650
|
-
};
|
|
2651
|
-
}
|
|
2652
|
-
} else {
|
|
2653
|
-
return {
|
|
2654
|
-
ok: false,
|
|
2655
|
-
debug_port: debugPort,
|
|
2656
|
-
state: pageState.state,
|
|
2657
|
-
page_state: {
|
|
2658
|
-
...pageState,
|
|
2659
|
-
launch_attempt: launchAttempt
|
|
2660
|
-
}
|
|
2661
|
-
};
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
2666
|
-
if (
|
|
2667
|
-
pageState.state === "DEBUG_PORT_UNREACHABLE"
|
|
2668
|
-
|| pageState.state === "LOGIN_REQUIRED"
|
|
2669
|
-
|| pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
2670
|
-
) {
|
|
2671
|
-
break;
|
|
2672
|
-
}
|
|
2673
|
-
await openBossRecommendTab(debugPort);
|
|
2674
|
-
await sleep(settleMs);
|
|
2675
|
-
pageState = await inspectBossRecommendPageState(debugPort, {
|
|
2676
|
-
timeoutMs: inspectTimeoutMs,
|
|
2677
|
-
pollMs
|
|
2678
|
-
});
|
|
2679
|
-
if (pageState.state === "RECOMMEND_READY") {
|
|
2680
|
-
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
2681
|
-
return {
|
|
2682
|
-
ok: stableState.state === "RECOMMEND_READY",
|
|
2683
|
-
debug_port: debugPort,
|
|
2684
|
-
state: stableState.state,
|
|
2685
|
-
page_state: {
|
|
2686
|
-
...stableState,
|
|
2687
|
-
launch_attempt: launchAttempt
|
|
2688
|
-
}
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
return {
|
|
2694
|
-
ok: false,
|
|
2695
|
-
debug_port: debugPort,
|
|
2696
|
-
state: pageState.state || "UNKNOWN",
|
|
2697
|
-
page_state: {
|
|
2698
|
-
...pageState,
|
|
2699
|
-
launch_attempt: launchAttempt
|
|
2700
|
-
}
|
|
2701
|
-
};
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
export async function listRecommendJobs({ workspaceRoot, port, runtime = null }) {
|
|
2705
|
-
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
2706
|
-
if (!searchDir) {
|
|
2707
|
-
return {
|
|
2708
|
-
ok: false,
|
|
2709
|
-
stdout: "",
|
|
2710
|
-
stderr: "boss-recommend-search-cli package not found",
|
|
2711
|
-
error: {
|
|
2712
|
-
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
2713
|
-
message: "boss-recommend-search-cli 目录不存在。"
|
|
2714
|
-
}
|
|
2715
|
-
};
|
|
2716
|
-
}
|
|
2717
|
-
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
2718
|
-
const args = [
|
|
2719
|
-
cliPath,
|
|
2720
|
-
"--list-jobs",
|
|
2721
|
-
"--port",
|
|
2722
|
-
String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
|
|
2723
|
-
];
|
|
2724
|
-
const result = await runProcess({
|
|
2725
|
-
command: "node",
|
|
2726
|
-
args,
|
|
2727
|
-
cwd: searchDir,
|
|
2728
|
-
timeoutMs: 180000,
|
|
2729
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
2730
|
-
signal: runtime?.signal,
|
|
2731
|
-
onOutput: (event) => {
|
|
2732
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
2733
|
-
},
|
|
2734
|
-
onHeartbeat: (event) => {
|
|
2735
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
2736
|
-
}
|
|
2737
|
-
});
|
|
2738
|
-
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
2739
|
-
const jobs = Array.isArray(structured?.result?.jobs) ? structured.result.jobs : [];
|
|
2740
|
-
const missingOutputError = result.code === 0 && !structured
|
|
2741
|
-
? {
|
|
2742
|
-
code: "RECOMMEND_JOB_LIST_NO_OUTPUT",
|
|
2743
|
-
message: "岗位列表读取完成但未返回可解析结果。"
|
|
2744
|
-
}
|
|
2745
|
-
: null;
|
|
2746
|
-
return {
|
|
2747
|
-
ok: result.code === 0 && structured?.status === "COMPLETED" && jobs.length > 0,
|
|
2748
|
-
stdout: result.stdout,
|
|
2749
|
-
stderr: result.stderr,
|
|
2750
|
-
structured,
|
|
2751
|
-
jobs,
|
|
2752
|
-
error: structured?.error || missingOutputError || (
|
|
2753
|
-
result.code === 0
|
|
2754
|
-
? {
|
|
2755
|
-
code: "RECOMMEND_JOB_LIST_EMPTY",
|
|
2756
|
-
message: "未读取到可选岗位。"
|
|
2757
|
-
}
|
|
2758
|
-
: result.error_code === "ABORTED"
|
|
2759
|
-
? {
|
|
2760
|
-
code: "PROCESS_ABORTED",
|
|
2761
|
-
message: "岗位列表读取已取消。"
|
|
2762
|
-
}
|
|
2763
|
-
: {
|
|
2764
|
-
code: "RECOMMEND_JOB_LIST_FAILED",
|
|
2765
|
-
message: "岗位列表读取失败。"
|
|
2766
|
-
}
|
|
2767
|
-
)
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
export async function runRecommendSearchCli({
|
|
2772
|
-
workspaceRoot,
|
|
2773
|
-
searchParams,
|
|
2774
|
-
selectedJob,
|
|
2775
|
-
pageScope = "recommend",
|
|
2776
|
-
runtime = null
|
|
2777
|
-
}) {
|
|
2778
|
-
const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
|
|
2779
|
-
if (!searchDir) {
|
|
2780
|
-
return {
|
|
2781
|
-
ok: false,
|
|
2782
|
-
stdout: "",
|
|
2783
|
-
stderr: "boss-recommend-search-cli package not found",
|
|
2784
|
-
error: {
|
|
2785
|
-
code: "RECOMMEND_SEARCH_CLI_MISSING",
|
|
2786
|
-
message: "boss-recommend-search-cli 目录不存在。"
|
|
2787
|
-
}
|
|
2788
|
-
};
|
|
2789
|
-
}
|
|
2790
|
-
const cliPath = resolveRecommendSearchCliEntry(searchDir);
|
|
2791
|
-
const args = [
|
|
2792
|
-
cliPath,
|
|
2793
|
-
"--school-tag",
|
|
2794
|
-
serializeSchoolTagSelection(searchParams.school_tag),
|
|
2795
|
-
"--degree",
|
|
2796
|
-
serializeDegreeSelection(searchParams.degree),
|
|
2797
|
-
"--gender",
|
|
2798
|
-
searchParams.gender,
|
|
2799
|
-
"--recent-not-view",
|
|
2800
|
-
searchParams.recent_not_view,
|
|
2801
|
-
"--port",
|
|
2802
|
-
String(resolveWorkspaceDebugPort(workspaceRoot))
|
|
2803
|
-
];
|
|
2804
|
-
const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
|
|
2805
|
-
args.push("--page-scope", normalizedPageScope);
|
|
2806
|
-
if (normalizedPageScope === "featured") {
|
|
2807
|
-
args.push("--calibration", resolveFavoriteCalibrationPath(workspaceRoot));
|
|
2808
|
-
}
|
|
2809
|
-
const normalizedSelectedJob = String(selectedJob || "").trim();
|
|
2810
|
-
if (normalizedSelectedJob) {
|
|
2811
|
-
args.push("--job", normalizedSelectedJob);
|
|
2812
|
-
}
|
|
2813
|
-
const result = await runProcess({
|
|
2814
|
-
command: "node",
|
|
2815
|
-
args,
|
|
2816
|
-
cwd: searchDir,
|
|
2817
|
-
timeoutMs: 180000,
|
|
2818
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
2819
|
-
signal: runtime?.signal,
|
|
2820
|
-
onOutput: (event) => {
|
|
2821
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
2822
|
-
},
|
|
2823
|
-
onHeartbeat: (event) => {
|
|
2824
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
2825
|
-
}
|
|
2826
|
-
});
|
|
2827
|
-
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
2828
|
-
const missingOutputError = result.code === 0 && !structured
|
|
2829
|
-
? {
|
|
2830
|
-
code: "RECOMMEND_SEARCH_NO_OUTPUT",
|
|
2831
|
-
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
2832
|
-
}
|
|
2833
|
-
: null;
|
|
2834
|
-
return {
|
|
2835
|
-
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
2836
|
-
stdout: result.stdout,
|
|
2837
|
-
stderr: result.stderr,
|
|
2838
|
-
structured,
|
|
2839
|
-
summary: structured?.result || null,
|
|
2840
|
-
error: structured?.error || missingOutputError || (
|
|
2841
|
-
result.code === 0
|
|
2842
|
-
? null
|
|
2843
|
-
: result.error_code === "ABORTED"
|
|
2844
|
-
? {
|
|
2845
|
-
code: "PROCESS_ABORTED",
|
|
2846
|
-
message: "推荐页筛选命令已取消。"
|
|
2847
|
-
}
|
|
2848
|
-
: {
|
|
2849
|
-
code: "RECOMMEND_SEARCH_FAILED",
|
|
2850
|
-
message: "推荐页筛选命令执行失败。"
|
|
2851
|
-
}
|
|
2852
|
-
)
|
|
2853
|
-
};
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
export async function runRecommendScreenCli({
|
|
2857
|
-
workspaceRoot,
|
|
2858
|
-
screenParams,
|
|
2859
|
-
pageScope = "recommend",
|
|
2860
|
-
inputSummary = null,
|
|
2861
|
-
resume = null,
|
|
2862
|
-
runtime = null
|
|
2863
|
-
}) {
|
|
2864
|
-
const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
|
|
2865
|
-
if (!screenDir) {
|
|
2866
|
-
return {
|
|
2867
|
-
ok: false,
|
|
2868
|
-
stdout: "",
|
|
2869
|
-
stderr: "boss-recommend-screen-cli package not found",
|
|
2870
|
-
error: {
|
|
2871
|
-
code: "RECOMMEND_SCREEN_CLI_MISSING",
|
|
2872
|
-
message: "boss-recommend-screen-cli 目录不存在。"
|
|
2873
|
-
}
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2876
|
-
const configPath = resolveScreenConfigPath(workspaceRoot);
|
|
2877
|
-
const loaded = loadScreenConfig(configPath);
|
|
2878
|
-
if (!loaded.ok) {
|
|
2879
|
-
return {
|
|
2880
|
-
ok: false,
|
|
2881
|
-
stdout: "",
|
|
2882
|
-
stderr: loaded.error,
|
|
2883
|
-
error: {
|
|
2884
|
-
code: "SCREEN_CONFIG_ERROR",
|
|
2885
|
-
message: loaded.error
|
|
2886
|
-
}
|
|
2887
|
-
};
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
const fixedOutput = normalizeText(resume?.output_csv || "");
|
|
2891
|
-
const outputName = `recommend_screen_result_${Date.now()}.csv`;
|
|
2892
|
-
let outputPath = fixedOutput ? path.resolve(fixedOutput) : outputName;
|
|
2893
|
-
if (!fixedOutput) {
|
|
2894
|
-
if (loaded.config.outputDir) {
|
|
2895
|
-
const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
|
|
2896
|
-
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
2897
|
-
outputPath = path.join(resolvedOutputDir, outputName);
|
|
2898
|
-
} else {
|
|
2899
|
-
const desktopDir = getDesktopDir();
|
|
2900
|
-
fs.mkdirSync(desktopDir, { recursive: true });
|
|
2901
|
-
outputPath = path.join(desktopDir, outputName);
|
|
2902
|
-
}
|
|
2903
|
-
} else {
|
|
2904
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
const checkpointPath = normalizeText(resume?.checkpoint_path || "")
|
|
2908
|
-
? path.resolve(String(resume.checkpoint_path))
|
|
2909
|
-
: null;
|
|
2910
|
-
const pauseControlPath = normalizeText(resume?.pause_control_path || "")
|
|
2911
|
-
? path.resolve(String(resume.pause_control_path))
|
|
2912
|
-
: null;
|
|
2913
|
-
const resumeRequested = resume?.resume === true;
|
|
2914
|
-
const requireCheckpoint = resume?.require_checkpoint === true;
|
|
2915
|
-
if (resumeRequested && requireCheckpoint) {
|
|
2916
|
-
if (!checkpointPath) {
|
|
2917
|
-
return {
|
|
2918
|
-
ok: false,
|
|
2919
|
-
paused: false,
|
|
2920
|
-
stdout: "",
|
|
2921
|
-
stderr: "",
|
|
2922
|
-
structured: null,
|
|
2923
|
-
summary: null,
|
|
2924
|
-
error: {
|
|
2925
|
-
code: "RESUME_CHECKPOINT_MISSING",
|
|
2926
|
-
message: "恢复执行缺少 checkpoint_path,无法从上次进度继续。"
|
|
2927
|
-
}
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
if (!fs.existsSync(checkpointPath)) {
|
|
2931
|
-
return {
|
|
2932
|
-
ok: false,
|
|
2933
|
-
paused: false,
|
|
2934
|
-
stdout: "",
|
|
2935
|
-
stderr: "",
|
|
2936
|
-
structured: null,
|
|
2937
|
-
summary: null,
|
|
2938
|
-
error: {
|
|
2939
|
-
code: "RESUME_CHECKPOINT_MISSING",
|
|
2940
|
-
message: `恢复执行未找到 checkpoint 文件:${checkpointPath}`
|
|
2941
|
-
}
|
|
2942
|
-
};
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
|
-
const cliPath = resolveRecommendScreenCliEntry(screenDir);
|
|
2947
|
-
const args = [
|
|
2948
|
-
cliPath,
|
|
2949
|
-
"--baseurl",
|
|
2950
|
-
loaded.config.baseUrl,
|
|
2951
|
-
"--apikey",
|
|
2952
|
-
loaded.config.apiKey,
|
|
2953
|
-
"--model",
|
|
2954
|
-
loaded.config.model,
|
|
2955
|
-
"--port",
|
|
2956
|
-
String(resolveWorkspaceDebugPort(workspaceRoot)),
|
|
2957
|
-
"--criteria",
|
|
2958
|
-
screenParams.criteria,
|
|
2959
|
-
"--post-action",
|
|
2960
|
-
screenParams.post_action,
|
|
2961
|
-
"--post-action-confirmed",
|
|
2962
|
-
"true",
|
|
2963
|
-
"--output",
|
|
2964
|
-
outputPath
|
|
2965
|
-
];
|
|
2966
|
-
const normalizedPageScope = normalizePageScope(pageScope) || "recommend";
|
|
2967
|
-
args.push("--page-scope", normalizedPageScope);
|
|
2968
|
-
|
|
2969
|
-
if (loaded.config.openaiOrganization) {
|
|
2970
|
-
args.push("--openai-organization", loaded.config.openaiOrganization);
|
|
2971
|
-
}
|
|
2972
|
-
if (loaded.config.openaiProject) {
|
|
2973
|
-
args.push("--openai-project", loaded.config.openaiProject);
|
|
2974
|
-
}
|
|
2975
|
-
const llmThinkingLevel = resolveLlmThinkingLevel(loaded.config);
|
|
2976
|
-
if (llmThinkingLevel) {
|
|
2977
|
-
args.push("--thinking-level", llmThinkingLevel);
|
|
2978
|
-
}
|
|
2979
|
-
const sharedLlmTransport = resolveSharedLlmTransportConfig(loaded.config);
|
|
2980
|
-
if (sharedLlmTransport.llmTimeoutMs) {
|
|
2981
|
-
args.push("--llm-timeout-ms", String(sharedLlmTransport.llmTimeoutMs));
|
|
2982
|
-
}
|
|
2983
|
-
if (sharedLlmTransport.llmMaxRetries) {
|
|
2984
|
-
args.push("--llm-max-retries", String(sharedLlmTransport.llmMaxRetries));
|
|
2985
|
-
}
|
|
2986
|
-
args.push("--human-rest", String(resolveHumanRestEnabled(loaded.config)));
|
|
2987
|
-
if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
|
|
2988
|
-
args.push("--targetCount", String(screenParams.target_count));
|
|
2989
|
-
}
|
|
2990
|
-
if (screenParams.post_action === "greet"
|
|
2991
|
-
&& Number.isInteger(screenParams.max_greet_count)
|
|
2992
|
-
&& screenParams.max_greet_count > 0) {
|
|
2993
|
-
args.push("--max-greet-count", String(screenParams.max_greet_count));
|
|
2994
|
-
}
|
|
2995
|
-
if (checkpointPath) {
|
|
2996
|
-
args.push("--checkpoint-path", checkpointPath);
|
|
2997
|
-
}
|
|
2998
|
-
if (pauseControlPath) {
|
|
2999
|
-
args.push("--pause-control-path", pauseControlPath);
|
|
3000
|
-
}
|
|
3001
|
-
if (resumeRequested) {
|
|
3002
|
-
args.push("--resume");
|
|
3003
|
-
}
|
|
3004
|
-
const serializedInputSummary = serializeInputSummary(inputSummary);
|
|
3005
|
-
if (serializedInputSummary) {
|
|
3006
|
-
args.push("--input-summary-json", serializedInputSummary);
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
let inferredProgress = {
|
|
3010
|
-
processed: 0,
|
|
3011
|
-
passed: 0,
|
|
3012
|
-
skipped: 0,
|
|
3013
|
-
greet_count: 0
|
|
3014
|
-
};
|
|
3015
|
-
let inferredTracker = createScreenProgressTracker();
|
|
3016
|
-
const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
|
|
3017
|
-
|
|
3018
|
-
const result = await runProcess({
|
|
3019
|
-
command: "node",
|
|
3020
|
-
args,
|
|
3021
|
-
cwd: screenDir,
|
|
3022
|
-
timeoutMs: screenTimeoutMs,
|
|
3023
|
-
heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
|
|
3024
|
-
signal: runtime?.signal,
|
|
3025
|
-
onOutput: (event) => {
|
|
3026
|
-
safeInvokeCallback(runtime?.onOutput, event);
|
|
3027
|
-
},
|
|
3028
|
-
onLine: (event) => {
|
|
3029
|
-
const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
|
|
3030
|
-
if (!parsed) return;
|
|
3031
|
-
inferredProgress = parsed.progress;
|
|
3032
|
-
inferredTracker = parsed.tracker;
|
|
3033
|
-
safeInvokeCallback(runtime?.onProgress, {
|
|
3034
|
-
...inferredProgress,
|
|
3035
|
-
line: parsed.line
|
|
3036
|
-
});
|
|
3037
|
-
},
|
|
3038
|
-
onHeartbeat: (event) => {
|
|
3039
|
-
safeInvokeCallback(runtime?.onHeartbeat, event);
|
|
3040
|
-
}
|
|
3041
|
-
});
|
|
3042
|
-
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
3043
|
-
const status = normalizeText(structured?.status || "").toUpperCase();
|
|
3044
|
-
const summary = structured?.result || null;
|
|
3045
|
-
if (summary) {
|
|
3046
|
-
safeInvokeCallback(runtime?.onProgress, {
|
|
3047
|
-
processed: Number.isInteger(summary.processed_count) ? summary.processed_count : inferredProgress.processed,
|
|
3048
|
-
passed: Number.isInteger(summary.passed_count) ? summary.passed_count : inferredProgress.passed,
|
|
3049
|
-
skipped: Number.isInteger(summary.skipped_count) ? summary.skipped_count : inferredProgress.skipped,
|
|
3050
|
-
greet_count: Number.isInteger(summary.greet_count) ? summary.greet_count : inferredProgress.greet_count
|
|
3051
|
-
});
|
|
3052
|
-
}
|
|
3053
|
-
const missingOutputError = result.code === 0 && !structured
|
|
3054
|
-
? {
|
|
3055
|
-
code: "RECOMMEND_SCREEN_NO_OUTPUT",
|
|
3056
|
-
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
3057
|
-
}
|
|
3058
|
-
: null;
|
|
3059
|
-
return {
|
|
3060
|
-
ok: result.code === 0 && status === "COMPLETED",
|
|
3061
|
-
paused: result.code === 0 && status === "PAUSED",
|
|
3062
|
-
stdout: result.stdout,
|
|
3063
|
-
stderr: result.stderr,
|
|
3064
|
-
structured,
|
|
3065
|
-
summary,
|
|
3066
|
-
error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
|
|
3067
|
-
};
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
export const __testables = {
|
|
3071
|
-
runProcess,
|
|
3072
|
-
parseJsonOutput,
|
|
3073
|
-
parseScreenProgressLine,
|
|
3074
|
-
resolveRecommendScreenTimeoutMs,
|
|
3075
|
-
buildRecommendScreenProcessError,
|
|
3076
|
-
normalizePageScope,
|
|
3077
|
-
buildRecommendTabStateExpression,
|
|
3078
|
-
buildRecommendTabSwitchExpression
|
|
3079
|
-
};
|