@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0

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