@reconcrap/boss-recommend-mcp 1.3.39 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +86 -33
  2. package/package.json +62 -9
  3. package/skills/boss-chat/SKILL.md +5 -4
  4. package/skills/boss-recommend-pipeline/SKILL.md +21 -31
  5. package/skills/boss-recruit-pipeline/README.md +17 -0
  6. package/skills/boss-recruit-pipeline/SKILL.md +55 -0
  7. package/src/chat-mcp.js +1333 -0
  8. package/src/chat-runtime-config.js +559 -0
  9. package/src/cli.js +1254 -225
  10. package/src/core/browser/index.js +378 -0
  11. package/src/core/capture/index.js +298 -0
  12. package/src/core/cv-acquisition/index.js +219 -0
  13. package/src/core/greet-quota/index.js +54 -0
  14. package/src/core/infinite-list/index.js +459 -0
  15. package/src/core/reporting/legacy-csv.js +332 -0
  16. package/src/core/run/index.js +286 -0
  17. package/src/core/screening/index.js +1166 -0
  18. package/src/core/self-heal/index.js +848 -0
  19. package/src/domains/chat/cards.js +129 -0
  20. package/src/domains/chat/constants.js +183 -0
  21. package/src/domains/chat/detail.js +1369 -0
  22. package/src/domains/chat/index.js +7 -0
  23. package/src/domains/chat/jobs.js +334 -0
  24. package/src/domains/chat/page-guard.js +88 -0
  25. package/src/domains/chat/roots.js +56 -0
  26. package/src/domains/chat/run-service.js +1101 -0
  27. package/src/domains/recommend/actions.js +457 -0
  28. package/src/domains/recommend/cards.js +228 -0
  29. package/src/domains/recommend/constants.js +141 -0
  30. package/src/domains/recommend/detail.js +341 -0
  31. package/src/domains/recommend/filters.js +581 -0
  32. package/src/domains/recommend/index.js +10 -0
  33. package/src/domains/recommend/jobs.js +232 -0
  34. package/src/domains/recommend/refresh.js +204 -0
  35. package/src/domains/recommend/roots.js +78 -0
  36. package/src/domains/recommend/run-service.js +903 -0
  37. package/src/domains/recommend/scopes.js +245 -0
  38. package/src/domains/recruit/actions.js +277 -0
  39. package/src/domains/recruit/cards.js +66 -0
  40. package/src/domains/recruit/constants.js +130 -0
  41. package/src/domains/recruit/detail.js +414 -0
  42. package/src/domains/recruit/index.js +9 -0
  43. package/src/domains/recruit/instruction-parser.js +451 -0
  44. package/src/domains/recruit/refresh.js +40 -0
  45. package/src/domains/recruit/roots.js +67 -0
  46. package/src/domains/recruit/run-service.js +580 -0
  47. package/src/domains/recruit/search.js +1149 -0
  48. package/src/index.js +578 -419
  49. package/src/recommend-mcp.js +1257 -0
  50. package/src/recruit-mcp.js +1035 -0
  51. package/src/adapters.js +0 -3079
  52. package/src/boss-chat.js +0 -1037
  53. package/src/pipeline.js +0 -2249
  54. package/src/recommend-healing-config.js +0 -131
  55. package/src/recommend-healing-rules.json +0 -261
  56. package/src/self-heal.js +0 -2237
  57. package/src/test-adapters-runtime.js +0 -628
  58. package/src/test-boss-chat.js +0 -3196
  59. package/src/test-index-async.js +0 -498
  60. package/src/test-parser.js +0 -742
  61. package/src/test-pipeline.js +0 -2703
  62. package/src/test-run-state.js +0 -152
  63. package/src/test-self-heal.js +0 -224
  64. package/vendor/boss-chat-cli/README.md +0 -134
  65. package/vendor/boss-chat-cli/package.json +0 -53
  66. package/vendor/boss-chat-cli/src/app.js +0 -1501
  67. package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
  68. package/vendor/boss-chat-cli/src/cli.js +0 -1713
  69. package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
  70. package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
  71. package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
  72. package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
  73. package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
  74. package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
  75. package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
  76. package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
  77. package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
  78. package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
  79. package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
  80. package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
  81. package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
  82. package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
  83. package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -7072
  84. package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
  85. package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
  86. package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2423
  87. package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
  88. package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
package/src/cli.js CHANGED
@@ -2,54 +2,64 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
- import { spawn } from "node:child_process";
5
+ import { spawn, spawnSync } from "node:child_process";
6
6
  import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
- import { startServer } from "./index.js";
9
8
  import {
10
- ensureBossRecommendPageReady,
11
- getFeaturedCalibrationResolution,
12
- getScreenConfigResolution,
13
- inspectBossRecommendPageState,
14
- runRecommendCalibration,
15
- runPipelinePreflight,
16
- switchRecommendTab,
17
- waitRecommendFeaturedDetailReady
18
- } from "./adapters.js";
9
+ assertNoForbiddenCdpCalls,
10
+ bringPageToFront,
11
+ connectToChromeTarget,
12
+ enableDomains,
13
+ getDocumentRoot,
14
+ querySelector,
15
+ sleep as sleepMs
16
+ } from "./core/browser/index.js";
17
+ import {
18
+ bossChatHealthCheckTool,
19
+ cancelBossChatRunTool,
20
+ getBossChatRunTool,
21
+ pauseBossChatRunTool,
22
+ prepareBossChatRunTool,
23
+ resumeBossChatRunTool
24
+ } from "./chat-mcp.js";
25
+ import { listRecommendJobsTool } from "./recommend-mcp.js";
19
26
  import {
20
- cancelBossChatRun,
21
- ensureBossChatRuntimeReady,
22
- getBossChatHealthCheck,
23
- getBossChatRun,
24
- pauseBossChatRun,
25
- prepareBossChatRun,
26
- resolveBossChatRuntimeLayout,
27
- resumeBossChatRun,
28
- startBossChatRun
29
- } from "./boss-chat.js";
30
- import { runRecommendPipeline } from "./pipeline.js";
27
+ getBossScreenConfigResolution,
28
+ resolveBossChatRuntimeLayout as resolveCdpBossChatRuntimeLayout,
29
+ resolveBossScreeningConfig
30
+ } from "./chat-runtime-config.js";
31
+ import { startServer } from "./index.js";
31
32
 
32
33
  const require = createRequire(import.meta.url);
33
34
  const currentFilePath = fileURLToPath(import.meta.url);
34
35
  const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
35
36
  const packageJsonPath = path.join(packageRoot, "package.json");
36
37
  const skillName = "boss-recommend-pipeline";
37
- const bundledSkillNames = [skillName, "boss-chat"];
38
+ const recruitSkillName = "boss-recruit-pipeline";
39
+ const chatSkillName = "boss-chat";
40
+ const bundledSkillNames = [skillName, recruitSkillName, chatSkillName];
38
41
  const exampleConfigPath = path.join(packageRoot, "config", "screening-config.example.json");
39
42
  const bossUrl = "https://www.zhipin.com/web/chat/recommend";
43
+ const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
40
44
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
45
+ const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
46
+ const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
41
47
  const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "openclaw"];
42
48
  const defaultMcpServerName = "boss-recommend";
43
49
  const defaultMcpCommand = "npx";
44
50
  const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
45
51
  const recommendMcpBinaryName = "boss-recommend-mcp";
46
- const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
52
+ const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h", "list-jobs", "jobs", "recommend-jobs"]);
47
53
  const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
48
54
  const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
49
55
  const installConfigDefaults = Object.freeze({
50
56
  llmThinkingLevel: "low",
51
57
  humanRestEnabled: false
52
58
  });
59
+ const bossChatRuntimeChildDirs = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
60
+ const bossChatCliUnsupportedStartCode = "CHAT_CLI_ASYNC_UNSUPPORTED_CDP_ONLY";
61
+ const calibrateUnsupportedCode = "CALIBRATE_UNSUPPORTED_CDP_ONLY";
62
+ const recommendCliRunUnsupportedCode = "RECOMMEND_CLI_RUN_UNSUPPORTED_CDP_ONLY";
53
63
 
54
64
  function getSkillSourceDir(name = skillName) {
55
65
  return path.join(packageRoot, "skills", name);
@@ -113,6 +123,125 @@ function pathExists(targetPath) {
113
123
  }
114
124
  }
115
125
 
126
+ function normalizeText(value) {
127
+ return String(value || "").replace(/\s+/g, " ").trim();
128
+ }
129
+
130
+ function isUnsafeRuntimeDirectory(targetPath) {
131
+ const resolved = path.resolve(String(targetPath || ""));
132
+ if (!resolved) return true;
133
+ if (path.parse(resolved).root.toLowerCase() === resolved.toLowerCase()) return true;
134
+ const normalized = resolved.replace(/\\/g, "/").toLowerCase();
135
+ if (process.platform === "win32") {
136
+ return (
137
+ normalized.endsWith("/windows")
138
+ || normalized.endsWith("/windows/system32")
139
+ || normalized.endsWith("/windows/syswow64")
140
+ || normalized.endsWith("/program files")
141
+ || normalized.endsWith("/program files (x86)")
142
+ );
143
+ }
144
+ return ["/system", "/usr", "/bin", "/sbin"].some((prefix) => (
145
+ normalized === prefix || normalized.startsWith(`${prefix}/`)
146
+ ));
147
+ }
148
+
149
+ function getBossChatRuntimeDirectories(runtime) {
150
+ return [
151
+ runtime.data_dir,
152
+ ...bossChatRuntimeChildDirs.map((name) => path.join(runtime.data_dir, name))
153
+ ];
154
+ }
155
+
156
+ function ensureBossChatRuntimeReadyLocal(workspaceRoot) {
157
+ const runtime = resolveCdpBossChatRuntimeLayout(workspaceRoot);
158
+ const runtimeDirectories = getBossChatRuntimeDirectories(runtime);
159
+ const created = [];
160
+ const existed = [];
161
+ const failed = [];
162
+ let migration = {
163
+ attempted: false,
164
+ performed: false,
165
+ source: runtime.migration_source_dir,
166
+ target: runtime.data_dir,
167
+ message: runtime.migration_source_dir
168
+ ? `Pending legacy boss-chat migration from ${runtime.migration_source_dir}`
169
+ : ""
170
+ };
171
+
172
+ if (isUnsafeRuntimeDirectory(runtime.data_dir)) {
173
+ return {
174
+ ...runtime,
175
+ directories: runtimeDirectories,
176
+ created,
177
+ existed,
178
+ failed: [
179
+ {
180
+ path: runtime.data_dir,
181
+ message: `Refusing unsafe boss-chat runtime path: ${runtime.data_dir}. Please use BOSS_CHAT_HOME in a writable user directory.`
182
+ }
183
+ ],
184
+ migration,
185
+ blocked_reason: "UNSAFE_DATA_DIR"
186
+ };
187
+ }
188
+
189
+ if (runtime.migration_source_dir) {
190
+ try {
191
+ fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
192
+ recursive: true,
193
+ force: false,
194
+ errorOnExist: false
195
+ });
196
+ migration = {
197
+ attempted: true,
198
+ performed: true,
199
+ source: runtime.migration_source_dir,
200
+ target: runtime.data_dir,
201
+ message: `Migrated legacy boss-chat runtime from ${runtime.migration_source_dir} to ${runtime.data_dir}. Legacy source was preserved.`
202
+ };
203
+ } catch (error) {
204
+ migration = {
205
+ attempted: true,
206
+ performed: false,
207
+ source: runtime.migration_source_dir,
208
+ target: runtime.data_dir,
209
+ message: error?.message || "Legacy boss-chat migration failed."
210
+ };
211
+ failed.push({
212
+ path: runtime.data_dir,
213
+ message: `Legacy migration failed: ${migration.message}`
214
+ });
215
+ }
216
+ }
217
+
218
+ for (const directory of runtimeDirectories) {
219
+ try {
220
+ const existedBefore = pathExists(directory);
221
+ ensureDir(directory);
222
+ if (existedBefore) {
223
+ existed.push(directory);
224
+ } else {
225
+ created.push(directory);
226
+ }
227
+ } catch (error) {
228
+ failed.push({
229
+ path: directory,
230
+ message: error?.message || String(error)
231
+ });
232
+ }
233
+ }
234
+
235
+ return {
236
+ ...runtime,
237
+ directories: runtimeDirectories,
238
+ created,
239
+ existed,
240
+ failed,
241
+ migration
242
+ };
243
+ }
244
+
116
245
  function readJsonObjectFileSafe(filePath) {
117
246
  if (!pathExists(filePath)) return {};
118
247
  try {
@@ -178,6 +307,235 @@ function getLegacyUserConfigPath() {
178
307
  return path.join(getCodexHome(), "boss-recommend-mcp", "screening-config.json");
179
308
  }
180
309
 
310
+ function getUserCalibrationPath() {
311
+ return path.join(getCodexHome(), "boss-recommend-mcp", "favorite-calibration.json");
312
+ }
313
+
314
+ function isUsableCalibrationFile(filePath) {
315
+ if (!filePath || !pathExists(filePath)) return false;
316
+ const parsed = readJsonObjectFileSafe(filePath);
317
+ return Boolean(
318
+ parsed
319
+ && parsed.favoritePosition
320
+ && Number.isFinite(parsed.favoritePosition.pageX)
321
+ && Number.isFinite(parsed.favoritePosition.pageY)
322
+ );
323
+ }
324
+
325
+ function resolveFavoriteCalibrationPath(workspaceRoot) {
326
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_CALIBRATION_FILE || "");
327
+ if (fromEnv) return path.resolve(fromEnv);
328
+
329
+ const configResolution = resolveBossScreeningConfig(workspaceRoot);
330
+ const screenConfigPath = configResolution.config_path || getUserConfigPath();
331
+ const screenConfig = readJsonObjectFileSafe(screenConfigPath);
332
+ const calibrationFile = normalizeText(screenConfig?.calibrationFile || "");
333
+ if (calibrationFile && screenConfigPath) {
334
+ return path.resolve(path.dirname(screenConfigPath), calibrationFile);
335
+ }
336
+ return getUserCalibrationPath();
337
+ }
338
+
339
+ function resolveRecruitCalibrationScriptPath(workspaceRoot) {
340
+ const fromEnv = normalizeText(process.env.BOSS_RECOMMEND_RECRUIT_CALIBRATION_SCRIPT || "");
341
+ const workspaceResolved = path.resolve(String(workspaceRoot || process.cwd()));
342
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
343
+ const candidates = [
344
+ fromEnv,
345
+ path.join(workspaceResolved, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
346
+ path.join(packageRoot, "..", "..", "boss recruit pipeline", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
347
+ path.join(appData, "npm", "node_modules", "@reconcrap", "boss-recruit-mcp", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
348
+ path.join(workspaceResolved, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs"),
349
+ path.join(packageRoot, "..", "boss-recruit-mcp-main", "vendor", "boss-screen-cli", "calibrate-favorite-position-v2.cjs")
350
+ ].filter(Boolean).map((item) => path.resolve(item));
351
+
352
+ for (const candidate of new Set(candidates)) {
353
+ if (pathExists(candidate)) return candidate;
354
+ }
355
+ return null;
356
+ }
357
+
358
+ function getFeaturedCalibrationResolutionLocal(workspaceRoot) {
359
+ const calibrationPath = resolveFavoriteCalibrationPath(workspaceRoot);
360
+ return {
361
+ calibration_path: calibrationPath,
362
+ calibration_exists: pathExists(calibrationPath),
363
+ calibration_usable: isUsableCalibrationFile(calibrationPath),
364
+ calibration_script_path: resolveRecruitCalibrationScriptPath(workspaceRoot)
365
+ };
366
+ }
367
+
368
+ function runProcessSyncLocal({ command, args = [], cwd = process.cwd() } = {}) {
369
+ try {
370
+ const result = spawnSync(command, args, {
371
+ cwd,
372
+ encoding: "utf8",
373
+ env: process.env,
374
+ shell: false,
375
+ windowsHide: true
376
+ });
377
+ const stdout = String(result.stdout || "").trim();
378
+ const stderr = String(result.stderr || "").trim();
379
+ const output = [stdout, stderr].filter(Boolean).join("\n").trim();
380
+ return {
381
+ ok: result.status === 0 && !result.error,
382
+ status: Number.isInteger(result.status) ? result.status : -1,
383
+ stdout,
384
+ stderr,
385
+ output,
386
+ error_code: result.error?.code || null,
387
+ error_message: result.error?.message || ""
388
+ };
389
+ } catch (error) {
390
+ return {
391
+ ok: false,
392
+ status: -1,
393
+ stdout: "",
394
+ stderr: "",
395
+ output: "",
396
+ error_code: error.code || "SPAWN_FAILED",
397
+ error_message: error.message || String(error)
398
+ };
399
+ }
400
+ }
401
+
402
+ function parseMajorVersion(raw) {
403
+ const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
404
+ if (!match) return null;
405
+ const major = Number.parseInt(match[1], 10);
406
+ return Number.isFinite(major) ? major : null;
407
+ }
408
+
409
+ function buildNodeCommandCheckLocal() {
410
+ const probe = runProcessSyncLocal({
411
+ command: "node",
412
+ args: ["--version"]
413
+ });
414
+ const major = parseMajorVersion(probe.output);
415
+ const versionOk = Number.isInteger(major) && major >= 18;
416
+ return {
417
+ key: "node_cli",
418
+ ok: probe.ok && versionOk,
419
+ path: "node --version",
420
+ message: probe.ok
421
+ ? (versionOk
422
+ ? `Node 命令可用 (${probe.output || "unknown version"})`
423
+ : `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
424
+ : `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
425
+ };
426
+ }
427
+
428
+ function buildNodePackageCheckLocal({ key, moduleName, cwd, missingMessage }) {
429
+ if (!cwd || !pathExists(cwd)) {
430
+ return {
431
+ key,
432
+ ok: false,
433
+ path: moduleName,
434
+ module: moduleName,
435
+ install_cwd: null,
436
+ message: missingMessage
437
+ };
438
+ }
439
+ const probe = runProcessSyncLocal({
440
+ command: "node",
441
+ args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
442
+ cwd
443
+ });
444
+ return {
445
+ key,
446
+ ok: probe.ok,
447
+ path: moduleName,
448
+ module: moduleName,
449
+ install_cwd: cwd,
450
+ message: probe.ok
451
+ ? `${moduleName} npm 依赖可用`
452
+ : `缺少 npm 依赖 ${moduleName},请在 boss-recommend-mcp 目录执行 npm install。`
453
+ };
454
+ }
455
+
456
+ function buildRuntimeDependencyChecksLocal({ dependencyDir = packageRoot } = {}) {
457
+ return [
458
+ buildNodeCommandCheckLocal(),
459
+ buildNodePackageCheckLocal({
460
+ key: "npm_dep_chrome_remote_interface",
461
+ moduleName: "chrome-remote-interface",
462
+ cwd: dependencyDir,
463
+ missingMessage: "无法校验 chrome-remote-interface:boss-recommend-mcp package 目录不存在。"
464
+ }),
465
+ buildNodePackageCheckLocal({
466
+ key: "npm_dep_ws",
467
+ moduleName: "ws",
468
+ cwd: dependencyDir,
469
+ missingMessage: "无法校验 ws:boss-recommend-mcp package 目录不存在。"
470
+ }),
471
+ buildNodePackageCheckLocal({
472
+ key: "npm_dep_sharp",
473
+ moduleName: "sharp",
474
+ cwd: dependencyDir,
475
+ missingMessage: "无法校验 sharp:boss-recommend-mcp package 目录不存在。"
476
+ })
477
+ ];
478
+ }
479
+
480
+ function resolveWorkspaceDebugPortLocal(workspaceRoot) {
481
+ const fromEnv = parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT);
482
+ if (fromEnv) return fromEnv;
483
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
484
+ const config = readJsonObjectFileSafe(configResolution.resolved_path);
485
+ return parsePositivePort(config?.debugPort) || 9222;
486
+ }
487
+
488
+ function buildScreenConfigCheckLocal(workspaceRoot, configResolution) {
489
+ const screenConfig = resolveBossScreeningConfig(workspaceRoot);
490
+ const pathForMessage = screenConfig.config_path || configResolution.resolved_path || configResolution.writable_path;
491
+ return {
492
+ key: "screen_config",
493
+ ok: screenConfig.ok,
494
+ path: pathForMessage,
495
+ reason: screenConfig.ok ? "OK" : (screenConfig.error?.code || "SCREEN_CONFIG_ERROR"),
496
+ message: screenConfig.ok ? "screening-config.json 可用" : (screenConfig.error?.message || "screening-config.json 不可用")
497
+ };
498
+ }
499
+
500
+ function runPipelinePreflightLocal(workspaceRoot, options = {}) {
501
+ const pageScope = normalizePageScope(options.pageScope) || "recommend";
502
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
503
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
504
+ const checks = [
505
+ buildScreenConfigCheckLocal(workspaceRoot, configResolution),
506
+ {
507
+ key: "favorite_calibration",
508
+ ok: calibrationResolution.calibration_usable,
509
+ path: calibrationResolution.calibration_path,
510
+ optional: pageScope !== "featured",
511
+ message: calibrationResolution.calibration_usable
512
+ ? "favorite-calibration.json 可用"
513
+ : "favorite-calibration.json 不存在或无效(精选页收藏仅支持校准坐标点击)"
514
+ }
515
+ ];
516
+ checks.push(...buildRuntimeDependencyChecksLocal({ dependencyDir: packageRoot }));
517
+
518
+ const requiredCheckKeys = new Set([
519
+ "screen_config",
520
+ "node_cli",
521
+ "npm_dep_chrome_remote_interface",
522
+ "npm_dep_ws",
523
+ "npm_dep_sharp"
524
+ ]);
525
+ if (pageScope === "featured") {
526
+ requiredCheckKeys.add("favorite_calibration");
527
+ }
528
+
529
+ return {
530
+ ok: checks.every((item) => !requiredCheckKeys.has(item.key) || item.ok),
531
+ checks,
532
+ debug_port: resolveWorkspaceDebugPortLocal(workspaceRoot),
533
+ config_resolution: configResolution,
534
+ calibration_path: calibrationResolution.calibration_path,
535
+ page_scope: pageScope
536
+ };
537
+ }
538
+
181
539
  function getSkillTargetDir(name = skillName) {
182
540
  return path.join(getCodexHome(), "skills", name);
183
541
  }
@@ -224,6 +582,12 @@ function parsePositivePort(raw) {
224
582
  return Number.isFinite(port) && port > 0 ? port : null;
225
583
  }
226
584
 
585
+ function parseNonNegativeInteger(raw, fallback = undefined) {
586
+ if (raw === undefined || raw === null || raw === "") return fallback;
587
+ const parsed = Number.parseInt(String(raw), 10);
588
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
589
+ }
590
+
227
591
  function parseBossChatTargetCountOption(raw) {
228
592
  if (raw === undefined || raw === null) return undefined;
229
593
  const text = String(raw).trim();
@@ -362,7 +726,7 @@ function buildMcpLaunchConfig(options = {}) {
362
726
  ? args
363
727
  : command === "boss-recommend-mcp"
364
728
  ? ["start"]
365
- : buildDefaultMcpArgs();
729
+ : buildDefaultMcpArgs(options);
366
730
  const launchConfig = { command, args: launchArgs };
367
731
  if (env && typeof env === "object" && !Array.isArray(env) && Object.keys(env).length > 0) {
368
732
  launchConfig.env = env;
@@ -431,24 +795,56 @@ function parseAgentTargets(rawValue) {
431
795
  return unique;
432
796
  }
433
797
 
798
+ function getExternalAppSupportBaseDirs() {
799
+ const home = os.homedir();
800
+ if (process.platform === "win32") {
801
+ return dedupePaths([
802
+ process.env.APPDATA || "",
803
+ path.join(home, "AppData", "Roaming")
804
+ ]);
805
+ }
806
+ if (process.platform === "darwin") {
807
+ return dedupePaths([
808
+ path.join(home, "Library", "Application Support")
809
+ ]);
810
+ }
811
+ return dedupePaths([
812
+ process.env.XDG_CONFIG_HOME || "",
813
+ path.join(home, ".config")
814
+ ]);
815
+ }
816
+
817
+ function buildAppUserPaths({ dirNames = [], tail = [] } = {}) {
818
+ const paths = [];
819
+ for (const baseDir of getExternalAppSupportBaseDirs()) {
820
+ const discovered = discoverAppDataDirsByPattern(baseDir, /^trae(?:[\s\-_]?cn)?$/i);
821
+ const names = dedupeLower([...dirNames, ...discovered]);
822
+ for (const dirName of names) {
823
+ paths.push(path.join(baseDir, dirName, "User", ...tail));
824
+ }
825
+ }
826
+ return dedupePaths(paths);
827
+ }
828
+
434
829
  function getKnownExternalMcpConfigPathsByAgent() {
435
830
  const home = os.homedir();
436
- const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
437
- const traeDirNames = dedupeLower([
831
+ const appBases = getExternalAppSupportBaseDirs();
832
+ const traeDirNames = [
438
833
  "Trae",
439
834
  "Trae CN",
440
835
  "TraeCN",
441
836
  "trae-cn",
442
- "trae_cn",
443
- ...discoverAppDataDirsByPattern(appData, /^trae(?:[\s\-_]?cn)?$/i)
444
- ]);
445
- const traeConfigPaths = traeDirNames.map((dir) => path.join(appData, dir, "User", "mcp.json"));
837
+ "trae_cn"
838
+ ];
839
+ const traeConfigPaths = buildAppUserPaths({ dirNames: traeDirNames, tail: ["mcp.json"] });
840
+ const cursorConfigPaths = appBases.map((baseDir) => path.join(baseDir, "Cursor", "User", "mcp.json"));
841
+ const openClawConfigPaths = appBases.map((baseDir) => path.join(baseDir, "OpenClaw", "User", "mcp.json"));
446
842
  return {
447
- cursor: [path.join(appData, "Cursor", "User", "mcp.json"), path.join(home, ".cursor", "mcp.json")],
843
+ cursor: [...cursorConfigPaths, path.join(home, ".cursor", "mcp.json")],
448
844
  trae: [...traeConfigPaths, path.join(home, ".trae", "mcp.json"), path.join(home, ".trae-cn", "mcp.json")],
449
845
  "trae-cn": [...traeConfigPaths, path.join(home, ".trae-cn", "mcp.json"), path.join(home, ".trae", "mcp.json")],
450
846
  claude: [path.join(home, ".claude", "mcp.json")],
451
- openclaw: [path.join(home, ".openclaw", "mcp.json")]
847
+ openclaw: [path.join(home, ".openclaw", "mcp.json"), ...openClawConfigPaths]
452
848
  };
453
849
  }
454
850
 
@@ -475,21 +871,40 @@ function mergeMcpServerConfigFile(filePath, options = {}) {
475
871
  ? current.mcpServers
476
872
  : {};
477
873
  const existingEntry = existingServers[serverName];
874
+ const retainedServers = {};
875
+ const migratedLegacyServers = [];
876
+ for (const [name, config] of Object.entries(existingServers)) {
877
+ if (name === serverName) continue;
878
+ if (isBossMcpServerEntry(name, config)) {
879
+ migratedLegacyServers.push(name);
880
+ continue;
881
+ }
882
+ retainedServers[name] = config;
883
+ }
478
884
  const merged = {
479
885
  ...current,
480
886
  mcpServers: {
481
- ...existingServers,
887
+ ...retainedServers,
482
888
  [serverName]: launchConfig
483
889
  }
484
890
  };
485
891
 
486
892
  ensureDir(path.dirname(filePath));
893
+ const before = pathExists(filePath) ? fs.readFileSync(filePath, "utf8") : "";
894
+ const next = JSON.stringify(merged, null, 2);
895
+ let backupFile = null;
896
+ if (before && before.trim() !== next.trim()) {
897
+ backupFile = `${filePath}.boss-mcp-migration-${new Date().toISOString().replace(/[:.]/g, "-")}.bak`;
898
+ fs.writeFileSync(backupFile, before, "utf8");
899
+ }
487
900
  fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
488
- const updated = JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
901
+ const updated = before.trim() !== next.trim() || JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
489
902
  return {
490
903
  file: filePath,
491
904
  server: serverName,
492
- updated
905
+ updated,
906
+ migrated_legacy_servers: migratedLegacyServers,
907
+ backup_file: backupFile
493
908
  };
494
909
  }
495
910
 
@@ -505,7 +920,9 @@ function installExternalMcpConfigs(options = {}) {
505
920
  file: target,
506
921
  server: merged.server,
507
922
  created: !existed,
508
- updated: merged.updated
923
+ updated: merged.updated,
924
+ migrated_legacy_servers: merged.migrated_legacy_servers,
925
+ backup_file: merged.backup_file
509
926
  });
510
927
  } catch (error) {
511
928
  skipped.push({
@@ -519,25 +936,30 @@ function installExternalMcpConfigs(options = {}) {
519
936
 
520
937
  function getKnownExternalSkillBaseDirsByAgent() {
521
938
  const home = os.homedir();
522
- const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
523
- const traeDirNames = dedupeLower([
939
+ const appBases = getExternalAppSupportBaseDirs();
940
+ const traeDirNames = [
524
941
  "Trae",
525
942
  "Trae CN",
526
943
  "TraeCN",
527
944
  "trae-cn",
528
- "trae_cn",
529
- ...discoverAppDataDirsByPattern(appData, /^trae(?:[\s\-_]?cn)?$/i)
530
- ]);
531
- const traeSkillDirs = traeDirNames.map((dir) => path.join(appData, dir, "User", "skills"));
945
+ "trae_cn"
946
+ ];
947
+ const traeSkillDirs = buildAppUserPaths({ dirNames: traeDirNames, tail: ["skills"] });
948
+ const cursorSkillDirs = appBases.map((baseDir) => path.join(baseDir, "Cursor", "User", "skills"));
949
+ const openClawSkillDirs = appBases.map((baseDir) => path.join(baseDir, "OpenClaw", "User", "skills"));
532
950
  return {
533
- cursor: [path.join(home, ".cursor", "skills"), path.join(appData, "Cursor", "User", "skills")],
951
+ cursor: [path.join(home, ".cursor", "skills"), ...cursorSkillDirs],
534
952
  trae: [path.join(home, ".trae", "skills"), path.join(home, ".trae-cn", "skills"), ...traeSkillDirs],
535
953
  "trae-cn": [path.join(home, ".trae-cn", "skills"), path.join(home, ".trae", "skills"), ...traeSkillDirs],
536
954
  claude: [path.join(home, ".claude", "skills")],
537
- openclaw: [path.join(home, ".openclaw", "skills"), path.join(appData, "OpenClaw", "User", "skills")]
955
+ openclaw: [path.join(home, ".openclaw", "skills"), ...openClawSkillDirs]
538
956
  };
539
957
  }
540
958
 
959
+ function serializeMcpLaunchConfig(launchConfig) {
960
+ return JSON.stringify(launchConfig || {}).toLowerCase().replace(/\\/g, "/");
961
+ }
962
+
541
963
  function isRecommendMcpLaunchConfig(launchConfig) {
542
964
  if (!launchConfig || typeof launchConfig !== "object") return false;
543
965
  const command = String(launchConfig.command || "").toLowerCase();
@@ -551,14 +973,33 @@ function isRecommendMcpLaunchConfig(launchConfig) {
551
973
  );
552
974
  }
553
975
 
976
+ function isBossMcpServerEntry(name, launchConfig) {
977
+ const lowerName = String(name || "").toLowerCase();
978
+ const serialized = serializeMcpLaunchConfig(launchConfig);
979
+ return (
980
+ /boss[-_\s]?(recommend|recruit|chat)/i.test(lowerName)
981
+ || serialized.includes(recommendMcpPackageName.toLowerCase())
982
+ || serialized.includes("@reconcrap/boss-recruit-mcp")
983
+ || serialized.includes("@reconcrap/boss-chat")
984
+ || serialized.includes("boss-recommend-mcp")
985
+ || serialized.includes("boss-recruit-mcp")
986
+ || serialized.includes("boss-chat")
987
+ || serialized.includes("boss recommend pipeline")
988
+ || serialized.includes("boss recruit pipeline")
989
+ );
990
+ }
991
+
554
992
  function inspectMcpServerEntries(filePath) {
555
993
  if (!pathExists(filePath)) {
556
994
  return {
557
995
  exists: false,
558
996
  has_boss_recommend: false,
559
997
  has_boss_recruit: false,
998
+ has_boss_chat: false,
560
999
  recommend_server_names: [],
561
- recruit_server_names: []
1000
+ recruit_server_names: [],
1001
+ chat_server_names: [],
1002
+ boss_server_names: []
562
1003
  };
563
1004
  }
564
1005
  const parsed = readJsonObjectFileSafe(filePath);
@@ -567,8 +1008,13 @@ function inspectMcpServerEntries(filePath) {
567
1008
  : {};
568
1009
  const recommendNames = [];
569
1010
  const recruitNames = [];
1011
+ const chatNames = [];
1012
+ const bossNames = [];
570
1013
  for (const [name, config] of Object.entries(servers)) {
571
1014
  const lowerName = String(name || "").toLowerCase();
1015
+ if (isBossMcpServerEntry(name, config)) {
1016
+ bossNames.push(name);
1017
+ }
572
1018
  if (isRecommendMcpLaunchConfig(config) || lowerName.includes("boss-recommend")) {
573
1019
  recommendNames.push(name);
574
1020
  }
@@ -580,13 +1026,23 @@ function inspectMcpServerEntries(filePath) {
580
1026
  ) {
581
1027
  recruitNames.push(name);
582
1028
  }
1029
+ if (
1030
+ lowerName.includes("boss-chat")
1031
+ || serialized.includes("@reconcrap/boss-chat")
1032
+ || serialized.includes("boss-chat")
1033
+ ) {
1034
+ chatNames.push(name);
1035
+ }
583
1036
  }
584
1037
  return {
585
1038
  exists: true,
586
1039
  has_boss_recommend: recommendNames.length > 0,
587
1040
  has_boss_recruit: recruitNames.length > 0,
1041
+ has_boss_chat: chatNames.length > 0,
588
1042
  recommend_server_names: recommendNames,
589
- recruit_server_names: recruitNames
1043
+ recruit_server_names: recruitNames,
1044
+ chat_server_names: chatNames,
1045
+ boss_server_names: bossNames
590
1046
  };
591
1047
  }
592
1048
 
@@ -610,9 +1066,16 @@ function mirrorSkillToExternalDirs(options = {}) {
610
1066
  for (const bundledSkillName of bundledSkillNames) {
611
1067
  try {
612
1068
  const targetDir = path.join(baseDir, bundledSkillName);
1069
+ const legacyBeforeCopy = isLegacyBossSkillDir(targetDir);
613
1070
  ensureDir(path.dirname(targetDir));
614
1071
  fs.cpSync(getSkillSourceDir(bundledSkillName), targetDir, { recursive: true, force: true });
615
- mirrored.push({ base_dir: baseDir, target_dir: targetDir, skill: bundledSkillName });
1072
+ fs.writeFileSync(path.join(targetDir, ".installed-version"), `${packageVersion}\n`, "utf8");
1073
+ mirrored.push({
1074
+ base_dir: baseDir,
1075
+ target_dir: targetDir,
1076
+ skill: bundledSkillName,
1077
+ replaced_legacy: legacyBeforeCopy
1078
+ });
616
1079
  } catch (error) {
617
1080
  skipped.push({ base_dir: baseDir, skill: bundledSkillName, reason: error.message });
618
1081
  }
@@ -621,6 +1084,23 @@ function mirrorSkillToExternalDirs(options = {}) {
621
1084
  return { baseDirs, mirrored, skipped };
622
1085
  }
623
1086
 
1087
+ function isLegacyBossSkillDir(targetDir) {
1088
+ const skillFile = path.join(targetDir, "SKILL.md");
1089
+ if (!pathExists(skillFile)) return false;
1090
+ try {
1091
+ const content = fs.readFileSync(skillFile, "utf8").toLowerCase();
1092
+ return (
1093
+ content.includes("@reconcrap/boss-recruit-mcp")
1094
+ || content.includes("@reconcrap/boss-chat")
1095
+ || content.includes("boss-screen-cli")
1096
+ || content.includes(`runtime.${"evaluate"}`)
1097
+ || content.includes("page js")
1098
+ );
1099
+ } catch {
1100
+ return false;
1101
+ }
1102
+ }
1103
+
624
1104
  function syncSkillAssets(options = {}) {
625
1105
  const force = options.force === true;
626
1106
  const results = [];
@@ -670,9 +1150,9 @@ function pathStartsWith(filePath, rootPath) {
670
1150
  return file.startsWith(root);
671
1151
  }
672
1152
 
673
- function resolveCliConfigTarget(options = {}) {
1153
+ async function resolveCliConfigTarget(options = {}) {
674
1154
  const workspaceRoot = getWorkspaceRoot(options);
675
- const resolution = getScreenConfigResolution(workspaceRoot);
1155
+ const resolution = getBossScreenConfigResolution(workspaceRoot);
676
1156
  const workspacePreferred = (resolution.candidate_paths || []).find((item) => pathStartsWith(item, workspaceRoot)) || null;
677
1157
  const configPath = resolution.writable_path || resolution.resolved_path || workspacePreferred || getUserConfigPath();
678
1158
  return {
@@ -698,8 +1178,8 @@ function applyMissingInstallConfigDefaults(config = {}) {
698
1178
  };
699
1179
  }
700
1180
 
701
- function ensureUserConfig(options = {}) {
702
- const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
1181
+ async function ensureUserConfig(options = {}) {
1182
+ const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
703
1183
  const writeTargets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
704
1184
  let lastError = null;
705
1185
  for (const targetPath of writeTargets) {
@@ -744,16 +1224,16 @@ function ensureUserConfig(options = {}) {
744
1224
  throw lastError || new Error("No writable target for screening-config.json");
745
1225
  }
746
1226
 
747
- function collectRuntimeDirectories(options = {}) {
1227
+ async function collectRuntimeDirectories(options = {}) {
748
1228
  const workspaceRoot = getWorkspaceRoot(options);
749
1229
  const stateHome = getStateHome();
750
- const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
1230
+ const runtime = resolveCdpBossChatRuntimeLayout(workspaceRoot);
751
1231
  const bossChatRoot = runtime.data_dir;
752
1232
  const recommendRuntimeDirs = [
753
1233
  stateHome,
754
1234
  path.join(stateHome, "runs")
755
1235
  ];
756
- const bossChatRuntimeDirs = runtime.directories || [bossChatRoot];
1236
+ const bossChatRuntimeDirs = getBossChatRuntimeDirectories(runtime);
757
1237
  return {
758
1238
  workspaceRoot,
759
1239
  stateHome,
@@ -767,9 +1247,9 @@ function collectRuntimeDirectories(options = {}) {
767
1247
  };
768
1248
  }
769
1249
 
770
- function ensureRuntimeDirectories(options = {}) {
771
- const { workspaceRoot, stateHome } = collectRuntimeDirectories(options);
772
- const runtime = ensureBossChatRuntimeReady(workspaceRoot);
1250
+ async function ensureRuntimeDirectories(options = {}) {
1251
+ const { workspaceRoot, stateHome } = await collectRuntimeDirectories(options);
1252
+ const runtime = ensureBossChatRuntimeReadyLocal(workspaceRoot);
773
1253
  const recommendCreated = [];
774
1254
  const recommendExisted = [];
775
1255
  const failed = [...runtime.failed];
@@ -813,8 +1293,8 @@ function readJsonObjectFile(filePath) {
813
1293
  return parsed;
814
1294
  }
815
1295
 
816
- function loadBestExistingUserConfig(options = {}) {
817
- const { resolution, configPath, workspacePreferred } = resolveCliConfigTarget(options);
1296
+ async function loadBestExistingUserConfig(options = {}) {
1297
+ const { resolution, configPath, workspacePreferred } = await resolveCliConfigTarget(options);
818
1298
  const candidates = dedupePaths([
819
1299
  ...(resolution.candidate_paths || []),
820
1300
  configPath,
@@ -837,8 +1317,8 @@ function loadBestExistingUserConfig(options = {}) {
837
1317
  return { path: configPath, config: {} };
838
1318
  }
839
1319
 
840
- function writeConfigWithFallback(nextConfig, options = {}) {
841
- const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
1320
+ async function writeConfigWithFallback(nextConfig, options = {}) {
1321
+ const { configPath, workspacePreferred } = await resolveCliConfigTarget(options);
842
1322
  const targets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
843
1323
  let lastError = null;
844
1324
  for (const target of targets) {
@@ -860,14 +1340,14 @@ function writeConfigWithFallback(nextConfig, options = {}) {
860
1340
  throw lastError || new Error("No writable target for screening-config.json");
861
1341
  }
862
1342
 
863
- function persistDebugPortSelection(port, options = {}) {
864
- const { config } = loadBestExistingUserConfig(options);
1343
+ async function persistDebugPortSelection(port, options = {}) {
1344
+ const { config } = await loadBestExistingUserConfig(options);
865
1345
  config.debugPort = port;
866
- const configPath = writeConfigWithFallback(config, options);
1346
+ const configPath = await writeConfigWithFallback(config, options);
867
1347
  return { port, configPath };
868
1348
  }
869
1349
 
870
- function setDebugPort(options = {}) {
1350
+ async function setDebugPort(options = {}) {
871
1351
  const selected = parsePositivePort(options.port);
872
1352
  if (!selected) {
873
1353
  throw new Error("Missing required --port <number> for set-port.");
@@ -876,7 +1356,7 @@ function setDebugPort(options = {}) {
876
1356
  return persistDebugPortSelection(selected, options);
877
1357
  }
878
1358
 
879
- function setScreeningConfig(options = {}) {
1359
+ async function setScreeningConfig(options = {}) {
880
1360
  const baseUrl = String(options["base-url"] || options.baseUrl || "").trim();
881
1361
  const apiKey = String(options["api-key"] || options.apiKey || "").trim();
882
1362
  const model = String(options.model || "").trim();
@@ -884,7 +1364,7 @@ function setScreeningConfig(options = {}) {
884
1364
  throw new Error("Missing required fields: --base-url, --api-key, --model");
885
1365
  }
886
1366
 
887
- const { config: existing } = loadBestExistingUserConfig(options);
1367
+ const { config: existing } = await loadBestExistingUserConfig(options);
888
1368
  const nextConfig = {
889
1369
  ...existing,
890
1370
  baseUrl,
@@ -909,7 +1389,7 @@ function setScreeningConfig(options = {}) {
909
1389
  if (debugPort) {
910
1390
  nextConfig.debugPort = debugPort;
911
1391
  }
912
- const configPath = writeConfigWithFallback(nextConfig, options);
1392
+ const configPath = await writeConfigWithFallback(nextConfig, options);
913
1393
  return { path: configPath, updated: true };
914
1394
  }
915
1395
 
@@ -926,6 +1406,20 @@ async function listChromeTabs(port) {
926
1406
  return Array.isArray(data) ? data : [];
927
1407
  }
928
1408
 
1409
+ function buildBossPageState(payload) {
1410
+ return {
1411
+ key: "boss_page_state",
1412
+ ...payload
1413
+ };
1414
+ }
1415
+
1416
+ function extractSampleUrls(tabs, limit = 5) {
1417
+ return tabs
1418
+ .map((tab) => tab?.url)
1419
+ .filter(Boolean)
1420
+ .slice(0, limit);
1421
+ }
1422
+
929
1423
  function findChromeOnboardingUrl(tabs) {
930
1424
  for (const tab of tabs) {
931
1425
  if (typeof tab?.url === "string" && chromeOnboardingUrlPattern.test(tab.url)) {
@@ -935,6 +1429,383 @@ function findChromeOnboardingUrl(tabs) {
935
1429
  return null;
936
1430
  }
937
1431
 
1432
+ function isBossRecommendTab(tab) {
1433
+ return typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend");
1434
+ }
1435
+
1436
+ function findBossRecommendTab(tabs = []) {
1437
+ return tabs.find((tab) => isBossRecommendTab(tab)) || null;
1438
+ }
1439
+
1440
+ function isBossLoginTab(tab) {
1441
+ const url = String(tab?.url || "");
1442
+ const title = String(tab?.title || "");
1443
+ return (
1444
+ url === bossLoginUrl
1445
+ || bossLoginUrlPattern.test(url)
1446
+ || bossLoginTitlePattern.test(title)
1447
+ );
1448
+ }
1449
+
1450
+ function findBossPageTab(tabs = []) {
1451
+ return tabs.find((tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")) || null;
1452
+ }
1453
+
1454
+ function getNodeAttribute(node, name) {
1455
+ const attributes = node?.attributes || [];
1456
+ for (let index = 0; index < attributes.length; index += 2) {
1457
+ if (attributes[index] === name) return attributes[index + 1] || "";
1458
+ }
1459
+ return "";
1460
+ }
1461
+
1462
+ function uniqueMethodNames(methodLog = []) {
1463
+ return Array.from(new Set(methodLog.map((entry) => entry?.method).filter(Boolean)));
1464
+ }
1465
+
1466
+ async function inspectBossRecommendPageStateCdp(port, options = {}) {
1467
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1468
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
1469
+ const expectedUrl = options.expectedUrl || bossUrl;
1470
+ const deadline = Date.now() + timeoutMs;
1471
+ let lastError = null;
1472
+ let lastTabs = [];
1473
+
1474
+ while (Date.now() <= deadline) {
1475
+ try {
1476
+ const tabs = await listChromeTabs(port);
1477
+ lastTabs = tabs;
1478
+ const recommendTab = findBossRecommendTab(tabs);
1479
+ if (recommendTab) {
1480
+ if (isBossLoginTab(recommendTab)) {
1481
+ return buildBossPageState({
1482
+ ok: false,
1483
+ state: "LOGIN_REQUIRED",
1484
+ path: recommendTab.url || bossLoginUrl,
1485
+ current_url: recommendTab.url || bossLoginUrl,
1486
+ title: recommendTab.title || null,
1487
+ requires_login: true,
1488
+ expected_url: expectedUrl,
1489
+ login_url: bossLoginUrl,
1490
+ message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
1491
+ });
1492
+ }
1493
+ return buildBossPageState({
1494
+ ok: true,
1495
+ state: "RECOMMEND_READY",
1496
+ path: recommendTab.url,
1497
+ current_url: recommendTab.url,
1498
+ title: recommendTab.title || null,
1499
+ requires_login: false,
1500
+ expected_url: expectedUrl,
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 = findBossPageTab(tabs);
1521
+ if (bossTab) {
1522
+ const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
1523
+ return buildBossPageState({
1524
+ ok: false,
1525
+ state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
1526
+ path: bossTab.url,
1527
+ current_url: bossTab.url,
1528
+ title: bossTab.title || null,
1529
+ requires_login: requiresLogin,
1530
+ expected_url: expectedUrl,
1531
+ login_url: requiresLogin ? bossLoginUrl : undefined,
1532
+ message: requiresLogin
1533
+ ? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
1534
+ : "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
1535
+ });
1536
+ }
1537
+ } catch (error) {
1538
+ lastError = error;
1539
+ }
1540
+
1541
+ await sleepMs(pollMs);
1542
+ }
1543
+
1544
+ if (lastError) {
1545
+ return buildBossPageState({
1546
+ ok: false,
1547
+ state: "DEBUG_PORT_UNREACHABLE",
1548
+ path: `http://127.0.0.1:${port}`,
1549
+ current_url: null,
1550
+ title: null,
1551
+ requires_login: false,
1552
+ expected_url: expectedUrl,
1553
+ message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
1554
+ error: lastError.message
1555
+ });
1556
+ }
1557
+
1558
+ const onboardingUrl = findChromeOnboardingUrl(lastTabs);
1559
+ if (onboardingUrl) {
1560
+ return buildBossPageState({
1561
+ ok: false,
1562
+ state: "CHROME_ONBOARDING_INTERCEPTED",
1563
+ path: onboardingUrl,
1564
+ current_url: onboardingUrl,
1565
+ title: null,
1566
+ requires_login: false,
1567
+ expected_url: expectedUrl,
1568
+ message: "Chrome 当前停留在登录或引导页,尚未稳定到 Boss 推荐页。",
1569
+ sample_urls: extractSampleUrls(lastTabs)
1570
+ });
1571
+ }
1572
+
1573
+ return buildBossPageState({
1574
+ ok: false,
1575
+ state: "BOSS_TAB_NOT_FOUND",
1576
+ path: expectedUrl,
1577
+ current_url: null,
1578
+ title: null,
1579
+ requires_login: false,
1580
+ expected_url: expectedUrl,
1581
+ message: "未检测到 Boss 推荐页标签页。",
1582
+ sample_urls: extractSampleUrls(lastTabs)
1583
+ });
1584
+ }
1585
+
1586
+ async function withRecommendTargetCdp(port, callback) {
1587
+ const connection = await connectToChromeTarget({
1588
+ port,
1589
+ targetPredicate: (target) => isBossRecommendTab(target)
1590
+ });
1591
+ try {
1592
+ return await callback(connection);
1593
+ } finally {
1594
+ await connection.close();
1595
+ }
1596
+ }
1597
+
1598
+ async function bringBossRecommendTabToFrontCdp(port) {
1599
+ try {
1600
+ return await withRecommendTargetCdp(port, async ({ client, methodLog }) => {
1601
+ await enableDomains(client, ["Page"]);
1602
+ await bringPageToFront(client);
1603
+ assertNoForbiddenCdpCalls(methodLog);
1604
+ return {
1605
+ ok: true,
1606
+ method_log: uniqueMethodNames(methodLog)
1607
+ };
1608
+ });
1609
+ } catch (error) {
1610
+ return {
1611
+ ok: false,
1612
+ error: error.message || String(error)
1613
+ };
1614
+ }
1615
+ }
1616
+
1617
+ async function probeRecommendIframeStateCdp(port, options = {}) {
1618
+ const expectedUrl = options.expectedUrl || bossUrl;
1619
+ try {
1620
+ return await withRecommendTargetCdp(port, async ({ client, target, methodLog }) => {
1621
+ await enableDomains(client, ["Page", "DOM"]);
1622
+ const root = await getDocumentRoot(client, { depth: 1, pierce: true });
1623
+ const iframeNodeId = await querySelector(client, root.nodeId, 'iframe[name="recommendFrame"]');
1624
+ if (!iframeNodeId) {
1625
+ assertNoForbiddenCdpCalls(methodLog);
1626
+ return buildBossPageState({
1627
+ ok: false,
1628
+ state: "NO_RECOMMEND_IFRAME",
1629
+ path: target.url || expectedUrl,
1630
+ current_url: target.url || null,
1631
+ title: target.title || null,
1632
+ expected_url: expectedUrl,
1633
+ message: "recommend iframe 尚未挂载。",
1634
+ method_log: uniqueMethodNames(methodLog)
1635
+ });
1636
+ }
1637
+
1638
+ const described = await client.DOM.describeNode({
1639
+ nodeId: iframeNodeId,
1640
+ depth: 1,
1641
+ pierce: true
1642
+ });
1643
+ const iframeNode = described.node || {};
1644
+ const frameDocument = iframeNode.contentDocument || null;
1645
+ const frameUrl = frameDocument?.documentURL || getNodeAttribute(iframeNode, "src") || null;
1646
+ assertNoForbiddenCdpCalls(methodLog);
1647
+ if (!frameDocument?.nodeId) {
1648
+ return buildBossPageState({
1649
+ ok: false,
1650
+ state: "RECOMMEND_IFRAME_DOCUMENT_PENDING",
1651
+ path: target.url || expectedUrl,
1652
+ current_url: target.url || null,
1653
+ title: target.title || null,
1654
+ expected_url: expectedUrl,
1655
+ frame_url: frameUrl,
1656
+ message: "recommend iframe 已挂载但文档尚未就绪。",
1657
+ method_log: uniqueMethodNames(methodLog)
1658
+ });
1659
+ }
1660
+
1661
+ return buildBossPageState({
1662
+ ok: true,
1663
+ state: "RECOMMEND_IFRAME_READY",
1664
+ path: target.url || expectedUrl,
1665
+ current_url: target.url || null,
1666
+ title: target.title || null,
1667
+ expected_url: expectedUrl,
1668
+ frame_url: frameUrl,
1669
+ iframe_node_id: iframeNodeId,
1670
+ frame_document_node_id: frameDocument.nodeId,
1671
+ message: "recommend iframe 已通过 CDP DOM 检测就绪。",
1672
+ method_log: uniqueMethodNames(methodLog)
1673
+ });
1674
+ });
1675
+ } catch (error) {
1676
+ return buildBossPageState({
1677
+ ok: false,
1678
+ state: "RECOMMEND_IFRAME_PROBE_FAILED",
1679
+ path: expectedUrl,
1680
+ current_url: null,
1681
+ title: null,
1682
+ expected_url: expectedUrl,
1683
+ message: "recommend iframe CDP DOM 检测失败。",
1684
+ error: error.message || String(error)
1685
+ });
1686
+ }
1687
+ }
1688
+
1689
+ async function waitForRecommendIframeReadyCdp(port, options = {}) {
1690
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
1691
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1692
+ const deadline = Date.now() + timeoutMs;
1693
+ let lastState = null;
1694
+
1695
+ while (Date.now() <= deadline) {
1696
+ lastState = await probeRecommendIframeStateCdp(port, options);
1697
+ if (lastState?.state === "RECOMMEND_IFRAME_READY") return lastState;
1698
+ await sleepMs(pollMs);
1699
+ }
1700
+
1701
+ return lastState || buildBossPageState({
1702
+ ok: false,
1703
+ state: "NO_RECOMMEND_IFRAME",
1704
+ path: options.expectedUrl || bossUrl,
1705
+ current_url: null,
1706
+ title: null,
1707
+ expected_url: options.expectedUrl || bossUrl,
1708
+ message: "recommend iframe 尚未就绪。"
1709
+ });
1710
+ }
1711
+
1712
+ async function verifyRecommendPageStableCdp(port, options = {}) {
1713
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1000;
1714
+ const recheckTimeoutMs = Number.isFinite(options.recheckTimeoutMs) ? options.recheckTimeoutMs : 6000;
1715
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1716
+
1717
+ await sleepMs(settleMs);
1718
+ const recheck = await inspectBossRecommendPageStateCdp(port, {
1719
+ timeoutMs: recheckTimeoutMs,
1720
+ pollMs
1721
+ });
1722
+ if (recheck.state !== "RECOMMEND_READY") return recheck;
1723
+
1724
+ const iframeState = await waitForRecommendIframeReadyCdp(port, {
1725
+ timeoutMs: recheckTimeoutMs,
1726
+ pollMs
1727
+ });
1728
+ if (iframeState.state === "RECOMMEND_IFRAME_READY") {
1729
+ return buildBossPageState({
1730
+ ...recheck,
1731
+ ok: true,
1732
+ state: "RECOMMEND_READY",
1733
+ frame_url: iframeState.frame_url || null,
1734
+ iframe_state: iframeState,
1735
+ method_log: iframeState.method_log || []
1736
+ });
1737
+ }
1738
+
1739
+ return buildBossPageState({
1740
+ ...iframeState,
1741
+ state: iframeState.state || "NO_RECOMMEND_IFRAME",
1742
+ message: iframeState.message || "Boss recommend 页面已打开,但 iframe 尚未就绪。"
1743
+ });
1744
+ }
1745
+
1746
+ async function navigateExistingTargetToBossRecommendCdp(port) {
1747
+ let connection = null;
1748
+ try {
1749
+ connection = await connectToChromeTarget({
1750
+ port,
1751
+ targetPredicate: (target) => target?.type === "page"
1752
+ });
1753
+ await enableDomains(connection.client, ["Page"]);
1754
+ await connection.client.Page.navigate({ url: bossUrl });
1755
+ assertNoForbiddenCdpCalls(connection.methodLog);
1756
+ return {
1757
+ ok: true,
1758
+ via: "cdp_page_navigate",
1759
+ target_id: connection.target?.id || null,
1760
+ method_log: uniqueMethodNames(connection.methodLog)
1761
+ };
1762
+ } catch (error) {
1763
+ return {
1764
+ ok: false,
1765
+ via: "cdp_page_navigate",
1766
+ error: error.message || String(error)
1767
+ };
1768
+ } finally {
1769
+ if (connection) await connection.close();
1770
+ }
1771
+ }
1772
+
1773
+ async function openBossRecommendTabCdp(port) {
1774
+ const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossUrl)}`;
1775
+ const attempts = ["PUT", "GET"];
1776
+ let lastError = null;
1777
+
1778
+ for (const method of attempts) {
1779
+ try {
1780
+ const response = await fetch(endpoint, { method });
1781
+ if (response.ok) {
1782
+ let payload = null;
1783
+ try {
1784
+ payload = await response.json();
1785
+ } catch {}
1786
+ return {
1787
+ ok: true,
1788
+ via: "devtools_http_new_tab",
1789
+ method,
1790
+ target_id: payload?.id || null,
1791
+ current_url: payload?.url || bossUrl
1792
+ };
1793
+ }
1794
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
1795
+ } catch (error) {
1796
+ lastError = error;
1797
+ }
1798
+ }
1799
+
1800
+ const fallback = await navigateExistingTargetToBossRecommendCdp(port);
1801
+ if (fallback.ok) return fallback;
1802
+ return {
1803
+ ok: false,
1804
+ via: "devtools_http_new_tab",
1805
+ error: lastError?.message || fallback.error || "Failed to open Boss recommend tab via DevTools"
1806
+ };
1807
+ }
1808
+
938
1809
  function getDefaultChromeExecutableCandidates() {
939
1810
  const candidates = [process.env.BOSS_RECOMMEND_CHROME_PATH].filter(Boolean);
940
1811
  if (process.platform === "win32") {
@@ -997,16 +1868,118 @@ function resolveDefaultChromeUserDataDir(port) {
997
1868
  return legacyExisting || sharedPath;
998
1869
  }
999
1870
 
1871
+ function getLaunchChromeTiming(options = {}) {
1872
+ if (options["slow-live"] || options.slowLive) {
1873
+ return {
1874
+ initialTimeoutMs: 5000,
1875
+ inspectTimeoutMs: 20000,
1876
+ pollMs: 1000,
1877
+ settleMs: 2000
1878
+ };
1879
+ }
1880
+ return {
1881
+ initialTimeoutMs: 1500,
1882
+ inspectTimeoutMs: 6000,
1883
+ pollMs: 800,
1884
+ settleMs: 1000
1885
+ };
1886
+ }
1887
+
1888
+ async function ensureBossRecommendPageReadyCdp(port, options = {}) {
1889
+ const attempts = Number.isFinite(options.attempts) ? Math.max(0, options.attempts) : 3;
1890
+ const inspectTimeoutMs = Number.isFinite(options.inspectTimeoutMs) ? options.inspectTimeoutMs : 6000;
1891
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 800;
1892
+ const settleMs = Number.isFinite(options.settleMs) ? options.settleMs : 1000;
1893
+
1894
+ let pageState = await inspectBossRecommendPageStateCdp(port, {
1895
+ timeoutMs: inspectTimeoutMs,
1896
+ pollMs
1897
+ });
1898
+ if (pageState.state === "RECOMMEND_READY") {
1899
+ const stableState = await verifyRecommendPageStableCdp(port, {
1900
+ settleMs,
1901
+ recheckTimeoutMs: inspectTimeoutMs,
1902
+ pollMs
1903
+ });
1904
+ return {
1905
+ ok: stableState.state === "RECOMMEND_READY",
1906
+ debug_port: port,
1907
+ state: stableState.state,
1908
+ page_state: stableState
1909
+ };
1910
+ }
1911
+
1912
+ if (pageState.state === "LOGIN_REQUIRED") {
1913
+ return {
1914
+ ok: false,
1915
+ debug_port: port,
1916
+ state: pageState.state,
1917
+ page_state: pageState
1918
+ };
1919
+ }
1920
+
1921
+ let openAttempt = null;
1922
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1923
+ if (pageState.state === "DEBUG_PORT_UNREACHABLE" || pageState.state === "LOGIN_REQUIRED") break;
1924
+ openAttempt = await openBossRecommendTabCdp(port);
1925
+ await sleepMs(settleMs);
1926
+ pageState = await inspectBossRecommendPageStateCdp(port, {
1927
+ timeoutMs: inspectTimeoutMs,
1928
+ pollMs
1929
+ });
1930
+ if (pageState.state === "RECOMMEND_READY") {
1931
+ const stableState = await verifyRecommendPageStableCdp(port, {
1932
+ settleMs,
1933
+ recheckTimeoutMs: inspectTimeoutMs,
1934
+ pollMs
1935
+ });
1936
+ return {
1937
+ ok: stableState.state === "RECOMMEND_READY",
1938
+ debug_port: port,
1939
+ state: stableState.state,
1940
+ page_state: {
1941
+ ...stableState,
1942
+ open_attempt: openAttempt
1943
+ }
1944
+ };
1945
+ }
1946
+ if (pageState.state === "LOGIN_REQUIRED") break;
1947
+ }
1948
+
1949
+ return {
1950
+ ok: false,
1951
+ debug_port: port,
1952
+ state: pageState.state || "UNKNOWN",
1953
+ page_state: {
1954
+ ...pageState,
1955
+ open_attempt: openAttempt
1956
+ }
1957
+ };
1958
+ }
1959
+
1000
1960
  async function launchChrome(options = {}) {
1001
1961
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1002
1962
  process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
1963
+ const timing = getLaunchChromeTiming(options);
1003
1964
 
1004
- const initialState = await inspectBossRecommendPageState(port, { timeoutMs: 1500, pollMs: 400 });
1965
+ const initialState = await inspectBossRecommendPageStateCdp(port, {
1966
+ timeoutMs: timing.initialTimeoutMs,
1967
+ pollMs: timing.pollMs
1968
+ });
1005
1969
  if (initialState.state !== "DEBUG_PORT_UNREACHABLE") {
1006
1970
  console.log(`Reusing existing Chrome debug instance on port ${port}`);
1007
- const pageState = await ensureBossRecommendPageReady(getWorkspaceRoot(options), { port, attempts: 2 });
1971
+ const pageState = await ensureBossRecommendPageReadyCdp(port, {
1972
+ attempts: 2,
1973
+ inspectTimeoutMs: timing.inspectTimeoutMs,
1974
+ pollMs: timing.pollMs,
1975
+ settleMs: timing.settleMs
1976
+ });
1008
1977
  if (pageState.ok) {
1009
1978
  console.log("Boss recommend page is ready.");
1979
+ const frontResult = await bringBossRecommendTabToFrontCdp(port);
1980
+ if (frontResult.ok) {
1981
+ console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
1982
+ }
1010
1983
  } else {
1011
1984
  console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
1012
1985
  }
@@ -1037,9 +2010,19 @@ async function launchChrome(options = {}) {
1037
2010
  child.unref();
1038
2011
  console.log(`Chrome launched with remote debugging port ${port}`);
1039
2012
  console.log(`User data dir: ${userDataDir}`);
1040
- const pageState = await ensureBossRecommendPageReady(getWorkspaceRoot(options), { port, attempts: 6 });
2013
+ await sleepMs(timing.settleMs + 1200);
2014
+ const pageState = await ensureBossRecommendPageReadyCdp(port, {
2015
+ attempts: 6,
2016
+ inspectTimeoutMs: timing.inspectTimeoutMs,
2017
+ pollMs: timing.pollMs,
2018
+ settleMs: timing.settleMs
2019
+ });
1041
2020
  if (pageState.ok) {
1042
2021
  console.log("Boss recommend page is ready.");
2022
+ const frontResult = await bringBossRecommendTabToFrontCdp(port);
2023
+ if (frontResult.ok) {
2024
+ console.log(`CDP methods: ${frontResult.method_log.join(", ") || "none"}`);
2025
+ }
1043
2026
  } else {
1044
2027
  console.log(pageState.page_state?.message || "Boss recommend page is not ready.");
1045
2028
  }
@@ -1051,99 +2034,37 @@ function getCalibrationTimeoutMs(options = {}) {
1051
2034
  return Math.max(5000, parsed);
1052
2035
  }
1053
2036
 
1054
- async function calibrate(options = {}) {
2037
+ function buildUnsupportedCalibrateResponse(options = {}) {
1055
2038
  const workspaceRoot = getWorkspaceRoot(options);
1056
2039
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1057
- process.env.BOSS_RECOMMEND_CHROME_PORT = String(port);
1058
- persistDebugPortSelection(port, options);
1059
2040
  const timeoutMs = getCalibrationTimeoutMs(options);
1060
2041
  const outputPath = String(options.output || "").trim()
1061
2042
  ? path.resolve(String(options.output))
1062
2043
  : null;
1063
-
1064
- console.log("Calibration checklist:");
1065
- console.log("0. 本校准仅用于推荐页“精选(tab)”的收藏点击定位。");
1066
- console.log("1. 工具会优先复用当前调试端口 Chrome,并确保 recommend 页面可访问。");
1067
- console.log("2. 工具会尝试自动切换到推荐页“精选”tab;校准过程中请不要再切换 tab。");
1068
- console.log("3. 先点一次收藏,再点一次取消收藏。");
1069
- console.log("4. 关闭详情页。");
1070
- console.log(`5. 校准监听窗口约 ${Math.round(timeoutMs / 1000)} 秒。`);
1071
- console.log("");
1072
-
1073
- const preState = await inspectBossRecommendPageState(port, { timeoutMs: 2000, pollMs: 500 });
1074
- if (preState.state === "DEBUG_PORT_UNREACHABLE") {
1075
- await launchChrome({ ...options, port: String(port) });
1076
- if (process.exitCode && process.exitCode !== 0) {
1077
- return;
1078
- }
1079
- } else {
1080
- console.log(`Detected existing Chrome debug instance on port ${port}; calibration will reuse it.`);
1081
- }
1082
-
1083
- const pageReady = await ensureBossRecommendPageReady(workspaceRoot, {
1084
- port,
1085
- attempts: 4
1086
- });
1087
- if (pageReady.ok) {
1088
- const switchResult = await switchRecommendTab(workspaceRoot, {
1089
- port,
1090
- target_status: "3"
1091
- });
1092
- if (switchResult?.ok) {
1093
- console.log("已自动切换到推荐页“精选”tab,请直接在当前页面打开人选详情并完成收藏/取消收藏。");
1094
- } else {
1095
- console.log("未能自动切换到“精选”tab,请手动切换到精选后再执行收藏/取消收藏。");
1096
- }
1097
- } else {
1098
- console.log("未能确认 recommend 页面就绪,请手动进入推荐页并切换到精选 tab 后再继续校准操作。");
1099
- }
1100
-
1101
- console.log(`等待你打开“精选”候选人详情页(最多 ${Math.round(timeoutMs / 1000)} 秒),检测到后自动开始校准监听...`);
1102
- const detailReady = await waitRecommendFeaturedDetailReady(workspaceRoot, {
1103
- port,
1104
- timeoutMs,
1105
- pollMs: 400
1106
- });
1107
- if (!detailReady.ok) {
1108
- console.error(detailReady.message || "未检测到可校准的精选详情页。");
1109
- console.error("请先打开任意精选候选人详情页并保持在前台,然后重新运行 calibrate。");
1110
- process.exitCode = 1;
1111
- return;
1112
- }
1113
- const detailSource = detailReady.detail_state?.source || "unknown";
1114
- const detailSelector = detailReady.detail_state?.selector || "unknown";
1115
- console.log(`已检测到详情页(source=${detailSource}, selector=${detailSelector}),即将启动校准脚本。`);
1116
- await new Promise((resolve) => setTimeout(resolve, 600));
1117
-
1118
- const result = await runRecommendCalibration(workspaceRoot, {
2044
+ return {
2045
+ status: "FAILED",
2046
+ error: {
2047
+ code: calibrateUnsupportedCode,
2048
+ message: "boss-recommend-mcp calibrate is fenced during the CDP-only rewrite because the old calibration route delegated to page-JS/Runtime-based adapter behavior and an external calibration script. A replacement must use CDP DOM/Input only and pass a live safe calibration gate before this command is re-enabled.",
2049
+ retryable: false
2050
+ },
2051
+ cdp_only: true,
2052
+ runtime_evaluate_used: false,
2053
+ method_summary: {},
2054
+ method_log: [],
1119
2055
  port,
2056
+ timeout_ms: timeoutMs,
1120
2057
  output: outputPath,
1121
- timeoutMs,
1122
- runtime: {
1123
- onOutput: (event) => {
1124
- const text = String(event?.text || "");
1125
- if (!text) return;
1126
- if (event?.stream === "stderr") {
1127
- process.stderr.write(text);
1128
- } else {
1129
- process.stdout.write(text);
1130
- }
1131
- }
2058
+ calibration_resolution: getFeaturedCalibrationResolutionLocal(workspaceRoot),
2059
+ guidance: {
2060
+ current_workaround: "Use an existing favorite-calibration.json if present; `doctor --page-scope featured` will report whether it is usable.",
2061
+ next_development_task: "Implement CDP-only featured detail/action discovery and a user-approved live calibration gate before restoring this command."
1132
2062
  }
1133
- });
1134
- if (result.ok) {
1135
- console.log(`Calibration saved: ${result.calibration_path}`);
1136
- return;
1137
- }
2063
+ };
2064
+ }
1138
2065
 
1139
- console.error(result.error?.message || "Calibration failed.");
1140
- console.error("如果你在校准开始后才从推荐切到精选,请先切到精选 tab 后重新运行 calibrate。");
1141
- if (result.calibration_script_path) {
1142
- console.error(`Calibration script: ${result.calibration_script_path}`);
1143
- }
1144
- if (result.calibration_path) {
1145
- console.error(`Calibration target: ${result.calibration_path}`);
1146
- }
2066
+ async function calibrate(options = {}) {
2067
+ printJson(buildUnsupportedCalibrateResponse(options));
1147
2068
  process.exitCode = 1;
1148
2069
  }
1149
2070
 
@@ -1215,11 +2136,22 @@ async function printDoctor(options = {}) {
1215
2136
  const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1216
2137
  const workspaceRoot = getWorkspaceRoot(options);
1217
2138
  const pageScope = normalizePageScope(options["page-scope"] || options.pageScope) || "recommend";
1218
- const preflight = runPipelinePreflight(workspaceRoot, { pageScope });
2139
+ const preflight = runPipelinePreflightLocal(workspaceRoot, { pageScope });
1219
2140
  const checks = preflight.checks.slice();
1220
- const configResolution = getScreenConfigResolution(workspaceRoot);
1221
- const calibrationResolution = getFeaturedCalibrationResolution(workspaceRoot);
1222
- const pageState = await inspectBossRecommendPageState(port, { timeoutMs: 2000, pollMs: 500 });
2141
+ const configResolution = getBossScreenConfigResolution(workspaceRoot);
2142
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(workspaceRoot);
2143
+ const timing = getLaunchChromeTiming(options);
2144
+ let pageState = await inspectBossRecommendPageStateCdp(port, {
2145
+ timeoutMs: options["slow-live"] || options.slowLive ? timing.initialTimeoutMs : 2000,
2146
+ pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2147
+ });
2148
+ if (pageState.state === "RECOMMEND_READY") {
2149
+ pageState = await verifyRecommendPageStableCdp(port, {
2150
+ settleMs: options["slow-live"] || options.slowLive ? timing.settleMs : 800,
2151
+ recheckTimeoutMs: options["slow-live"] || options.slowLive ? timing.inspectTimeoutMs : 3000,
2152
+ pollMs: options["slow-live"] || options.slowLive ? timing.pollMs : 500
2153
+ });
2154
+ }
1223
2155
  const resolvedConfigPath = configResolution.resolved_path || configResolution.writable_path;
1224
2156
  const userConfigExists = (
1225
2157
  (resolvedConfigPath && fs.existsSync(resolvedConfigPath))
@@ -1245,10 +2177,11 @@ async function printDoctor(options = {}) {
1245
2177
  checks.push({
1246
2178
  key: "featured_calibration_script",
1247
2179
  ok: Boolean(calibrationResolution.calibration_script_path),
2180
+ optional: true,
1248
2181
  path: calibrationResolution.calibration_script_path,
1249
2182
  message: calibrationResolution.calibration_script_path
1250
2183
  ? "已检测到 boss-recruit-mcp 校准脚本。"
1251
- : "未检测到 boss-recruit-mcp 校准脚本,精选页自动校准不可用。"
2184
+ : "未检测到 boss-recruit-mcp 校准脚本;CDP-only package 已禁用旧精选页自动校准。"
1252
2185
  });
1253
2186
  checks.push({
1254
2187
  key: "featured_calibration_file",
@@ -1322,7 +2255,7 @@ async function printDoctor(options = {}) {
1322
2255
  }
1323
2256
 
1324
2257
  printJson({
1325
- ok: checks.every((item) => item.ok),
2258
+ ok: checks.every((item) => item.ok || item.optional),
1326
2259
  port,
1327
2260
  checks,
1328
2261
  config_resolution: configResolution,
@@ -1340,8 +2273,8 @@ async function printDoctor(options = {}) {
1340
2273
  function printPaths() {
1341
2274
  const codexHome = getCodexHome();
1342
2275
  const stateHome = getStateHome();
1343
- const calibrationResolution = getFeaturedCalibrationResolution(process.cwd());
1344
- const bossChatRuntime = resolveBossChatRuntimeLayout(getWorkspaceRoot({}));
2276
+ const calibrationResolution = getFeaturedCalibrationResolutionLocal(process.cwd());
2277
+ const bossChatRuntime = resolveCdpBossChatRuntimeLayout(getWorkspaceRoot({}));
1345
2278
  console.log(`package_root=${packageRoot}`);
1346
2279
  console.log(`skill_sources=${bundledSkillNames.map((name) => getSkillSourceDir(name)).join(" | ")}`);
1347
2280
  console.log(`codex_home=${codexHome}`);
@@ -1362,26 +2295,28 @@ function printHelp() {
1362
2295
  console.log("Usage:");
1363
2296
  console.log(" boss-recommend-mcp Start the MCP server");
1364
2297
  console.log(" boss-recommend-mcp start Start the MCP server");
1365
- console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
1366
- console.log(" boss-recommend-mcp chat <subcommand> Run bundled boss-chat commands via the recommend package");
1367
- console.log(" boss-recommend-mcp install Install skill/MCP templates and auto-init screening-config.json (supports --agent trae-cn/cursor/...)");
1368
- console.log(" boss-recommend-mcp install-skill Install bundled Codex skills");
2298
+ console.log(" boss-recommend-mcp run Disabled until the one-shot CLI has a CDP-only async replacement");
2299
+ console.log(" boss-recommend-mcp list-jobs CDP-only list of exact recommend job names for cron/one-shot inputs");
2300
+ console.log(" boss-recommend-mcp chat <subcommand> Run CDP-only boss-chat health/prepare/status commands");
2301
+ console.log(" boss-recommend-mcp install Install/migrate skills and MCP configs; replaces legacy Boss MCP routes (supports --agent trae-cn/openclaw/...)");
2302
+ console.log(" boss-recommend-mcp install-skill Install bundled Codex skills (recommend/recruit/chat)");
1369
2303
  console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
1370
2304
  console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
1371
2305
  console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
1372
2306
  console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
1373
2307
  console.log(" boss-recommend-mcp doctor Check config/runtime/calibration prerequisites (supports --agent trae-cn/cursor/...)");
1374
- console.log(" boss-recommend-mcp calibrate Run featured favorite calibration via recruit calibration script");
2308
+ console.log(" boss-recommend-mcp calibrate Disabled until CDP-only featured calibration is live-verified");
1375
2309
  console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
1376
2310
  console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
1377
2311
  console.log("");
1378
2312
  console.log("Run command:");
1379
- console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" [--confirmation-json '{...}'] [--overrides-json '{...}'] [--follow-up-json '{...}']");
1380
- console.log(" boss-recommend-mcp chat run --job \"算法工程师\" --start-from unread --criteria \"有 AI Agent 经验\" --targetCount 20 [--greeting-text \"你好,方便发下简历吗?\"] # 后台启动,不自动轮询");
2313
+ console.log(" boss-recommend-mcp run --instruction \"推荐页上筛选211男生,近14天没有,有大模型平台经验\" # returns RECOMMEND_CLI_RUN_UNSUPPORTED_CDP_ONLY during rewrite; use MCP start_recommend_pipeline_run");
2314
+ console.log(" boss-recommend-mcp list-jobs --slow-live --port 9222");
2315
+ console.log(" boss-recommend-mcp chat prepare-run --slow-live --port 9222 # CDP-only preflight; start runs through MCP start_boss_chat_run");
1381
2316
  console.log(" boss-recommend-mcp config set --base-url <url> --api-key <key> --model <model> [--thinking-level off|low|medium|high|current] [--openai-organization <id>] [--openai-project <id>]");
1382
2317
  console.log(" boss-recommend-mcp install --agent trae-cn");
1383
2318
  console.log(" boss-recommend-mcp doctor --agent trae-cn --page-scope featured");
1384
- console.log(" boss-recommend-mcp calibrate --port 9222 [--timeout-ms 60000] [--output <path>]");
2319
+ console.log(" boss-recommend-mcp calibrate --port 9222 # returns CALIBRATE_UNSUPPORTED_CDP_ONLY during rewrite");
1385
2320
  }
1386
2321
 
1387
2322
  function printMcpConfig(options = {}) {
@@ -1397,10 +2332,10 @@ function printMcpConfig(options = {}) {
1397
2332
  }
1398
2333
  }
1399
2334
 
1400
- function installAll(options = {}) {
1401
- const runtimeDirsResult = ensureRuntimeDirectories(options);
2335
+ async function installAll(options = {}) {
2336
+ const runtimeDirsResult = await ensureRuntimeDirectories(options);
1402
2337
  const skillResults = installSkill();
1403
- const configResult = ensureUserConfig(options);
2338
+ const configResult = await ensureUserConfig(options);
1404
2339
  const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
1405
2340
  const externalMcpResult = installExternalMcpConfigs(options);
1406
2341
  const externalSkillResult = mirrorSkillToExternalDirs(options);
@@ -1440,7 +2375,14 @@ function installAll(options = {}) {
1440
2375
  console.log(`Auto-configured external MCP files: ${externalMcpResult.applied.length}`);
1441
2376
  for (const item of externalMcpResult.applied) {
1442
2377
  const action = item.created ? "created" : item.updated ? "updated" : "unchanged";
1443
- console.log(`- ${item.file} (${action})`);
2378
+ const migrated = Array.isArray(item.migrated_legacy_servers) && item.migrated_legacy_servers.length > 0
2379
+ ? `; migrated legacy servers: ${item.migrated_legacy_servers.join(", ")}`
2380
+ : "";
2381
+ const backup = item.backup_file ? `; backup: ${item.backup_file}` : "";
2382
+ console.log(`- ${item.file} (${action}${migrated}${backup})`);
2383
+ }
2384
+ for (const item of externalMcpResult.skipped) {
2385
+ console.warn(`External MCP warning: ${item.file} -> ${item.reason}`);
1444
2386
  }
1445
2387
  } else {
1446
2388
  console.log("No external MCP config target detected. Set BOSS_RECOMMEND_MCP_CONFIG_TARGETS to auto-configure custom agents.");
@@ -1448,7 +2390,10 @@ function installAll(options = {}) {
1448
2390
  if (externalSkillResult.baseDirs.length > 0) {
1449
2391
  console.log(`Mirrored skill to external dirs: ${externalSkillResult.mirrored.length}`);
1450
2392
  for (const item of externalSkillResult.mirrored) {
1451
- console.log(`- ${item.target_dir}`);
2393
+ console.log(`- ${item.target_dir}${item.replaced_legacy ? " (replaced legacy skill)" : ""}`);
2394
+ }
2395
+ for (const item of externalSkillResult.skipped) {
2396
+ console.warn(`External skill warning: ${item.base_dir} / ${item.skill} -> ${item.reason}`);
1452
2397
  }
1453
2398
  } else {
1454
2399
  console.log("No external skill dir detected. Set BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS to mirror skill for non-Codex agents.");
@@ -1458,26 +2403,78 @@ function installAll(options = {}) {
1458
2403
  }
1459
2404
  }
1460
2405
 
1461
- async function runPipelineOnce(options) {
2406
+ function buildUnsupportedRecommendCliRunResponse({
2407
+ instruction,
2408
+ confirmation,
2409
+ overrides,
2410
+ followUp,
2411
+ workspaceRoot,
2412
+ port
2413
+ } = {}) {
2414
+ return {
2415
+ status: "FAILED",
2416
+ error: {
2417
+ code: recommendCliRunUnsupportedCode,
2418
+ message: "boss-recommend-mcp run is fenced during the CDP-only rewrite because the old one-shot CLI route can reach page-JS/Runtime-based orchestration. Use the MCP tool start_recommend_pipeline_run for CDP-only recommend runs until a live-verified one-shot CLI replacement exists.",
2419
+ retryable: false
2420
+ },
2421
+ cdp_only: true,
2422
+ runtime_evaluate_used: false,
2423
+ method_summary: {},
2424
+ method_log: [],
2425
+ run_mode: "mcp_async_required",
2426
+ port,
2427
+ target_url: bossUrl,
2428
+ input: {
2429
+ workspace_root: workspaceRoot,
2430
+ instruction,
2431
+ confirmation: confirmation ?? null,
2432
+ overrides: overrides ?? null,
2433
+ follow_up: followUp ?? null
2434
+ },
2435
+ guidance: {
2436
+ recommended_tool: "start_recommend_pipeline_run",
2437
+ next_development_task: "Implement a CDP-only CLI wrapper that starts a durable shared run-service session, persists run state, and exits only after its live gate proves no Runtime.* methods are reachable."
2438
+ }
2439
+ };
2440
+ }
2441
+
2442
+ async function runPipelineOnce(options = {}) {
1462
2443
  const instruction = getRunInstruction(options);
1463
2444
  const confirmation = getRunConfirmation(options);
1464
2445
  const overrides = getRunOverrides(options);
1465
2446
  const followUp = getRunFollowUp(options);
1466
2447
  const workspaceRoot = getWorkspaceRoot(options);
1467
- const explicitPort = parsePositivePort(options.port);
1468
- if (explicitPort) {
1469
- process.env.BOSS_RECOMMEND_CHROME_PORT = String(explicitPort);
1470
- persistDebugPortSelection(explicitPort, options);
1471
- }
2448
+ const port = parsePositivePort(options.port) || parsePositivePort(process.env.BOSS_RECOMMEND_CHROME_PORT) || 9222;
1472
2449
 
1473
- const result = await runRecommendPipeline({
2450
+ printJson(buildUnsupportedRecommendCliRunResponse({
1474
2451
  workspaceRoot,
1475
2452
  instruction,
1476
2453
  confirmation,
1477
2454
  overrides,
1478
- followUp
1479
- });
1480
- printJson(result);
2455
+ followUp,
2456
+ port
2457
+ }));
2458
+ process.exitCode = 1;
2459
+ }
2460
+
2461
+ function buildRecommendJobListCliInput(options = {}) {
2462
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2463
+ const host = String(options.host || "").trim();
2464
+ return {
2465
+ host: host || undefined,
2466
+ port: parsePositivePort(options.port),
2467
+ target_url_includes: targetUrlIncludes || undefined,
2468
+ allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2469
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true
2470
+ };
2471
+ }
2472
+
2473
+ async function listRecommendJobsCli(options = {}) {
2474
+ printJson(await listRecommendJobsTool({
2475
+ workspaceRoot: getWorkspaceRoot(options),
2476
+ args: buildRecommendJobListCliInput(options)
2477
+ }));
1481
2478
  }
1482
2479
 
1483
2480
  function buildBossChatCliInput(options = {}) {
@@ -1487,6 +2484,8 @@ function buildBossChatCliInput(options = {}) {
1487
2484
  ?? options.greetingText
1488
2485
  ?? options.greeting;
1489
2486
  const greetingText = typeof greetingTextRaw === "string" ? greetingTextRaw.trim() : undefined;
2487
+ const targetUrlIncludes = String(options["target-url-includes"] || options.target_url_includes || "").trim();
2488
+ const host = String(options.host || "").trim();
1490
2489
  return {
1491
2490
  profile: typeof options.profile === "string" ? options.profile.trim() : undefined,
1492
2491
  job: typeof options.job === "string" ? options.job.trim() : undefined,
@@ -1494,7 +2493,14 @@ function buildBossChatCliInput(options = {}) {
1494
2493
  criteria: typeof options.criteria === "string" ? options.criteria.trim() : undefined,
1495
2494
  greeting_text: greetingText || undefined,
1496
2495
  target_count: parseBossChatTargetCountOption(options.targetCount || options["target-count"] || options.target_count),
2496
+ host: host || undefined,
1497
2497
  port: parsePositivePort(options.port),
2498
+ target_url_includes: targetUrlIncludes || undefined,
2499
+ allow_navigate: !(options["no-navigate"] === true || options.noNavigate === true || options.allow_navigate === false),
2500
+ slow_live: options["slow-live"] === true || options.slowLive === true || options.slow_live === true,
2501
+ detail_limit: parseNonNegativeInteger(options["detail-limit"] ?? options.detail_limit),
2502
+ delay_ms: parseNonNegativeInteger(options["delay-ms"] ?? options.delay_ms),
2503
+ max_candidates: parseNonNegativeInteger(options["max-candidates"] ?? options.max_candidates),
1498
2504
  dry_run: options["dry-run"] === true || options.dryRun === true,
1499
2505
  no_state: options["no-state"] === true || options.noState === true,
1500
2506
  safe_pacing: parseBooleanOption(options["safe-pacing"] ?? options.safe_pacing),
@@ -1509,67 +2515,69 @@ function getBossChatCliRunTarget(options = {}) {
1509
2515
  };
1510
2516
  }
1511
2517
 
2518
+ function buildUnsupportedBossChatCliStartResponse(subcommand) {
2519
+ return {
2520
+ status: "FAILED",
2521
+ error: {
2522
+ code: bossChatCliUnsupportedStartCode,
2523
+ message: `boss-recommend-mcp chat ${subcommand} is fenced during the CDP-only rewrite because a one-shot CLI process cannot keep the live CDP session and run lifecycle alive after it exits. Use the MCP tool start_boss_chat_run, or the live chat harness, for CDP-only chat runs.`,
2524
+ retryable: false
2525
+ },
2526
+ cdp_only: true,
2527
+ runtime_evaluate_used: false,
2528
+ method_summary: {},
2529
+ method_log: []
2530
+ };
2531
+ }
2532
+
1512
2533
  async function runBossChatCliCommand(subcommand, options = {}) {
1513
2534
  const workspaceRoot = getWorkspaceRoot(options);
2535
+ const input = buildBossChatCliInput(options);
1514
2536
  if (subcommand === "health-check") {
1515
- printJson(getBossChatHealthCheck(workspaceRoot, {
1516
- port: parsePositivePort(options.port)
1517
- }));
1518
- return;
1519
- }
1520
-
1521
- if (subcommand === "prepare-run") {
1522
- printJson(await prepareBossChatRun({
2537
+ printJson(await bossChatHealthCheckTool({
1523
2538
  workspaceRoot,
1524
- input: buildBossChatCliInput(options)
2539
+ args: input
1525
2540
  }));
1526
2541
  return;
1527
2542
  }
1528
2543
 
1529
- if (subcommand === "run") {
1530
- printJson(await startBossChatRun({
2544
+ if (subcommand === "prepare-run") {
2545
+ printJson(await prepareBossChatRunTool({
1531
2546
  workspaceRoot,
1532
- input: buildBossChatCliInput(options)
2547
+ args: input
1533
2548
  }));
1534
2549
  return;
1535
2550
  }
1536
2551
 
1537
- if (subcommand === "start-run") {
1538
- printJson(await startBossChatRun({
1539
- workspaceRoot,
1540
- input: buildBossChatCliInput(options)
1541
- }));
2552
+ if (subcommand === "run" || subcommand === "start-run") {
2553
+ printJson(buildUnsupportedBossChatCliStartResponse(subcommand));
1542
2554
  return;
1543
2555
  }
1544
2556
 
1545
2557
  if (subcommand === "get-run") {
1546
- printJson(await getBossChatRun({
1547
- workspaceRoot,
1548
- input: getBossChatCliRunTarget(options)
2558
+ printJson(getBossChatRunTool({
2559
+ args: getBossChatCliRunTarget(options)
1549
2560
  }));
1550
2561
  return;
1551
2562
  }
1552
2563
 
1553
2564
  if (subcommand === "pause-run") {
1554
- printJson(await pauseBossChatRun({
1555
- workspaceRoot,
1556
- input: getBossChatCliRunTarget(options)
2565
+ printJson(pauseBossChatRunTool({
2566
+ args: getBossChatCliRunTarget(options)
1557
2567
  }));
1558
2568
  return;
1559
2569
  }
1560
2570
 
1561
2571
  if (subcommand === "resume-run") {
1562
- printJson(await resumeBossChatRun({
1563
- workspaceRoot,
1564
- input: getBossChatCliRunTarget(options)
2572
+ printJson(resumeBossChatRunTool({
2573
+ args: getBossChatCliRunTarget(options)
1565
2574
  }));
1566
2575
  return;
1567
2576
  }
1568
2577
 
1569
2578
  if (subcommand === "cancel-run") {
1570
- printJson(await cancelBossChatRun({
1571
- workspaceRoot,
1572
- input: getBossChatCliRunTarget(options)
2579
+ printJson(cancelBossChatRunTool({
2580
+ args: getBossChatCliRunTarget(options)
1573
2581
  }));
1574
2582
  return;
1575
2583
  }
@@ -1601,6 +2609,23 @@ export async function runCli(argv = process.argv) {
1601
2609
  process.exitCode = 1;
1602
2610
  }
1603
2611
  break;
2612
+ case "list-jobs":
2613
+ case "jobs":
2614
+ case "recommend-jobs":
2615
+ try {
2616
+ await listRecommendJobsCli(options);
2617
+ } catch (error) {
2618
+ printJson({
2619
+ status: "FAILED",
2620
+ error: {
2621
+ code: "RECOMMEND_JOB_LIST_CLI_FAILED",
2622
+ message: error.message || "Failed to list recommend jobs",
2623
+ retryable: true
2624
+ }
2625
+ });
2626
+ process.exitCode = 1;
2627
+ }
2628
+ break;
1604
2629
  case "chat":
1605
2630
  try {
1606
2631
  const chatSubcommand = String(argv[3] || "").trim().toLowerCase();
@@ -1620,7 +2645,7 @@ export async function runCli(argv = process.argv) {
1620
2645
  break;
1621
2646
  case "install":
1622
2647
  try {
1623
- installAll(options);
2648
+ await installAll(options);
1624
2649
  } catch (error) {
1625
2650
  console.error(error.message || "Install failed.");
1626
2651
  process.exitCode = 1;
@@ -1632,8 +2657,8 @@ export async function runCli(argv = process.argv) {
1632
2657
  }
1633
2658
  break;
1634
2659
  case "init-config": {
1635
- const runtimeDirsResult = ensureRuntimeDirectories(options);
1636
- const result = ensureUserConfig(options);
2660
+ const runtimeDirsResult = await ensureRuntimeDirectories(options);
2661
+ const result = await ensureUserConfig(options);
1637
2662
  console.log(
1638
2663
  `Runtime directories prepared: created=${runtimeDirsResult.created.length}, existing=${runtimeDirsResult.existed.length}, failed=${runtimeDirsResult.failed.length}`
1639
2664
  );
@@ -1657,7 +2682,7 @@ export async function runCli(argv = process.argv) {
1657
2682
  }
1658
2683
  case "set-port": {
1659
2684
  try {
1660
- const result = setDebugPort(options);
2685
+ const result = await setDebugPort(options);
1661
2686
  console.log(`Preferred debug port saved: ${result.port}`);
1662
2687
  console.log(`Updated config: ${result.configPath}`);
1663
2688
  console.log("Port priority for runtime commands: --port > BOSS_RECOMMEND_CHROME_PORT > screening-config.json.debugPort > 9222");
@@ -1668,8 +2693,8 @@ export async function runCli(argv = process.argv) {
1668
2693
  break;
1669
2694
  }
1670
2695
  case "set-config": {
1671
- try {
1672
- const result = setScreeningConfig(options);
2696
+ try {
2697
+ const result = await setScreeningConfig(options);
1673
2698
  console.log(`screening-config.json updated: ${result.path}`);
1674
2699
  } catch (error) {
1675
2700
  console.error(error.message || "Failed to write screening-config.json.");
@@ -1682,7 +2707,7 @@ export async function runCli(argv = process.argv) {
1682
2707
  if (!sub || sub.startsWith("--") || sub === "set") {
1683
2708
  const configOptions = sub === "set" ? parseOptions(argv.slice(4)) : options;
1684
2709
  try {
1685
- const result = setScreeningConfig(configOptions);
2710
+ const result = await setScreeningConfig(configOptions);
1686
2711
  console.log(`screening-config.json updated: ${result.path}`);
1687
2712
  } catch (error) {
1688
2713
  console.error(error.message || "Failed to write screening-config.json.");
@@ -1712,7 +2737,7 @@ export async function runCli(argv = process.argv) {
1712
2737
  await launchChrome(options);
1713
2738
  break;
1714
2739
  case "where":
1715
- printPaths();
2740
+ await printPaths();
1716
2741
  break;
1717
2742
  case "help":
1718
2743
  case "--help":
@@ -1727,18 +2752,22 @@ export async function runCli(argv = process.argv) {
1727
2752
  }
1728
2753
 
1729
2754
  export const __testables = {
2755
+ buildRecommendJobListCliInput,
1730
2756
  buildBossChatCliInput,
1731
2757
  buildDefaultMcpArgs,
1732
2758
  buildMcpLaunchConfig,
2759
+ buildUnsupportedRecommendCliRunResponse,
1733
2760
  collectRuntimeDirectories,
1734
- ensureBossChatRuntimeReady,
2761
+ ensureBossChatRuntimeReady: ensureBossChatRuntimeReadyLocal,
1735
2762
  ensureRuntimeDirectories,
1736
2763
  getBossChatCliRunTarget,
1737
2764
  getDefaultMcpPackageSpecifier,
1738
2765
  getRunFollowUp,
2766
+ inspectMcpServerEntries,
1739
2767
  installSkill,
1740
2768
  isInstalledPackageRoot,
1741
- resolveBossChatRuntimeLayout,
2769
+ mergeMcpServerConfigFile,
2770
+ resolveBossChatRuntimeLayout: resolveCdpBossChatRuntimeLayout,
1742
2771
  runBossChatCliCommand,
1743
2772
  runPipelineOnce
1744
2773
  };