@reconcrap/boss-recruit-mcp 1.0.7 → 1.0.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/README.md CHANGED
@@ -84,6 +84,18 @@ $CODEX_HOME/boss-recruit-mcp/screening-config.json
84
84
  2. 填写以下字段:
85
85
 
86
86
  - `baseUrl` / `apiKey` / `model` 必填
87
+ - OpenAI 推荐:
88
+ - `baseUrl`: `https://api.openai.com/v1`
89
+ - `apiKey`: OpenAI API Key
90
+ - `model`: 例如 `gpt-4.1-mini`
91
+ - 也支持通过环境变量注入:
92
+ - `OPENAI_API_KEY`(可替代配置文件中的 `apiKey`)
93
+ - `OPENAI_BASE_URL`(可替代配置文件中的 `baseUrl`)
94
+ - `OPENAI_MODEL`(可替代配置文件中的 `model`)
95
+ - `OPENAI_ORG_ID` / `OPENAI_PROJECT_ID`(可选)
96
+ - 配置文件可选字段:
97
+ - `openaiOrganization`
98
+ - `openaiProject`
87
99
  - `debugPort` 可选,默认 `9222`
88
100
  - `calibrationFile` 可选;不填时默认使用 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
89
101
  - `outputDir` 可选;不填时默认输出到用户桌面
@@ -111,6 +123,12 @@ boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,
111
123
  boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人" --confirmation-json "{\"keyword_confirmed\":true,\"keyword_value\":\"推荐系统\",\"search_params_confirmed\":true}" --overrides-json "{\"city\":\"杭州\",\"degree\":\"本科\",\"schools\":[\"985\",\"211\",\"qs100\"],\"filter_recent_viewed\":true,\"target_count\":10}"
112
124
  ```
113
125
 
126
+ PowerShell 下更推荐用文件方式,避免引号转义导致 `INVALID_CLI_INPUT`:
127
+
128
+ ```bash
129
+ boss-recruit-mcp run --instruction-file request.txt --confirmation-file confirmation.json --overrides-file overrides.json
130
+ ```
131
+
114
132
  如果命令行中放长文本不方便,改用文件:
115
133
 
116
134
  ```bash
@@ -124,19 +142,16 @@ boss-recruit-mcp run --instruction-file request.txt --confirmation-file confirma
124
142
 
125
143
  先确认你要使用的 Chrome 远程调试端口。推荐 `9222`,但如果你已经有一个正在运行的远程调试 Chrome,也可以继续使用那个端口。确认端口后,再执行下面的命令。
126
144
 
127
- 推荐先启动调试 Chrome:
145
+ 执行校准(会自动尝试打开 Boss 搜索页):
128
146
 
129
147
  ```bash
130
- boss-recruit-mcp launch-chrome --port <port>
148
+ boss-recruit-mcp calibrate --port <port>
131
149
  ```
132
150
 
133
- `launch-chrome` 会自动为该端口创建独立的 Chrome profile 目录,避免复用已有 Chrome 实例导致调试端口未生效。
134
- 命令还会检查新打开的 Boss 页面是否仍停留在 `search` 页面;如果跳转到了登录页或其他页面,说明需要用户先手动登录 Boss。
135
-
136
- 然后执行校准:
151
+ 如果你想自定义监听窗口(默认 60 秒):
137
152
 
138
153
  ```bash
139
- boss-recruit-mcp calibrate --port <port>
154
+ boss-recruit-mcp calibrate --port <port> --timeout-ms 60000
140
155
  ```
141
156
 
142
157
  如果你的 `screening-config.json` 里配置了自定义 `calibrationFile` 路径,而该路径当前不存在,直接把校准结果输出到那个路径:
@@ -200,6 +215,34 @@ boss-recruit-mcp doctor --port <port>
200
215
  - 若当前运行环境不允许启动子进程,会返回更明确的权限错误码而不是笼统失败
201
216
  - 配置文件查找顺序:`BOSS_RECRUIT_SCREEN_CONFIG` > 工作区 `boss-recruit-mcp/config/screening-config.json` > 用户目录 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` > 包内示例配置
202
217
 
218
+ ## 终版测试方案
219
+
220
+ 回归脚本位置:`scripts/regression.ps1`
221
+
222
+ 1. 快速回归(不依赖 Boss 页面,验证 CLI 输入与状态机基础逻辑):
223
+
224
+ ```bash
225
+ npm run test:regression:win
226
+ ```
227
+
228
+ 2. 全链路回归(依赖 Chrome 调试端口 + 已登录 Boss):
229
+
230
+ ```bash
231
+ powershell -ExecutionPolicy Bypass -File scripts/regression.ps1 -Mode full -Port 9222
232
+ ```
233
+
234
+ 3. 负向回归(错误端口,确保不会误返回 `COMPLETED`):
235
+
236
+ ```bash
237
+ powershell -ExecutionPolicy Bypass -File scripts/regression.ps1 -Mode negative
238
+ ```
239
+
240
+ 判定标准:
241
+
242
+ - 不应出现 `INVALID_CLI_INPUT`
243
+ - 正常场景允许 `COMPLETED` 或可诊断的 `FAILED`
244
+ - 错误端口场景必须是 `FAILED`,且不是参数解析类错误
245
+
203
246
  ## 发布
204
247
 
205
248
  ```bash
@@ -1,5 +1,7 @@
1
1
  {
2
- "baseUrl": "https://your-llm-endpoint.example.com/v1",
3
- "apiKey": "replace-with-your-api-key",
4
- "model": "replace-with-your-model-name"
2
+ "baseUrl": "https://api.openai.com/v1",
3
+ "apiKey": "replace-with-openai-api-key",
4
+ "model": "gpt-4.1-mini",
5
+ "openaiOrganization": "optional-org-id",
6
+ "openaiProject": "optional-project-id"
5
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -18,7 +18,8 @@
18
18
  "start": "node src/index.js",
19
19
  "cli": "node src/cli.js",
20
20
  "install:local": "node src/cli.js install",
21
- "test:parser": "node src/test-parser.js"
21
+ "test:parser": "node src/test-parser.js",
22
+ "test:regression:win": "powershell -ExecutionPolicy Bypass -File scripts/regression.ps1 -Mode quick"
22
23
  },
23
24
  "files": [
24
25
  "bin",
@@ -33,8 +33,13 @@ boss-recruit-mcp mcp-config --client all
33
33
  - 默认优先走 MCP;如果当前 agent 无法再添加 MCP,也可以改用 `boss-recruit-mcp run` 作为 CLI fallback。
34
34
  - 正式开始前,必须先做一轮参数确认,分开展示已识别参数、待确认参数、缺失参数。
35
35
  - 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
36
+ - 在正式执行前,必须单独让用户确认筛选 `criteria`(尤其学历/学校/论文等硬性条件)无误,不能只确认关键词和搜索参数。
36
37
  - 端口未确认时,必须先询问用户是否使用推荐的 `9222`,或提供一个已有的其他远程调试端口,不能直接默认 `9222`。
37
- - 新打开 Chrome 实例后,要检查页面是否仍停留在 Boss search;如果跳转到其他页面,必须提示用户先手动登录 Boss,再继续。
38
+ - 任何需要打开 Chrome 的动作前,先检查调试端口是否已有可用实例;端口可连时必须复用,不要再新开一个 9222 实例。
39
+ - 若页面未停留在 Boss search(例如跳到登录页或首页),必须提示用户先手动登录 Boss,再继续。
38
40
  - 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
41
+ - 学历/学校硬性条件不能在 criteria 清洗时被剔除;即使已提取到搜索参数,criteria 仍需保留原始约束语义(例如“本科学历必须是985”)。
39
42
  - 如果缺少 `favorite-calibration.json`,必须指导用户在当前环境重新校准,不能搜索或复制历史遗留校准文件来顶替。
43
+ - 校准提示使用两阶段:先问“是否准备好开始校准”,不要问“是否已完成校准”;用户确认后应直接启动 `boss-recruit-mcp calibrate --port <port>`。
44
+ - 校准里的“打开详情 -> 收藏 -> 取消收藏 -> 关闭详情”属于动作说明,不要要求用户先手动完成这些步骤后再回复“可以校准”。
40
45
  - 若缺失参数仍未补齐,只能在用户明确确认接受默认值和质量风险后继续,不能静默按默认执行。
@@ -27,12 +27,12 @@
27
27
  - 建议使用 `9222`
28
28
  - 但也允许用户明确提供一个已在使用的其他远程调试端口
29
29
  3. 在用户确认端口前,不要直接假设 `9222` 并执行任何依赖端口的命令。
30
- 4. 端口确认后,再检查依赖与配置:
30
+ 4. 端口确认后,先检查依赖与端口状态:
31
31
  - `boss-recruit-mcp doctor --port <port>`
32
- 5. Chrome 未以远程调试模式启动,优先帮助用户启动:
33
- - `boss-recruit-mcp launch-chrome --port <port>`
34
- - 打开页面:`https://www.zhipin.com/web/chat/search`
35
- 6. 启动新的 Chrome debugging 实例后,必须检查页面是否仍停留在 `https://www.zhipin.com/web/chat/search`:
32
+ 5. 任何“准备打开 Chrome”的动作前,必须先判断该端口是否已有可用实例:
33
+ - 若调试端口可连,禁止再新开 Chrome;直接复用现有实例,并确保页面在 `https://www.zhipin.com/web/chat/search`
34
+ - 仅当调试端口不可连时,才执行 `boss-recruit-mcp launch-chrome --port <port>`
35
+ 6. 若执行 `launch-chrome` 后页面没有停留在 `https://www.zhipin.com/web/chat/search`:
36
36
  - 若仍在 search 页面,可继续;
37
37
  - 若跳转到登录页、首页或其他 Boss 页面,视为“需要重新登录”;
38
38
  - 必须明确提示用户手动登录 Boss,并等待用户回复“已登录/可以继续”后,才能继续后续动作。
@@ -48,15 +48,18 @@
48
48
  - 必须明确告诉用户“当前期望的校准文件路径”;
49
49
  - 应指导用户用 `boss-recruit-mcp calibrate --port <port> --output <expected-path>` 直接生成到该路径;
50
50
  - 不要静默改写配置,也不要把别处的文件复制过去。
51
- - 提示语必须包含这几步:
52
- - 打开 Boss 直聘
53
- - 去到搜索页面
54
- - 随便打开一位人选的详情页
55
- - 点击收藏按钮
56
- - 再次点击取消该人选的收藏
51
+ - 校准动作说明用于“让用户了解流程”,不要求用户先手动点击页面:
52
+ - 打开 Boss 直聘搜索页面
53
+ - 打开任意一位人选详情页
54
+ - 执行一次收藏,再取消收藏
57
55
  - 关闭详情页
56
+ - 校准对话必须是“两阶段”:
57
+ - 第一阶段:先完整说明校准步骤,然后只问“是否准备好开始校准”;
58
+ - 第二阶段:用户确认“准备好了”后,直接启动校准命令,并明确说明“你不需要先手动完成页面点击,我会立即启动校准流程”。
59
+ - 不要把“请确认你已完成校准”当作启动校准的前置问题;启动前应确认“准备开始”,不是“已经完成”。
60
+ - 不要要求用户先完成页面操作再回复“可以校准/已完成校准”;应在用户确认“准备开始”后立即执行校准。
58
61
  - 然后执行:
59
- - `boss-recruit-mcp calibrate --port <port>`
62
+ - `boss-recruit-mcp calibrate --port <port>`(默认监听 60 秒)
60
63
  - 默认校准文件路径:
61
64
  - `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
62
65
 
@@ -69,6 +72,7 @@
69
72
  - `keyword_confirmed` (boolean): 是否确认关键词
70
73
  - `keyword_value` (string): 用户确认或改写后的关键词
71
74
  - `search_params_confirmed` (boolean): 用户是否已明确确认当前参数集
75
+ - `criteria_confirmed` (boolean): 用户是否已明确确认筛选 criteria(尤其硬性约束)
72
76
  - `use_default_for_missing` (boolean): 用户是否明确同意对缺失参数使用默认值
73
77
  - `overrides` (object, optional)
74
78
  - `city` (string)
@@ -142,9 +146,13 @@
142
146
  - 明确给出校准步骤与命令;
143
147
  - 明确指出期望生成到哪个路径;
144
148
  - 不要在本机搜索并复用其他 `favorite-calibration.json`。
145
- 4. 若 `launch-chrome` 后发现页面没有停留在 search,而是跳到了登录页、首页或其他页面:
149
+ - 校准引导必须先问“是否准备好开始校准”;用户确认准备好后,再执行:
150
+ - `boss-recruit-mcp calibrate --port <port>`
151
+ - 不要要求用户先手动完成“收藏/取消收藏”等页面动作,再触发 `calibrate`。
152
+ - 除非调试端口不可连,否则不要额外先执行 `launch-chrome`。
153
+ 4. 若校准流程中发现页面没有停留在 search,或跳到了登录页、首页或其他页面:
146
154
  - 明确告诉用户“当前需要手动登录 Boss”;
147
- - 明确要求用户在新打开的 Chrome 窗口中完成登录;
155
+ - 明确要求用户在当前可见的 Chrome 窗口中完成登录;
148
156
  - 等用户回复“已登录,可以继续”后,再继续下一步;
149
157
  - 不要在用户未确认登录完成前直接执行搜索、校准或流水线。
150
158
  5. 只有当以上条件满足时,才首次进入流水线解析:
@@ -171,6 +179,7 @@
171
179
  8. 若返回 `NEED_CONFIRMATION`:
172
180
  - 询问用户是否确认 `proposed_keyword`;
173
181
  - 同时也要让用户确认其他已提取参数里是否有误;
182
+ - 必须单独确认筛选 `criteria` 是否准确(尤其学历/学校/论文等硬性条件不能丢失);
174
183
  - 若 `required_confirmations` 或 `pending_questions` 里包含 `filter_recent_viewed`,必须明确补问:是否需要过滤近 14 天查看过的人选;
175
184
  - 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
176
185
  - 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用;
@@ -180,6 +189,7 @@
180
189
  - 必须得到用户明确确认“可以按默认值继续”后,才能继续执行。
181
190
  10. 只有在以下条件都满足后,才允许正式开始:
182
191
  - 用户已经确认已提取参数无误;
192
+ - 用户已经明确确认 `criteria` 无误;
183
193
  - 缺失参数已补齐,或用户已明确接受默认值;
184
194
  - `NEED_CONFIRMATION` 分支中的关键词也已确认。
185
195
  11. 若返回 `COMPLETED`:
@@ -200,6 +210,7 @@
200
210
  - 优先鼓励用户一次性给全这些字段:城市、学历、学校标签、目标人数、核心方向关键词。
201
211
  - 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
202
212
  - 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
213
+ - 当用户明确给出“学历/学校”硬性条件(如“本科学历必须是985”),即使这些信息已被提取到搜索参数,`criteria` 里也必须保留原始约束,不可剔除。
203
214
  - 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
204
215
  - 若用户输入 `qs50`、`qs200`、`qs500` 等任意 `QS数字` 学校标签,统一按 `<=100 -> qs100`、`>100 -> qs500` 处理;不要把原始 `QS200` 再传到底层搜索命令。
205
216
  - 若用户没有明确提到“是否过滤近 14 天查看过的人选”,必须在参数确认阶段主动补问,不能静默默认开启或关闭。
package/src/cli.js CHANGED
@@ -194,10 +194,53 @@ function parseJsonOption(value, label) {
194
194
  return undefined;
195
195
  }
196
196
 
197
+ const raw = String(value).replace(/^\uFEFF/, "").trim();
198
+ const normalizedQuotes = raw
199
+ .replace(/[“”]/g, "\"")
200
+ .replace(/[‘’]/g, "'");
201
+
202
+ const candidates = [];
203
+ const pushCandidate = (item) => {
204
+ if (typeof item === "string" && item.trim()) {
205
+ candidates.push(item.trim());
206
+ }
207
+ };
208
+
209
+ pushCandidate(raw);
210
+ if (normalizedQuotes !== raw) {
211
+ pushCandidate(normalizedQuotes);
212
+ }
213
+ if (
214
+ normalizedQuotes.length >= 2
215
+ && normalizedQuotes.startsWith("'")
216
+ && normalizedQuotes.endsWith("'")
217
+ ) {
218
+ pushCandidate(normalizedQuotes.slice(1, -1));
219
+ }
220
+
221
+ let lastError = null;
222
+ for (const candidate of candidates) {
223
+ try {
224
+ return JSON.parse(candidate);
225
+ } catch (error) {
226
+ lastError = error;
227
+ try {
228
+ const unwrapped = JSON.parse(candidate);
229
+ if (typeof unwrapped === "string") {
230
+ return JSON.parse(unwrapped);
231
+ }
232
+ } catch {
233
+ // Continue trying next candidate.
234
+ }
235
+ }
236
+ }
237
+
197
238
  try {
198
- return JSON.parse(String(value));
239
+ return JSON.parse(raw);
199
240
  } catch (error) {
200
- throw new Error(`Invalid ${label} JSON: ${error.message}`);
241
+ const hint = "Tip: in PowerShell prefer --*-file or wrap JSON with single quotes.";
242
+ const reason = lastError?.message || error.message;
243
+ throw new Error(`Invalid ${label} JSON: ${reason}. ${hint}`);
201
244
  }
202
245
  }
203
246
 
@@ -345,6 +388,29 @@ async function inspectBossPageState(port, options = {}) {
345
388
  });
346
389
  }
347
390
 
391
+ async function openBossSearchTab(port) {
392
+ const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(bossUrl)}`;
393
+ const attempts = ["PUT", "GET"];
394
+ let lastError = null;
395
+
396
+ for (const method of attempts) {
397
+ try {
398
+ const response = await fetch(endpoint, { method });
399
+ if (response.ok) {
400
+ return { ok: true, method };
401
+ }
402
+ lastError = new Error(`DevTools /json/new returned ${response.status}`);
403
+ } catch (error) {
404
+ lastError = error;
405
+ }
406
+ }
407
+
408
+ return {
409
+ ok: false,
410
+ error: lastError?.message || "Failed to open Boss search tab via DevTools /json/new"
411
+ };
412
+ }
413
+
348
414
  function hasModule(moduleName) {
349
415
  try {
350
416
  require.resolve(moduleName);
@@ -360,6 +426,15 @@ function getDebugPort(options = {}) {
360
426
  return Number.isFinite(port) && port > 0 ? port : 9222;
361
427
  }
362
428
 
429
+ function getCalibrationTimeoutMs(options = {}) {
430
+ const raw = options["timeout-ms"] || options.timeoutMs || options.timeout || "60000";
431
+ const timeout = Number.parseInt(String(raw), 10);
432
+ if (!Number.isFinite(timeout) || timeout <= 0) {
433
+ return 60000;
434
+ }
435
+ return Math.max(5000, timeout);
436
+ }
437
+
363
438
  function getChromeExecutable() {
364
439
  const candidates = [
365
440
  process.env.BOSS_RECRUIT_CHROME_PATH,
@@ -450,65 +525,161 @@ async function printDoctor(options) {
450
525
  async function calibrate(options) {
451
526
  const port = getDebugPort(options);
452
527
  const output = options.output ? path.resolve(String(options.output)) : getUserCalibrationPath();
453
- console.log("Before calibration:");
454
- console.log("1. Open Boss search page.");
455
- console.log("2. Open any candidate detail page.");
456
- console.log("3. Click the favorite button once.");
457
- console.log("4. Click again to cancel favorite for that candidate.");
458
- console.log("5. Close the detail page after calibration completes.");
528
+ const timeoutMs = getCalibrationTimeoutMs(options);
529
+
530
+ console.log("Calibration checklist:");
531
+ console.log("1. The tool will auto-open Boss search page now.");
532
+ console.log("2. Open any candidate detail page in that Boss window.");
533
+ console.log("3. Click favorite once, then click again to unfavorite.");
534
+ console.log("4. Close the detail page.");
535
+ console.log(`5. The calibration listener will wait for ${Math.round(timeoutMs / 1000)} seconds.`);
459
536
  console.log("");
537
+
538
+ let launchResult = null;
539
+ const preState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
540
+ if (preState.state === "DEBUG_PORT_UNREACHABLE") {
541
+ launchResult = await launchChrome(options);
542
+ if (process.exitCode && process.exitCode !== 0) {
543
+ return;
544
+ }
545
+ } else {
546
+ console.log(`Detected existing Chrome debug instance on port ${port}; calibration will reuse it.`);
547
+ let pageState = preState;
548
+ if (pageState.state !== "SEARCH_READY") {
549
+ const openResult = await openBossSearchTab(port);
550
+ if (openResult.ok) {
551
+ console.log(
552
+ `Requested Boss search tab via DevTools /json/new (${openResult.method}) before calibration`
553
+ );
554
+ } else {
555
+ console.log(
556
+ `Could not request Boss search tab via DevTools /json/new before calibration: ${openResult.error}`
557
+ );
558
+ }
559
+ pageState = await inspectBossPageState(port, { timeoutMs: 6000, pollMs: 1000 });
560
+ }
561
+ launchResult = {
562
+ ok: pageState.state === "SEARCH_READY",
563
+ state: pageState.state,
564
+ pageState,
565
+ reused_existing_instance: true
566
+ };
567
+ }
568
+
569
+ if (launchResult?.state === "LOGIN_REQUIRED") {
570
+ console.log("Boss page requires login. Please log in in the opened Chrome window, then complete the checklist within the listener window.");
571
+ } else if (launchResult?.state === "SEARCH_READY") {
572
+ console.log("Boss search page is ready. Start the checklist now.");
573
+ } else {
574
+ console.log("Proceeding with calibration listener. If no click is captured, retry after ensuring Boss search page is open.");
575
+ }
576
+ console.log("");
577
+
460
578
  const code = await runNodeScript(calibrationScriptPath, [
461
579
  "--port",
462
580
  String(port),
463
581
  "--output",
464
- output
582
+ output,
583
+ "--timeout-ms",
584
+ String(timeoutMs)
465
585
  ]);
466
586
  process.exitCode = code;
467
587
  }
468
588
 
469
589
  async function launchChrome(options) {
470
- const chromePath = getChromeExecutable();
471
- if (!chromePath) {
472
- console.error("Chrome executable not found. Set BOSS_RECRUIT_CHROME_PATH or install Google Chrome.");
473
- process.exitCode = 1;
474
- return;
475
- }
476
590
  const port = getDebugPort(options);
477
- const userDataDir = getChromeUserDataDir(port, options);
478
- const args = [
479
- `--remote-debugging-port=${port}`,
480
- `--user-data-dir=${userDataDir}`,
481
- "--new-window",
482
- bossUrl
483
- ];
484
- const child = spawn(chromePath, args, {
485
- detached: true,
486
- stdio: "ignore",
487
- windowsHide: false
488
- });
489
- child.unref();
490
- console.log(`Chrome launched with remote debugging port ${port}`);
491
- console.log(`User data dir: ${userDataDir}`);
492
- console.log(`URL: ${bossUrl}`);
591
+ const initialState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
592
+ let usedExistingInstance = initialState.state !== "DEBUG_PORT_UNREACHABLE";
593
+
594
+ if (usedExistingInstance) {
595
+ console.log(`Reusing existing Chrome debug instance on port ${port}`);
596
+
597
+ if (initialState.state !== "SEARCH_READY") {
598
+ const openResult = await openBossSearchTab(port);
599
+ if (openResult.ok) {
600
+ console.log(
601
+ `Requested Boss search tab via DevTools /json/new (${openResult.method}) on port ${port}`
602
+ );
603
+ } else {
604
+ console.log(
605
+ `Could not request Boss search tab via DevTools /json/new: ${openResult.error}`
606
+ );
607
+ }
608
+ }
609
+ } else {
610
+ const chromePath = getChromeExecutable();
611
+ if (!chromePath) {
612
+ console.error("Chrome executable not found. Set BOSS_RECRUIT_CHROME_PATH or install Google Chrome.");
613
+ process.exitCode = 1;
614
+ return { ok: false, state: "CHROME_NOT_FOUND" };
615
+ }
616
+
617
+ const userDataDir = getChromeUserDataDir(port, options);
618
+ const args = [
619
+ `--remote-debugging-port=${port}`,
620
+ `--user-data-dir=${userDataDir}`,
621
+ "--new-window",
622
+ bossUrl
623
+ ];
624
+ const child = spawn(chromePath, args, {
625
+ detached: true,
626
+ stdio: "ignore",
627
+ windowsHide: false
628
+ });
629
+ child.unref();
630
+ console.log(`Chrome launched with remote debugging port ${port}`);
631
+ console.log(`User data dir: ${userDataDir}`);
632
+ console.log(`URL: ${bossUrl}`);
633
+ }
634
+
635
+ let pageState = await inspectBossPageState(port, { timeoutMs: 12000, pollMs: 1000 });
636
+ if (pageState.state === "BOSS_TAB_NOT_FOUND") {
637
+ const openResult = await openBossSearchTab(port);
638
+ if (openResult.ok) {
639
+ console.log(`Requested Boss search tab via DevTools /json/new (${openResult.method})`);
640
+ pageState = await inspectBossPageState(port, { timeoutMs: 6000, pollMs: 1000 });
641
+ } else {
642
+ console.log(`Could not request Boss search tab via DevTools /json/new: ${openResult.error}`);
643
+ }
644
+ }
493
645
 
494
- const pageState = await inspectBossPageState(port, { timeoutMs: 12000, pollMs: 1000 });
495
646
  if (pageState.state === "SEARCH_READY") {
496
647
  console.log("Boss search page is ready.");
497
648
  console.log(`Current URL: ${pageState.current_url}`);
498
- return;
649
+ return {
650
+ ok: true,
651
+ state: "SEARCH_READY",
652
+ pageState,
653
+ reused_existing_instance: usedExistingInstance
654
+ };
499
655
  }
500
656
 
501
657
  if (pageState.state === "LOGIN_REQUIRED") {
502
658
  console.log("Boss page redirected away from search. Manual login is required.");
503
659
  console.log(`Current URL: ${pageState.current_url}`);
504
660
  console.log("Please log in to Boss manually in the opened Chrome window, then tell the AI agent to continue.");
505
- return;
661
+ return {
662
+ ok: false,
663
+ state: "LOGIN_REQUIRED",
664
+ pageState,
665
+ reused_existing_instance: usedExistingInstance
666
+ };
506
667
  }
507
668
 
669
+ if (usedExistingInstance && pageState.state === "DEBUG_PORT_UNREACHABLE") {
670
+ // Existing instance may have been closed while launching; surface it clearly.
671
+ usedExistingInstance = false;
672
+ }
508
673
  console.log(pageState.message);
509
674
  if (pageState.current_url) {
510
675
  console.log(`Current URL: ${pageState.current_url}`);
511
676
  }
677
+ return {
678
+ ok: false,
679
+ state: pageState.state || "UNKNOWN",
680
+ pageState,
681
+ reused_existing_instance: usedExistingInstance
682
+ };
512
683
  }
513
684
 
514
685
  function printHelp() {
@@ -523,14 +694,17 @@ function printHelp() {
523
694
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
524
695
  console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
525
696
  console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
526
- console.log(" boss-recruit-mcp calibrate Run favorite-button calibration and save favorite-calibration.json");
527
- console.log(" boss-recruit-mcp launch-chrome Launch Chrome in remote-debugging mode, open Boss search, and check login state");
697
+ console.log(" boss-recruit-mcp calibrate Auto-open Boss search page, then run favorite-button calibration");
698
+ console.log(" boss-recruit-mcp launch-chrome Reuse existing Chrome debug instance when possible; otherwise launch one, open Boss search, and check login state");
528
699
  console.log(" boss-recruit-mcp where Print installed package, skill, and config paths");
529
700
  console.log("");
530
701
  console.log("Run command:");
531
702
  console.log(" boss-recruit-mcp run --instruction \"找杭州本科做过推荐系统的人\" [--confirmation-json '{...}'] [--overrides-json '{...}']");
532
703
  console.log(" boss-recruit-mcp run --instruction-file request.txt [--confirmation-file confirmation.json] [--overrides-file overrides.json]");
533
704
  console.log("");
705
+ console.log("Calibration command:");
706
+ console.log(" boss-recruit-mcp calibrate --port 9222 [--timeout-ms 60000] [--output <path>]");
707
+ console.log("");
534
708
  console.log("MCP config command:");
535
709
  console.log(" boss-recruit-mcp mcp-config --client cursor");
536
710
  console.log(" boss-recruit-mcp mcp-config --client all --output-dir <dir>");
package/src/parser.js CHANGED
@@ -240,8 +240,6 @@ function buildScreenCriteria(text, searchParams) {
240
240
  const filtered = clauses.filter((clause) => {
241
241
  if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
242
242
  if (/地点|城市/.test(clause)) return false;
243
- if (/学历|本科|硕士|博士/.test(clause) && !/论文|项目|经验/.test(clause)) return false;
244
- if (/985|211|qs\s*\d+|双一流|统招(?:本科)?|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
245
243
  if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
246
244
  if (isCountPlanningClause(clause)) return false;
247
245
  return true;
@@ -460,8 +458,10 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
460
458
  );
461
459
  const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
462
460
  const needs_recent_viewed_filter_confirmation = searchParams.filter_recent_viewed === null;
463
- const pending_questions = needs_recent_viewed_filter_confirmation
464
- ? [
461
+ const needs_criteria_confirmation = confirmation?.criteria_confirmed !== true;
462
+ const pending_questions = [
463
+ ...(needs_recent_viewed_filter_confirmation
464
+ ? [
465
465
  {
466
466
  field: "filter_recent_viewed",
467
467
  question: "是否需要过滤近14天查看过的人选?",
@@ -471,7 +471,17 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
471
471
  ]
472
472
  }
473
473
  ]
474
- : [];
474
+ : []),
475
+ ...(needs_criteria_confirmation
476
+ ? [
477
+ {
478
+ field: "criteria",
479
+ question: "请确认筛选 criteria 是否准确无误(尤其是硬性约束条件)?",
480
+ value: baseScreenParams.criteria
481
+ }
482
+ ]
483
+ : [])
484
+ ];
475
485
 
476
486
  return {
477
487
  parsed,
@@ -482,6 +492,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
482
492
  suspicious_fields,
483
493
  needs_keyword_confirmation: keywordResolution.needsConfirmation,
484
494
  needs_recent_viewed_filter_confirmation,
495
+ needs_criteria_confirmation,
485
496
  needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
486
497
  proposed_keyword: keywordResolution.proposedKeyword,
487
498
  pending_questions,
package/src/pipeline.js CHANGED
@@ -13,6 +13,9 @@ function buildRequiredConfirmations(parsedResult) {
13
13
  if (parsedResult.needs_recent_viewed_filter_confirmation) {
14
14
  confirmations.push("filter_recent_viewed");
15
15
  }
16
+ if (parsedResult.needs_criteria_confirmation) {
17
+ confirmations.push("criteria");
18
+ }
16
19
  if (parsedResult.has_unresolved_missing_fields) {
17
20
  confirmations.push("missing_fields_or_defaults");
18
21
  }
@@ -155,6 +158,7 @@ export async function runRecruitPipeline({
155
158
  parsed.needs_keyword_confirmation
156
159
  || parsed.needs_search_params_confirmation
157
160
  || parsed.needs_recent_viewed_filter_confirmation
161
+ || parsed.needs_criteria_confirmation
158
162
  ) {
159
163
  return buildNeedConfirmationResponse(parsed);
160
164
  }
@@ -203,6 +207,22 @@ export async function runRecruitPipeline({
203
207
  );
204
208
  }
205
209
 
210
+ if (!Number.isInteger(searchResult.candidate_count)) {
211
+ return buildFailedResponse(
212
+ "SEARCH_RESULT_UNVERIFIED",
213
+ "搜索流程未能确认候选人数量,说明搜索步骤可能没有真正完成,已停止后续筛选。",
214
+ {
215
+ search_params: parsed.searchParams,
216
+ screen_params: parsed.screenParams,
217
+ diagnostics: {
218
+ candidate_count: searchResult.candidate_count,
219
+ stdout: searchResult.stdout?.slice(-1200),
220
+ stderr: searchResult.stderr?.slice(-1200)
221
+ }
222
+ }
223
+ );
224
+ }
225
+
206
226
  if (searchResult.candidate_count === 0) {
207
227
  return buildFailedResponse(
208
228
  "SEARCH_EMPTY_RESULT",