@reconcrap/boss-recommend-mcp 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -91,10 +91,10 @@ CLI fallback 的状态机与 MCP 保持一致:
91
91
  ## Setup Checklist
92
92
 
93
93
  执行前先检查:
94
-
95
- - `boss-recommend-mcp` 是否已安装
96
- - `screening-config.json` 是否存在且包含可用模型配置
97
- - Chrome 远程调试端口是否可连
94
+
95
+ - `boss-recommend-mcp` 是否已安装
96
+ - `screening-config.json` 是否存在且 `baseUrl/apiKey/model` 均已由用户填写为可用值(不能是模板占位符)
97
+ - Chrome 远程调试端口是否可连
98
98
  - 当前 Chrome 是否停留在 `https://www.zhipin.com/web/chat/recommend`
99
99
 
100
100
  在开始执行 recommend-search-cli / recommend-screen-cli 前,必须做页面就绪门禁:
@@ -102,14 +102,19 @@ CLI fallback 的状态机与 MCP 保持一致:
102
102
  - 检查 Chrome DevTools 端口是否可连接
103
103
  - 检查 Boss 是否已登录
104
104
  - 检查当前页面是否已停留在 recommend 页面
105
- - 任一不满足时,不得继续执行搜索和筛选;必须先提示用户在**正确端口**的 Chrome 中完成登录并导航到 recommend 页面,待用户确认“已就绪”后再重试
105
+ - 若端口不可连接:先自动尝试启动 Chrome,并且必须使用 `--remote-debugging-port=<port>` + `--user-data-dir=<profile>`
106
+ - 若检测到 Boss 已登录但不在 recommend 页面:先自动 navigate 到 `https://www.zhipin.com/web/chat/recommend`
107
+ - 若检测到 Boss 未登录:提示用户先登录;用户登录后先 navigate 到 recommend 页面再继续
108
+ - 自动修复后仍失败时,才提示用户介入并等待“已就绪”后重试
106
109
 
107
110
  ## Preflight 失败自动修复
108
111
 
109
- 当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
110
-
111
- 1. 优先读取 `diagnostics.recovery.agent_prompt`,直接把这段提示词交给 AI agent 执行环境修复。
112
- 2. `diagnostics.recovery.agent_prompt` 不存在,使用下面的兜底提示词(严格顺序,不可跳步):
112
+ 当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
113
+
114
+ 1. `diagnostics.checks` 中 `screen_config` 失败,优先引导用户填写 `screening-config.json` `baseUrl/apiKey/model`(必须让用户提供真实值,不可保留模板值)。
115
+ 2. 优先查看 `diagnostics.auto_repair`,若有自动修复动作则先基于其结果继续执行或给出最小化补救提示。
116
+ 3. 若自动修复后仍失败,再读取 `diagnostics.recovery.agent_prompt`,直接把这段提示词交给 AI agent 执行环境修复。
117
+ 4. 若 `diagnostics.recovery.agent_prompt` 不存在,使用下面的兜底提示词(严格顺序,不可跳步):
113
118
 
114
119
  ```text
115
120
  你是环境修复 agent。请根据 diagnostics.checks 修复依赖,必须串行执行:
package/src/adapters.js CHANGED
@@ -8,6 +8,12 @@ const currentFilePath = fileURLToPath(import.meta.url);
8
8
  const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
9
9
  const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
10
10
  const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
11
+ const bossLoginUrlPattern = /zhipin\.com\/web\/user|passport\.zhipin\.com/i;
12
+ const screenConfigTemplateDefaults = {
13
+ baseUrl: "https://api.openai.com/v1",
14
+ apiKey: "replace-with-openai-api-key",
15
+ model: "gpt-4.1-mini"
16
+ };
11
17
 
12
18
  function getCodexHome() {
13
19
  return process.env.CODEX_HOME
@@ -23,6 +29,10 @@ function getDesktopDir() {
23
29
  return path.join(os.homedir(), "Desktop");
24
30
  }
25
31
 
32
+ function ensureDir(targetPath) {
33
+ fs.mkdirSync(targetPath, { recursive: true });
34
+ }
35
+
26
36
  function pathExists(targetPath) {
27
37
  try {
28
38
  return fs.existsSync(targetPath);
@@ -71,6 +81,45 @@ function readJsonFile(filePath) {
71
81
  }
72
82
  }
73
83
 
84
+ function validateScreenConfig(config) {
85
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
86
+ return {
87
+ ok: false,
88
+ message: "screening-config.json 缺失或格式无效。请填写 baseUrl、apiKey、model。"
89
+ };
90
+ }
91
+ const baseUrl = String(config.baseUrl || "").trim();
92
+ const apiKey = String(config.apiKey || "").trim();
93
+ const model = String(config.model || "").trim();
94
+ const missing = [];
95
+ if (!baseUrl) missing.push("baseUrl");
96
+ if (!apiKey) missing.push("apiKey");
97
+ if (!model) missing.push("model");
98
+ if (missing.length > 0) {
99
+ return {
100
+ ok: false,
101
+ message: `screening-config.json 缺少必填字段:${missing.join(", ")}。`
102
+ };
103
+ }
104
+ if (/^replace-with/i.test(apiKey) || apiKey === screenConfigTemplateDefaults.apiKey) {
105
+ return {
106
+ ok: false,
107
+ message: "screening-config.json 的 apiKey 仍是模板占位符,请填写真实 API Key。"
108
+ };
109
+ }
110
+ if (
111
+ baseUrl === screenConfigTemplateDefaults.baseUrl
112
+ && apiKey === screenConfigTemplateDefaults.apiKey
113
+ && model === screenConfigTemplateDefaults.model
114
+ ) {
115
+ return {
116
+ ok: false,
117
+ message: "screening-config.json 仍是默认模板值,请填写 baseUrl、apiKey、model。"
118
+ };
119
+ }
120
+ return { ok: true, message: "screening-config.json 校验通过。" };
121
+ }
122
+
74
123
  function resolveWorkspaceDebugPort(workspaceRoot) {
75
124
  const fromEnv = parsePositiveInteger(process.env.BOSS_RECOMMEND_CHROME_PORT);
76
125
  if (fromEnv) return fromEnv;
@@ -78,6 +127,63 @@ function resolveWorkspaceDebugPort(workspaceRoot) {
78
127
  return parsePositiveInteger(config?.debugPort) || 9222;
79
128
  }
80
129
 
130
+ function getChromeExecutable() {
131
+ const candidates = [
132
+ process.env.BOSS_RECOMMEND_CHROME_PATH,
133
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
134
+ path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
135
+ path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
136
+ ].filter(Boolean);
137
+ return candidates.find((candidate) => pathExists(candidate)) || null;
138
+ }
139
+
140
+ function getChromeUserDataDir(port) {
141
+ const profileDir = path.join(getCodexHome(), "boss-recommend-mcp", `chrome-profile-${port}`);
142
+ ensureDir(profileDir);
143
+ return profileDir;
144
+ }
145
+
146
+ function launchChromeWithDebugPort(port) {
147
+ const chromePath = getChromeExecutable();
148
+ if (!chromePath) {
149
+ return {
150
+ ok: false,
151
+ code: "CHROME_EXECUTABLE_NOT_FOUND",
152
+ message: "未找到 Chrome 可执行文件,请安装 Chrome 或设置 BOSS_RECOMMEND_CHROME_PATH。"
153
+ };
154
+ }
155
+ const userDataDir = getChromeUserDataDir(port);
156
+ const args = [
157
+ `--remote-debugging-port=${port}`,
158
+ `--user-data-dir=${userDataDir}`,
159
+ "--no-first-run",
160
+ "--no-default-browser-check",
161
+ "--new-window",
162
+ bossRecommendUrl
163
+ ];
164
+
165
+ try {
166
+ const child = spawn(chromePath, args, {
167
+ detached: true,
168
+ stdio: "ignore",
169
+ windowsHide: false
170
+ });
171
+ child.unref();
172
+ return {
173
+ ok: true,
174
+ code: "CHROME_LAUNCHED",
175
+ chrome_path: chromePath,
176
+ user_data_dir: userDataDir
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ ok: false,
181
+ code: "CHROME_LAUNCH_FAILED",
182
+ message: error.message || "Chrome 启动失败。"
183
+ };
184
+ }
185
+ }
186
+
81
187
  function resolveRecommendSearchCliDir(workspaceRoot) {
82
188
  const localDir = path.join(workspaceRoot, "boss-recommend-search-cli");
83
189
  if (pathExists(localDir)) return localDir;
@@ -388,16 +494,11 @@ function parseJsonOutput(text) {
388
494
 
389
495
  function loadScreenConfig(configPath) {
390
496
  const parsed = readJsonFile(configPath);
391
- if (!parsed) {
392
- return {
393
- ok: false,
394
- error: `Screen config file not found or invalid: ${configPath}`
395
- };
396
- }
397
- if (!parsed.baseUrl || !parsed.apiKey || !parsed.model) {
497
+ const validation = validateScreenConfig(parsed);
498
+ if (!validation.ok) {
398
499
  return {
399
500
  ok: false,
400
- error: "Invalid screen config: baseUrl/apiKey/model are required"
501
+ error: `${validation.message} (path: ${configPath})`
401
502
  };
402
503
  }
403
504
  return { ok: true, config: parsed };
@@ -411,6 +512,8 @@ export function runPipelinePreflight(workspaceRoot) {
411
512
  const searchDir = resolveRecommendSearchCliDir(workspaceRoot);
412
513
  const screenDir = resolveRecommendScreenCliDir(workspaceRoot);
413
514
  const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
515
+ const screenConfigParsed = readJsonFile(screenConfigPath);
516
+ const screenConfigValidation = validateScreenConfig(screenConfigParsed);
414
517
  const checks = [
415
518
  {
416
519
  key: "recommend_search_cli_dir",
@@ -438,9 +541,9 @@ export function runPipelinePreflight(workspaceRoot) {
438
541
  },
439
542
  {
440
543
  key: "screen_config",
441
- ok: pathExists(screenConfigPath),
544
+ ok: screenConfigValidation.ok,
442
545
  path: screenConfigPath,
443
- message: "screening-config.json 不存在"
546
+ message: screenConfigValidation.ok ? "screening-config.json 可用" : screenConfigValidation.message
444
547
  }
445
548
  ];
446
549
  checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
@@ -452,6 +555,123 @@ export function runPipelinePreflight(workspaceRoot) {
452
555
  };
453
556
  }
454
557
 
558
+ function collectFailedCheckKeys(checks = []) {
559
+ return new Set(
560
+ checks
561
+ .filter((item) => item && item.ok === false && typeof item.key === "string")
562
+ .map((item) => item.key)
563
+ );
564
+ }
565
+
566
+ function collectNpmInstallDirsFromChecks(checks = [], workspaceRoot) {
567
+ const npmKeys = new Set([
568
+ "npm_dep_chrome_remote_interface_search",
569
+ "npm_dep_chrome_remote_interface_screen",
570
+ "npm_dep_ws"
571
+ ]);
572
+ const dirs = checks
573
+ .filter((item) => item && item.ok === false && npmKeys.has(item.key))
574
+ .map((item) => item.install_cwd)
575
+ .filter((item) => typeof item === "string" && item.trim())
576
+ .map((item) => path.resolve(item));
577
+ if (dirs.length > 0) {
578
+ return [...new Set(dirs)];
579
+ }
580
+ return [path.resolve(workspaceRoot)];
581
+ }
582
+
583
+ function installNpmDependencies(checks, workspaceRoot) {
584
+ const dirs = collectNpmInstallDirsFromChecks(checks, workspaceRoot);
585
+ const commandResults = [];
586
+ let allOk = true;
587
+ for (const cwd of dirs) {
588
+ const result = runProcessSync({
589
+ command: "npm",
590
+ args: ["install"],
591
+ cwd
592
+ });
593
+ commandResults.push({
594
+ cwd,
595
+ ok: result.ok,
596
+ output: result.output || result.error_message || ""
597
+ });
598
+ if (!result.ok) allOk = false;
599
+ }
600
+ return {
601
+ ok: allOk,
602
+ action: "install_npm_dependencies",
603
+ changed: true,
604
+ command_results: commandResults,
605
+ message: allOk ? "npm 依赖自动安装完成。" : "npm 依赖自动安装失败。"
606
+ };
607
+ }
608
+
609
+ function installPillowIfPossible() {
610
+ const detected = detectPythonCommand();
611
+ if (!detected.ok || !detected.command) {
612
+ return {
613
+ ok: false,
614
+ action: "install_pillow",
615
+ changed: false,
616
+ message: "未检测到可用 python 命令,无法自动安装 Pillow。"
617
+ };
618
+ }
619
+ const install = runProcessSync({
620
+ command: detected.command,
621
+ args: ["-m", "pip", "install", "pillow"]
622
+ });
623
+ return {
624
+ ok: install.ok,
625
+ action: "install_pillow",
626
+ changed: install.ok,
627
+ message: install.ok ? "Pillow 自动安装完成。" : `Pillow 自动安装失败:${install.output || install.error_message || "unknown"}`
628
+ };
629
+ }
630
+
631
+ export function attemptPipelineAutoRepair(workspaceRoot, preflight = {}) {
632
+ const checks = Array.isArray(preflight.checks) ? preflight.checks : [];
633
+ const failed = collectFailedCheckKeys(checks);
634
+ const actions = [];
635
+
636
+ if (
637
+ failed.has("npm_dep_chrome_remote_interface_search")
638
+ || failed.has("npm_dep_chrome_remote_interface_screen")
639
+ || failed.has("npm_dep_ws")
640
+ ) {
641
+ if (!failed.has("node_cli")) {
642
+ actions.push(installNpmDependencies(checks, workspaceRoot));
643
+ } else {
644
+ actions.push({
645
+ ok: false,
646
+ action: "install_npm_dependencies",
647
+ changed: false,
648
+ message: "Node 命令不可用,跳过 npm 自动安装。"
649
+ });
650
+ }
651
+ }
652
+
653
+ if (failed.has("python_pillow")) {
654
+ if (!failed.has("python_cli")) {
655
+ actions.push(installPillowIfPossible());
656
+ } else {
657
+ actions.push({
658
+ ok: false,
659
+ action: "install_pillow",
660
+ changed: false,
661
+ message: "python 命令不可用,跳过 Pillow 自动安装。"
662
+ });
663
+ }
664
+ }
665
+
666
+ const attempted = actions.length > 0;
667
+ const nextPreflight = runPipelinePreflight(workspaceRoot);
668
+ return {
669
+ attempted,
670
+ actions,
671
+ preflight: nextPreflight
672
+ };
673
+ }
674
+
455
675
  function sleep(ms) {
456
676
  return new Promise((resolve) => setTimeout(resolve, ms));
457
677
  }
@@ -519,15 +739,18 @@ export async function inspectBossRecommendPageState(port, options = {}) {
519
739
  (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
520
740
  );
521
741
  if (bossTab) {
742
+ const requiresLogin = bossLoginUrlPattern.test(bossTab.url);
522
743
  return buildBossPageState({
523
744
  ok: false,
524
- state: "LOGIN_REQUIRED",
745
+ state: requiresLogin ? "LOGIN_REQUIRED" : "BOSS_NOT_ON_RECOMMEND",
525
746
  path: bossTab.url,
526
747
  current_url: bossTab.url,
527
748
  title: bossTab.title || null,
528
- requires_login: true,
749
+ requires_login: requiresLogin,
529
750
  expected_url: expectedUrl,
530
- message: "Boss 页面没有停留在 recommend 页面,通常表示需要重新登录。"
751
+ message: requiresLogin
752
+ ? "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
753
+ : "Boss 已登录但当前不在 recommend 页面,将尝试自动跳转。"
531
754
  });
532
755
  }
533
756
  } catch (error) {
@@ -647,13 +870,39 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
647
870
  page_state: stableState
648
871
  };
649
872
  }
650
- if (pageState.state === "LOGIN_REQUIRED") {
651
- return {
652
- ok: false,
653
- debug_port: debugPort,
654
- state: pageState.state,
655
- page_state: pageState
656
- };
873
+
874
+ let launchAttempt = null;
875
+ if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
876
+ launchAttempt = launchChromeWithDebugPort(debugPort);
877
+ if (launchAttempt.ok) {
878
+ await sleep(settleMs + 1200);
879
+ pageState = await inspectBossRecommendPageState(debugPort, {
880
+ timeoutMs: inspectTimeoutMs,
881
+ pollMs
882
+ });
883
+ if (pageState.state === "RECOMMEND_READY") {
884
+ const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
885
+ return {
886
+ ok: stableState.state === "RECOMMEND_READY",
887
+ debug_port: debugPort,
888
+ state: stableState.state,
889
+ page_state: {
890
+ ...stableState,
891
+ launch_attempt: launchAttempt
892
+ }
893
+ };
894
+ }
895
+ } else {
896
+ return {
897
+ ok: false,
898
+ debug_port: debugPort,
899
+ state: pageState.state,
900
+ page_state: {
901
+ ...pageState,
902
+ launch_attempt: launchAttempt
903
+ }
904
+ };
905
+ }
657
906
  }
658
907
 
659
908
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
@@ -672,15 +921,10 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
672
921
  ok: stableState.state === "RECOMMEND_READY",
673
922
  debug_port: debugPort,
674
923
  state: stableState.state,
675
- page_state: stableState
676
- };
677
- }
678
- if (pageState.state === "LOGIN_REQUIRED") {
679
- return {
680
- ok: false,
681
- debug_port: debugPort,
682
- state: pageState.state,
683
- page_state: pageState
924
+ page_state: {
925
+ ...stableState,
926
+ launch_attempt: launchAttempt
927
+ }
684
928
  };
685
929
  }
686
930
  }
@@ -689,7 +933,10 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
689
933
  ok: false,
690
934
  debug_port: debugPort,
691
935
  state: pageState.state || "UNKNOWN",
692
- page_state: pageState
936
+ page_state: {
937
+ ...pageState,
938
+ launch_attempt: launchAttempt
939
+ }
693
940
  };
694
941
  }
695
942
 
package/src/cli.js CHANGED
@@ -582,7 +582,7 @@ function printHelp() {
582
582
  console.log(" boss-recommend-mcp Start the MCP server");
583
583
  console.log(" boss-recommend-mcp start Start the MCP server");
584
584
  console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
585
- console.log(" boss-recommend-mcp install Install Codex skill and initialize user config");
585
+ console.log(" boss-recommend-mcp install Install Codex skill and MCP config templates (does not create screening-config.json)");
586
586
  console.log(" boss-recommend-mcp install-skill Install only the Codex skill");
587
587
  console.log(" boss-recommend-mcp init-config Create ~/.codex/boss-recommend-mcp/screening-config.json if missing");
588
588
  console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
@@ -610,12 +610,11 @@ function printMcpConfig(options = {}) {
610
610
 
611
611
  function installAll() {
612
612
  const skillTarget = installSkill();
613
- const configResult = ensureUserConfig();
614
613
  const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
615
614
  const externalMcpResult = installExternalMcpConfigs({});
616
615
  const externalSkillResult = mirrorSkillToExternalDirs();
617
616
  console.log(`Skill installed to: ${skillTarget}`);
618
- console.log(configResult.created ? `Config template created at: ${configResult.path}` : `Config already exists at: ${configResult.path}`);
617
+ console.log("screening-config.json 不会在 install 阶段自动创建。首次运行请按 doctor / agent 提示填写 baseUrl、apiKey、model。");
619
618
  console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
620
619
  for (const item of mcpTemplateResult.files) {
621
620
  console.log(`- ${item.client}: ${item.file}`);
package/src/pipeline.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { parseRecommendInstruction } from "./parser.js";
2
- import {
3
- ensureBossRecommendPageReady,
4
- runPipelinePreflight,
5
- runRecommendSearchCli,
6
- runRecommendScreenCli
7
- } from "./adapters.js";
2
+ import {
3
+ attemptPipelineAutoRepair,
4
+ ensureBossRecommendPageReady,
5
+ runPipelinePreflight,
6
+ runRecommendSearchCli,
7
+ runRecommendScreenCli
8
+ } from "./adapters.js";
8
9
 
9
10
  function dedupe(values = []) {
10
11
  return [...new Set(values.filter(Boolean))];
@@ -46,25 +47,38 @@ function formatCommandBlock(commands = []) {
46
47
  return commands.map((command) => `- ${command}`).join("\n");
47
48
  }
48
49
 
49
- function buildPreflightRecovery(checks = [], workspaceRoot) {
50
- const failed = failedCheckSet(checks);
51
- if (failed.size === 0) return null;
52
-
53
- const needNode = failed.has("node_cli");
54
- const needNpm = (
55
- failed.has("npm_dep_chrome_remote_interface_search")
56
- || failed.has("npm_dep_chrome_remote_interface_screen")
57
- || failed.has("npm_dep_ws")
50
+ function buildPreflightRecovery(checks = [], workspaceRoot) {
51
+ const failed = failedCheckSet(checks);
52
+ if (failed.size === 0) return null;
53
+
54
+ const needScreenConfig = failed.has("screen_config");
55
+ const needNode = failed.has("node_cli");
56
+ const needNpm = (
57
+ failed.has("npm_dep_chrome_remote_interface_search")
58
+ || failed.has("npm_dep_chrome_remote_interface_screen")
59
+ || failed.has("npm_dep_ws")
58
60
  );
59
61
  const needPython = failed.has("python_cli");
60
62
  const needPillow = failed.has("python_pillow");
61
-
62
- const ordered_steps = [];
63
- if (needNode) {
64
- ordered_steps.push({
65
- id: "install_nodejs",
66
- title: "安装 Node.js >= 18",
67
- blocked_by: [],
63
+
64
+ const ordered_steps = [];
65
+ if (needScreenConfig) {
66
+ const configCheck = checks.find((item) => item?.key === "screen_config");
67
+ ordered_steps.push({
68
+ id: "fill_screening_config",
69
+ title: "填写 screening-config.json(baseUrl / apiKey / model)",
70
+ blocked_by: [],
71
+ commands: [
72
+ `打开并填写:${configCheck?.path || "~/.codex/boss-recommend-mcp/screening-config.json"}`,
73
+ "确认 baseUrl、apiKey、model 都是可用值(不要保留模板占位符)。"
74
+ ]
75
+ });
76
+ }
77
+ if (needNode) {
78
+ ordered_steps.push({
79
+ id: "install_nodejs",
80
+ title: "安装 Node.js >= 18",
81
+ blocked_by: [],
68
82
  commands: [
69
83
  "winget install OpenJS.NodeJS.LTS",
70
84
  "node --version"
@@ -102,14 +116,21 @@ function buildPreflightRecovery(checks = [], workspaceRoot) {
102
116
  });
103
117
  }
104
118
 
105
- const promptLines = [
106
- "你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
107
- "1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
108
- "2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
109
- "3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
110
- "4) python_pillow 失败 -> 最后安装 Pillow。",
111
- "每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
112
- ];
119
+ const promptLines = [
120
+ "你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
121
+ "1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
122
+ "2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
123
+ "3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
124
+ "4) python_pillow 失败 -> 最后安装 Pillow。",
125
+ "每一步完成后都重新运行 doctor,直到所有检查通过后再重试流水线。"
126
+ ];
127
+ if (needScreenConfig) {
128
+ promptLines.splice(
129
+ 1,
130
+ 0,
131
+ "0) 若 screen_config 失败:先让用户提供并填写 baseUrl、apiKey、model(不得使用模板占位符)。"
132
+ );
133
+ }
113
134
 
114
135
  if (needNpm) {
115
136
  const npmCommands = buildNpmInstallCommands(checks, workspaceRoot);
@@ -190,15 +211,23 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
190
211
  const currentUrl = pageState?.current_url || null;
191
212
  const state = pageState?.state || "UNKNOWN";
192
213
  const isPortIssue = state === "DEBUG_PORT_UNREACHABLE";
214
+ const needsLogin = state === "LOGIN_REQUIRED" || state === "LOGIN_REQUIRED_AFTER_REDIRECT";
215
+ const launchAttempt = pageState?.launch_attempt || null;
216
+ const launchLine = launchAttempt?.ok
217
+ ? `已自动启动 Chrome(--remote-debugging-port=${debugPort},--user-data-dir=${launchAttempt.user_data_dir || "auto"})。`
218
+ : null;
193
219
  const steps = [
194
220
  `请先在可连接到 DevTools 端口 ${debugPort} 的 Chrome 实例中完成以下操作:`,
221
+ ...(launchLine ? [launchLine] : []),
195
222
  "1) 确认当前 Chrome 与本次运行使用同一个远程调试端口。",
196
223
  isPortIssue
197
224
  ? `2) 若端口不可连接,请用远程调试方式启动 Chrome(示例:chrome.exe --remote-debugging-port=${debugPort})。`
198
225
  : "2) 确认端口可连接且浏览器窗口保持打开。",
199
- "3) 登录 Boss 直聘(如已掉线请重新登录)。",
200
- `4) 登录后导航并停留在推荐页:${expectedUrl}`,
201
- "5) 完成后回复“已就绪”,我再继续执行搜索和筛选。"
226
+ needsLogin
227
+ ? "3) 当前检测到 Boss 未登录,请先在该 Chrome 实例完成登录。"
228
+ : "3) 如 Boss 登录态失效,请先重新登录。",
229
+ `4) 登录完成后先导航并停留在推荐页:${expectedUrl}`,
230
+ "5) 完成后回复“已就绪”,我会继续执行并优先自动导航到推荐页。"
202
231
  ];
203
232
  return {
204
233
  debug_port: debugPort,
@@ -210,23 +239,25 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
210
239
  }
211
240
 
212
241
  const defaultDependencies = {
242
+ attemptPipelineAutoRepair,
213
243
  parseRecommendInstruction,
214
- ensureBossRecommendPageReady,
215
- runPipelinePreflight,
216
- runRecommendSearchCli,
217
- runRecommendScreenCli
244
+ ensureBossRecommendPageReady,
245
+ runPipelinePreflight,
246
+ runRecommendSearchCli,
247
+ runRecommendScreenCli
218
248
  };
219
249
 
220
- export async function runRecommendPipeline(
221
- { workspaceRoot, instruction, confirmation, overrides },
222
- dependencies = defaultDependencies
223
- ) {
224
- const {
225
- parseRecommendInstruction: parseInstruction,
226
- ensureBossRecommendPageReady: ensureRecommendPageReady,
227
- runPipelinePreflight: runPreflight,
228
- runRecommendSearchCli: searchCli,
229
- runRecommendScreenCli: screenCli
250
+ export async function runRecommendPipeline(
251
+ { workspaceRoot, instruction, confirmation, overrides },
252
+ dependencies = defaultDependencies
253
+ ) {
254
+ const {
255
+ attemptPipelineAutoRepair: attemptAutoRepair,
256
+ parseRecommendInstruction: parseInstruction,
257
+ ensureBossRecommendPageReady: ensureRecommendPageReady,
258
+ runPipelinePreflight: runPreflight,
259
+ runRecommendSearchCli: searchCli,
260
+ runRecommendScreenCli: screenCli
230
261
  } = dependencies;
231
262
  const startedAt = Date.now();
232
263
  const parsed = parseInstruction({ instruction, confirmation, overrides });
@@ -249,23 +280,49 @@ export async function runRecommendPipeline(
249
280
  return buildNeedConfirmationResponse(parsed);
250
281
  }
251
282
 
252
- const preflight = runPreflight(workspaceRoot);
253
- if (!preflight.ok) {
254
- const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
255
- return buildFailedResponse(
256
- "PIPELINE_PREFLIGHT_FAILED",
257
- "Recommend 流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
258
- {
259
- search_params: parsed.searchParams,
260
- screen_params: parsed.screenParams,
261
- diagnostics: {
262
- checks: preflight.checks,
263
- debug_port: preflight.debug_port,
264
- recovery
265
- }
266
- }
267
- );
268
- }
283
+ let preflight = runPreflight(workspaceRoot);
284
+ let autoRepair = null;
285
+ if (!preflight.ok) {
286
+ if (typeof attemptAutoRepair === "function") {
287
+ autoRepair = attemptAutoRepair(workspaceRoot, preflight);
288
+ if (autoRepair?.preflight) {
289
+ preflight = autoRepair.preflight;
290
+ }
291
+ }
292
+ }
293
+
294
+ if (!preflight.ok) {
295
+ const screenConfigCheck = preflight.checks?.find((item) => item?.key === "screen_config" && item?.ok === false);
296
+ const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
297
+ return buildFailedResponse(
298
+ "PIPELINE_PREFLIGHT_FAILED",
299
+ "Recommend 流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
300
+ {
301
+ search_params: parsed.searchParams,
302
+ screen_params: parsed.screenParams,
303
+ required_user_action: screenConfigCheck ? "provide_screening_config" : undefined,
304
+ guidance: screenConfigCheck
305
+ ? {
306
+ config_path: screenConfigCheck.path,
307
+ agent_prompt: [
308
+ "请先让用户填写 screening-config.json 的以下字段:",
309
+ "1) baseUrl",
310
+ "2) apiKey",
311
+ "3) model",
312
+ `配置文件路径:${screenConfigCheck.path}`,
313
+ "注意:不要使用模板占位符(例如 replace-with-openai-api-key)。填写完成后重试。"
314
+ ].join("\n")
315
+ }
316
+ : undefined,
317
+ diagnostics: {
318
+ checks: preflight.checks,
319
+ debug_port: preflight.debug_port,
320
+ auto_repair: autoRepair,
321
+ recovery
322
+ }
323
+ }
324
+ );
325
+ }
269
326
 
270
327
  const pageCheck = await ensureRecommendPageReady(workspaceRoot, {
271
328
  port: preflight.debug_port
@@ -353,6 +353,161 @@ async function testPreflightRecoveryPlanOrder() {
353
353
  assert.equal(result.diagnostics.recovery.agent_prompt.includes("不要并行跳步"), true);
354
354
  }
355
355
 
356
+ async function testPreflightAutoRepairCanUnblockPipeline() {
357
+ let repairCalled = false;
358
+ const result = await runRecommendPipeline(
359
+ {
360
+ workspaceRoot: process.cwd(),
361
+ instruction: "test",
362
+ confirmation: {},
363
+ overrides: {}
364
+ },
365
+ {
366
+ parseRecommendInstruction: () => createParsed(),
367
+ runPipelinePreflight: () => ({
368
+ ok: false,
369
+ debug_port: 9222,
370
+ checks: [{ key: "npm_dep_ws", ok: false, install_cwd: process.cwd() }]
371
+ }),
372
+ attemptPipelineAutoRepair: () => {
373
+ repairCalled = true;
374
+ return {
375
+ attempted: true,
376
+ actions: [{ ok: true, action: "install_npm_dependencies" }],
377
+ preflight: { ok: true, debug_port: 9222, checks: [] }
378
+ };
379
+ },
380
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
381
+ runRecommendSearchCli: async () => ({ ok: true, summary: { candidate_count: 1, applied_filters: {} } }),
382
+ runRecommendScreenCli: async () => ({ ok: true, summary: { processed_count: 1, passed_count: 1, skipped_count: 0 } })
383
+ }
384
+ );
385
+
386
+ assert.equal(repairCalled, true);
387
+ assert.equal(result.status, "COMPLETED");
388
+ }
389
+
390
+ async function testPreflightAutoRepairStillFailShouldExposeDiagnostics() {
391
+ const result = await runRecommendPipeline(
392
+ {
393
+ workspaceRoot: process.cwd(),
394
+ instruction: "test",
395
+ confirmation: {},
396
+ overrides: {}
397
+ },
398
+ {
399
+ parseRecommendInstruction: () => createParsed(),
400
+ runPipelinePreflight: () => ({
401
+ ok: false,
402
+ debug_port: 9222,
403
+ checks: [{ key: "node_cli", ok: false }]
404
+ }),
405
+ attemptPipelineAutoRepair: () => ({
406
+ attempted: true,
407
+ actions: [{ ok: false, action: "install_npm_dependencies" }],
408
+ preflight: {
409
+ ok: false,
410
+ debug_port: 9222,
411
+ checks: [{ key: "node_cli", ok: false }]
412
+ }
413
+ }),
414
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
415
+ runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
416
+ runRecommendScreenCli: async () => ({ ok: true, summary: {} })
417
+ }
418
+ );
419
+
420
+ assert.equal(result.status, "FAILED");
421
+ assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
422
+ assert.equal(result.diagnostics.auto_repair.attempted, true);
423
+ }
424
+
425
+ async function testScreenConfigFailureShouldRequireUserProvidedConfig() {
426
+ const result = await runRecommendPipeline(
427
+ {
428
+ workspaceRoot: process.cwd(),
429
+ instruction: "test",
430
+ confirmation: {},
431
+ overrides: {}
432
+ },
433
+ {
434
+ parseRecommendInstruction: () => createParsed(),
435
+ runPipelinePreflight: () => ({
436
+ ok: false,
437
+ debug_port: 9222,
438
+ checks: [
439
+ {
440
+ key: "screen_config",
441
+ ok: false,
442
+ path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
443
+ message: "screening-config.json 缺失或格式无效"
444
+ }
445
+ ]
446
+ }),
447
+ attemptPipelineAutoRepair: () => ({
448
+ attempted: false,
449
+ actions: [],
450
+ preflight: {
451
+ ok: false,
452
+ debug_port: 9222,
453
+ checks: [
454
+ {
455
+ key: "screen_config",
456
+ ok: false,
457
+ path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
458
+ message: "screening-config.json 缺失或格式无效"
459
+ }
460
+ ]
461
+ }
462
+ }),
463
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
464
+ runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
465
+ runRecommendScreenCli: async () => ({ ok: true, summary: {} })
466
+ }
467
+ );
468
+
469
+ assert.equal(result.status, "FAILED");
470
+ assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
471
+ assert.equal(result.required_user_action, "provide_screening_config");
472
+ assert.equal(result.guidance.config_path.includes("screening-config.json"), true);
473
+ assert.equal(result.guidance.agent_prompt.includes("baseUrl"), true);
474
+ assert.equal(result.guidance.agent_prompt.includes("apiKey"), true);
475
+ assert.equal(result.guidance.agent_prompt.includes("model"), true);
476
+ }
477
+
478
+ async function testScreenConfigRecoveryStepShouldBeFirst() {
479
+ const result = await runRecommendPipeline(
480
+ {
481
+ workspaceRoot: "C:/workspace/boss-recommend-mcp",
482
+ instruction: "test",
483
+ confirmation: {},
484
+ overrides: {}
485
+ },
486
+ {
487
+ parseRecommendInstruction: () => createParsed(),
488
+ runPipelinePreflight: () => ({
489
+ ok: false,
490
+ debug_port: 9222,
491
+ checks: [
492
+ {
493
+ key: "screen_config",
494
+ ok: false,
495
+ path: "C:/Users/test/.codex/boss-recommend-mcp/screening-config.json",
496
+ message: "screening-config.json 缺失或格式无效"
497
+ },
498
+ { key: "node_cli", ok: false }
499
+ ]
500
+ }),
501
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
502
+ runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
503
+ runRecommendScreenCli: async () => ({ ok: true, summary: {} })
504
+ }
505
+ );
506
+
507
+ assert.equal(result.status, "FAILED");
508
+ assert.equal(result.diagnostics.recovery.ordered_steps[0].id, "fill_screening_config");
509
+ }
510
+
356
511
  async function main() {
357
512
  await testNeedConfirmationGate();
358
513
  await testNeedSchoolTagConfirmationGate();
@@ -364,6 +519,10 @@ async function main() {
364
519
  await testLoginRequiredShouldReturnGuidance();
365
520
  await testDebugPortUnreachableShouldReturnConnectionCode();
366
521
  await testPreflightRecoveryPlanOrder();
522
+ await testPreflightAutoRepairCanUnblockPipeline();
523
+ await testPreflightAutoRepairStillFailShouldExposeDiagnostics();
524
+ await testScreenConfigFailureShouldRequireUserProvidedConfig();
525
+ await testScreenConfigRecoveryStepShouldBeFirst();
367
526
  console.log("pipeline tests passed");
368
527
  }
369
528