@reconcrap/boss-recruit-mcp 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ npx @reconcrap/boss-recruit-mcp install
25
25
  - 安装 Codex skill 到 `$CODEX_HOME/skills/boss-recruit-pipeline`
26
26
  - 初始化用户配置到 `$CODEX_HOME/boss-recruit-mcp/screening-config.json`
27
27
  - 包内自带 `boss-search-cli` 与 `boss-screen-cli` 运行时文件,无需额外目录结构
28
+ - 不包含 `favorite-calibration.json`,首次使用前需要自行校准生成
28
29
 
29
30
  ## 准备配置
30
31
 
@@ -37,8 +38,9 @@ $CODEX_HOME/boss-recruit-mcp/screening-config.json
37
38
  2. 填写以下字段:
38
39
 
39
40
  - `baseUrl` / `apiKey` / `model` 必填
40
- - `calibrationFile` 可选,默认走 `boss-screen-cli/favorite-calibration.json`
41
- - `outputDir` 可选,不填则输出到 `boss-screen-cli` 目录
41
+ - `debugPort` 可选,默认 `9222`
42
+ - `calibrationFile` 可选;不填时默认使用 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
43
+ - `outputDir` 可选;不填时默认输出到用户桌面
42
44
 
43
45
  ## 运行
44
46
 
@@ -48,6 +50,40 @@ boss-recruit-mcp start
48
50
 
49
51
  该服务通过 stdio 与 MCP client 通信。
50
52
 
53
+ ## Chrome 与校准
54
+
55
+ 推荐先启动调试 Chrome:
56
+
57
+ ```bash
58
+ boss-recruit-mcp launch-chrome --port 9222
59
+ ```
60
+
61
+ 然后执行校准:
62
+
63
+ ```bash
64
+ boss-recruit-mcp calibrate --port 9222
65
+ ```
66
+
67
+ 校准前请按这个顺序操作:
68
+
69
+ 1. 打开 Boss 直聘搜索页
70
+ 2. 随便打开一位人选的详情页
71
+ 3. 点击收藏按钮
72
+ 4. 再次点击,取消这位人选的收藏
73
+ 5. 关闭详情页
74
+
75
+ 校准文件默认生成到:
76
+
77
+ ```bash
78
+ $CODEX_HOME/boss-recruit-mcp/favorite-calibration.json
79
+ ```
80
+
81
+ 也可以用下面的命令检查依赖、配置和校准文件:
82
+
83
+ ```bash
84
+ boss-recruit-mcp doctor --port 9222
85
+ ```
86
+
51
87
  ## 工具输入
52
88
 
53
89
  ```json
@@ -70,6 +106,7 @@ boss-recruit-mcp start
70
106
  - 确认后自动执行:搜索 CLI -> 筛选 CLI
71
107
  - 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
72
108
  - 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
109
+ - 若缺少 `favorite-calibration.json`,会返回 `CALIBRATION_REQUIRED`
73
110
  - 若当前运行环境不允许启动子进程,会返回更明确的权限错误码而不是笼统失败
74
111
  - 配置文件查找顺序:`BOSS_RECRUIT_SCREEN_CONFIG` > 工作区 `boss-recruit-mcp/config/screening-config.json` > 用户目录 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` > 包内示例配置
75
112
 
@@ -1,7 +1,5 @@
1
1
  {
2
2
  "baseUrl": "https://your-llm-endpoint.example.com/v1",
3
3
  "apiKey": "replace-with-your-api-key",
4
- "model": "replace-with-your-model-name",
5
- "calibrationFile": "../boss-screen-cli/favorite-calibration.json",
6
- "outputDir": "../boss-screen-cli/output"
4
+ "model": "replace-with-your-model-name"
7
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -5,16 +5,18 @@
5
5
  ## 安装
6
6
 
7
7
  ```powershell
8
- npx boss-recruit-mcp install
8
+ npx @reconcrap/boss-recruit-mcp install
9
9
  ```
10
10
 
11
11
  这条命令会:
12
12
 
13
13
  - 把 skill 安装到 `$CODEX_HOME/skills/boss-recruit-pipeline`
14
14
  - 在 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` 创建配置模板
15
+ - 默认把筛选结果输出到用户桌面
15
16
 
16
17
  ## 前置要求
17
18
 
18
19
  - Chrome 已使用 `--remote-debugging-port=9222` 启动
19
20
  - Boss 页面已登录
20
21
  - 已在用户配置中填写有效的 `baseUrl`、`apiKey`、`model`
22
+ - 已生成 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
@@ -16,6 +16,37 @@
16
16
  - 再按自然语言 criteria 做二次 LLM 筛选
17
17
  - 适合“做过某方向 + 额外论文/项目/经历要求”的招聘请求
18
18
 
19
+ ## Setup First
20
+
21
+ 在真正调用 `run_recruit_pipeline` 前,先检查运行条件,不要直接进入搜索:
22
+
23
+ 1. 先检查 MCP 是否已安装 / 可调用;若未安装,优先执行:
24
+ - `npx @reconcrap/boss-recruit-mcp install`
25
+ 2. 先检查依赖与配置;优先执行:
26
+ - `boss-recruit-mcp doctor --port 9222`
27
+ 3. 若用户还未确认 Chrome 调试端口,先提议 `9222`。
28
+ 4. 若用户同意 `9222`,或给出新的端口,后续所有动作都使用该端口。
29
+ 5. 若 Chrome 未以远程调试模式启动,优先帮助用户启动:
30
+ - `boss-recruit-mcp launch-chrome --port <port>`
31
+ - 打开页面:`https://www.zhipin.com/web/chat/search`
32
+ 6. 只有在以上条件满足后,才继续调用流水线。
33
+
34
+ ## Calibration Requirement
35
+
36
+ - 如果缺少 `favorite-calibration.json`,不要直接搜索筛选。
37
+ - 需要明确提醒用户先做校准。
38
+ - 提示语必须包含这几步:
39
+ - 打开 Boss 直聘
40
+ - 去到搜索页面
41
+ - 随便打开一位人选的详情页
42
+ - 点击收藏按钮
43
+ - 再次点击取消该人选的收藏
44
+ - 关闭详情页
45
+ - 然后执行:
46
+ - `boss-recruit-mcp calibrate --port <port>`
47
+ - 默认校准文件路径:
48
+ - `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
49
+
19
50
  ## Required MCP Tool
20
51
 
21
52
  - Tool name: `run_recruit_pipeline`
@@ -26,7 +57,18 @@
26
57
 
27
58
  ## Execution Policy
28
59
 
29
- 1. 收到招聘指令后,先调用一次 `run_recruit_pipeline`(只传 `instruction`)。
60
+ 1. 收到招聘指令后,先做 setup 检查:
61
+ - MCP 是否可用
62
+ - Chrome 调试端口是否已确认
63
+ - Chrome 是否已启动到 Boss 搜索页面
64
+ - `favorite-calibration.json` 是否存在
65
+ 2. 若缺少依赖或 MCP 未启动:
66
+ - 自动安装依赖并帮助用户启动 MCP;
67
+ - 优先使用 `npx @reconcrap/boss-recruit-mcp install`
68
+ - 然后使用用户的 MCP 配置启动 `boss-recruit-mcp`
69
+ 3. 若缺少校准文件:
70
+ - 明确提示用户先完成校准,不要直接调用流水线。
71
+ 4. 只有当以上条件满足时,才首次调用 `run_recruit_pipeline`(只传 `instruction`)。
30
72
  2. 若返回 `NEED_INPUT`:
31
73
  - 一次性向用户列出 `missing_fields` 所有缺失项;
32
74
  - 缺失项常见含义:
@@ -44,6 +86,7 @@
44
86
  5. 若返回 `FAILED`:
45
87
  - 先提炼 `error.code`、`error.message`、`diagnostics`;
46
88
  - 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
89
+ - 如果是 `CALIBRATION_REQUIRED`,明确提醒用户执行校准,并给出校准步骤;
47
90
  - 如果是 `SEARCH_PROCESS_PERMISSION_DENIED` 或 `SCREEN_PROCESS_PERMISSION_DENIED`,明确说明“当前环境拒绝创建子进程”,建议用户在本地终端直接运行 MCP;
48
91
  - 如果是 `SEARCH_CLI_MISSING` / `SCREEN_CLI_MISSING` / `SCREEN_CONFIG_ERROR`,直接告诉用户缺什么,不要只说“重试”;
49
92
  - 若是可修复输入问题,提示用户修正条件后重试。
@@ -61,6 +104,8 @@
61
104
  - 如果失败原因明显是环境问题,要直接说明不是用户输入有误。
62
105
  - 如果工具已经返回 `diagnostics.checks`,优先基于这些检查项生成排障建议。
63
106
  - 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
107
+ - 如果端口还没确认,优先询问用户是否使用 `9222`,并把它作为推荐值。
108
+ - 如果需要打开 Chrome,优先帮用户执行而不是只给命令。
64
109
 
65
110
  ## Example
66
111
 
@@ -70,10 +115,13 @@
70
115
 
71
116
  期望行为:
72
117
 
73
- 1. 首次调用流水线。
74
- 2. keyword 被自动提取为 `AI infra`,先让用户确认。
75
- 3. 确认后再次调用。
76
- 4. 成功则返回通过人数与 CSV 路径;失败则按错误类型给出下一步。
118
+ 1. 先提议 Chrome 调试端口 `9222`。
119
+ 2. 用户确认后,启动调试 Chrome 并打开 Boss 搜索页面。
120
+ 3. 先检查校准文件是否存在;若不存在,提醒用户按步骤完成校准。
121
+ 4. 环境就绪后再首次调用流水线。
122
+ 5. 若 keyword 被自动提取为 `AI infra`,先让用户确认。
123
+ 6. 确认后再次调用。
124
+ 7. 成功则返回通过人数与 CSV 路径;失败则按错误类型给出下一步。
77
125
 
78
126
  ## Response Style
79
127
 
package/src/adapters.js CHANGED
@@ -16,6 +16,14 @@ function getUserConfigPath() {
16
16
  return path.join(getCodexHome(), "boss-recruit-mcp", "screening-config.json");
17
17
  }
18
18
 
19
+ function getUserCalibrationPath() {
20
+ return path.join(getCodexHome(), "boss-recruit-mcp", "favorite-calibration.json");
21
+ }
22
+
23
+ function getDesktopDir() {
24
+ return path.join(os.homedir(), "Desktop");
25
+ }
26
+
19
27
  function resolveScreenConfigPath(workspaceRoot) {
20
28
  const envConfigPath = process.env.BOSS_RECRUIT_SCREEN_CONFIG
21
29
  ? path.resolve(process.env.BOSS_RECRUIT_SCREEN_CONFIG)
@@ -182,10 +190,35 @@ function loadScreenConfig(configPath) {
182
190
  }
183
191
  }
184
192
 
193
+ function resolveDebugPort(config) {
194
+ const fromEnv = Number.parseInt(process.env.BOSS_RECRUIT_CHROME_PORT || "", 10);
195
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
196
+ const fromConfig = Number.parseInt(String(config?.debugPort || ""), 10);
197
+ if (Number.isFinite(fromConfig) && fromConfig > 0) return fromConfig;
198
+ return 9222;
199
+ }
200
+
201
+ function resolveWorkspaceDebugPort(workspaceRoot) {
202
+ const configPath = resolveScreenConfigPath(workspaceRoot);
203
+ if (pathExists(configPath)) {
204
+ const loaded = loadScreenConfig(configPath);
205
+ if (loaded.ok) {
206
+ return resolveDebugPort(loaded.config);
207
+ }
208
+ }
209
+ return resolveDebugPort(null);
210
+ }
211
+
185
212
  export function runPipelinePreflight(workspaceRoot) {
186
213
  const searchDir = resolveSearchCliDir(workspaceRoot);
187
214
  const screenDir = resolveScreenCliDir(workspaceRoot);
188
215
  const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
216
+ const loaded = pathExists(screenConfigPath) ? loadScreenConfig(screenConfigPath) : null;
217
+ const calibrationPath = loaded?.ok
218
+ ? (loaded.config.calibrationFile
219
+ ? path.resolve(path.dirname(screenConfigPath), loaded.config.calibrationFile)
220
+ : getUserCalibrationPath())
221
+ : getUserCalibrationPath();
189
222
  const checks = [
190
223
  {
191
224
  key: "search_cli_dir",
@@ -216,6 +249,12 @@ export function runPipelinePreflight(workspaceRoot) {
216
249
  ok: pathExists(screenConfigPath),
217
250
  path: screenConfigPath,
218
251
  message: "screening-config.json 不存在"
252
+ },
253
+ {
254
+ key: "favorite_calibration",
255
+ ok: pathExists(calibrationPath),
256
+ path: calibrationPath,
257
+ message: "favorite-calibration.json 不存在,请先完成收藏按钮校准"
219
258
  }
220
259
  ];
221
260
 
@@ -231,6 +270,7 @@ function localDirHint(workspaceRoot, dirName) {
231
270
 
232
271
  export async function runSearchCli({ workspaceRoot, searchParams }) {
233
272
  const searchDir = resolveSearchCliDir(workspaceRoot);
273
+ const debugPort = resolveWorkspaceDebugPort(workspaceRoot);
234
274
  if (!searchDir) {
235
275
  return {
236
276
  ok: false,
@@ -251,7 +291,9 @@ export async function runSearchCli({ workspaceRoot, searchParams }) {
251
291
  "--schools",
252
292
  searchParams.schools.join(","),
253
293
  "--city",
254
- searchParams.city
294
+ searchParams.city,
295
+ "--port",
296
+ String(debugPort)
255
297
  ];
256
298
 
257
299
  const result = await runProcess({
@@ -304,7 +346,8 @@ export async function runScreenCli({ workspaceRoot, screenParams }) {
304
346
 
305
347
  const calibration = loaded.config.calibrationFile
306
348
  ? path.resolve(configBaseDir, loaded.config.calibrationFile)
307
- : path.join(screenDir, "favorite-calibration.json");
349
+ : getUserCalibrationPath();
350
+ const debugPort = resolveDebugPort(loaded.config);
308
351
 
309
352
  const outputName = `筛选结果_${Date.now()}.csv`;
310
353
  let outputPath = outputName;
@@ -312,6 +355,10 @@ export async function runScreenCli({ workspaceRoot, screenParams }) {
312
355
  const resolvedOutputDir = path.resolve(configBaseDir, loaded.config.outputDir);
313
356
  fs.mkdirSync(resolvedOutputDir, { recursive: true });
314
357
  outputPath = path.join(resolvedOutputDir, outputName);
358
+ } else {
359
+ const desktopDir = getDesktopDir();
360
+ fs.mkdirSync(desktopDir, { recursive: true });
361
+ outputPath = path.join(desktopDir, outputName);
315
362
  }
316
363
 
317
364
  const args = [
@@ -322,6 +369,8 @@ export async function runScreenCli({ workspaceRoot, screenParams }) {
322
369
  loaded.config.apiKey,
323
370
  "--model",
324
371
  loaded.config.model,
372
+ "--port",
373
+ String(debugPort),
325
374
  "--criteria",
326
375
  screenParams.criteria,
327
376
  "--targetCount",
package/src/cli.js CHANGED
@@ -2,13 +2,24 @@ 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";
6
+ import { createRequire } from "node:module";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { startServer } from "./index.js";
9
+ import { runPipelinePreflight } from "./adapters.js";
7
10
 
11
+ const require = createRequire(import.meta.url);
8
12
  const currentFilePath = fileURLToPath(import.meta.url);
9
13
  const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
10
14
  const skillSourceDir = path.join(packageRoot, "skills", "boss-recruit-pipeline");
11
15
  const exampleConfigPath = path.join(packageRoot, "config", "screening-config.example.json");
16
+ const calibrationScriptPath = path.join(
17
+ packageRoot,
18
+ "vendor",
19
+ "boss-screen-cli",
20
+ "calibrate-favorite-position-v2.cjs"
21
+ );
22
+ const bossUrl = "https://www.zhipin.com/web/chat/search";
12
23
 
13
24
  function getCodexHome() {
14
25
  return process.env.CODEX_HOME
@@ -20,6 +31,74 @@ function ensureDir(targetPath) {
20
31
  fs.mkdirSync(targetPath, { recursive: true });
21
32
  }
22
33
 
34
+ function getDesktopDir() {
35
+ return path.join(os.homedir(), "Desktop");
36
+ }
37
+
38
+ function getUserConfigPath() {
39
+ return path.join(getCodexHome(), "boss-recruit-mcp", "screening-config.json");
40
+ }
41
+
42
+ function getUserCalibrationPath() {
43
+ return path.join(getCodexHome(), "boss-recruit-mcp", "favorite-calibration.json");
44
+ }
45
+
46
+ function parseOptions(args) {
47
+ const options = {};
48
+ for (let i = 0; i < args.length; i++) {
49
+ const token = args[i];
50
+ if (!token.startsWith("--")) continue;
51
+ const key = token.slice(2);
52
+ const next = args[i + 1];
53
+ if (next && !next.startsWith("--")) {
54
+ options[key] = next;
55
+ i += 1;
56
+ } else {
57
+ options[key] = true;
58
+ }
59
+ }
60
+ return options;
61
+ }
62
+
63
+ function hasModule(moduleName) {
64
+ try {
65
+ require.resolve(moduleName);
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function getDebugPort(options = {}) {
73
+ const raw = options.port || process.env.BOSS_RECRUIT_CHROME_PORT || "9222";
74
+ const port = Number.parseInt(String(raw), 10);
75
+ return Number.isFinite(port) && port > 0 ? port : 9222;
76
+ }
77
+
78
+ function getChromeExecutable() {
79
+ const candidates = [
80
+ process.env.BOSS_RECRUIT_CHROME_PATH,
81
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
82
+ path.join(process.env.ProgramFiles || "", "Google", "Chrome", "Application", "chrome.exe"),
83
+ path.join(process.env["ProgramFiles(x86)"] || "", "Google", "Chrome", "Application", "chrome.exe")
84
+ ].filter(Boolean);
85
+
86
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
87
+ }
88
+
89
+ function runNodeScript(scriptPath, args) {
90
+ return new Promise((resolve) => {
91
+ const child = spawn(process.execPath, [scriptPath, ...args], {
92
+ cwd: packageRoot,
93
+ stdio: "inherit",
94
+ windowsHide: false,
95
+ shell: false
96
+ });
97
+ child.on("close", (code) => resolve(code ?? 0));
98
+ child.on("error", () => resolve(1));
99
+ });
100
+ }
101
+
23
102
  function installSkill() {
24
103
  const codexHome = getCodexHome();
25
104
  const targetDir = path.join(codexHome, "skills", "boss-recruit-pipeline");
@@ -30,15 +109,96 @@ function installSkill() {
30
109
 
31
110
  function ensureUserConfig() {
32
111
  const targetDir = path.join(getCodexHome(), "boss-recruit-mcp");
33
- const targetPath = path.join(targetDir, "screening-config.json");
112
+ const targetPath = getUserConfigPath();
34
113
  ensureDir(targetDir);
35
114
  if (!fs.existsSync(targetPath)) {
36
- fs.copyFileSync(exampleConfigPath, targetPath);
115
+ const template = JSON.parse(fs.readFileSync(exampleConfigPath, "utf8"));
116
+ template.outputDir = getDesktopDir();
117
+ fs.writeFileSync(targetPath, JSON.stringify(template, null, 2), "utf8");
37
118
  return { path: targetPath, created: true };
38
119
  }
39
120
  return { path: targetPath, created: false };
40
121
  }
41
122
 
123
+ function printDoctor(options) {
124
+ const port = getDebugPort(options);
125
+ const checks = runPipelinePreflight(process.cwd()).checks.slice();
126
+ const userConfigPath = getUserConfigPath();
127
+ checks.push({
128
+ key: "user_config",
129
+ ok: fs.existsSync(userConfigPath),
130
+ path: userConfigPath,
131
+ message: "用户配置不存在"
132
+ });
133
+ checks.push({
134
+ key: "calibration_script",
135
+ ok: fs.existsSync(calibrationScriptPath),
136
+ path: calibrationScriptPath,
137
+ message: "校准脚本不存在"
138
+ });
139
+ checks.push({
140
+ key: "dependency_ws",
141
+ ok: hasModule("ws"),
142
+ path: "ws",
143
+ message: "缺少 ws 依赖"
144
+ });
145
+ checks.push({
146
+ key: "dependency_chrome_remote_interface",
147
+ ok: hasModule("chrome-remote-interface"),
148
+ path: "chrome-remote-interface",
149
+ message: "缺少 chrome-remote-interface 依赖"
150
+ });
151
+ checks.push({
152
+ key: "chrome_debug_port",
153
+ ok: true,
154
+ path: `http://localhost:${port}`,
155
+ message: `建议使用 Chrome 调试端口 ${port}`
156
+ });
157
+ console.log(JSON.stringify({ ok: checks.every((item) => item.ok), port, checks }, null, 2));
158
+ }
159
+
160
+ async function calibrate(options) {
161
+ const port = getDebugPort(options);
162
+ const output = options.output ? path.resolve(String(options.output)) : getUserCalibrationPath();
163
+ console.log("Before calibration:");
164
+ console.log("1. Open Boss search page.");
165
+ console.log("2. Open any candidate detail page.");
166
+ console.log("3. Click the favorite button once.");
167
+ console.log("4. Click again to cancel favorite for that candidate.");
168
+ console.log("5. Close the detail page after calibration completes.");
169
+ console.log("");
170
+ const code = await runNodeScript(calibrationScriptPath, [
171
+ "--port",
172
+ String(port),
173
+ "--output",
174
+ output
175
+ ]);
176
+ process.exitCode = code;
177
+ }
178
+
179
+ function launchChrome(options) {
180
+ const chromePath = getChromeExecutable();
181
+ if (!chromePath) {
182
+ console.error("Chrome executable not found. Set BOSS_RECRUIT_CHROME_PATH or install Google Chrome.");
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ const port = getDebugPort(options);
187
+ const args = [
188
+ `--remote-debugging-port=${port}`,
189
+ "--new-window",
190
+ bossUrl
191
+ ];
192
+ const child = spawn(chromePath, args, {
193
+ detached: true,
194
+ stdio: "ignore",
195
+ windowsHide: false
196
+ });
197
+ child.unref();
198
+ console.log(`Chrome launched with remote debugging port ${port}`);
199
+ console.log(`URL: ${bossUrl}`);
200
+ }
201
+
42
202
  function printHelp() {
43
203
  console.log("boss-recruit-mcp");
44
204
  console.log("");
@@ -48,6 +208,9 @@ function printHelp() {
48
208
  console.log(" boss-recruit-mcp install Install Codex skill and initialize user config");
49
209
  console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
50
210
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
211
+ console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
212
+ console.log(" boss-recruit-mcp calibrate Run favorite-button calibration and save favorite-calibration.json");
213
+ console.log(" boss-recruit-mcp launch-chrome Launch Chrome in remote-debugging mode and open Boss search");
51
214
  console.log(" boss-recruit-mcp where Print installed package, skill, and config paths");
52
215
  }
53
216
 
@@ -57,7 +220,9 @@ function printPaths() {
57
220
  console.log(`skill_source=${skillSourceDir}`);
58
221
  console.log(`codex_home=${codexHome}`);
59
222
  console.log(`skill_target=${path.join(codexHome, "skills", "boss-recruit-pipeline")}`);
60
- console.log(`config_target=${path.join(codexHome, "boss-recruit-mcp", "screening-config.json")}`);
223
+ console.log(`config_target=${getUserConfigPath()}`);
224
+ console.log(`calibration_target=${getUserCalibrationPath()}`);
225
+ console.log(`desktop_output_default=${getDesktopDir()}`);
61
226
  }
62
227
 
63
228
  function installAll() {
@@ -72,11 +237,13 @@ function installAll() {
72
237
  console.log("");
73
238
  console.log("Next steps:");
74
239
  console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
75
- console.log("2. Start Chrome with --remote-debugging-port=9222 and log in to Boss.");
76
- console.log("3. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
240
+ console.log("2. Run `boss-recruit-mcp launch-chrome --port 9222` and log in to Boss if needed.");
241
+ console.log("3. Run `boss-recruit-mcp calibrate --port 9222` to generate favorite-calibration.json.");
242
+ console.log("4. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
77
243
  }
78
244
 
79
245
  const command = process.argv[2] || "start";
246
+ const options = parseOptions(process.argv.slice(3));
80
247
 
81
248
  switch (command) {
82
249
  case "start":
@@ -97,6 +264,15 @@ switch (command) {
97
264
  );
98
265
  break;
99
266
  }
267
+ case "doctor":
268
+ printDoctor(options);
269
+ break;
270
+ case "calibrate":
271
+ await calibrate(options);
272
+ break;
273
+ case "launch-chrome":
274
+ launchChrome(options);
275
+ break;
100
276
  case "where":
101
277
  printPaths();
102
278
  break;
package/src/pipeline.js CHANGED
@@ -131,9 +131,14 @@ export async function runRecruitPipeline({
131
131
 
132
132
  const preflight = runPipelinePreflight(workspaceRoot);
133
133
  if (!preflight.ok) {
134
+ const missingCalibration = preflight.checks.find(
135
+ (check) => check.key === "favorite_calibration" && !check.ok
136
+ );
134
137
  return buildFailedResponse(
135
- "PIPELINE_PREFLIGHT_FAILED",
136
- "招聘流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
138
+ missingCalibration ? "CALIBRATION_REQUIRED" : "PIPELINE_PREFLIGHT_FAILED",
139
+ missingCalibration
140
+ ? "尚未完成收藏按钮校准,请先生成 favorite-calibration.json 后再执行筛选。"
141
+ : "招聘流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
137
142
  {
138
143
  search_params: parsed.searchParams,
139
144
  screen_params: parsed.screenParams,
@@ -18,6 +18,7 @@ const apiKey = args.apikey || args.apiKey;
18
18
  const model = args.model;
19
19
  const criteria = args.criteria;
20
20
  const targetCount = parseInt(args.target || args.targetCount || '10');
21
+ const debugPort = Number.parseInt(args.port || '9222', 10);
21
22
  const configFile = args.config || 'favorite-calibration.json';
22
23
  const outputCsv = args.output || `筛选结果_${Date.now()}.csv`;
23
24
 
@@ -39,7 +40,7 @@ function loadCalibration() {
39
40
 
40
41
  async function getChromeTab() {
41
42
  return new Promise((resolve, reject) => {
42
- http.get('http://localhost:9222/json/list', (res) => {
43
+ http.get(`http://localhost:${debugPort}/json/list`, (res) => {
43
44
  let data = '';
44
45
  res.on('data', chunk => data += chunk);
45
46
  res.on('end', () => {
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ const WebSocket = require('ws');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const args = process.argv.slice(2).reduce((acc, arg, i, arr) => {
7
+ if (arg.startsWith('--')) {
8
+ const key = arg.slice(2);
9
+ acc[key] = arr[i + 1] && !arr[i + 1].startsWith('--') ? arr[i + 1] : true;
10
+ }
11
+ return acc;
12
+ }, {});
13
+
14
+ const debugPort = Number.parseInt(args.port || '9222', 10);
15
+ const outputFile = path.resolve(args.output || path.join(process.cwd(), 'favorite-calibration.json'));
16
+
17
+ console.log('\x1b[36m========================================\x1b[0m');
18
+ console.log('\x1b[36mFavorite Button Calibration Tool\x1b[0m');
19
+ console.log('\x1b[36m========================================\x1b[0m');
20
+ console.log();
21
+
22
+ async function main() {
23
+ let tabs;
24
+ try {
25
+ const response = await fetch(`http://localhost:${debugPort}/json/list`);
26
+ tabs = await response.json();
27
+ } catch (e) {
28
+ console.log('\x1b[31mFailed to connect to Chrome DevTools: ' + e.message + '\x1b[0m');
29
+ process.exit(1);
30
+ }
31
+
32
+ const bossTab = tabs.find(tab => tab.url && tab.url.includes('zhipin.com'));
33
+ if (!bossTab) {
34
+ console.log('\x1b[31mBOSS page not found!\x1b[0m');
35
+ process.exit(1);
36
+ }
37
+
38
+ console.log('\x1b[32mFound: ' + bossTab.title + '\x1b[0m');
39
+ console.log('\x1b[90mURL: ' + bossTab.url + '\x1b[0m');
40
+
41
+ const wsUrl = bossTab.webSocketDebuggerUrl;
42
+ const ws = new WebSocket(wsUrl);
43
+
44
+ await new Promise((resolve, reject) => {
45
+ ws.on('open', resolve);
46
+ ws.on('error', reject);
47
+ });
48
+ console.log('\x1b[32mConnected!\x1b[0m');
49
+
50
+ const js = `(function() {
51
+ window.__calibrationData = null;
52
+
53
+ var iframes = document.querySelectorAll('iframe');
54
+ for (var i = 0; i < iframes.length; i++) {
55
+ var iframe = iframes[i];
56
+ if (iframe.src && iframe.src.includes('c-resume')) {
57
+ try {
58
+ var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
59
+ var canvases = iframeDoc.querySelectorAll('canvas');
60
+ if (canvases.length > 0) {
61
+ var canvas = canvases[0];
62
+ canvas.addEventListener('click', function(e) {
63
+ window.__calibrationData = {
64
+ clicked: true,
65
+ pageX: e.pageX,
66
+ pageY: e.pageY,
67
+ clientX: e.clientX,
68
+ clientY: e.clientY,
69
+ canvasX: e.clientX - canvas.getBoundingClientRect().left,
70
+ canvasY: e.clientY - canvas.getBoundingClientRect().top
71
+ };
72
+ }, { once: true });
73
+ return { success: true, canvasId: canvas.id };
74
+ }
75
+ } catch (e) {
76
+ return { success: false, error: e.message };
77
+ }
78
+ break;
79
+ }
80
+ }
81
+ return { success: false, error: 'Canvas not found' };
82
+ })()`;
83
+
84
+ let msgId = 1;
85
+
86
+ function sendMessage(method, params) {
87
+ return new Promise((resolve) => {
88
+ const id = msgId++;
89
+ const handler = (data) => {
90
+ try {
91
+ const msg = JSON.parse(data);
92
+ if (msg.id === id) {
93
+ ws.off('message', handler);
94
+ resolve(msg);
95
+ }
96
+ } catch (e) {}
97
+ };
98
+ ws.on('message', handler);
99
+ ws.send(JSON.stringify({ id, method, params }));
100
+ });
101
+ }
102
+
103
+ const result = await sendMessage('Runtime.evaluate', { expression: js, returnByValue: true });
104
+
105
+ if (result.result && result.result.result && result.result.result.value) {
106
+ const val = result.result.result.value;
107
+ if (val.success) {
108
+ console.log('\x1b[32mClick listener added to canvas!\x1b[0m');
109
+ } else {
110
+ console.log('\x1b[31mFailed to add listener: ' + val.error + '\x1b[0m');
111
+ ws.close();
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ console.log();
117
+ console.log('\x1b[33m========================================\x1b[0m');
118
+ console.log('\x1b[33mCALIBRATION MODE - Click the FAVORITE BUTTON now!\x1b[0m');
119
+ console.log('\x1b[33m========================================\x1b[0m');
120
+ console.log();
121
+ console.log('\x1b[36mMonitoring for 30 seconds...\x1b[0m');
122
+ console.log();
123
+
124
+ const checkJs = `(function() {
125
+ if (window.__calibrationData && window.__calibrationData.clicked) {
126
+ var data = window.__calibrationData;
127
+ window.__calibrationData = null;
128
+ return { clicked: true, position: data };
129
+ }
130
+ return { clicked: false };
131
+ })()`;
132
+
133
+ const endTime = Date.now() + 30000;
134
+ let clickResult = null;
135
+
136
+ while (Date.now() < endTime && ws.readyState === WebSocket.OPEN && !clickResult) {
137
+ const checkResult = await sendMessage('Runtime.evaluate', { expression: checkJs, returnByValue: true });
138
+
139
+ if (checkResult.result && checkResult.result.result && checkResult.result.result.value) {
140
+ const val = checkResult.result.result.value;
141
+ if (val.clicked) {
142
+ clickResult = val.position;
143
+ break;
144
+ }
145
+ }
146
+
147
+ await new Promise(r => setTimeout(r, 300));
148
+ process.stdout.write('\x1b[90m.\x1b[0m');
149
+ }
150
+
151
+ if (clickResult) {
152
+ console.log();
153
+ console.log();
154
+ console.log('\x1b[32m========================================\x1b[0m');
155
+ console.log('\x1b[32mCLICK CAPTURED!\x1b[0m');
156
+ console.log('\x1b[32m========================================\x1b[0m');
157
+ console.log();
158
+ console.log('\x1b[37m pageX: ' + clickResult.pageX + '\x1b[0m');
159
+ console.log('\x1b[37m pageY: ' + clickResult.pageY + '\x1b[0m');
160
+ console.log('\x1b[37m canvasX: ' + clickResult.canvasX + '\x1b[0m');
161
+ console.log('\x1b[37m canvasY: ' + clickResult.canvasY + '\x1b[0m');
162
+
163
+ const calibration = {
164
+ timestamp: new Date().toISOString().replace('T', ' ').slice(0, 19),
165
+ favoritePosition: {
166
+ pageX: clickResult.pageX,
167
+ pageY: clickResult.pageY,
168
+ canvasX: clickResult.canvasX,
169
+ canvasY: clickResult.canvasY
170
+ }
171
+ };
172
+
173
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
174
+ fs.writeFileSync(outputFile, JSON.stringify(calibration, null, 2), 'utf8');
175
+ console.log();
176
+ console.log('\x1b[32mCalibration saved to: ' + outputFile + '\x1b[0m');
177
+ } else {
178
+ console.log();
179
+ console.log();
180
+ console.log('\x1b[31mNo click detected within 30 seconds.\x1b[0m');
181
+ }
182
+
183
+ ws.close();
184
+ console.log();
185
+ console.log('\x1b[36mDone.\x1b[0m');
186
+ }
187
+
188
+ main().catch(err => {
189
+ console.error('\x1b[31mError: ' + err.message + '\x1b[0m');
190
+ process.exit(1);
191
+ });
@@ -3,7 +3,9 @@ import { BossSearcher } from './boss-searcher.js';
3
3
 
4
4
  class BossSearchCLI {
5
5
  constructor() {
6
- this.searcher = new BossSearcher(9222);
6
+ const args = this.parseArgs();
7
+ this.args = args;
8
+ this.searcher = new BossSearcher(args.port);
7
9
  }
8
10
 
9
11
  async run() {
@@ -11,12 +13,10 @@ class BossSearchCLI {
11
13
  console.log(' Boss直聘搜索自动化工具');
12
14
  console.log('========================================\n');
13
15
 
14
- const args = this.parseArgs();
15
-
16
16
  const connected = await this.searcher.connect();
17
17
  if (!connected) {
18
18
  console.log('\n请确保Chrome已通过以下命令启动:');
19
- console.log('chrome.exe --remote-debugging-port=9222');
19
+ console.log(`chrome.exe --remote-debugging-port=${this.args.port}`);
20
20
  process.exit(1);
21
21
  }
22
22
 
@@ -35,12 +35,12 @@ class BossSearchCLI {
35
35
 
36
36
  // 第二步:设置其他过滤条件(城市、学历、院校等)
37
37
  if (args.city) {
38
- await this.searcher.setCity(args.city);
38
+ await this.searcher.setCity(this.args.city);
39
39
  await this.searcher.sleep(500);
40
40
  console.log('');
41
41
  }
42
42
 
43
- await this.searchWithConfig(args);
43
+ await this.searchWithConfig(this.args);
44
44
  } catch (error) {
45
45
  console.error('❌ 执行出错:', error);
46
46
  } finally {
@@ -54,6 +54,7 @@ class BossSearchCLI {
54
54
  degree: '不限',
55
55
  schools: [],
56
56
  city: null,
57
+ port: 9222,
57
58
  experience: '不限',
58
59
  ageMin: null,
59
60
  ageMax: null
@@ -84,6 +85,11 @@ class BossSearchCLI {
84
85
  });
85
86
  } else if (arg === '--city' || arg === '-c') {
86
87
  args.city = argv[++i];
88
+ } else if (arg === '--port' || arg === '-p') {
89
+ const port = Number.parseInt(argv[++i], 10);
90
+ if (Number.isFinite(port) && port > 0) {
91
+ args.port = port;
92
+ }
87
93
  }
88
94
  }
89
95
 
@@ -1,9 +0,0 @@
1
- {
2
- "timestamp": "2026-03-22 14:06:49",
3
- "favoritePosition": {
4
- "pageX": 760,
5
- "pageY": 64,
6
- "canvasX": 760,
7
- "canvasY": 64
8
- }
9
- }