@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 +50 -7
- package/config/screening-config.example.json +5 -3
- package/package.json +3 -2
- package/skills/boss-recruit-pipeline/README.md +6 -1
- package/skills/boss-recruit-pipeline/SKILL.md +25 -14
- package/src/cli.js +210 -36
- package/src/parser.js +16 -5
- package/src/pipeline.js +20 -0
- package/src/test-parser.js +80 -10
- package/vendor/boss-screen-cli/boss-screen-cli.cjs +895 -55
- package/vendor/boss-screen-cli/calibrate-favorite-position-v2.cjs +50 -29
- package/vendor/boss-search-cli/src/boss-searcher.js +107 -0
- package/vendor/boss-search-cli/src/cli.js +27 -10
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
|
-
|
|
145
|
+
执行校准(会自动尝试打开 Boss 搜索页):
|
|
128
146
|
|
|
129
147
|
```bash
|
|
130
|
-
boss-recruit-mcp
|
|
148
|
+
boss-recruit-mcp calibrate --port <port>
|
|
131
149
|
```
|
|
132
150
|
|
|
133
|
-
|
|
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://
|
|
3
|
-
"apiKey": "replace-with-
|
|
4
|
-
"model": "
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
6.
|
|
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
|
-
|
|
149
|
+
- 校准引导必须先问“是否准备好开始校准”;用户确认准备好后,再执行:
|
|
150
|
+
- `boss-recruit-mcp calibrate --port <port>`
|
|
151
|
+
- 不要要求用户先手动完成“收藏/取消收藏”等页面动作,再触发 `calibrate`。
|
|
152
|
+
- 除非调试端口不可连,否则不要额外先执行 `launch-chrome`。
|
|
153
|
+
4. 若校准流程中发现页面没有停留在 search,或跳到了登录页、首页或其他页面:
|
|
146
154
|
- 明确告诉用户“当前需要手动登录 Boss”;
|
|
147
|
-
-
|
|
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(
|
|
239
|
+
return JSON.parse(raw);
|
|
199
240
|
} catch (error) {
|
|
200
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
console.log("
|
|
456
|
-
console.log("
|
|
457
|
-
console.log("
|
|
458
|
-
console.log("
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
527
|
-
console.log(" boss-recruit-mcp launch-chrome
|
|
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
|
|
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",
|