@reconcrap/boss-recommend-mcp 1.1.3 → 1.1.5

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/src/adapters.js CHANGED
@@ -3,1838 +3,2123 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn, spawnSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
-
7
- const currentFilePath = fileURLToPath(import.meta.url);
8
- const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
9
- const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
10
- const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
11
- const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
12
- const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
13
- const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
14
- const screenConfigTemplateDefaults = {
15
- baseUrl: "https://api.openai.com/v1",
16
- apiKey: "replace-with-openai-api-key",
17
- model: "gpt-4.1-mini"
18
- };
19
- const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
20
-
21
- function getCodexHome() {
22
- return process.env.CODEX_HOME
23
- ? path.resolve(process.env.CODEX_HOME)
24
- : path.join(os.homedir(), ".codex");
25
- }
26
-
27
- function getStateHome() {
28
- return process.env.BOSS_RECOMMEND_HOME
29
- ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
30
- : path.join(os.homedir(), ".boss-recommend-mcp");
31
- }
32
-
33
- function getUserConfigPath() {
34
- return path.join(getStateHome(), "screening-config.json");
35
- }
36
-
37
- function getLegacyUserConfigPath() {
38
- return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
39
- }
40
-
41
- function getDesktopDir() {
42
- return path.join(os.homedir(), "Desktop");
43
- }
44
-
45
- function ensureDir(targetPath) {
46
- fs.mkdirSync(targetPath, { recursive: true });
47
- }
48
-
49
- function pathExists(targetPath) {
50
- try {
51
- return fs.existsSync(targetPath);
52
- } catch {
53
- return false;
54
- }
55
- }
56
-
57
- function parsePositiveInteger(raw) {
58
- const value = Number.parseInt(String(raw || ""), 10);
59
- return Number.isFinite(value) && value > 0 ? value : null;
60
- }
61
-
62
- function normalizeText(value) {
63
- return String(value || "").replace(/\s+/g, " ").trim();
64
- }
65
-
66
- function isRootDirectory(targetPath) {
67
- const resolved = path.resolve(String(targetPath || ""));
68
- const parsed = path.parse(resolved);
69
- return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
70
- }
71
-
72
- function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
73
- const root = path.resolve(String(workspaceRoot || ""));
74
- const normalized = root.replace(/\\/g, "/").toLowerCase();
75
- if (process.platform === "win32") {
76
- return (
77
- normalized.endsWith("/windows")
78
- || normalized.endsWith("/windows/system32")
79
- || normalized.endsWith("/windows/syswow64")
80
- || normalized.endsWith("/program files")
81
- || normalized.endsWith("/program files (x86)")
82
- );
83
- }
84
- return (
85
- normalized === "/system"
86
- || normalized.startsWith("/system/")
87
- || normalized === "/usr"
88
- || normalized.startsWith("/usr/")
89
- || normalized === "/bin"
90
- || normalized.startsWith("/bin/")
91
- || normalized === "/sbin"
92
- || normalized.startsWith("/sbin/")
93
- );
94
- }
95
-
96
- function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
97
- const root = path.resolve(String(workspaceRoot || process.cwd()));
98
- const home = path.resolve(os.homedir());
99
- return (
100
- isEphemeralNpxWorkspaceRoot(root)
101
- || isRootDirectory(root)
102
- || root.toLowerCase() === home.toLowerCase()
103
- || isSystemDirectoryWorkspaceRoot(root)
104
- );
6
+ import CDP from "chrome-remote-interface";
7
+
8
+ const currentFilePath = fileURLToPath(import.meta.url);
9
+ const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
10
+ const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
11
+ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
12
+ const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
13
+ const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
14
+ const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
15
+ const screenConfigTemplateDefaults = {
16
+ baseUrl: "https://api.openai.com/v1",
17
+ apiKey: "replace-with-openai-api-key",
18
+ model: "gpt-4.1-mini"
19
+ };
20
+ const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
21
+
22
+ function getCodexHome() {
23
+ return process.env.CODEX_HOME
24
+ ? path.resolve(process.env.CODEX_HOME)
25
+ : path.join(os.homedir(), ".codex");
26
+ }
27
+
28
+ function getStateHome() {
29
+ return process.env.BOSS_RECOMMEND_HOME
30
+ ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
31
+ : path.join(os.homedir(), ".boss-recommend-mcp");
32
+ }
33
+
34
+ function getUserConfigPath() {
35
+ return path.join(getStateHome(), "screening-config.json");
36
+ }
37
+
38
+ function getLegacyUserConfigPath() {
39
+ return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
40
+ }
41
+
42
+ function getDesktopDir() {
43
+ return path.join(os.homedir(), "Desktop");
44
+ }
45
+
46
+ function ensureDir(targetPath) {
47
+ fs.mkdirSync(targetPath, { recursive: true });
48
+ }
49
+
50
+ function pathExists(targetPath) {
51
+ try {
52
+ return fs.existsSync(targetPath);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function parsePositiveInteger(raw) {
59
+ const value = Number.parseInt(String(raw || ""), 10);
60
+ return Number.isFinite(value) && value > 0 ? value : null;
61
+ }
62
+
63
+ function normalizeText(value) {
64
+ return String(value || "").replace(/\s+/g, " ").trim();
65
+ }
66
+
67
+ function isRootDirectory(targetPath) {
68
+ const resolved = path.resolve(String(targetPath || ""));
69
+ const parsed = path.parse(resolved);
70
+ return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
71
+ }
72
+
73
+ function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
74
+ const root = path.resolve(String(workspaceRoot || ""));
75
+ const normalized = root.replace(/\\/g, "/").toLowerCase();
76
+ if (process.platform === "win32") {
77
+ return (
78
+ normalized.endsWith("/windows")
79
+ || normalized.endsWith("/windows/system32")
80
+ || normalized.endsWith("/windows/syswow64")
81
+ || normalized.endsWith("/program files")
82
+ || normalized.endsWith("/program files (x86)")
83
+ );
84
+ }
85
+ return (
86
+ normalized === "/system"
87
+ || normalized.startsWith("/system/")
88
+ || normalized === "/usr"
89
+ || normalized.startsWith("/usr/")
90
+ || normalized === "/bin"
91
+ || normalized.startsWith("/bin/")
92
+ || normalized === "/sbin"
93
+ || normalized.startsWith("/sbin/")
94
+ );
95
+ }
96
+
97
+ function shouldIgnoreWorkspaceConfigRoot(workspaceRoot) {
98
+ const root = path.resolve(String(workspaceRoot || process.cwd()));
99
+ const home = path.resolve(os.homedir());
100
+ return (
101
+ isEphemeralNpxWorkspaceRoot(root)
102
+ || isRootDirectory(root)
103
+ || root.toLowerCase() === home.toLowerCase()
104
+ || isSystemDirectoryWorkspaceRoot(root)
105
+ );
106
+ }
107
+
108
+ function resolveWorkspaceConfigCandidates(workspaceRoot) {
109
+ const root = path.resolve(String(workspaceRoot || process.cwd()));
110
+ if (shouldIgnoreWorkspaceConfigRoot(root)) {
111
+ return [];
112
+ }
113
+ const directPath = path.join(root, "config", "screening-config.json");
114
+ const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
115
+ const candidates = [directPath];
116
+ if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
117
+ candidates.push(nestedPath);
118
+ }
119
+ return Array.from(new Set(candidates));
120
+ }
121
+
122
+ function serializeDegreeSelection(value) {
123
+ if (Array.isArray(value)) {
124
+ const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
125
+ return normalized.length ? normalized.join(",") : "不限";
126
+ }
127
+ const normalized = String(value || "").trim();
128
+ return normalized || "不限";
129
+ }
130
+
131
+ function serializeSchoolTagSelection(value) {
132
+ if (Array.isArray(value)) {
133
+ const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
134
+ if (!normalized.length) return "不限";
135
+ if (normalized.includes("不限")) {
136
+ return normalized.length === 1
137
+ ? "不限"
138
+ : normalized.filter((item) => item !== "不限").join(",");
139
+ }
140
+ return normalized.join(",");
141
+ }
142
+ const normalized = String(value || "").trim();
143
+ return normalized || "不限";
144
+ }
145
+
146
+ function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
147
+ const root = path.resolve(String(workspaceRoot || ""));
148
+ const normalized = root.replace(/\\/g, "/").toLowerCase();
149
+ return (
150
+ normalized.includes("/appdata/local/npm-cache/_npx/")
151
+ || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
152
+ );
153
+ }
154
+
155
+ function buildScreenConfigCandidateMap(workspaceRoot) {
156
+ return {
157
+ env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
158
+ ? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
159
+ : null,
160
+ workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
161
+ user_path: getUserConfigPath(),
162
+ legacy_path: getLegacyUserConfigPath()
163
+ };
164
+ }
165
+
166
+ function resolveScreenConfigCandidates(workspaceRoot) {
167
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
168
+ return [
169
+ candidateMap.env_path,
170
+ candidateMap.user_path,
171
+ ...candidateMap.workspace_paths,
172
+ candidateMap.legacy_path
173
+ ].filter(Boolean);
174
+ }
175
+
176
+ function canWriteDirectory(targetDir) {
177
+ try {
178
+ ensureDir(targetDir);
179
+ fs.accessSync(targetDir, fs.constants.W_OK);
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ function resolveWritableScreenConfigPath(workspaceRoot) {
187
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
188
+ const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
189
+ if (candidateMap.env_path) {
190
+ return candidateMap.env_path;
191
+ }
192
+ if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
193
+ return candidateMap.user_path;
194
+ }
195
+ if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
196
+ return workspacePreferred;
197
+ }
198
+ if (workspacePreferred) {
199
+ return workspacePreferred;
200
+ }
201
+ return candidateMap.user_path || candidateMap.legacy_path;
202
+ }
203
+
204
+ function resolveScreenConfigPath(workspaceRoot) {
205
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
206
+ if (candidateMap.env_path) {
207
+ return candidateMap.env_path;
208
+ }
209
+ if (candidateMap.user_path && pathExists(candidateMap.user_path)) {
210
+ return candidateMap.user_path;
211
+ }
212
+ const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
213
+ if (existingWorkspacePath) {
214
+ return existingWorkspacePath;
215
+ }
216
+ const writablePath = resolveWritableScreenConfigPath(workspaceRoot);
217
+ if (writablePath) {
218
+ return writablePath;
219
+ }
220
+ return candidateMap.legacy_path;
221
+ }
222
+
223
+ export function getScreenConfigResolution(workspaceRoot) {
224
+ const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
225
+ const candidate_paths = resolveScreenConfigCandidates(workspaceRoot);
226
+ const resolved_path = resolveScreenConfigPath(workspaceRoot) || null;
227
+ const workspace_root = path.resolve(String(workspaceRoot || process.cwd()));
228
+ return {
229
+ resolved_path,
230
+ candidate_paths,
231
+ workspace_root,
232
+ workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
233
+ workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspace_root),
234
+ writable_path: resolveWritableScreenConfigPath(workspaceRoot),
235
+ legacy_path: candidateMap.legacy_path
236
+ };
237
+ }
238
+
239
+ function readJsonFile(filePath) {
240
+ if (!filePath || !pathExists(filePath)) return null;
241
+ try {
242
+ const raw = fs.readFileSync(filePath, "utf8");
243
+ return JSON.parse(raw);
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function validateScreenConfig(config) {
250
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
251
+ return {
252
+ ok: false,
253
+ reason: "INVALID_OR_MISSING_CONFIG",
254
+ message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
255
+ };
256
+ }
257
+ const baseUrl = String(config.baseUrl || "").trim();
258
+ const apiKey = String(config.apiKey || "").trim();
259
+ const model = String(config.model || "").trim();
260
+ const missing = [];
261
+ if (!baseUrl) missing.push("baseUrl");
262
+ if (!apiKey) missing.push("apiKey");
263
+ if (!model) missing.push("model");
264
+ if (missing.length > 0) {
265
+ return {
266
+ ok: false,
267
+ reason: "MISSING_REQUIRED_FIELDS",
268
+ message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
269
+ };
270
+ }
271
+ if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
272
+ return {
273
+ ok: false,
274
+ reason: "PLACEHOLDER_API_KEY",
275
+ message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
276
+ };
277
+ }
278
+ if (
279
+ baseUrl === screenConfigTemplateDefaults.baseUrl
280
+ && apiKey === screenConfigTemplateDefaults.apiKey
281
+ && model === screenConfigTemplateDefaults.model
282
+ ) {
283
+ return {
284
+ ok: false,
285
+ reason: "PLACEHOLDER_TEMPLATE_VALUES",
286
+ message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
287
+ };
288
+ }
289
+ return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
290
+ }
291
+
292
+ function resolveWorkspaceDebugPort(workspaceRoot) {
293
+ const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
294
+ if (fromEnv) return fromEnv;
295
+ const config = readJsonFile(resolveScreenConfigPath(workspaceRoot));
296
+ return parsePositiveInteger(config?.debugPort) || 9222;
297
+ }
298
+
299
+ function getDefaultChromeExecutableCandidates() {
300
+ const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
301
+ if (process.platform === "win32") {
302
+ candidates.push(
303
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
304
+ path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
305
+ path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
306
+ );
307
+ } else if (process.platform === "darwin") {
308
+ candidates.push(
309
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
310
+ path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
311
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
312
+ );
313
+ } else {
314
+ candidates.push(
315
+ "/usr/bin/google-chrome",
316
+ "/usr/bin/google-chrome-stable",
317
+ "/usr/bin/chromium-browser",
318
+ "/usr/bin/chromium",
319
+ "/snap/bin/chromium"
320
+ );
321
+ }
322
+ return Array.from(new Set(candidates.filter(Boolean)));
323
+ }
324
+
325
+ function getChromeExecutable() {
326
+ const candidates = getDefaultChromeExecutableCandidates();
327
+ return candidates.find((candidate) => pathExists(candidate)) || null;
328
+ }
329
+
330
+ function getChromeUserDataDir(port) {
331
+ const profileDir = resolveDefaultChromeUserDataDir(port);
332
+ ensureDir(profileDir);
333
+ return profileDir;
334
+ }
335
+
336
+ function getSharedChromeUserDataDir(port) {
337
+ return path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
338
+ }
339
+
340
+ function getLegacyRecruitChromeUserDataDir(port) {
341
+ return path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
342
+ }
343
+
344
+ function getLegacyRecommendChromeUserDataDir(port) {
345
+ return path.join(getStateHome(), `chrome-profile-${port}`);
346
+ }
347
+
348
+ function resolveDefaultChromeUserDataDir(port) {
349
+ const sharedPath = getSharedChromeUserDataDir(port);
350
+ if (pathExists(sharedPath)) {
351
+ return sharedPath;
352
+ }
353
+ const legacyPaths = [
354
+ getLegacyRecruitChromeUserDataDir(port),
355
+ getLegacyRecommendChromeUserDataDir(port)
356
+ ];
357
+ const legacyExisting = legacyPaths.find((candidate) => pathExists(candidate));
358
+ return legacyExisting || sharedPath;
359
+ }
360
+
361
+ function launchChromeWithDebugPort(port) {
362
+ const chromePath = getChromeExecutable();
363
+ if (!chromePath) {
364
+ return {
365
+ ok: false,
366
+ code: "CHROME_EXECUTABLE_NOT_FOUND",
367
+ message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
368
+ };
369
+ }
370
+ const userDataDir = getChromeUserDataDir(port);
371
+ const args = [
372
+ `--remote-debugging-port=${port}`,
373
+ `--user-data-dir=${userDataDir}`,
374
+ "--no-first-run",
375
+ "--no-default-browser-check",
376
+ "--new-window",
377
+ bossRecommendUrl
378
+ ];
379
+
380
+ try {
381
+ const child = spawn(chromePath, args, {
382
+ detached: true,
383
+ stdio: "ignore",
384
+ windowsHide: false
385
+ });
386
+ child.unref();
387
+ return {
388
+ ok: true,
389
+ code: "CHROME_LAUNCHED",
390
+ chrome_path: chromePath,
391
+ user_data_dir: userDataDir
392
+ };
393
+ } catch (error) {
394
+ return {
395
+ ok: false,
396
+ code: "CHROME_LAUNCH_FAILED",
397
+ message: error.message || "Chrome 启动失败。"
398
+ };
399
+ }
400
+ }
401
+
402
+ function resolveRecommendSearchCliDir(workspaceRoot) {
403
+ const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
404
+ if (pathExists(localDir)) return localDir;
405
+ const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-search-cli");
406
+ if (pathExists(vendoredDir)) return vendoredDir;
407
+ return null;
408
+ }
409
+
410
+ function resolveRecommendScreenCliDir(workspaceRoot) {
411
+ const localDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
412
+ if (pathExists(localDir)) return localDir;
413
+ const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-screen-cli");
414
+ if (pathExists(vendoredDir)) return vendoredDir;
415
+ return null;
416
+ }
417
+
418
+ function resolveRecommendScreenCliEntry(screenDir) {
419
+ const candidates = [
420
+ path.join(screenDir, "boss-recommend-screen-cli.cjs"),
421
+ path.join(screenDir, "boss-recommend-screen-cli.js")
422
+ ];
423
+ return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
424
+ }
425
+
426
+ function resolveRecommendSearchCliEntry(searchDir) {
427
+ const candidates = [
428
+ path.join(searchDir, "src", "cli.js"),
429
+ path.join(searchDir, "src", "cli.cjs")
430
+ ];
431
+ return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
432
+ }
433
+
434
+ function safeInvokeCallback(callback, payload) {
435
+ if (typeof callback !== "function") return;
436
+ try {
437
+ callback(payload);
438
+ } catch {
439
+ // Ignore callback errors to keep pipeline runtime stable.
440
+ }
441
+ }
442
+
443
+ function runProcess({
444
+ command,
445
+ args,
446
+ cwd,
447
+ timeoutMs,
448
+ onOutput,
449
+ onLine,
450
+ onHeartbeat,
451
+ heartbeatIntervalMs = 10_000,
452
+ signal
453
+ }) {
454
+ return new Promise((resolve) => {
455
+ let stdout = "";
456
+ let stderr = "";
457
+ let stdoutLineBuffer = "";
458
+ let stderrLineBuffer = "";
459
+ let settled = false;
460
+ let timer = null;
461
+ let heartbeatTimer = null;
462
+ let abortedBySignal = Boolean(signal?.aborted);
463
+ let abortListener = null;
464
+
465
+ function notifyHeartbeat(source) {
466
+ safeInvokeCallback(onHeartbeat, {
467
+ source,
468
+ command,
469
+ args,
470
+ cwd,
471
+ at: new Date().toISOString()
472
+ });
473
+ }
474
+
475
+ function emitLine(stream, line) {
476
+ const normalized = String(line ?? "").replace(/\r$/, "");
477
+ if (!normalized) return;
478
+ safeInvokeCallback(onLine, {
479
+ stream,
480
+ line: normalized,
481
+ at: new Date().toISOString()
482
+ });
483
+ }
484
+
485
+ function pushLineBuffer(stream, chunkText) {
486
+ if (stream === "stdout") {
487
+ stdoutLineBuffer += chunkText;
488
+ } else {
489
+ stderrLineBuffer += chunkText;
490
+ }
491
+ let buffer = stream === "stdout" ? stdoutLineBuffer : stderrLineBuffer;
492
+ let newlineIndex = buffer.indexOf("\n");
493
+ while (newlineIndex !== -1) {
494
+ emitLine(stream, buffer.slice(0, newlineIndex));
495
+ buffer = buffer.slice(newlineIndex + 1);
496
+ newlineIndex = buffer.indexOf("\n");
497
+ }
498
+ if (stream === "stdout") {
499
+ stdoutLineBuffer = buffer;
500
+ } else {
501
+ stderrLineBuffer = buffer;
502
+ }
503
+ }
504
+
505
+ function finish(payload) {
506
+ if (settled) return;
507
+ settled = true;
508
+ if (timer) clearTimeout(timer);
509
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
510
+ if (signal && typeof signal.removeEventListener === "function" && abortListener) {
511
+ signal.removeEventListener("abort", abortListener);
512
+ }
513
+ emitLine("stdout", stdoutLineBuffer);
514
+ emitLine("stderr", stderrLineBuffer);
515
+ stdoutLineBuffer = "";
516
+ stderrLineBuffer = "";
517
+ resolve(payload);
518
+ }
519
+
520
+ if (abortedBySignal) {
521
+ finish({
522
+ code: -1,
523
+ stdout,
524
+ stderr: "Process aborted before spawn",
525
+ error_code: "ABORTED"
526
+ });
527
+ return;
528
+ }
529
+
530
+ let child;
531
+ try {
532
+ child = spawn(command, args, {
533
+ cwd,
534
+ windowsHide: true,
535
+ shell: false,
536
+ env: process.env
537
+ });
538
+ } catch (error) {
539
+ finish({
540
+ code: -1,
541
+ stdout,
542
+ stderr: error.message,
543
+ error_code: error.code || "SPAWN_FAILED"
544
+ });
545
+ return;
546
+ }
547
+
548
+ if (signal && typeof signal.addEventListener === "function") {
549
+ abortListener = () => {
550
+ abortedBySignal = true;
551
+ try {
552
+ child.kill();
553
+ } catch {}
554
+ };
555
+ signal.addEventListener("abort", abortListener, { once: true });
556
+ }
557
+
558
+ if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
559
+ timer = setTimeout(() => {
560
+ try {
561
+ child.kill();
562
+ } catch {}
563
+ finish({
564
+ code: -1,
565
+ stdout,
566
+ stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
567
+ error_code: "TIMEOUT"
568
+ });
569
+ }, timeoutMs);
570
+ }
571
+
572
+ if (Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0) {
573
+ heartbeatTimer = setInterval(() => {
574
+ notifyHeartbeat("timer");
575
+ }, heartbeatIntervalMs);
576
+ }
577
+
578
+ child.stdout.on("data", (chunk) => {
579
+ const text = chunk.toString();
580
+ stdout += text;
581
+ pushLineBuffer("stdout", text);
582
+ safeInvokeCallback(onOutput, {
583
+ stream: "stdout",
584
+ text,
585
+ at: new Date().toISOString()
586
+ });
587
+ notifyHeartbeat("stdout");
588
+ });
589
+ child.stderr.on("data", (chunk) => {
590
+ const text = chunk.toString();
591
+ stderr += text;
592
+ pushLineBuffer("stderr", text);
593
+ safeInvokeCallback(onOutput, {
594
+ stream: "stderr",
595
+ text,
596
+ at: new Date().toISOString()
597
+ });
598
+ notifyHeartbeat("stderr");
599
+ });
600
+ child.on("close", (code) => {
601
+ if (abortedBySignal) {
602
+ finish({
603
+ code: -1,
604
+ stdout,
605
+ stderr: `${stderr}\nProcess aborted by signal`.trim(),
606
+ error_code: "ABORTED"
607
+ });
608
+ return;
609
+ }
610
+ finish({ code, stdout, stderr });
611
+ });
612
+ child.on("error", (error) => {
613
+ finish({
614
+ code: -1,
615
+ stdout,
616
+ stderr: `${stderr}\n${error.message}`.trim(),
617
+ error_code: error.code || "SPAWN_FAILED"
618
+ });
619
+ });
620
+ });
621
+ }
622
+
623
+ function runProcessSync({ command, args, cwd }) {
624
+ try {
625
+ const result = spawnSync(command, args, {
626
+ cwd,
627
+ windowsHide: true,
628
+ shell: false,
629
+ env: process.env,
630
+ encoding: "utf8"
631
+ });
632
+ const stdout = String(result.stdout || "").trim();
633
+ const stderr = String(result.stderr || "").trim();
634
+ return {
635
+ ok: result.status === 0,
636
+ status: result.status,
637
+ stdout,
638
+ stderr,
639
+ output: [stdout, stderr].filter(Boolean).join("\n").trim(),
640
+ error_code: result.error?.code || null,
641
+ error_message: result.error?.message || null
642
+ };
643
+ } catch (error) {
644
+ return {
645
+ ok: false,
646
+ status: -1,
647
+ stdout: "",
648
+ stderr: "",
649
+ output: "",
650
+ error_code: error.code || "SPAWN_FAILED",
651
+ error_message: error.message || String(error)
652
+ };
653
+ }
654
+ }
655
+
656
+ function parseMajorVersion(raw) {
657
+ const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
658
+ if (!match) return null;
659
+ const major = Number.parseInt(match[1], 10);
660
+ return Number.isFinite(major) ? major : null;
661
+ }
662
+
663
+ function buildNodeCommandCheck() {
664
+ const probe = runProcessSync({
665
+ command: "node",
666
+ args: ["--version"]
667
+ });
668
+ const major = parseMajorVersion(probe.output);
669
+ const versionOk = Number.isInteger(major) && major >= 18;
670
+ return {
671
+ key: "node_cli",
672
+ ok: probe.ok && versionOk,
673
+ path: "node --version",
674
+ message: probe.ok
675
+ ? (versionOk
676
+ ? `Node 命令可用 (${probe.output || "unknown version"})`
677
+ : `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
678
+ : `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
679
+ };
680
+ }
681
+
682
+ function detectPythonCommand() {
683
+ const python = runProcessSync({
684
+ command: "python",
685
+ args: ["--version"]
686
+ });
687
+ if (python.ok) {
688
+ return {
689
+ ok: true,
690
+ command: "python",
691
+ probe: python
692
+ };
693
+ }
694
+ const python3 = runProcessSync({
695
+ command: "python3",
696
+ args: ["--version"]
697
+ });
698
+ if (python3.ok) {
699
+ return {
700
+ ok: false,
701
+ command: null,
702
+ probe: python,
703
+ fallback: python3
704
+ };
705
+ }
706
+ return {
707
+ ok: false,
708
+ command: null,
709
+ probe: python,
710
+ fallback: null
711
+ };
712
+ }
713
+
714
+ function buildPythonCommandCheck() {
715
+ const detected = detectPythonCommand();
716
+ if (detected.ok) {
717
+ return {
718
+ key: "python_cli",
719
+ ok: true,
720
+ path: "python --version",
721
+ message: `Python 命令可用 (${detected.probe.output || "unknown version"})`
722
+ };
723
+ }
724
+ if (detected.fallback) {
725
+ return {
726
+ key: "python_cli",
727
+ ok: false,
728
+ path: "python --version",
729
+ message: `检测到 ${detected.fallback.output || "python3"},但当前流程依赖 python 命令;请创建 python 别名后重试。`
730
+ };
731
+ }
732
+ return {
733
+ key: "python_cli",
734
+ ok: false,
735
+ path: "python --version",
736
+ message: "未找到 python 命令,请安装 Python 并确保 python 在 PATH 中。"
737
+ };
738
+ }
739
+
740
+ function buildPillowCheck() {
741
+ const detected = detectPythonCommand();
742
+ if (!detected.ok || !detected.command) {
743
+ return {
744
+ key: "python_pillow",
745
+ ok: false,
746
+ path: "python -c \"import PIL\"",
747
+ message: "无法校验 Pillow:python 命令不可用。"
748
+ };
749
+ }
750
+ const probe = runProcessSync({
751
+ command: detected.command,
752
+ args: ["-c", "import PIL, PIL.Image; print(PIL.__version__)"]
753
+ });
754
+ return {
755
+ key: "python_pillow",
756
+ ok: probe.ok,
757
+ path: `${detected.command} -c "import PIL"`,
758
+ message: probe.ok
759
+ ? `Pillow 可用 (${probe.output || "version unknown"})`
760
+ : "Pillow 未安装。请执行 `python -m pip install pillow`。"
761
+ };
762
+ }
763
+
764
+ function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
765
+ if (!cwd || !pathExists(cwd)) {
766
+ return {
767
+ key,
768
+ ok: false,
769
+ path: moduleName,
770
+ module: moduleName,
771
+ install_cwd: null,
772
+ message: missingMessage
773
+ };
774
+ }
775
+ const probe = runProcessSync({
776
+ command: "node",
777
+ args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
778
+ cwd
779
+ });
780
+ return {
781
+ key,
782
+ ok: probe.ok,
783
+ path: moduleName,
784
+ module: moduleName,
785
+ install_cwd: cwd,
786
+ message: probe.ok
787
+ ? `${moduleName} npm 依赖可用`
788
+ : `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
789
+ };
790
+ }
791
+
792
+ function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
793
+ return [
794
+ buildNodeCommandCheck(),
795
+ buildPythonCommandCheck(),
796
+ buildPillowCheck(),
797
+ buildNodePackageCheck({
798
+ key: "npm_dep_chrome_remote_interface_search",
799
+ moduleName: "chrome-remote-interface",
800
+ cwd: searchDir,
801
+ missingMessage: "无法校验 chrome-remote-interface:boss-recommend-search-cli 目录不存在。"
802
+ }),
803
+ buildNodePackageCheck({
804
+ key: "npm_dep_chrome_remote_interface_screen",
805
+ moduleName: "chrome-remote-interface",
806
+ cwd: screenDir,
807
+ missingMessage: "无法校验 chrome-remote-interface:boss-recommend-screen-cli 目录不存在。"
808
+ }),
809
+ buildNodePackageCheck({
810
+ key: "npm_dep_ws",
811
+ moduleName: "ws",
812
+ cwd: screenDir,
813
+ missingMessage: "无法校验 ws:boss-recommend-screen-cli 目录不存在。"
814
+ })
815
+ ];
816
+ }
817
+
818
+ function parseJsonOutput(text) {
819
+ const trimmed = String(text || "").trim();
820
+ if (!trimmed) return null;
821
+ try {
822
+ return JSON.parse(trimmed);
823
+ } catch {}
824
+ const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
825
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
826
+ try {
827
+ return JSON.parse(lines[index]);
828
+ } catch {
829
+ continue;
830
+ }
831
+ }
832
+ return null;
833
+ }
834
+
835
+ function createScreenProgressTracker(currentTracker = {}) {
836
+ const outcome = String(currentTracker.outcome || "").trim();
837
+ return {
838
+ candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
839
+ outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
840
+ action_failed: currentTracker.action_failed === true
841
+ };
842
+ }
843
+
844
+ function finalizeCandidateProgress(progress, tracker) {
845
+ if (!Number.isInteger(tracker.candidate_index)) {
846
+ return false;
847
+ }
848
+
849
+ let changed = false;
850
+ if (tracker.action_failed === true) {
851
+ progress.skipped += 1;
852
+ changed = true;
853
+ } else if (tracker.outcome === "pass") {
854
+ progress.passed += 1;
855
+ changed = true;
856
+ } else if (tracker.outcome === "skip") {
857
+ progress.skipped += 1;
858
+ changed = true;
859
+ }
860
+
861
+ tracker.candidate_index = null;
862
+ tracker.outcome = null;
863
+ tracker.action_failed = false;
864
+ return changed;
865
+ }
866
+
867
+ function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
868
+ const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
869
+ if (!normalizedLine) return null;
870
+
871
+ const nextProgress = {
872
+ processed: Number.isInteger(currentProgress.processed) ? currentProgress.processed : 0,
873
+ passed: Number.isInteger(currentProgress.passed) ? currentProgress.passed : 0,
874
+ skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
875
+ greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
876
+ };
877
+ const nextTracker = createScreenProgressTracker(currentTracker);
878
+
879
+ let changed = false;
880
+ const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
881
+ if (processedMatch) {
882
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
883
+ changed = true;
884
+ }
885
+ const processed = Number.parseInt(processedMatch[1], 10);
886
+ if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
887
+ nextProgress.processed = processed;
888
+ changed = true;
889
+ }
890
+ nextTracker.candidate_index = processed;
891
+ nextTracker.outcome = null;
892
+ nextTracker.action_failed = false;
893
+ }
894
+
895
+ if (/筛选结果:\s*通过/u.test(normalizedLine)) {
896
+ if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
897
+ changed = true;
898
+ }
899
+ nextTracker.outcome = "pass";
900
+ nextTracker.action_failed = false;
901
+ } else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
902
+ if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
903
+ changed = true;
904
+ }
905
+ nextTracker.outcome = "skip";
906
+ nextTracker.action_failed = false;
907
+ }
908
+
909
+ if (/候选人处理失败\s*:/u.test(normalizedLine)) {
910
+ if (!nextTracker.action_failed) {
911
+ changed = true;
912
+ }
913
+ nextTracker.action_failed = true;
914
+ }
915
+
916
+ if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
917
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
918
+ changed = true;
919
+ }
920
+ }
921
+
922
+ const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
923
+ if (finalStateLine) {
924
+ if (finalizeCandidateProgress(nextProgress, nextTracker)) {
925
+ changed = true;
926
+ }
927
+ }
928
+
929
+ const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
930
+ if (greetMatch) {
931
+ const greetCount = Number.parseInt(greetMatch[1], 10);
932
+ if (Number.isInteger(greetCount) && greetCount >= 0 && greetCount !== nextProgress.greet_count) {
933
+ nextProgress.greet_count = greetCount;
934
+ changed = true;
935
+ }
936
+ }
937
+
938
+ if (!changed) return null;
939
+ return {
940
+ line: normalizedLine,
941
+ progress: nextProgress,
942
+ tracker: nextTracker
943
+ };
944
+ }
945
+
946
+ function resolveRecommendScreenTimeoutMs(runtime = null) {
947
+ const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
948
+ const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
949
+ return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
950
+ }
951
+
952
+ function buildRecommendScreenProcessError(result, screenTimeoutMs) {
953
+ if (result.code === 0) return null;
954
+ if (result.error_code === "TIMEOUT") {
955
+ return {
956
+ code: "TIMEOUT",
957
+ message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
958
+ };
959
+ }
960
+ if (result.error_code === "ABORTED") {
961
+ return {
962
+ code: "PROCESS_ABORTED",
963
+ message: "推荐页筛选命令已取消。"
964
+ };
965
+ }
966
+ return {
967
+ code: "RECOMMEND_SCREEN_FAILED",
968
+ message: "推荐页筛选命令执行失败。"
969
+ };
970
+ }
971
+
972
+ function loadScreenConfig(configPath) {
973
+ const parsed = readJsonFile(configPath);
974
+ const validation = validateScreenConfig(parsed);
975
+ if (!validation.ok) {
976
+ return {
977
+ ok: false,
978
+ error: `${validation.message} (path: ${configPath})`
979
+ };
980
+ }
981
+ return { ok: true, config: parsed };
982
+ }
983
+
984
+ function localDirHint(workspaceRoot, dirName) {
985
+ return path.join(workspaceRoot, dirName);
986
+ }
987
+
988
+ export function runPipelinePreflight(workspaceRoot) {
989
+ const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
990
+ const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
991
+ const searchDirExists = Boolean(searchDir && pathExists(searchDir));
992
+ const searchEntryPath = searchDir
993
+ ? resolveRecommendSearchCliEntry(searchDir)
994
+ : path.join(localDirHint(workspaceRoot, "boss-recommend-search-cli"), "src", "cli.js");
995
+ const searchEntryExists = Boolean(searchDir && pathExists(searchEntryPath));
996
+ const screenDirExists = Boolean(screenDir && pathExists(screenDir));
997
+ const screenEntryPath = screenDir
998
+ ? resolveRecommendScreenCliEntry(screenDir)
999
+ : path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
1000
+ const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
1001
+ const configResolution = getScreenConfigResolution(workspaceRoot);
1002
+ const screenConfigPath = configResolution.resolved_path;
1003
+ const screenConfigParsed = readJsonFile(screenConfigPath);
1004
+ const screenConfigValidation = validateScreenConfig(screenConfigParsed);
1005
+ const checks = [
1006
+ {
1007
+ key: "recommend_search_cli_dir",
1008
+ ok: searchDirExists,
1009
+ path: searchDir || localDirHint(workspaceRoot, "boss-recommend-search-cli"),
1010
+ message: searchDirExists
1011
+ ? "boss-recommend-search-cli 目录可用"
1012
+ : "boss-recommend-search-cli 目录不存在"
1013
+ },
1014
+ {
1015
+ key: "recommend_search_cli_entry",
1016
+ ok: searchEntryExists,
1017
+ path: searchEntryPath,
1018
+ message: searchEntryExists
1019
+ ? "boss-recommend-search-cli 入口文件可用"
1020
+ : "boss-recommend-search-cli 入口文件缺失"
1021
+ },
1022
+ {
1023
+ key: "recommend_screen_cli_dir",
1024
+ ok: screenDirExists,
1025
+ path: screenDir || localDirHint(workspaceRoot, "boss-recommend-screen-cli"),
1026
+ message: screenDirExists
1027
+ ? "boss-recommend-screen-cli 目录可用"
1028
+ : "boss-recommend-screen-cli 目录不存在"
1029
+ },
1030
+ {
1031
+ key: "recommend_screen_cli_entry",
1032
+ ok: screenEntryExists,
1033
+ path: screenEntryPath,
1034
+ message: screenEntryExists
1035
+ ? "boss-recommend-screen-cli 入口文件可用"
1036
+ : "boss-recommend-screen-cli 入口文件缺失"
1037
+ },
1038
+ {
1039
+ key: "screen_config",
1040
+ ok: screenConfigValidation.ok,
1041
+ path: screenConfigPath,
1042
+ reason: screenConfigValidation.reason || null,
1043
+ message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
1044
+ }
1045
+ ];
1046
+ checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
1047
+
1048
+ return {
1049
+ ok: checks.every((item) => item.ok),
1050
+ checks,
1051
+ debug_port: resolveWorkspaceDebugPort(workspaceRoot),
1052
+ config_resolution: configResolution
1053
+ };
1054
+ }
1055
+
1056
+ function collectFailedCheckKeys(checks = []) {
1057
+ return new Set(
1058
+ checks
1059
+ .filter((item) => item && item.ok === false && typeof item.key === "string")
1060
+ .map((item) => item.key)
1061
+ );
1062
+ }
1063
+
1064
+ function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
1065
+ const npmKeys = new Set([
1066
+ "npm_dep_chrome_remote_interface_search",
1067
+ "npm_dep_chrome_remote_interface_screen",
1068
+ "npm_dep_ws"
1069
+ ]);
1070
+ const dirs = checks
1071
+ .filter((item) => item && item.ok === false && npmKeys.has(item.key))
1072
+ .map((item) => item.install_cwd)
1073
+ .filter((item) => typeof item === "string" && item.trim())
1074
+ .map((item) => path.resolve(item));
1075
+ if (dirs.length > 0) {
1076
+ return [...new Set(dirs)];
1077
+ }
1078
+ return [path.resolve(workspaceRoot)];
1079
+ }
1080
+
1081
+ function installNpmDependencies(checks, workspaceRoot) {
1082
+ const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
1083
+ const commandResults = [];
1084
+ let allOk = true;
1085
+ for (const cwd of dirs) {
1086
+ const result = runProcessSync({
1087
+ command: "npm",
1088
+ args: ["install"],
1089
+ cwd
1090
+ });
1091
+ commandResults.push({
1092
+ cwd,
1093
+ ok: result.ok,
1094
+ output: result.output || result.error_message || ""
1095
+ });
1096
+ if (!result.ok) allOk = false;
1097
+ }
1098
+ return {
1099
+ ok: allOk,
1100
+ action: "install_npm_dependencies",
1101
+ changed: true,
1102
+ command_results: commandResults,
1103
+ message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
1104
+ };
1105
+ }
1106
+
1107
+ function installPillowIfPossible() {
1108
+ const detected = detectPythonCommand();
1109
+ if (!detected.ok || !detected.command) {
1110
+ return {
1111
+ ok: false,
1112
+ action: "install_pillow",
1113
+ changed: false,
1114
+ message: "未检测到可用 python 命令,无法自动安装 Pillow。"
1115
+ };
1116
+ }
1117
+ const install = runProcessSync({
1118
+ command: detected.command,
1119
+ args: ["-m", "pip", "install", "pillow"]
1120
+ });
1121
+ return {
1122
+ ok: install.ok,
1123
+ action: "install_pillow",
1124
+ changed: install.ok,
1125
+ message: install.ok ? "Pillow 自动安装完成。" : `Pillow 自动安装失败:${install.output || install.error_message || "unknown"}`
1126
+ };
1127
+ }
1128
+
1129
+ export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
1130
+ const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
1131
+ const failed = collectFailedCheckKeys(checks);
1132
+ const actions = [];
1133
+
1134
+ if (
1135
+ failed.has("npm_dep_chrome_remote_interface_search")
1136
+ || failed.has("npm_dep_chrome_remote_interface_screen")
1137
+ || failed.has("npm_dep_ws")
1138
+ ) {
1139
+ if (!failed.has("node_cli")) {
1140
+ actions.push(installNpmDependencies(checks, workspaceRoot));
1141
+ } else {
1142
+ actions.push({
1143
+ ok: false,
1144
+ action: "install_npm_dependencies",
1145
+ changed: false,
1146
+ message: "Node 命令不可用,跳过 npm 自动安装。"
1147
+ });
1148
+ }
1149
+ }
1150
+
1151
+ if (failed.has("python_pillow")) {
1152
+ if (!failed.has("python_cli")) {
1153
+ actions.push(installPillowIfPossible());
1154
+ } else {
1155
+ actions.push({
1156
+ ok: false,
1157
+ action: "install_pillow",
1158
+ changed: false,
1159
+ message: "python 命令不可用,跳过 Pillow 自动安装。"
1160
+ });
1161
+ }
1162
+ }
1163
+
1164
+ const attempted = actions.length > 0;
1165
+ const nextPreflight = runPipelinePreflight(workspaceRoot);
1166
+ return {
1167
+ attempted,
1168
+ actions,
1169
+ preflight: nextPreflight
1170
+ };
1171
+ }
1172
+
1173
+ function sleep(ms) {
1174
+ return new Promise((resolve) => setTimeout(resolve, ms));
1175
+ }
1176
+
1177
+ async function listChromeTabs(port) {
1178
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`);
1179
+ if (!response.ok) {
1180
+ throw new Error(`DevTools endpoint returned ${response.status}`);
1181
+ }
1182
+ const data = await response.json();
1183
+ return Array.isArray(data) ? data : [];
1184
+ }
1185
+
1186
+ function buildBossPageState(payload) {
1187
+ return {
1188
+ key: "boss_page_state",
1189
+ ...payload
1190
+ };
1191
+ }
1192
+
1193
+ function extractSampleUrls(tabs, limit = 5) {
1194
+ return tabs
1195
+ .map((tab) => tab?.url)
1196
+ .filter(Boolean)
1197
+ .slice(0, limit);
1198
+ }
1199
+
1200
+ function findChromeOnboardingUrl(tabs) {
1201
+ for (const tab of tabs) {
1202
+ if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
1203
+ return tab.url;
1204
+ }
1205
+ }
1206
+ return null;
1207
+ }
1208
+
1209
+ function isBossLoginTab(tab) {
1210
+ const url = String(tab?.url || "");
1211
+ const title = String(tab?.title || "");
1212
+ return (
1213
+ url === bossLoginUrl
1214
+ || bossLoginUrlPattern.test(url)
1215
+ || bossLoginTitlePattern.test(title)
1216
+ );
1217
+ }
1218
+
1219
+ export async function inspectBossRecommendPageState(port, options = {}) {
1220
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1221
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
1222
+ const expectedUrl = options.expectedUrl || bossRecommendUrl;
1223
+ const deadline = Date.now() + timeoutMs;
1224
+ let lastError = null;
1225
+ let lastTabs = [];
1226
+
1227
+ while (Date.now() < deadline) {
1228
+ try {
1229
+ const tabs = await listChromeTabs(port);
1230
+ lastTabs = tabs;
1231
+ const exactTab = tabs.find(
1232
+ (tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
1233
+ );
1234
+ if (exactTab) {
1235
+ if (isBossLoginTab(exactTab)) {
1236
+ return buildBossPageState({
1237
+ ok: false,
1238
+ state: "LOGIN_REQUIRED",
1239
+ path: exactTab.url || bossLoginUrl,
1240
+ current_url: exactTab.url || bossLoginUrl,
1241
+ title: exactTab.title || null,
1242
+ requires_login: true,
1243
+ expected_url: expectedUrl,
1244
+ login_url: bossLoginUrl,
1245
+ message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
1246
+ });
1247
+ }
1248
+ return buildBossPageState({
1249
+ ok: true,
1250
+ state: "RECOMMEND_READY",
1251
+ path: exactTab.url,
1252
+ current_url: exactTab.url,
1253
+ title: exactTab.title || null,
1254
+ requires_login: false,
1255
+ message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
1256
+ });
1257
+ }
1258
+
1259
+ const loginTab = tabs.find((tab) => isBossLoginTab(tab));
1260
+ if (loginTab) {
1261
+ return buildBossPageState({
1262
+ ok: false,
1263
+ state: "LOGIN_REQUIRED",
1264
+ path: loginTab.url || bossLoginUrl,
1265
+ current_url: loginTab.url || bossLoginUrl,
1266
+ title: loginTab.title || null,
1267
+ requires_login: true,
1268
+ expected_url: expectedUrl,
1269
+ login_url: bossLoginUrl,
1270
+ message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1271
+ });
1272
+ }
1273
+
1274
+ const bossTab = tabs.find(
1275
+ (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
1276
+ );
1277
+ if (bossTab) {
1278
+ const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
1279
+ return buildBossPageState({
1280
+ ok: false,
1281
+ state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
1282
+ path: bossTab.url,
1283
+ current_url: bossTab.url,
1284
+ title: bossTab.title || null,
1285
+ requires_login: requiresLogin,
1286
+ expected_url: expectedUrl,
1287
+ message: requiresLogin
1288
+ ? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1289
+ : "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
1290
+ });
1291
+ }
1292
+ } catch (error) {
1293
+ lastError = error;
1294
+ }
1295
+
1296
+ await sleep(pollMs);
1297
+ }
1298
+
1299
+ if (lastError) {
1300
+ return buildBossPageState({
1301
+ ok: false,
1302
+ state: "DEBUG_PORT_UNREACHABLE",
1303
+ path: `http://127.0.0.1:${port}`,
1304
+ current_url: null,
1305
+ title: null,
1306
+ requires_login: false,
1307
+ expected_url: expectedUrl,
1308
+ message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
1309
+ error: lastError.message
1310
+ });
1311
+ }
1312
+
1313
+ const onboardingUrl = findChromeOnboardingUrl(lastTabs);
1314
+ if (onboardingUrl) {
1315
+ return buildBossPageState({
1316
+ ok: false,
1317
+ state: "CHROME_ONBOARDING_INTERCEPTED",
1318
+ path: onboardingUrl,
1319
+ current_url: onboardingUrl,
1320
+ title: null,
1321
+ requires_login: false,
1322
+ expected_url: expectedUrl,
1323
+ message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
1324
+ sample_urls: extractSampleUrls(lastTabs)
1325
+ });
1326
+ }
1327
+
1328
+ return buildBossPageState({
1329
+ ok: false,
1330
+ state: "BOSS_TAB_NOT_FOUND",
1331
+ path: expectedUrl,
1332
+ current_url: null,
1333
+ title: null,
1334
+ requires_login: false,
1335
+ expected_url,
1336
+ message: "未检测到 Boss 推荐页标签页。",
1337
+ sample_urls: extractSampleUrls(lastTabs)
1338
+ });
1339
+ }
1340
+
1341
+ async function openBossRecommendTab(port) {
1342
+ const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossRecommendUrl)}`;
1343
+ const attempts = ["PUT", "GET"];
1344
+ let lastError = null;
1345
+
1346
+ for (const method of attempts) {
1347
+ try {
1348
+ const response = await fetch(endpoint, { method });
1349
+ if (response.ok) {
1350
+ return { ok: true, method };
1351
+ }
1352
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
1353
+ } catch (error) {
1354
+ lastError = error;
1355
+ }
1356
+ }
1357
+
1358
+ return {
1359
+ ok: false,
1360
+ error: lastError?.message || "Failed to open Boss recommend tab via DevTools /json/new"
1361
+ };
1362
+ }
1363
+
1364
+ async function verifyRecommendPageStable(port, options = {}) {
1365
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
1366
+ const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
1367
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
1368
+
1369
+ await sleep(settleMs);
1370
+ const recheck = await inspectBossRecommendPageState(port, {
1371
+ timeoutMs: recheckTimeoutMs,
1372
+ pollMs
1373
+ });
1374
+ if (recheck.state === "RECOMMEND_READY") {
1375
+ return recheck;
1376
+ }
1377
+ if (recheck.state === "LOGIN_REQUIRED") {
1378
+ return buildBossPageState({
1379
+ ...recheck,
1380
+ state: "LOGIN_REQUIRED_AFTER_REDIRECT",
1381
+ message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
1382
+ });
1383
+ }
1384
+ return recheck;
105
1385
  }
106
1386
 
107
- function resolveWorkspaceConfigCandidates(workspaceRoot) {
108
- const root = path.resolve(String(workspaceRoot || process.cwd()));
109
- if (shouldIgnoreWorkspaceConfigRoot(root)) {
110
- return [];
111
- }
112
- const directPath = path.join(root, "config", "screening-config.json");
113
- const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
114
- const candidates = [directPath];
115
- if (path.basename(root).toLowerCase() !== "boss-recommend-mcp") {
116
- candidates.push(nestedPath);
117
- }
118
- return Array.from(new Set(candidates));
1387
+ function pickBossRecommendReloadTarget(tabs = []) {
1388
+ return tabs.find(
1389
+ (tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
1390
+ ) || tabs.find(
1391
+ (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
1392
+ ) || null;
119
1393
  }
120
1394
 
121
- function serializeDegreeSelection(value) {
122
- if (Array.isArray(value)) {
123
- const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
124
- return normalized.length ? normalized.join(",") : "不限";
1395
+ async function evaluateCdpExpression(client, expression) {
1396
+ const result = await client.Runtime.evaluate({
1397
+ expression,
1398
+ returnByValue: true,
1399
+ awaitPromise: true
1400
+ });
1401
+ if (result.exceptionDetails) {
1402
+ throw new Error(result.exceptionDetails.exception?.description || "Runtime.evaluate failed");
125
1403
  }
126
- const normalized = String(value || "").trim();
127
- return normalized || "不限";
1404
+ return result.result?.value;
128
1405
  }
129
1406
 
130
- function serializeSchoolTagSelection(value) {
131
- if (Array.isArray(value)) {
132
- const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
133
- if (!normalized.length) return "不限";
134
- if (normalized.includes("不限")) {
135
- return normalized.length === 1
136
- ? "不限"
137
- : normalized.filter((item) => item !== "不限").join(",");
1407
+ function buildRecommendRefreshStateExpression() {
1408
+ return `(() => {
1409
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1410
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1411
+ || document.querySelector('iframe');
1412
+ if (!frame || !frame.contentDocument) {
1413
+ return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
138
1414
  }
139
- return normalized.join(",");
140
- }
141
- const normalized = String(value || "").trim();
142
- return normalized || "不限";
143
- }
144
-
145
- function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
146
- const root = path.resolve(String(workspaceRoot || ""));
147
- const normalized = root.replace(/\\/g, "/").toLowerCase();
148
- return (
149
- normalized.includes("/appdata/local/npm-cache/_npx/")
150
- || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
151
- );
152
- }
153
-
154
- function buildScreenConfigCandidateMap(workspaceRoot) {
155
- return {
156
- env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
157
- ? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
158
- : null,
159
- workspace_paths: resolveWorkspaceConfigCandidates(workspaceRoot),
160
- user_path: getUserConfigPath(),
161
- legacy_path: getLegacyUserConfigPath()
162
- };
163
- }
164
-
165
- function resolveScreenConfigCandidates(workspaceRoot) {
166
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
167
- return [
168
- candidateMap.env_path,
169
- candidateMap.user_path,
170
- ...candidateMap.workspace_paths,
171
- candidateMap.legacy_path
172
- ].filter(Boolean);
173
- }
174
-
175
- function canWriteDirectory(targetDir) {
176
- try {
177
- ensureDir(targetDir);
178
- fs.accessSync(targetDir, fs.constants.W_OK);
179
- return true;
180
- } catch {
181
- return false;
182
- }
183
- }
184
-
185
- function resolveWritableScreenConfigPath(workspaceRoot) {
186
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
187
- const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
188
- if (candidateMap.env_path) {
189
- return candidateMap.env_path;
190
- }
191
- if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
192
- return candidateMap.user_path;
193
- }
194
- if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
195
- return workspacePreferred;
196
- }
197
- if (workspacePreferred) {
198
- return workspacePreferred;
199
- }
200
- return candidateMap.user_path || candidateMap.legacy_path;
201
- }
202
-
203
- function resolveScreenConfigPath(workspaceRoot) {
204
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
205
- if (candidateMap.env_path) {
206
- return candidateMap.env_path;
207
- }
208
- if (candidateMap.user_path && pathExists(candidateMap.user_path)) {
209
- return candidateMap.user_path;
210
- }
211
- const existingWorkspacePath = candidateMap.workspace_paths.find((item) => pathExists(item));
212
- if (existingWorkspacePath) {
213
- return existingWorkspacePath;
214
- }
215
- const writablePath = resolveWritableScreenConfigPath(workspaceRoot);
216
- if (writablePath) {
217
- return writablePath;
218
- }
219
- return candidateMap.legacy_path;
220
- }
221
-
222
- export function getScreenConfigResolution(workspaceRoot) {
223
- const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
224
- const candidate_paths = resolveScreenConfigCandidates(workspaceRoot);
225
- const resolved_path = resolveScreenConfigPath(workspaceRoot) || null;
226
- const workspace_root = path.resolve(String(workspaceRoot || process.cwd()));
227
- return {
228
- resolved_path,
229
- candidate_paths,
230
- workspace_root,
231
- workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
232
- workspace_ignored_for_config: shouldIgnoreWorkspaceConfigRoot(workspace_root),
233
- writable_path: resolveWritableScreenConfigPath(workspaceRoot),
234
- legacy_path: candidateMap.legacy_path
235
- };
236
- }
237
-
238
- function readJsonFile(filePath) {
239
- if (!filePath || !pathExists(filePath)) return null;
240
- try {
241
- const raw = fs.readFileSync(filePath, "utf8");
242
- return JSON.parse(raw);
243
- } catch {
244
- return null;
245
- }
246
- }
247
-
248
- function validateScreenConfig(config) {
249
- if (!config || typeof config !== "object" || Array.isArray(config)) {
250
- return {
251
- ok: false,
252
- reason: "INVALID_OR_MISSING_CONFIG",
253
- message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
254
- };
255
- }
256
- const baseUrl = String(config.baseUrl || "").trim();
257
- const apiKey = String(config.apiKey || "").trim();
258
- const model = String(config.model || "").trim();
259
- const missing = [];
260
- if (!baseUrl) missing.push("baseUrl");
261
- if (!apiKey) missing.push("apiKey");
262
- if (!model) missing.push("model");
263
- if (missing.length > 0) {
264
- return {
265
- ok: false,
266
- reason: "MISSING_REQUIRED_FIELDS",
267
- message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
268
- };
269
- }
270
- if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
271
- return {
272
- ok: false,
273
- reason: "PLACEHOLDER_API_KEY",
274
- message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
275
- };
276
- }
277
- if (
278
- baseUrl === screenConfigTemplateDefaults.baseUrl
279
- && apiKey === screenConfigTemplateDefaults.apiKey
280
- && model === screenConfigTemplateDefaults.model
281
- ) {
282
- return {
283
- ok: false,
284
- reason: "PLACEHOLDER_TEMPLATE_VALUES",
285
- message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
286
- };
287
- }
288
- return { ok: true, reason: "OK", message: "screening-config.json 校验通过。" };
289
- }
290
-
291
- function resolveWorkspaceDebugPort(workspaceRoot) {
292
- const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
293
- if (fromEnv) return fromEnv;
294
- const config = readJsonFile(resolveScreenConfigPath(workspaceRoot));
295
- return parsePositiveInteger(config?.debugPort) || 9222;
296
- }
297
-
298
- function getDefaultChromeExecutableCandidates() {
299
- const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
300
- if (process.platform === "win32") {
301
- candidates.push(
302
- path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
303
- path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
304
- path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
305
- );
306
- } else if (process.platform === "darwin") {
307
- candidates.push(
308
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
309
- path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"),
310
- "/Applications/Chromium.app/Contents/MacOS/Chromium"
311
- );
312
- } else {
313
- candidates.push(
314
- "/usr/bin/google-chrome",
315
- "/usr/bin/google-chrome-stable",
316
- "/usr/bin/chromium-browser",
317
- "/usr/bin/chromium",
318
- "/snap/bin/chromium"
319
- );
320
- }
321
- return Array.from(new Set(candidates.filter(Boolean)));
322
- }
323
-
324
- function getChromeExecutable() {
325
- const candidates = getDefaultChromeExecutableCandidates();
326
- return candidates.find((candidate) => pathExists(candidate)) || null;
327
- }
328
-
329
- function getChromeUserDataDir(port) {
330
- const profileDir = resolveDefaultChromeUserDataDir(port);
331
- ensureDir(profileDir);
332
- return profileDir;
333
- }
334
-
335
- function getSharedChromeUserDataDir(port) {
336
- return path.join(getCodexHome(), "boss-mcp", `chrome-profile-${port}`);
337
- }
338
-
339
- function getLegacyRecruitChromeUserDataDir(port) {
340
- return path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
341
- }
342
-
343
- function getLegacyRecommendChromeUserDataDir(port) {
344
- return path.join(getStateHome(), `chrome-profile-${port}`);
345
- }
346
-
347
- function resolveDefaultChromeUserDataDir(port) {
348
- const sharedPath = getSharedChromeUserDataDir(port);
349
- if (pathExists(sharedPath)) {
350
- return sharedPath;
351
- }
352
- const legacyPaths = [
353
- getLegacyRecruitChromeUserDataDir(port),
354
- getLegacyRecommendChromeUserDataDir(port)
355
- ];
356
- const legacyExisting = legacyPaths.find((candidate) => pathExists(candidate));
357
- return legacyExisting || sharedPath;
358
- }
359
-
360
- function launchChromeWithDebugPort(port) {
361
- const chromePath = getChromeExecutable();
362
- if (!chromePath) {
363
- return {
364
- ok: false,
365
- code: "CHROME_EXECUTABLE_NOT_FOUND",
366
- message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
1415
+ const doc = frame.contentDocument;
1416
+ const isVisible = (el) => {
1417
+ if (!el) return false;
1418
+ const win = doc.defaultView;
1419
+ if (!win) return el.offsetParent !== null;
1420
+ const style = win.getComputedStyle(el);
1421
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.02) {
1422
+ return false;
1423
+ }
1424
+ const rect = el.getBoundingClientRect();
1425
+ return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
367
1426
  };
368
- }
369
- const userDataDir = getChromeUserDataDir(port);
370
- const args = [
371
- `--remote-debugging-port=${port}`,
372
- `--user-data-dir=${userDataDir}`,
373
- "--no-first-run",
374
- "--no-default-browser-check",
375
- "--new-window",
376
- bossRecommendUrl
377
- ];
378
-
379
- try {
380
- const child = spawn(chromePath, args, {
381
- detached: true,
382
- stdio: "ignore",
383
- windowsHide: false
384
- });
385
- child.unref();
1427
+ const finishedWrap = Array.from(doc.querySelectorAll('.finished-wrap')).find((el) => isVisible(el)) || null;
1428
+ const refreshButton = Array.from(doc.querySelectorAll('.finished-wrap .btn.btn-refresh, .finished-wrap .btn-refresh, .no-data-refresh .btn-refresh'))
1429
+ .find((el) => isVisible(el)) || null;
1430
+ const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
1431
+ const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
1432
+ const finishedText = finishedWrap ? String(finishedWrap.textContent || '').replace(/\\s+/g, ' ').trim() : '';
1433
+ const buttonText = refreshButton ? String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() : '';
386
1434
  return {
387
1435
  ok: true,
388
- code: "CHROME_LAUNCHED",
389
- chrome_path: chromePath,
390
- user_data_dir: userDataDir
1436
+ frame_url: (() => {
1437
+ try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
1438
+ })(),
1439
+ finished_wrap_visible: Boolean(finishedWrap),
1440
+ finished_wrap_text: finishedText || null,
1441
+ refresh_button_visible: Boolean(refreshButton),
1442
+ refresh_button_text: buttonText || null,
1443
+ candidate_count: candidateCards.length,
1444
+ total_card_count: cards.length,
1445
+ list_ready: candidateCards.length > 0
391
1446
  };
392
- } catch (error) {
393
- return {
394
- ok: false,
395
- code: "CHROME_LAUNCH_FAILED",
396
- message: error.message || "Chrome 启动失败。"
397
- };
398
- }
1447
+ })()`;
399
1448
  }
400
1449
 
401
- function resolveRecommendSearchCliDir(workspaceRoot) {
402
- const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
403
- if (pathExists(localDir)) return localDir;
404
- const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-search-cli");
405
- if (pathExists(vendoredDir)) return vendoredDir;
406
- return null;
407
- }
408
-
409
- function resolveRecommendScreenCliDir(workspaceRoot) {
410
- const localDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
411
- if (pathExists(localDir)) return localDir;
412
- const vendoredDir = path.join(packagedMcpDir, "vendor", "boss-recommend-screen-cli");
413
- if (pathExists(vendoredDir)) return vendoredDir;
414
- return null;
415
- }
416
-
417
- function resolveRecommendScreenCliEntry(screenDir) {
418
- const candidates = [
419
- path.join(screenDir, "boss-recommend-screen-cli.cjs"),
420
- path.join(screenDir, "boss-recommend-screen-cli.js")
421
- ];
422
- return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
423
- }
424
-
425
- function resolveRecommendSearchCliEntry(searchDir) {
426
- const candidates = [
427
- path.join(searchDir, "src", "cli.js"),
428
- path.join(searchDir, "src", "cli.cjs")
429
- ];
430
- return candidates.find((candidate) => pathExists(candidate)) || candidates[0];
431
- }
432
-
433
- function safeInvokeCallback(callback, payload) {
434
- if (typeof callback !== "function") return;
435
- try {
436
- callback(payload);
437
- } catch {
438
- // Ignore callback errors to keep pipeline runtime stable.
439
- }
440
- }
441
-
442
- function runProcess({
443
- command,
444
- args,
445
- cwd,
446
- timeoutMs,
447
- onOutput,
448
- onLine,
449
- onHeartbeat,
450
- heartbeatIntervalMs = 10_000,
451
- signal
452
- }) {
453
- return new Promise((resolve) => {
454
- let stdout = "";
455
- let stderr = "";
456
- let stdoutLineBuffer = "";
457
- let stderrLineBuffer = "";
458
- let settled = false;
459
- let timer = null;
460
- let heartbeatTimer = null;
461
- let abortedBySignal = Boolean(signal?.aborted);
462
- let abortListener = null;
463
-
464
- function notifyHeartbeat(source) {
465
- safeInvokeCallback(onHeartbeat, {
466
- source,
467
- command,
468
- args,
469
- cwd,
470
- at: new Date().toISOString()
471
- });
1450
+ function buildRecommendRefreshClickExpression() {
1451
+ return `(() => {
1452
+ const frame = document.querySelector('iframe[name="recommendFrame"]')
1453
+ || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1454
+ || document.querySelector('iframe');
1455
+ if (!frame || !frame.contentDocument) {
1456
+ return { ok: false, state: 'NO_RECOMMEND_IFRAME' };
472
1457
  }
473
-
474
- function emitLine(stream, line) {
475
- const normalized = String(line ?? "").replace(/\r$/, "");
476
- if (!normalized) return;
477
- safeInvokeCallback(onLine, {
478
- stream,
479
- line: normalized,
480
- at: new Date().toISOString()
481
- });
482
- }
483
-
484
- function pushLineBuffer(stream, chunkText) {
485
- if (stream === "stdout") {
486
- stdoutLineBuffer += chunkText;
487
- } else {
488
- stderrLineBuffer += chunkText;
1458
+ const doc = frame.contentDocument;
1459
+ const isVisible = (el) => {
1460
+ if (!el) return false;
1461
+ const win = doc.defaultView;
1462
+ if (!win) return el.offsetParent !== null;
1463
+ const style = win.getComputedStyle(el);
1464
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.02) {
1465
+ return false;
489
1466
  }
490
- let buffer = stream === "stdout" ? stdoutLineBuffer : stderrLineBuffer;
491
- let newlineIndex = buffer.indexOf("\n");
492
- while (newlineIndex !== -1) {
493
- emitLine(stream, buffer.slice(0, newlineIndex));
494
- buffer = buffer.slice(newlineIndex + 1);
495
- newlineIndex = buffer.indexOf("\n");
496
- }
497
- if (stream === "stdout") {
498
- stdoutLineBuffer = buffer;
499
- } else {
500
- stderrLineBuffer = buffer;
501
- }
502
- }
503
-
504
- function finish(payload) {
505
- if (settled) return;
506
- settled = true;
507
- if (timer) clearTimeout(timer);
508
- if (heartbeatTimer) clearInterval(heartbeatTimer);
509
- if (signal && typeof signal.removeEventListener === "function" && abortListener) {
510
- signal.removeEventListener("abort", abortListener);
511
- }
512
- emitLine("stdout", stdoutLineBuffer);
513
- emitLine("stderr", stderrLineBuffer);
514
- stdoutLineBuffer = "";
515
- stderrLineBuffer = "";
516
- resolve(payload);
517
- }
518
-
519
- if (abortedBySignal) {
520
- finish({
521
- code: -1,
522
- stdout,
523
- stderr: "Process aborted before spawn",
524
- error_code: "ABORTED"
525
- });
526
- return;
1467
+ const rect = el.getBoundingClientRect();
1468
+ return rect.width > 2 && rect.height > 2 && el.offsetParent !== null;
1469
+ };
1470
+ const refreshButton = Array.from(doc.querySelectorAll('.finished-wrap .btn.btn-refresh, .finished-wrap .btn-refresh, .no-data-refresh .btn-refresh'))
1471
+ .find((el) => isVisible(el)) || null;
1472
+ if (!refreshButton) {
1473
+ return { ok: false, state: 'REFRESH_BUTTON_NOT_FOUND' };
527
1474
  }
528
-
529
- let child;
530
1475
  try {
531
- child = spawn(command, args, {
532
- cwd,
533
- windowsHide: true,
534
- shell: false,
535
- env: process.env
536
- });
1476
+ refreshButton.click();
1477
+ return {
1478
+ ok: true,
1479
+ state: 'REFRESH_BUTTON_CLICKED',
1480
+ refresh_button_text: String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() || null
1481
+ };
537
1482
  } catch (error) {
538
- finish({
539
- code: -1,
540
- stdout,
541
- stderr: error.message,
542
- error_code: error.code || "SPAWN_FAILED"
543
- });
544
- return;
545
- }
546
-
547
- if (signal && typeof signal.addEventListener === "function") {
548
- abortListener = () => {
549
- abortedBySignal = true;
550
- try {
551
- child.kill();
552
- } catch {}
1483
+ return {
1484
+ ok: false,
1485
+ state: 'REFRESH_BUTTON_CLICK_FAILED',
1486
+ message: error?.message || String(error)
553
1487
  };
554
- signal.addEventListener("abort", abortListener, { once: true });
555
- }
556
-
557
- if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
558
- timer = setTimeout(() => {
559
- try {
560
- child.kill();
561
- } catch {}
562
- finish({
563
- code: -1,
564
- stdout,
565
- stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`.trim(),
566
- error_code: "TIMEOUT"
567
- });
568
- }, timeoutMs);
569
1488
  }
570
-
571
- if (Number.isFinite(heartbeatIntervalMs) && heartbeatIntervalMs > 0) {
572
- heartbeatTimer = setInterval(() => {
573
- notifyHeartbeat("timer");
574
- }, heartbeatIntervalMs);
575
- }
576
-
577
- child.stdout.on("data", (chunk) => {
578
- const text = chunk.toString();
579
- stdout += text;
580
- pushLineBuffer("stdout", text);
581
- safeInvokeCallback(onOutput, {
582
- stream: "stdout",
583
- text,
584
- at: new Date().toISOString()
585
- });
586
- notifyHeartbeat("stdout");
587
- });
588
- child.stderr.on("data", (chunk) => {
589
- const text = chunk.toString();
590
- stderr += text;
591
- pushLineBuffer("stderr", text);
592
- safeInvokeCallback(onOutput, {
593
- stream: "stderr",
594
- text,
595
- at: new Date().toISOString()
596
- });
597
- notifyHeartbeat("stderr");
598
- });
599
- child.on("close", (code) => {
600
- if (abortedBySignal) {
601
- finish({
602
- code: -1,
603
- stdout,
604
- stderr: `${stderr}\nProcess aborted by signal`.trim(),
605
- error_code: "ABORTED"
606
- });
607
- return;
608
- }
609
- finish({ code, stdout, stderr });
610
- });
611
- child.on("error", (error) => {
612
- finish({
613
- code: -1,
614
- stdout,
615
- stderr: `${stderr}\n${error.message}`.trim(),
616
- error_code: error.code || "SPAWN_FAILED"
617
- });
618
- });
619
- });
620
- }
621
-
622
- function runProcessSync({ command, args, cwd }) {
623
- try {
624
- const result = spawnSync(command, args, {
625
- cwd,
626
- windowsHide: true,
627
- shell: false,
628
- env: process.env,
629
- encoding: "utf8"
630
- });
631
- const stdout = String(result.stdout || "").trim();
632
- const stderr = String(result.stderr || "").trim();
633
- return {
634
- ok: result.status === 0,
635
- status: result.status,
636
- stdout,
637
- stderr,
638
- output: [stdout, stderr].filter(Boolean).join("\n").trim(),
639
- error_code: result.error?.code || null,
640
- error_message: result.error?.message || null
641
- };
642
- } catch (error) {
643
- return {
644
- ok: false,
645
- status: -1,
646
- stdout: "",
647
- stderr: "",
648
- output: "",
649
- error_code: error.code || "SPAWN_FAILED",
650
- error_message: error.message || String(error)
651
- };
652
- }
653
- }
654
-
655
- function parseMajorVersion(raw) {
656
- const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
657
- if (!match) return null;
658
- const major = Number.parseInt(match[1], 10);
659
- return Number.isFinite(major) ? major : null;
660
- }
661
-
662
- function buildNodeCommandCheck() {
663
- const probe = runProcessSync({
664
- command: "node",
665
- args: ["--version"]
666
- });
667
- const major = parseMajorVersion(probe.output);
668
- const versionOk = Number.isInteger(major) && major >= 18;
669
- return {
670
- key: "node_cli",
671
- ok: probe.ok && versionOk,
672
- path: "node --version",
673
- message: probe.ok
674
- ? (versionOk
675
- ? `Node 命令可用 (${probe.output || "unknown version"})`
676
- : `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
677
- : `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
678
- };
1489
+ })()`;
679
1490
  }
680
1491
 
681
- function detectPythonCommand() {
682
- const python = runProcessSync({
683
- command: "python",
684
- args: ["--version"]
685
- });
686
- if (python.ok) {
687
- return {
688
- ok: true,
689
- command: "python",
690
- probe: python
691
- };
692
- }
693
- const python3 = runProcessSync({
694
- command: "python3",
695
- args: ["--version"]
696
- });
697
- if (python3.ok) {
698
- return {
699
- ok: false,
700
- command: null,
701
- probe: python,
702
- fallback: python3
703
- };
704
- }
705
- return {
706
- ok: false,
707
- command: null,
708
- probe: python,
709
- fallback: null
710
- };
711
- }
712
-
713
- function buildPythonCommandCheck() {
714
- const detected = detectPythonCommand();
715
- if (detected.ok) {
716
- return {
717
- key: "python_cli",
718
- ok: true,
719
- path: "python --version",
720
- message: `Python 命令可用 (${detected.probe.output || "unknown version"})`
721
- };
722
- }
723
- if (detected.fallback) {
724
- return {
725
- key: "python_cli",
726
- ok: false,
727
- path: "python --version",
728
- message: `检测到 ${detected.fallback.output || "python3"},但当前流程依赖 python 命令;请创建 python 别名后重试。`
729
- };
730
- }
731
- return {
732
- key: "python_cli",
733
- ok: false,
734
- path: "python --version",
735
- message: "未找到 python 命令,请安装 Python 并确保 python 在 PATH 中。"
736
- };
737
- }
738
-
739
- function buildPillowCheck() {
740
- const detected = detectPythonCommand();
741
- if (!detected.ok || !detected.command) {
742
- return {
743
- key: "python_pillow",
744
- ok: false,
745
- path: "python -c \"import PIL\"",
746
- message: "无法校验 Pillow:python 命令不可用。"
747
- };
748
- }
749
- const probe = runProcessSync({
750
- command: detected.command,
751
- args: ["-c", "import PIL, PIL.Image; print(PIL.__version__)"]
752
- });
753
- return {
754
- key: "python_pillow",
755
- ok: probe.ok,
756
- path: `${detected.command} -c "import PIL"`,
757
- message: probe.ok
758
- ? `Pillow 可用 (${probe.output || "version unknown"})`
759
- : "Pillow 未安装。请执行 `python -m pip install pillow`。"
760
- };
761
- }
762
-
763
- function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
764
- if (!cwd || !pathExists(cwd)) {
765
- return {
766
- key,
767
- ok: false,
768
- path: moduleName,
769
- module: moduleName,
770
- install_cwd: null,
771
- message: missingMessage
772
- };
773
- }
774
- const probe = runProcessSync({
775
- command: "node",
776
- args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
777
- cwd
778
- });
779
- return {
780
- key,
781
- ok: probe.ok,
782
- path: moduleName,
783
- module: moduleName,
784
- install_cwd: cwd,
785
- message: probe.ok
786
- ? `${moduleName} npm 依赖可用`
787
- : `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
788
- };
789
- }
790
-
791
- function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
792
- return [
793
- buildNodeCommandCheck(),
794
- buildPythonCommandCheck(),
795
- buildPillowCheck(),
796
- buildNodePackageCheck({
797
- key: "npm_dep_chrome_remote_interface_search",
798
- moduleName: "chrome-remote-interface",
799
- cwd: searchDir,
800
- missingMessage: "无法校验 chrome-remote-interface:boss-recommend-search-cli 目录不存在。"
801
- }),
802
- buildNodePackageCheck({
803
- key: "npm_dep_chrome_remote_interface_screen",
804
- moduleName: "chrome-remote-interface",
805
- cwd: screenDir,
806
- missingMessage: "无法校验 chrome-remote-interface:boss-recommend-screen-cli 目录不存在。"
807
- }),
808
- buildNodePackageCheck({
809
- key: "npm_dep_ws",
810
- moduleName: "ws",
811
- cwd: screenDir,
812
- missingMessage: "无法校验 ws:boss-recommend-screen-cli 目录不存在。"
813
- })
814
- ];
815
- }
1492
+ export async function refreshBossRecommendList(workspaceRoot, options = {}) {
1493
+ const debugPort = Number.isFinite(options.port)
1494
+ ? options.port
1495
+ : resolveWorkspaceDebugPort(workspaceRoot);
1496
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
1497
+ const reloadTimeoutMs = Number.isFinite(options.reloadTimeoutMs) ? options.reloadTimeoutMs : 10000;
816
1498
 
817
- function parseJsonOutput(text) {
818
- const trimmed = String(text || "").trim();
819
- if (!trimmed) return null;
1499
+ let client = null;
820
1500
  try {
821
- return JSON.parse(trimmed);
822
- } catch {}
823
- const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
824
- for (let index = lines.length - 1; index >= 0; index -= 1) {
825
- try {
826
- return JSON.parse(lines[index]);
827
- } catch {
828
- continue;
829
- }
830
- }
831
- return null;
832
- }
833
-
834
- function createScreenProgressTracker(currentTracker = {}) {
835
- const outcome = String(currentTracker.outcome || "").trim();
836
- return {
837
- candidate_index: Number.isInteger(currentTracker.candidate_index) ? currentTracker.candidate_index : null,
838
- outcome: outcome === "pass" || outcome === "skip" ? outcome : null,
839
- action_failed: currentTracker.action_failed === true
840
- };
841
- }
842
-
843
- function finalizeCandidateProgress(progress, tracker) {
844
- if (!Number.isInteger(tracker.candidate_index)) {
845
- return false;
846
- }
847
-
848
- let changed = false;
849
- if (tracker.action_failed === true) {
850
- progress.skipped += 1;
851
- changed = true;
852
- } else if (tracker.outcome === "pass") {
853
- progress.passed += 1;
854
- changed = true;
855
- } else if (tracker.outcome === "skip") {
856
- progress.skipped += 1;
857
- changed = true;
858
- }
859
-
860
- tracker.candidate_index = null;
861
- tracker.outcome = null;
862
- tracker.action_failed = false;
863
- return changed;
864
- }
865
-
866
- function parseScreenProgressLine(line, currentProgress = {}, currentTracker = {}) {
867
- const normalizedLine = String(line || "").replace(/\s+/g, " ").trim();
868
- if (!normalizedLine) return null;
869
-
870
- const nextProgress = {
871
- processed: Number.isInteger(currentProgress.processed) ? currentProgress.processed : 0,
872
- passed: Number.isInteger(currentProgress.passed) ? currentProgress.passed : 0,
873
- skipped: Number.isInteger(currentProgress.skipped) ? currentProgress.skipped : 0,
874
- greet_count: Number.isInteger(currentProgress.greet_count) ? currentProgress.greet_count : 0
875
- };
876
- const nextTracker = createScreenProgressTracker(currentTracker);
877
-
878
- let changed = false;
879
- const processedMatch = normalizedLine.match(/处理第\s*(\d+)\s*位候选人/u);
880
- if (processedMatch) {
881
- if (finalizeCandidateProgress(nextProgress, nextTracker)) {
882
- changed = true;
883
- }
884
- const processed = Number.parseInt(processedMatch[1], 10);
885
- if (Number.isInteger(processed) && processed >= 0 && processed !== nextProgress.processed) {
886
- nextProgress.processed = processed;
887
- changed = true;
1501
+ const tabs = await listChromeTabs(debugPort);
1502
+ const target = pickBossRecommendReloadTarget(tabs);
1503
+ if (!target) {
1504
+ return {
1505
+ ok: false,
1506
+ action: "in_page_refresh",
1507
+ debug_port: debugPort,
1508
+ state: "BOSS_TAB_NOT_FOUND",
1509
+ message: "未找到可操作的 Boss recommend 标签页。",
1510
+ before_state: null,
1511
+ after_state: null
1512
+ };
888
1513
  }
889
- nextTracker.candidate_index = processed;
890
- nextTracker.outcome = null;
891
- nextTracker.action_failed = false;
892
- }
893
1514
 
894
- if (/筛选结果:\s*通过/u.test(normalizedLine)) {
895
- if (nextTracker.outcome !== "pass" || nextTracker.action_failed) {
896
- changed = true;
1515
+ client = await CDP({ port: debugPort, target });
1516
+ const { Page, Runtime } = client;
1517
+ if (Runtime && typeof Runtime.enable === "function") {
1518
+ await Runtime.enable();
897
1519
  }
898
- nextTracker.outcome = "pass";
899
- nextTracker.action_failed = false;
900
- } else if (/筛选结果:\s*不通过/u.test(normalizedLine)) {
901
- if (nextTracker.outcome !== "skip" || nextTracker.action_failed) {
902
- changed = true;
1520
+ if (Page && typeof Page.enable === "function") {
1521
+ await Page.enable();
903
1522
  }
904
- nextTracker.outcome = "skip";
905
- nextTracker.action_failed = false;
906
- }
907
-
908
- if (/候选人处理失败\s*:/u.test(normalizedLine)) {
909
- if (!nextTracker.action_failed) {
910
- changed = true;
1523
+ if (Page && typeof Page.bringToFront === "function") {
1524
+ await Page.bringToFront();
911
1525
  }
912
- nextTracker.action_failed = true;
913
- }
914
-
915
- if (/^\[关闭详情\].*成功/u.test(normalizedLine)) {
916
- if (finalizeCandidateProgress(nextProgress, nextTracker)) {
917
- changed = true;
918
- }
919
- }
920
-
921
- const finalStateLine = /Process timed out after|status"\s*:\s*"(?:COMPLETED|PAUSED|FAILED)"/iu.test(normalizedLine);
922
- if (finalStateLine) {
923
- if (finalizeCandidateProgress(nextProgress, nextTracker)) {
924
- changed = true;
925
- }
926
- }
927
-
928
- const greetMatch = normalizedLine.match(/greet[_\s-]*count\s*[:=]\s*(\d+)/iu);
929
- if (greetMatch) {
930
- const greetCount = Number.parseInt(greetMatch[1], 10);
931
- if (Number.isInteger(greetCount) && greetCount >= 0 && greetCount !== nextProgress.greet_count) {
932
- nextProgress.greet_count = greetCount;
933
- changed = true;
934
- }
935
- }
936
-
937
- if (!changed) return null;
938
- return {
939
- line: normalizedLine,
940
- progress: nextProgress,
941
- tracker: nextTracker
942
- };
943
- }
944
-
945
- function resolveRecommendScreenTimeoutMs(runtime = null) {
946
- const runtimeTimeoutMs = parsePositiveInteger(runtime?.timeoutMs);
947
- const envTimeoutMs = parsePositiveInteger(process.env.BOSS_RECOMMEND_SCREEN_TIMEOUT_MS);
948
- return runtimeTimeoutMs || envTimeoutMs || DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS;
949
- }
950
-
951
- function buildRecommendScreenProcessError(result, screenTimeoutMs) {
952
- if (result.code === 0) return null;
953
- if (result.error_code === "TIMEOUT") {
954
- return {
955
- code: "TIMEOUT",
956
- message: `推荐页筛选命令执行超时(${screenTimeoutMs}ms)。`
957
- };
958
- }
959
- if (result.error_code === "ABORTED") {
960
- return {
961
- code: "PROCESS_ABORTED",
962
- message: "推荐页筛选命令已取消。"
963
- };
964
- }
965
- return {
966
- code: "RECOMMEND_SCREEN_FAILED",
967
- message: "推荐页筛选命令执行失败。"
968
- };
969
- }
970
-
971
- function loadScreenConfig(configPath) {
972
- const parsed = readJsonFile(configPath);
973
- const validation = validateScreenConfig(parsed);
974
- if (!validation.ok) {
975
- return {
976
- ok: false,
977
- error: `${validation.message} (path: ${configPath})`
978
- };
979
- }
980
- return { ok: true, config: parsed };
981
- }
982
-
983
- function localDirHint(workspaceRoot, dirName) {
984
- return path.join(workspaceRoot, dirName);
985
- }
986
-
987
- export function runPipelinePreflight(workspaceRoot) {
988
- const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
989
- const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
990
- const searchDirExists = Boolean(searchDir && pathExists(searchDir));
991
- const searchEntryPath = searchDir
992
- ? resolveRecommendSearchCliEntry(searchDir)
993
- : path.join(localDirHint(workspaceRoot, "boss-recommend-search-cli"), "src", "cli.js");
994
- const searchEntryExists = Boolean(searchDir && pathExists(searchEntryPath));
995
- const screenDirExists = Boolean(screenDir && pathExists(screenDir));
996
- const screenEntryPath = screenDir
997
- ? resolveRecommendScreenCliEntry(screenDir)
998
- : path.join(localDirHint(workspaceRoot, "boss-recommend-screen-cli"), "boss-recommend-screen-cli.cjs");
999
- const screenEntryExists = Boolean(screenDir && pathExists(screenEntryPath));
1000
- const configResolution = getScreenConfigResolution(workspaceRoot);
1001
- const screenConfigPath = configResolution.resolved_path;
1002
- const screenConfigParsed = readJsonFile(screenConfigPath);
1003
- const screenConfigValidation = validateScreenConfig(screenConfigParsed);
1004
- const checks = [
1005
- {
1006
- key: "recommend_search_cli_dir",
1007
- ok: searchDirExists,
1008
- path: searchDir || localDirHint(workspaceRoot, "boss-recommend-search-cli"),
1009
- message: searchDirExists
1010
- ? "boss-recommend-search-cli 目录可用"
1011
- : "boss-recommend-search-cli 目录不存在"
1012
- },
1013
- {
1014
- key: "recommend_search_cli_entry",
1015
- ok: searchEntryExists,
1016
- path: searchEntryPath,
1017
- message: searchEntryExists
1018
- ? "boss-recommend-search-cli 入口文件可用"
1019
- : "boss-recommend-search-cli 入口文件缺失"
1020
- },
1021
- {
1022
- key: "recommend_screen_cli_dir",
1023
- ok: screenDirExists,
1024
- path: screenDir || localDirHint(workspaceRoot, "boss-recommend-screen-cli"),
1025
- message: screenDirExists
1026
- ? "boss-recommend-screen-cli 目录可用"
1027
- : "boss-recommend-screen-cli 目录不存在"
1028
- },
1029
- {
1030
- key: "recommend_screen_cli_entry",
1031
- ok: screenEntryExists,
1032
- path: screenEntryPath,
1033
- message: screenEntryExists
1034
- ? "boss-recommend-screen-cli 入口文件可用"
1035
- : "boss-recommend-screen-cli 入口文件缺失"
1036
- },
1037
- {
1038
- key: "screen_config",
1039
- ok: screenConfigValidation.ok,
1040
- path: screenConfigPath,
1041
- reason: screenConfigValidation.reason || null,
1042
- message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
1043
- }
1044
- ];
1045
- checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
1046
-
1047
- return {
1048
- ok: checks.every((item) => item.ok),
1049
- checks,
1050
- debug_port: resolveWorkspaceDebugPort(workspaceRoot),
1051
- config_resolution: configResolution
1052
- };
1053
- }
1054
-
1055
- function collectFailedCheckKeys(checks = []) {
1056
- return new Set(
1057
- checks
1058
- .filter((item) => item && item.ok === false && typeof item.key === "string")
1059
- .map((item) => item.key)
1060
- );
1061
- }
1062
-
1063
- function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
1064
- const npmKeys = new Set([
1065
- "npm_dep_chrome_remote_interface_search",
1066
- "npm_dep_chrome_remote_interface_screen",
1067
- "npm_dep_ws"
1068
- ]);
1069
- const dirs = checks
1070
- .filter((item) => item && item.ok === false && npmKeys.has(item.key))
1071
- .map((item) => item.install_cwd)
1072
- .filter((item) => typeof item === "string" && item.trim())
1073
- .map((item) => path.resolve(item));
1074
- if (dirs.length > 0) {
1075
- return [...new Set(dirs)];
1076
- }
1077
- return [path.resolve(workspaceRoot)];
1078
- }
1079
-
1080
- function installNpmDependencies(checks, workspaceRoot) {
1081
- const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
1082
- const commandResults = [];
1083
- let allOk = true;
1084
- for (const cwd of dirs) {
1085
- const result = runProcessSync({
1086
- command: "npm",
1087
- args: ["install"],
1088
- cwd
1089
- });
1090
- commandResults.push({
1091
- cwd,
1092
- ok: result.ok,
1093
- output: result.output || result.error_message || ""
1094
- });
1095
- if (!result.ok) allOk = false;
1096
- }
1097
- return {
1098
- ok: allOk,
1099
- action: "install_npm_dependencies",
1100
- changed: true,
1101
- command_results: commandResults,
1102
- message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
1103
- };
1104
- }
1105
-
1106
- function installPillowIfPossible() {
1107
- const detected = detectPythonCommand();
1108
- if (!detected.ok || !detected.command) {
1109
- return {
1110
- ok: false,
1111
- action: "install_pillow",
1112
- changed: false,
1113
- message: "未检测到可用 python 命令,无法自动安装 Pillow。"
1114
- };
1115
- }
1116
- const install = runProcessSync({
1117
- command: detected.command,
1118
- args: ["-m", "pip", "install", "pillow"]
1119
- });
1120
- return {
1121
- ok: install.ok,
1122
- action: "install_pillow",
1123
- changed: install.ok,
1124
- message: install.ok ? "Pillow 自动安装完成。" : `Pillow 自动安装失败:${install.output || install.error_message || "unknown"}`
1125
- };
1126
- }
1127
-
1128
- export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
1129
- const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
1130
- const failed = collectFailedCheckKeys(checks);
1131
- const actions = [];
1132
1526
 
1133
- if (
1134
- failed.has("npm_dep_chrome_remote_interface_search")
1135
- || failed.has("npm_dep_chrome_remote_interface_screen")
1136
- || failed.has("npm_dep_ws")
1137
- ) {
1138
- if (!failed.has("node_cli")) {
1139
- actions.push(installNpmDependencies(checks, workspaceRoot));
1140
- } else {
1141
- actions.push({
1527
+ const beforeState = await evaluateCdpExpression(client, buildRecommendRefreshStateExpression());
1528
+ if (!beforeState?.ok) {
1529
+ return {
1142
1530
  ok: false,
1143
- action: "install_npm_dependencies",
1144
- changed: false,
1145
- message: "Node 命令不可用,跳过 npm 自动安装。"
1146
- });
1531
+ action: "in_page_refresh",
1532
+ debug_port: debugPort,
1533
+ state: beforeState?.error || "NO_RECOMMEND_IFRAME",
1534
+ message: "未能读取 recommend iframe,无法执行页内刷新。",
1535
+ before_state: beforeState || null,
1536
+ after_state: null
1537
+ };
1147
1538
  }
1148
- }
1149
-
1150
- if (failed.has("python_pillow")) {
1151
- if (!failed.has("python_cli")) {
1152
- actions.push(installPillowIfPossible());
1153
- } else {
1154
- actions.push({
1539
+ if (!beforeState.refresh_button_visible) {
1540
+ return {
1155
1541
  ok: false,
1156
- action: "install_pillow",
1157
- changed: false,
1158
- message: "python 命令不可用,跳过 Pillow 自动安装。"
1159
- });
1542
+ action: "in_page_refresh",
1543
+ debug_port: debugPort,
1544
+ state: "REFRESH_BUTTON_NOT_FOUND",
1545
+ message: "推荐列表到底后未发现可点击的刷新按钮。",
1546
+ before_state: beforeState,
1547
+ after_state: beforeState
1548
+ };
1160
1549
  }
1161
- }
1162
-
1163
- const attempted = actions.length > 0;
1164
- const nextPreflight = runPipelinePreflight(workspaceRoot);
1165
- return {
1166
- attempted,
1167
- actions,
1168
- preflight: nextPreflight
1169
- };
1170
- }
1171
-
1172
- function sleep(ms) {
1173
- return new Promise((resolve) => setTimeout(resolve, ms));
1174
- }
1175
1550
 
1176
- async function listChromeTabs(port) {
1177
- const response = await fetch(`http://127.0.0.1:${port}/json/list`);
1178
- if (!response.ok) {
1179
- throw new Error(`DevTools endpoint returned ${response.status}`);
1180
- }
1181
- const data = await response.json();
1182
- return Array.isArray(data) ? data : [];
1183
- }
1184
-
1185
- function buildBossPageState(payload) {
1186
- return {
1187
- key: "boss_page_state",
1188
- ...payload
1189
- };
1190
- }
1191
-
1192
- function extractSampleUrls(tabs, limit = 5) {
1193
- return tabs
1194
- .map((tab) => tab?.url)
1195
- .filter(Boolean)
1196
- .slice(0, limit);
1197
- }
1198
-
1199
- function findChromeOnboardingUrl(tabs) {
1200
- for (const tab of tabs) {
1201
- if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
1202
- return tab.url;
1551
+ const clickResult = await evaluateCdpExpression(client, buildRecommendRefreshClickExpression());
1552
+ if (!clickResult?.ok) {
1553
+ return {
1554
+ ok: false,
1555
+ action: "in_page_refresh",
1556
+ debug_port: debugPort,
1557
+ state: clickResult?.state || "REFRESH_BUTTON_CLICK_FAILED",
1558
+ message: clickResult?.message || "页内刷新按钮点击失败。",
1559
+ before_state: beforeState,
1560
+ after_state: null
1561
+ };
1203
1562
  }
1204
- }
1205
- return null;
1206
- }
1207
1563
 
1208
- function isBossLoginTab(tab) {
1209
- const url = String(tab?.url || "");
1210
- const title = String(tab?.title || "");
1211
- return (
1212
- url === bossLoginUrl
1213
- || bossLoginUrlPattern.test(url)
1214
- || bossLoginTitlePattern.test(title)
1215
- );
1216
- }
1217
-
1218
- export async function inspectBossRecommendPageState(port, options = {}) {
1219
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1220
- const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
1221
- const expectedUrl = options.expectedUrl || bossRecommendUrl;
1222
- const deadline = Date.now() + timeoutMs;
1223
- let lastError = null;
1224
- let lastTabs = [];
1225
-
1226
- while (Date.now() < deadline) {
1227
- try {
1228
- const tabs = await listChromeTabs(port);
1229
- lastTabs = tabs;
1230
- const exactTab = tabs.find(
1231
- (tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
1232
- );
1233
- if (exactTab) {
1234
- if (isBossLoginTab(exactTab)) {
1235
- return buildBossPageState({
1236
- ok: false,
1237
- state: "LOGIN_REQUIRED",
1238
- path: exactTab.url || bossLoginUrl,
1239
- current_url: exactTab.url || bossLoginUrl,
1240
- title: exactTab.title || null,
1241
- requires_login: true,
1242
- expected_url: expectedUrl,
1243
- login_url: bossLoginUrl,
1244
- message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
1245
- });
1246
- }
1247
- return buildBossPageState({
1564
+ const deadline = Date.now() + reloadTimeoutMs;
1565
+ let lastState = beforeState;
1566
+ while (Date.now() < deadline) {
1567
+ await sleep(pollMs);
1568
+ lastState = await evaluateCdpExpression(client, buildRecommendRefreshStateExpression());
1569
+ if (lastState?.ok && lastState.finished_wrap_visible === false && lastState.list_ready === true) {
1570
+ return {
1248
1571
  ok: true,
1572
+ action: "in_page_refresh",
1573
+ debug_port: debugPort,
1249
1574
  state: "RECOMMEND_READY",
1250
- path: exactTab.url,
1251
- current_url: exactTab.url,
1252
- title: exactTab.title || null,
1253
- requires_login: false,
1254
- message: "Boss 推荐页已打开,且当前仍停留在 recommend 页面。"
1255
- });
1256
- }
1257
-
1258
- const loginTab = tabs.find((tab) => isBossLoginTab(tab));
1259
- if (loginTab) {
1260
- return buildBossPageState({
1261
- ok: false,
1262
- state: "LOGIN_REQUIRED",
1263
- path: loginTab.url || bossLoginUrl,
1264
- current_url: loginTab.url || bossLoginUrl,
1265
- title: loginTab.title || null,
1266
- requires_login: true,
1267
- expected_url: expectedUrl,
1268
- login_url: bossLoginUrl,
1269
- message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1270
- });
1271
- }
1272
-
1273
- const bossTab = tabs.find(
1274
- (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
1275
- );
1276
- if (bossTab) {
1277
- const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
1278
- return buildBossPageState({
1279
- ok: false,
1280
- state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
1281
- path: bossTab.url,
1282
- current_url: bossTab.url,
1283
- title: bossTab.title || null,
1284
- requires_login: requiresLogin,
1285
- expected_url: expectedUrl,
1286
- message: requiresLogin
1287
- ? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1288
- : "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
1289
- });
1575
+ message: "已点击页内刷新按钮并重新拿到候选人列表。",
1576
+ before_state: beforeState,
1577
+ after_state: lastState
1578
+ };
1290
1579
  }
1291
- } catch (error) {
1292
- lastError = error;
1293
1580
  }
1294
1581
 
1295
- await sleep(pollMs);
1296
- }
1297
-
1298
- if (lastError) {
1299
- return buildBossPageState({
1582
+ return {
1300
1583
  ok: false,
1301
- state: "DEBUG_PORT_UNREACHABLE",
1302
- path: `http://127.0.0.1:${port}`,
1303
- current_url: null,
1304
- title: null,
1305
- requires_login: false,
1306
- expected_url: expectedUrl,
1307
- message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
1308
- error: lastError.message
1309
- });
1310
- }
1311
-
1312
- const onboardingUrl = findChromeOnboardingUrl(lastTabs);
1313
- if (onboardingUrl) {
1314
- return buildBossPageState({
1584
+ action: "in_page_refresh",
1585
+ debug_port: debugPort,
1586
+ state: "LIST_NOT_RELOADED",
1587
+ message: "已点击页内刷新按钮,但候选人列表未在超时内重新就绪。",
1588
+ before_state: beforeState,
1589
+ after_state: lastState
1590
+ };
1591
+ } catch (error) {
1592
+ return {
1315
1593
  ok: false,
1316
- state: "CHROME_ONBOARDING_INTERCEPTED",
1317
- path: onboardingUrl,
1318
- current_url: onboardingUrl,
1319
- title: null,
1320
- requires_login: false,
1321
- expected_url: expectedUrl,
1322
- message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
1323
- sample_urls: extractSampleUrls(lastTabs)
1324
- });
1325
- }
1326
-
1327
- return buildBossPageState({
1328
- ok: false,
1329
- state: "BOSS_TAB_NOT_FOUND",
1330
- path: expectedUrl,
1331
- current_url: null,
1332
- title: null,
1333
- requires_login: false,
1334
- expected_url,
1335
- message: "未检测到 Boss 推荐页标签页。",
1336
- sample_urls: extractSampleUrls(lastTabs)
1337
- });
1338
- }
1339
-
1340
- async function openBossRecommendTab(port) {
1341
- const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossRecommendUrl)}`;
1342
- const attempts = ["PUT", "GET"];
1343
- let lastError = null;
1344
-
1345
- for (const method of attempts) {
1346
- try {
1347
- const response = await fetch(endpoint, { method });
1348
- if (response.ok) {
1349
- return { ok: true, method };
1350
- }
1351
- lastError = new Error(`DevTools /json/new returned ${response.status}`);
1352
- } catch (error) {
1353
- lastError = error;
1594
+ action: "in_page_refresh",
1595
+ debug_port: debugPort,
1596
+ state: "REFRESH_BUTTON_CLICK_FAILED",
1597
+ message: error?.message || "页内刷新失败。",
1598
+ before_state: null,
1599
+ after_state: null
1600
+ };
1601
+ } finally {
1602
+ if (client) {
1603
+ try {
1604
+ await client.close();
1605
+ } catch {}
1354
1606
  }
1355
1607
  }
1356
-
1357
- return {
1358
- ok: false,
1359
- error: lastError?.message || "Failed to open Boss recommend tab via DevTools /json/new"
1360
- };
1361
1608
  }
1362
1609
 
1363
- async function verifyRecommendPageStable(port, options = {}) {
1364
- const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1500;
1365
- const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 2500;
1366
- const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
1367
-
1368
- await sleep(settleMs);
1369
- const recheck = await inspectBossRecommendPageState(port, {
1370
- timeoutMs: recheckTimeoutMs,
1371
- pollMs
1372
- });
1373
- if (recheck.state === "RECOMMEND_READY") {
1374
- return recheck;
1375
- }
1376
- if (recheck.state === "LOGIN_REQUIRED") {
1377
- return buildBossPageState({
1378
- ...recheck,
1379
- state: "LOGIN_REQUIRED_AFTER_REDIRECT",
1380
- message: "Boss 页面曾进入 recommend 但随后跳转到其他页面,通常表示登录态失效。"
1381
- });
1382
- }
1383
- return recheck;
1384
- }
1385
-
1386
- export async function ensureBossRecommendPageReady(workspaceRoot, options = {}) {
1610
+ export async function reloadBossRecommendPage(workspaceRoot, options = {}) {
1387
1611
  const debugPort = Number.isFinite(options.port)
1388
1612
  ? options.port
1389
1613
  : resolveWorkspaceDebugPort(workspaceRoot);
1390
- const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
1391
- const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
1392
- const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1393
- const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
1394
-
1395
- let pageState = await inspectBossRecommendPageState(debugPort, {
1396
- timeoutMs: inspectTimeoutMs,
1397
- pollMs
1398
- });
1399
- if (pageState.state === "RECOMMEND_READY") {
1400
- const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1401
- return {
1402
- ok: stableState.state === "RECOMMEND_READY",
1403
- debug_port: debugPort,
1404
- state: stableState.state,
1405
- page_state: stableState
1406
- };
1407
- }
1614
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1200;
1615
+ const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 4000;
1616
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 600;
1408
1617
 
1409
- let launchAttempt = null;
1410
- if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1411
- return {
1412
- ok: false,
1413
- debug_port: debugPort,
1414
- state: pageState.state,
1415
- page_state: {
1416
- ...pageState,
1417
- launch_attempt: launchAttempt
1418
- }
1419
- };
1420
- }
1421
- if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
1422
- launchAttempt = launchChromeWithDebugPort(debugPort);
1423
- if (launchAttempt.ok) {
1424
- await sleep(settleMs + 1200);
1425
- pageState = await inspectBossRecommendPageState(debugPort, {
1426
- timeoutMs: inspectTimeoutMs,
1427
- pollMs
1428
- });
1429
- if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1430
- return {
1431
- ok: false,
1432
- debug_port: debugPort,
1433
- state: pageState.state,
1434
- page_state: {
1435
- ...pageState,
1436
- launch_attempt: launchAttempt
1437
- }
1438
- };
1439
- }
1440
- if (pageState.state === "RECOMMEND_READY") {
1441
- const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1442
- return {
1443
- ok: stableState.state === "RECOMMEND_READY",
1444
- debug_port: debugPort,
1445
- state: stableState.state,
1446
- page_state: {
1447
- ...stableState,
1448
- launch_attempt: launchAttempt
1449
- }
1450
- };
1451
- }
1452
- } else {
1618
+ let client = null;
1619
+ try {
1620
+ const tabs = await listChromeTabs(debugPort);
1621
+ const target = pickBossRecommendReloadTarget(tabs);
1622
+ if (!target) {
1453
1623
  return {
1454
1624
  ok: false,
1455
1625
  debug_port: debugPort,
1456
- state: pageState.state,
1457
- page_state: {
1458
- ...pageState,
1459
- launch_attempt: launchAttempt
1460
- }
1626
+ state: "BOSS_TAB_NOT_FOUND",
1627
+ page_state: null,
1628
+ message: "未找到可刷新的 Boss 标签页。"
1461
1629
  };
1462
1630
  }
1463
- }
1464
-
1465
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
1466
- if (
1467
- pageState.state === "DEBUG_PORT_UNREACHABLE"
1468
- || pageState.state === "LOGIN_REQUIRED"
1469
- || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
1470
- ) {
1471
- break;
1472
- }
1473
- await openBossRecommendTab(debugPort);
1474
- await sleep(settleMs);
1475
- pageState = await inspectBossRecommendPageState(debugPort, {
1476
- timeoutMs: inspectTimeoutMs,
1477
- pollMs
1478
- });
1479
- if (pageState.state === "RECOMMEND_READY") {
1480
- const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1481
- return {
1482
- ok: stableState.state === "RECOMMEND_READY",
1483
- debug_port: debugPort,
1484
- state: stableState.state,
1485
- page_state: {
1486
- ...stableState,
1487
- launch_attempt: launchAttempt
1488
- }
1489
- };
1490
- }
1491
- }
1492
1631
 
1493
- return {
1494
- ok: false,
1495
- debug_port: debugPort,
1496
- state: pageState.state || "UNKNOWN",
1497
- page_state: {
1498
- ...pageState,
1499
- launch_attempt: launchAttempt
1500
- }
1501
- };
1502
- }
1503
-
1504
- export async function listRecommendJobs({ workspaceRoot, port, runtime = null }) {
1505
- const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1506
- if (!searchDir) {
1507
- return {
1508
- ok: false,
1509
- stdout: "",
1510
- stderr: "boss-recommend-search-cli package not found",
1511
- error: {
1512
- code: "RECOMMEND_SEARCH_CLI_MISSING",
1513
- message: "boss-recommend-search-cli 目录不存在。"
1514
- }
1515
- };
1516
- }
1517
- const cliPath = resolveRecommendSearchCliEntry(searchDir);
1518
- const args = [
1519
- cliPath,
1520
- "--list-jobs",
1521
- "--port",
1522
- String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
1523
- ];
1524
- const result = await runProcess({
1525
- command: "node",
1526
- args,
1527
- cwd: searchDir,
1528
- timeoutMs: 180000,
1529
- heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1530
- signal: runtime?.signal,
1531
- onOutput: (event) => {
1532
- safeInvokeCallback(runtime?.onOutput, event);
1533
- },
1534
- onHeartbeat: (event) => {
1535
- safeInvokeCallback(runtime?.onHeartbeat, event);
1632
+ client = await CDP({ port: debugPort, target });
1633
+ const { Page } = client;
1634
+ if (Page && typeof Page.enable === "function") {
1635
+ await Page.enable();
1536
1636
  }
1537
- });
1538
- const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1539
- const jobs = Array.isArray(structured?.result?.jobs) ? structured.result.jobs : [];
1540
- const missingOutputError = result.code === 0 && !structured
1541
- ? {
1542
- code: "RECOMMEND_JOB_LIST_NO_OUTPUT",
1543
- message: "岗位列表读取完成但未返回可解析结果。"
1544
- }
1545
- : null;
1546
- return {
1547
- ok: result.code === 0 && structured?.status === "COMPLETED" && jobs.length > 0,
1548
- stdout: result.stdout,
1549
- stderr: result.stderr,
1550
- structured,
1551
- jobs,
1552
- error: structured?.error || missingOutputError || (
1553
- result.code === 0
1554
- ? {
1555
- code: "RECOMMEND_JOB_LIST_EMPTY",
1556
- message: "未读取到可选岗位。"
1557
- }
1558
- : result.error_code === "ABORTED"
1559
- ? {
1560
- code: "PROCESS_ABORTED",
1561
- message: "岗位列表读取已取消。"
1562
- }
1563
- : {
1564
- code: "RECOMMEND_JOB_LIST_FAILED",
1565
- message: "岗位列表读取失败。"
1566
- }
1567
- )
1568
- };
1569
- }
1570
-
1571
- export async function runRecommendSearchCli({ workspaceRoot, searchParams, selectedJob, runtime = null }) {
1572
- const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1573
- if (!searchDir) {
1574
- return {
1575
- ok: false,
1576
- stdout: "",
1577
- stderr: "boss-recommend-search-cli package not found",
1578
- error: {
1579
- code: "RECOMMEND_SEARCH_CLI_MISSING",
1580
- message: "boss-recommend-search-cli 目录不存在。"
1581
- }
1582
- };
1583
- }
1584
- const cliPath = resolveRecommendSearchCliEntry(searchDir);
1585
- const args = [
1586
- cliPath,
1587
- "--school-tag",
1588
- serializeSchoolTagSelection(searchParams.school_tag),
1589
- "--degree",
1590
- serializeDegreeSelection(searchParams.degree),
1591
- "--gender",
1592
- searchParams.gender,
1593
- "--recent-not-view",
1594
- searchParams.recent_not_view,
1595
- "--port",
1596
- String(resolveWorkspaceDebugPort(workspaceRoot))
1597
- ];
1598
- const normalizedSelectedJob = String(selectedJob || "").trim();
1599
- if (normalizedSelectedJob) {
1600
- args.push("--job", normalizedSelectedJob);
1601
- }
1602
- const result = await runProcess({
1603
- command: "node",
1604
- args,
1605
- cwd: searchDir,
1606
- timeoutMs: 180000,
1607
- heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1608
- signal: runtime?.signal,
1609
- onOutput: (event) => {
1610
- safeInvokeCallback(runtime?.onOutput, event);
1611
- },
1612
- onHeartbeat: (event) => {
1613
- safeInvokeCallback(runtime?.onHeartbeat, event);
1637
+ if (Page && typeof Page.bringToFront === "function") {
1638
+ await Page.bringToFront();
1614
1639
  }
1615
- });
1616
- const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1617
- const missingOutputError = result.code === 0 && !structured
1618
- ? {
1619
- code: "RECOMMEND_SEARCH_NO_OUTPUT",
1620
- message: "推荐页筛选命令执行结束但未返回可解析结果。"
1621
- }
1622
- : null;
1623
- return {
1624
- ok: result.code === 0 && structured?.status === "COMPLETED",
1625
- stdout: result.stdout,
1626
- stderr: result.stderr,
1627
- structured,
1628
- summary: structured?.result || null,
1629
- error: structured?.error || missingOutputError || (
1630
- result.code === 0
1631
- ? null
1632
- : result.error_code === "ABORTED"
1633
- ? {
1634
- code: "PROCESS_ABORTED",
1635
- message: "推荐页筛选命令已取消。"
1636
- }
1637
- : {
1638
- code: "RECOMMEND_SEARCH_FAILED",
1639
- message: "推荐页筛选命令执行失败。"
1640
- }
1641
- )
1642
- };
1643
- }
1640
+ await Page.reload({ ignoreCache: true });
1644
1641
 
1645
- export async function runRecommendScreenCli({ workspaceRoot, screenParams, resume = null, runtime = null }) {
1646
- const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
1647
- if (!screenDir) {
1642
+ const stableState = await verifyRecommendPageStable(debugPort, {
1643
+ settleMs,
1644
+ recheckTimeoutMs,
1645
+ pollMs
1646
+ });
1648
1647
  return {
1649
- ok: false,
1650
- stdout: "",
1651
- stderr: "boss-recommend-screen-cli package not found",
1652
- error: {
1653
- code: "RECOMMEND_SCREEN_CLI_MISSING",
1654
- message: "boss-recommend-screen-cli 目录不存在。"
1655
- }
1648
+ ok: stableState.state === "RECOMMEND_READY",
1649
+ debug_port: debugPort,
1650
+ state: stableState.state,
1651
+ page_state: stableState,
1652
+ reloaded_url: target.url || null
1656
1653
  };
1657
- }
1658
- const configPath = resolveScreenConfigPath(workspaceRoot);
1659
- const loaded = loadScreenConfig(configPath);
1660
- if (!loaded.ok) {
1654
+ } catch (error) {
1661
1655
  return {
1662
1656
  ok: false,
1663
- stdout: "",
1664
- stderr: loaded.error,
1665
- error: {
1666
- code: "SCREEN_CONFIG_ERROR",
1667
- message: loaded.error
1668
- }
1657
+ debug_port: debugPort,
1658
+ state: "RELOAD_FAILED",
1659
+ page_state: null,
1660
+ message: error?.message || "刷新 Boss recommend 页面失败。"
1669
1661
  };
1670
- }
1671
-
1672
- const fixedOutput = normalizeText(resume?.output_csv || "");
1673
- const outputName = `recommend_screen_result_${Date.now()}.csv`;
1674
- let outputPath = fixedOutput ? path.resolve(fixedOutput) : outputName;
1675
- if (!fixedOutput) {
1676
- if (loaded.config.outputDir) {
1677
- const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
1678
- fs.mkdirSync(resolvedOutputDir, { recursive: true });
1679
- outputPath = path.join(resolvedOutputDir, outputName);
1680
- } else {
1681
- const desktopDir = getDesktopDir();
1682
- fs.mkdirSync(desktopDir, { recursive: true });
1683
- outputPath = path.join(desktopDir, outputName);
1684
- }
1685
- } else {
1686
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1687
- }
1688
-
1689
- const checkpointPath = normalizeText(resume?.checkpoint_path || "")
1690
- ? path.resolve(String(resume.checkpoint_path))
1691
- : null;
1692
- const pauseControlPath = normalizeText(resume?.pause_control_path || "")
1693
- ? path.resolve(String(resume.pause_control_path))
1694
- : null;
1695
- const resumeRequested = resume?.resume === true;
1696
- const requireCheckpoint = resume?.require_checkpoint === true;
1697
- if (resumeRequested && requireCheckpoint) {
1698
- if (!checkpointPath) {
1699
- return {
1700
- ok: false,
1701
- paused: false,
1702
- stdout: "",
1703
- stderr: "",
1704
- structured: null,
1705
- summary: null,
1706
- error: {
1707
- code: "RESUME_CHECKPOINT_MISSING",
1708
- message: "恢复执行缺少 checkpoint_path,无法从上次进度继续。"
1709
- }
1710
- };
1711
- }
1712
- if (!fs.existsSync(checkpointPath)) {
1713
- return {
1714
- ok: false,
1715
- paused: false,
1716
- stdout: "",
1717
- stderr: "",
1718
- structured: null,
1719
- summary: null,
1720
- error: {
1721
- code: "RESUME_CHECKPOINT_MISSING",
1722
- message: `恢复执行未找到 checkpoint 文件:${checkpointPath}`
1723
- }
1724
- };
1725
- }
1726
- }
1727
-
1728
- const cliPath = resolveRecommendScreenCliEntry(screenDir);
1729
- const args = [
1730
- cliPath,
1731
- "--baseurl",
1732
- loaded.config.baseUrl,
1733
- "--apikey",
1734
- loaded.config.apiKey,
1735
- "--model",
1736
- loaded.config.model,
1737
- "--port",
1738
- String(resolveWorkspaceDebugPort(workspaceRoot)),
1739
- "--criteria",
1740
- screenParams.criteria,
1741
- "--post-action",
1742
- screenParams.post_action,
1743
- "--post-action-confirmed",
1744
- "true",
1745
- "--output",
1746
- outputPath
1747
- ];
1748
-
1749
- if (loaded.config.openaiOrganization) {
1750
- args.push("--openai-organization", loaded.config.openaiOrganization);
1751
- }
1752
- if (loaded.config.openaiProject) {
1753
- args.push("--openai-project", loaded.config.openaiProject);
1754
- }
1755
- if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
1756
- args.push("--targetCount", String(screenParams.target_count));
1757
- }
1758
- if (screenParams.post_action === "greet"
1759
- && Number.isInteger(screenParams.max_greet_count)
1760
- && screenParams.max_greet_count > 0) {
1761
- args.push("--max-greet-count", String(screenParams.max_greet_count));
1762
- }
1763
- if (checkpointPath) {
1764
- args.push("--checkpoint-path", checkpointPath);
1765
- }
1766
- if (pauseControlPath) {
1767
- args.push("--pause-control-path", pauseControlPath);
1768
- }
1769
- if (resumeRequested) {
1770
- args.push("--resume");
1771
- }
1772
-
1773
- let inferredProgress = {
1774
- processed: 0,
1775
- passed: 0,
1776
- skipped: 0,
1777
- greet_count: 0
1778
- };
1779
- let inferredTracker = createScreenProgressTracker();
1780
- const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
1781
-
1782
- const result = await runProcess({
1783
- command: "node",
1784
- args,
1785
- cwd: screenDir,
1786
- timeoutMs: screenTimeoutMs,
1787
- heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1788
- signal: runtime?.signal,
1789
- onOutput: (event) => {
1790
- safeInvokeCallback(runtime?.onOutput, event);
1791
- },
1792
- onLine: (event) => {
1793
- const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
1794
- if (!parsed) return;
1795
- inferredProgress = parsed.progress;
1796
- inferredTracker = parsed.tracker;
1797
- safeInvokeCallback(runtime?.onProgress, {
1798
- ...inferredProgress,
1799
- line: parsed.line
1800
- });
1801
- },
1802
- onHeartbeat: (event) => {
1803
- safeInvokeCallback(runtime?.onHeartbeat, event);
1662
+ } finally {
1663
+ if (client) {
1664
+ try {
1665
+ await client.close();
1666
+ } catch {}
1804
1667
  }
1805
- });
1806
- const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1807
- const status = normalizeText(structured?.status || "").toUpperCase();
1808
- const summary = structured?.result || null;
1809
- if (summary) {
1810
- safeInvokeCallback(runtime?.onProgress, {
1811
- processed: Number.isInteger(summary.processed_count) ? summary.processed_count : inferredProgress.processed,
1812
- passed: Number.isInteger(summary.passed_count) ? summary.passed_count : inferredProgress.passed,
1813
- skipped: Number.isInteger(summary.skipped_count) ? summary.skipped_count : inferredProgress.skipped,
1814
- greet_count: Number.isInteger(summary.greet_count) ? summary.greet_count : inferredProgress.greet_count
1815
- });
1816
1668
  }
1817
- const missingOutputError = result.code === 0 && !structured
1818
- ? {
1819
- code: "RECOMMEND_SCREEN_NO_OUTPUT",
1820
- message: "推荐页筛选命令执行结束但未返回可解析结果。"
1821
- }
1822
- : null;
1823
- return {
1824
- ok: result.code === 0 && status === "COMPLETED",
1825
- paused: result.code === 0 && status === "PAUSED",
1826
- stdout: result.stdout,
1827
- stderr: result.stderr,
1828
- structured,
1829
- summary,
1830
- error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
1831
- };
1832
1669
  }
1833
1670
 
1834
- export const __testables = {
1835
- runProcess,
1836
- parseJsonOutput,
1837
- parseScreenProgressLine,
1838
- resolveRecommendScreenTimeoutMs,
1839
- buildRecommendScreenProcessError
1840
- };
1671
+ export async function ensureBossRecommendPageReady(workspaceRoot, options = {}) {
1672
+ const debugPort = Number.isFinite(options.port)
1673
+ ? options.port
1674
+ : resolveWorkspaceDebugPort(workspaceRoot);
1675
+ const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
1676
+ const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
1677
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1678
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 800;
1679
+
1680
+ let pageState = await inspectBossRecommendPageState(debugPort, {
1681
+ timeoutMs: inspectTimeoutMs,
1682
+ pollMs
1683
+ });
1684
+ if (pageState.state === "RECOMMEND_READY") {
1685
+ const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1686
+ return {
1687
+ ok: stableState.state === "RECOMMEND_READY",
1688
+ debug_port: debugPort,
1689
+ state: stableState.state,
1690
+ page_state: stableState
1691
+ };
1692
+ }
1693
+
1694
+ let launchAttempt = null;
1695
+ if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1696
+ return {
1697
+ ok: false,
1698
+ debug_port: debugPort,
1699
+ state: pageState.state,
1700
+ page_state: {
1701
+ ...pageState,
1702
+ launch_attempt: launchAttempt
1703
+ }
1704
+ };
1705
+ }
1706
+ if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
1707
+ launchAttempt = launchChromeWithDebugPort(debugPort);
1708
+ if (launchAttempt.ok) {
1709
+ await sleep(settleMs + 1200);
1710
+ pageState = await inspectBossRecommendPageState(debugPort, {
1711
+ timeoutMs: inspectTimeoutMs,
1712
+ pollMs
1713
+ });
1714
+ if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1715
+ return {
1716
+ ok: false,
1717
+ debug_port: debugPort,
1718
+ state: pageState.state,
1719
+ page_state: {
1720
+ ...pageState,
1721
+ launch_attempt: launchAttempt
1722
+ }
1723
+ };
1724
+ }
1725
+ if (pageState.state === "RECOMMEND_READY") {
1726
+ const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1727
+ return {
1728
+ ok: stableState.state === "RECOMMEND_READY",
1729
+ debug_port: debugPort,
1730
+ state: stableState.state,
1731
+ page_state: {
1732
+ ...stableState,
1733
+ launch_attempt: launchAttempt
1734
+ }
1735
+ };
1736
+ }
1737
+ } else {
1738
+ return {
1739
+ ok: false,
1740
+ debug_port: debugPort,
1741
+ state: pageState.state,
1742
+ page_state: {
1743
+ ...pageState,
1744
+ launch_attempt: launchAttempt
1745
+ }
1746
+ };
1747
+ }
1748
+ }
1749
+
1750
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1751
+ if (
1752
+ pageState.state === "DEBUG_PORT_UNREACHABLE"
1753
+ || pageState.state === "LOGIN_REQUIRED"
1754
+ || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
1755
+ ) {
1756
+ break;
1757
+ }
1758
+ await openBossRecommendTab(debugPort);
1759
+ await sleep(settleMs);
1760
+ pageState = await inspectBossRecommendPageState(debugPort, {
1761
+ timeoutMs: inspectTimeoutMs,
1762
+ pollMs
1763
+ });
1764
+ if (pageState.state === "RECOMMEND_READY") {
1765
+ const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
1766
+ return {
1767
+ ok: stableState.state === "RECOMMEND_READY",
1768
+ debug_port: debugPort,
1769
+ state: stableState.state,
1770
+ page_state: {
1771
+ ...stableState,
1772
+ launch_attempt: launchAttempt
1773
+ }
1774
+ };
1775
+ }
1776
+ }
1777
+
1778
+ return {
1779
+ ok: false,
1780
+ debug_port: debugPort,
1781
+ state: pageState.state || "UNKNOWN",
1782
+ page_state: {
1783
+ ...pageState,
1784
+ launch_attempt: launchAttempt
1785
+ }
1786
+ };
1787
+ }
1788
+
1789
+ export async function listRecommendJobs({ workspaceRoot, port, runtime = null }) {
1790
+ const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1791
+ if (!searchDir) {
1792
+ return {
1793
+ ok: false,
1794
+ stdout: "",
1795
+ stderr: "boss-recommend-search-cli package not found",
1796
+ error: {
1797
+ code: "RECOMMEND_SEARCH_CLI_MISSING",
1798
+ message: "boss-recommend-search-cli 目录不存在。"
1799
+ }
1800
+ };
1801
+ }
1802
+ const cliPath = resolveRecommendSearchCliEntry(searchDir);
1803
+ const args = [
1804
+ cliPath,
1805
+ "--list-jobs",
1806
+ "--port",
1807
+ String(parsePositiveInteger(port) || resolveWorkspaceDebugPort(workspaceRoot))
1808
+ ];
1809
+ const result = await runProcess({
1810
+ command: "node",
1811
+ args,
1812
+ cwd: searchDir,
1813
+ timeoutMs: 180000,
1814
+ heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1815
+ signal: runtime?.signal,
1816
+ onOutput: (event) => {
1817
+ safeInvokeCallback(runtime?.onOutput, event);
1818
+ },
1819
+ onHeartbeat: (event) => {
1820
+ safeInvokeCallback(runtime?.onHeartbeat, event);
1821
+ }
1822
+ });
1823
+ const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1824
+ const jobs = Array.isArray(structured?.result?.jobs) ? structured.result.jobs : [];
1825
+ const missingOutputError = result.code === 0 && !structured
1826
+ ? {
1827
+ code: "RECOMMEND_JOB_LIST_NO_OUTPUT",
1828
+ message: "岗位列表读取完成但未返回可解析结果。"
1829
+ }
1830
+ : null;
1831
+ return {
1832
+ ok: result.code === 0 && structured?.status === "COMPLETED" && jobs.length > 0,
1833
+ stdout: result.stdout,
1834
+ stderr: result.stderr,
1835
+ structured,
1836
+ jobs,
1837
+ error: structured?.error || missingOutputError || (
1838
+ result.code === 0
1839
+ ? {
1840
+ code: "RECOMMEND_JOB_LIST_EMPTY",
1841
+ message: "未读取到可选岗位。"
1842
+ }
1843
+ : result.error_code === "ABORTED"
1844
+ ? {
1845
+ code: "PROCESS_ABORTED",
1846
+ message: "岗位列表读取已取消。"
1847
+ }
1848
+ : {
1849
+ code: "RECOMMEND_JOB_LIST_FAILED",
1850
+ message: "岗位列表读取失败。"
1851
+ }
1852
+ )
1853
+ };
1854
+ }
1855
+
1856
+ export async function runRecommendSearchCli({ workspaceRoot, searchParams, selectedJob, runtime = null }) {
1857
+ const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
1858
+ if (!searchDir) {
1859
+ return {
1860
+ ok: false,
1861
+ stdout: "",
1862
+ stderr: "boss-recommend-search-cli package not found",
1863
+ error: {
1864
+ code: "RECOMMEND_SEARCH_CLI_MISSING",
1865
+ message: "boss-recommend-search-cli 目录不存在。"
1866
+ }
1867
+ };
1868
+ }
1869
+ const cliPath = resolveRecommendSearchCliEntry(searchDir);
1870
+ const args = [
1871
+ cliPath,
1872
+ "--school-tag",
1873
+ serializeSchoolTagSelection(searchParams.school_tag),
1874
+ "--degree",
1875
+ serializeDegreeSelection(searchParams.degree),
1876
+ "--gender",
1877
+ searchParams.gender,
1878
+ "--recent-not-view",
1879
+ searchParams.recent_not_view,
1880
+ "--port",
1881
+ String(resolveWorkspaceDebugPort(workspaceRoot))
1882
+ ];
1883
+ const normalizedSelectedJob = String(selectedJob || "").trim();
1884
+ if (normalizedSelectedJob) {
1885
+ args.push("--job", normalizedSelectedJob);
1886
+ }
1887
+ const result = await runProcess({
1888
+ command: "node",
1889
+ args,
1890
+ cwd: searchDir,
1891
+ timeoutMs: 180000,
1892
+ heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
1893
+ signal: runtime?.signal,
1894
+ onOutput: (event) => {
1895
+ safeInvokeCallback(runtime?.onOutput, event);
1896
+ },
1897
+ onHeartbeat: (event) => {
1898
+ safeInvokeCallback(runtime?.onHeartbeat, event);
1899
+ }
1900
+ });
1901
+ const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1902
+ const missingOutputError = result.code === 0 && !structured
1903
+ ? {
1904
+ code: "RECOMMEND_SEARCH_NO_OUTPUT",
1905
+ message: "推荐页筛选命令执行结束但未返回可解析结果。"
1906
+ }
1907
+ : null;
1908
+ return {
1909
+ ok: result.code === 0 && structured?.status === "COMPLETED",
1910
+ stdout: result.stdout,
1911
+ stderr: result.stderr,
1912
+ structured,
1913
+ summary: structured?.result || null,
1914
+ error: structured?.error || missingOutputError || (
1915
+ result.code === 0
1916
+ ? null
1917
+ : result.error_code === "ABORTED"
1918
+ ? {
1919
+ code: "PROCESS_ABORTED",
1920
+ message: "推荐页筛选命令已取消。"
1921
+ }
1922
+ : {
1923
+ code: "RECOMMEND_SEARCH_FAILED",
1924
+ message: "推荐页筛选命令执行失败。"
1925
+ }
1926
+ )
1927
+ };
1928
+ }
1929
+
1930
+ export async function runRecommendScreenCli({ workspaceRoot, screenParams, resume = null, runtime = null }) {
1931
+ const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
1932
+ if (!screenDir) {
1933
+ return {
1934
+ ok: false,
1935
+ stdout: "",
1936
+ stderr: "boss-recommend-screen-cli package not found",
1937
+ error: {
1938
+ code: "RECOMMEND_SCREEN_CLI_MISSING",
1939
+ message: "boss-recommend-screen-cli 目录不存在。"
1940
+ }
1941
+ };
1942
+ }
1943
+ const configPath = resolveScreenConfigPath(workspaceRoot);
1944
+ const loaded = loadScreenConfig(configPath);
1945
+ if (!loaded.ok) {
1946
+ return {
1947
+ ok: false,
1948
+ stdout: "",
1949
+ stderr: loaded.error,
1950
+ error: {
1951
+ code: "SCREEN_CONFIG_ERROR",
1952
+ message: loaded.error
1953
+ }
1954
+ };
1955
+ }
1956
+
1957
+ const fixedOutput = normalizeText(resume?.output_csv || "");
1958
+ const outputName = `recommend_screen_result_${Date.now()}.csv`;
1959
+ let outputPath = fixedOutput ? path.resolve(fixedOutput) : outputName;
1960
+ if (!fixedOutput) {
1961
+ if (loaded.config.outputDir) {
1962
+ const resolvedOutputDir = path.resolve(path.dirname(configPath), loaded.config.outputDir);
1963
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
1964
+ outputPath = path.join(resolvedOutputDir, outputName);
1965
+ } else {
1966
+ const desktopDir = getDesktopDir();
1967
+ fs.mkdirSync(desktopDir, { recursive: true });
1968
+ outputPath = path.join(desktopDir, outputName);
1969
+ }
1970
+ } else {
1971
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1972
+ }
1973
+
1974
+ const checkpointPath = normalizeText(resume?.checkpoint_path || "")
1975
+ ? path.resolve(String(resume.checkpoint_path))
1976
+ : null;
1977
+ const pauseControlPath = normalizeText(resume?.pause_control_path || "")
1978
+ ? path.resolve(String(resume.pause_control_path))
1979
+ : null;
1980
+ const resumeRequested = resume?.resume === true;
1981
+ const requireCheckpoint = resume?.require_checkpoint === true;
1982
+ if (resumeRequested && requireCheckpoint) {
1983
+ if (!checkpointPath) {
1984
+ return {
1985
+ ok: false,
1986
+ paused: false,
1987
+ stdout: "",
1988
+ stderr: "",
1989
+ structured: null,
1990
+ summary: null,
1991
+ error: {
1992
+ code: "RESUME_CHECKPOINT_MISSING",
1993
+ message: "恢复执行缺少 checkpoint_path,无法从上次进度继续。"
1994
+ }
1995
+ };
1996
+ }
1997
+ if (!fs.existsSync(checkpointPath)) {
1998
+ return {
1999
+ ok: false,
2000
+ paused: false,
2001
+ stdout: "",
2002
+ stderr: "",
2003
+ structured: null,
2004
+ summary: null,
2005
+ error: {
2006
+ code: "RESUME_CHECKPOINT_MISSING",
2007
+ message: `恢复执行未找到 checkpoint 文件:${checkpointPath}`
2008
+ }
2009
+ };
2010
+ }
2011
+ }
2012
+
2013
+ const cliPath = resolveRecommendScreenCliEntry(screenDir);
2014
+ const args = [
2015
+ cliPath,
2016
+ "--baseurl",
2017
+ loaded.config.baseUrl,
2018
+ "--apikey",
2019
+ loaded.config.apiKey,
2020
+ "--model",
2021
+ loaded.config.model,
2022
+ "--port",
2023
+ String(resolveWorkspaceDebugPort(workspaceRoot)),
2024
+ "--criteria",
2025
+ screenParams.criteria,
2026
+ "--post-action",
2027
+ screenParams.post_action,
2028
+ "--post-action-confirmed",
2029
+ "true",
2030
+ "--output",
2031
+ outputPath
2032
+ ];
2033
+
2034
+ if (loaded.config.openaiOrganization) {
2035
+ args.push("--openai-organization", loaded.config.openaiOrganization);
2036
+ }
2037
+ if (loaded.config.openaiProject) {
2038
+ args.push("--openai-project", loaded.config.openaiProject);
2039
+ }
2040
+ if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
2041
+ args.push("--targetCount", String(screenParams.target_count));
2042
+ }
2043
+ if (screenParams.post_action === "greet"
2044
+ && Number.isInteger(screenParams.max_greet_count)
2045
+ && screenParams.max_greet_count > 0) {
2046
+ args.push("--max-greet-count", String(screenParams.max_greet_count));
2047
+ }
2048
+ if (checkpointPath) {
2049
+ args.push("--checkpoint-path", checkpointPath);
2050
+ }
2051
+ if (pauseControlPath) {
2052
+ args.push("--pause-control-path", pauseControlPath);
2053
+ }
2054
+ if (resumeRequested) {
2055
+ args.push("--resume");
2056
+ }
2057
+
2058
+ let inferredProgress = {
2059
+ processed: 0,
2060
+ passed: 0,
2061
+ skipped: 0,
2062
+ greet_count: 0
2063
+ };
2064
+ let inferredTracker = createScreenProgressTracker();
2065
+ const screenTimeoutMs = resolveRecommendScreenTimeoutMs(runtime);
2066
+
2067
+ const result = await runProcess({
2068
+ command: "node",
2069
+ args,
2070
+ cwd: screenDir,
2071
+ timeoutMs: screenTimeoutMs,
2072
+ heartbeatIntervalMs: runtime?.heartbeatIntervalMs,
2073
+ signal: runtime?.signal,
2074
+ onOutput: (event) => {
2075
+ safeInvokeCallback(runtime?.onOutput, event);
2076
+ },
2077
+ onLine: (event) => {
2078
+ const parsed = parseScreenProgressLine(event?.line, inferredProgress, inferredTracker);
2079
+ if (!parsed) return;
2080
+ inferredProgress = parsed.progress;
2081
+ inferredTracker = parsed.tracker;
2082
+ safeInvokeCallback(runtime?.onProgress, {
2083
+ ...inferredProgress,
2084
+ line: parsed.line
2085
+ });
2086
+ },
2087
+ onHeartbeat: (event) => {
2088
+ safeInvokeCallback(runtime?.onHeartbeat, event);
2089
+ }
2090
+ });
2091
+ const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
2092
+ const status = normalizeText(structured?.status || "").toUpperCase();
2093
+ const summary = structured?.result || null;
2094
+ if (summary) {
2095
+ safeInvokeCallback(runtime?.onProgress, {
2096
+ processed: Number.isInteger(summary.processed_count) ? summary.processed_count : inferredProgress.processed,
2097
+ passed: Number.isInteger(summary.passed_count) ? summary.passed_count : inferredProgress.passed,
2098
+ skipped: Number.isInteger(summary.skipped_count) ? summary.skipped_count : inferredProgress.skipped,
2099
+ greet_count: Number.isInteger(summary.greet_count) ? summary.greet_count : inferredProgress.greet_count
2100
+ });
2101
+ }
2102
+ const missingOutputError = result.code === 0 && !structured
2103
+ ? {
2104
+ code: "RECOMMEND_SCREEN_NO_OUTPUT",
2105
+ message: "推荐页筛选命令执行结束但未返回可解析结果。"
2106
+ }
2107
+ : null;
2108
+ return {
2109
+ ok: result.code === 0 && status === "COMPLETED",
2110
+ paused: result.code === 0 && status === "PAUSED",
2111
+ stdout: result.stdout,
2112
+ stderr: result.stderr,
2113
+ structured,
2114
+ summary,
2115
+ error: structured?.error || missingOutputError || buildRecommendScreenProcessError(result, screenTimeoutMs)
2116
+ };
2117
+ }
2118
+
2119
+ export const __testables = {
2120
+ runProcess,
2121
+ parseJsonOutput,
2122
+ parseScreenProgressLine,
2123
+ resolveRecommendScreenTimeoutMs,
2124
+ buildRecommendScreenProcessError
2125
+ };