@reconcrap/boss-recruit-mcp 1.0.5 → 1.0.7
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 +54 -4
- package/package.json +1 -1
- package/skills/boss-recruit-pipeline/README.md +7 -0
- package/skills/boss-recruit-pipeline/SKILL.md +20 -7
- package/src/adapters.js +5 -1
- package/src/cli.js +154 -5
- package/src/index.js +1 -0
- package/src/parser.js +84 -6
- package/src/pipeline.js +15 -3
- package/src/test-parser.js +93 -8
- package/vendor/boss-screen-cli/boss-screen-cli.cjs +4 -3
- package/vendor/boss-search-cli/src/boss-searcher.js +50 -7
- package/vendor/boss-search-cli/src/cli.js +49 -6
package/README.md
CHANGED
|
@@ -24,9 +24,55 @@ npx @reconcrap/boss-recruit-mcp install
|
|
|
24
24
|
|
|
25
25
|
- 安装 Codex skill 到 `$CODEX_HOME/skills/boss-recruit-pipeline`
|
|
26
26
|
- 初始化用户配置到 `$CODEX_HOME/boss-recruit-mcp/screening-config.json`
|
|
27
|
+
- 生成通用 MCP 配置模板到 `$CODEX_HOME/boss-recruit-mcp/agent-mcp-configs`
|
|
27
28
|
- 包内自带 `boss-search-cli` 与 `boss-screen-cli` 运行时文件,无需额外目录结构
|
|
28
29
|
- 不包含 `favorite-calibration.json`,首次使用前需要自行校准生成
|
|
29
30
|
|
|
31
|
+
## 跨 Agent 快速接入(Cursor / Trae / Claude Code / OpenClaw)
|
|
32
|
+
|
|
33
|
+
生成 MCP 配置模板:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
boss-recruit-mcp mcp-config --client all
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
默认会输出到:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
$CODEX_HOME/boss-recruit-mcp/agent-mcp-configs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
包含:
|
|
46
|
+
|
|
47
|
+
- `mcp.cursor.json`
|
|
48
|
+
- `mcp.trae.json`
|
|
49
|
+
- `mcp.claudecode.json`
|
|
50
|
+
- `mcp.openclaw.json`
|
|
51
|
+
- `mcp.generic.json`
|
|
52
|
+
|
|
53
|
+
把对应文件里的 `mcpServers` 合并到你的 AI 客户端 MCP 配置中即可。
|
|
54
|
+
|
|
55
|
+
如果你只需要某一个客户端模板:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
boss-recruit-mcp mcp-config --client cursor
|
|
59
|
+
boss-recruit-mcp mcp-config --client claudecode
|
|
60
|
+
boss-recruit-mcp mcp-config --client trae
|
|
61
|
+
boss-recruit-mcp mcp-config --client openclaw
|
|
62
|
+
boss-recruit-mcp mcp-config --client generic
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
默认模板会使用:
|
|
66
|
+
|
|
67
|
+
- `command: npx`
|
|
68
|
+
- `args: ["-y", "@reconcrap/boss-recruit-mcp@latest", "start"]`
|
|
69
|
+
|
|
70
|
+
如果你希望改成本地全局命令:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
boss-recruit-mcp mcp-config --client generic --command boss-recruit-mcp --args-json "[\"start\"]"
|
|
74
|
+
```
|
|
75
|
+
|
|
30
76
|
## 准备配置
|
|
31
77
|
|
|
32
78
|
1. 初始化后编辑用户配置文件:
|
|
@@ -41,6 +87,7 @@ $CODEX_HOME/boss-recruit-mcp/screening-config.json
|
|
|
41
87
|
- `debugPort` 可选,默认 `9222`
|
|
42
88
|
- `calibrationFile` 可选;不填时默认使用 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
|
|
43
89
|
- `outputDir` 可选;不填时默认输出到用户桌面
|
|
90
|
+
- 学校标签支持 `统招本科` / `双一流院校` / `985` / `211` / `qs100` / `qs500`;如果输入 `qs50`、`qs200`、`qs500` 等其他 `QS数字`,会按 `<=100 -> qs100`、`>100 -> qs500` 归一
|
|
44
91
|
|
|
45
92
|
## 运行
|
|
46
93
|
|
|
@@ -55,13 +102,13 @@ boss-recruit-mcp start
|
|
|
55
102
|
如果当前 AI agent 无法添加新的 MCP、MCP 数量受限,或者只支持 shell/命令执行,也可以直接调用同一后端的 CLI fallback:
|
|
56
103
|
|
|
57
104
|
```bash
|
|
58
|
-
boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,城市杭州,本科,学校 985/211/QS100,目标 10
|
|
105
|
+
boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,城市杭州,本科,学校 985/211/QS100,目标 10 人,过滤近14天查看过的人选"
|
|
59
106
|
```
|
|
60
107
|
|
|
61
108
|
也支持通过 JSON 传确认信息与覆盖参数:
|
|
62
109
|
|
|
63
110
|
```bash
|
|
64
|
-
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\"],\"target_count\":10}"
|
|
111
|
+
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}"
|
|
65
112
|
```
|
|
66
113
|
|
|
67
114
|
如果命令行中放长文本不方便,改用文件:
|
|
@@ -132,6 +179,7 @@ boss-recruit-mcp doctor --port <port>
|
|
|
132
179
|
"use_default_for_missing": false
|
|
133
180
|
},
|
|
134
181
|
"overrides": {
|
|
182
|
+
"filter_recent_viewed": true,
|
|
135
183
|
"target_count": 500
|
|
136
184
|
}
|
|
137
185
|
}
|
|
@@ -140,10 +188,12 @@ boss-recruit-mcp doctor --port <port>
|
|
|
140
188
|
## 行为说明
|
|
141
189
|
|
|
142
190
|
- 若缺 `city/degree/schools/keyword/target_count`,返回 `NEED_INPUT`
|
|
143
|
-
- 若 keyword
|
|
191
|
+
- 若 keyword 由语义自动抽取、搜索参数仍未被用户明确确认,或用户未说明是否过滤近 14 天查看过的人选,返回 `NEED_CONFIRMATION`
|
|
144
192
|
- 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
|
|
193
|
+
- 若用户没提“是否过滤近14天查看”,会在 `pending_questions` 里返回该问题,调用方应先补问再继续
|
|
145
194
|
- 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
|
|
146
|
-
-
|
|
195
|
+
- `target_count` 表示“目标处理人数”,不是“目标通过人数”;状态一旦是 `COMPLETED`,就表示本轮已完成,不应因通过人数不足而自动重跑
|
|
196
|
+
- 确认后自动执行:搜索 CLI -> 点击搜索 -> 勾选“过滤近14天查看”(如启用) -> 筛选 CLI
|
|
147
197
|
- 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
|
|
148
198
|
- 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
|
|
149
199
|
- 若缺少 `favorite-calibration.json`,会返回 `CALIBRATION_REQUIRED`
|
package/package.json
CHANGED
|
@@ -12,8 +12,15 @@ npx @reconcrap/boss-recruit-mcp install
|
|
|
12
12
|
|
|
13
13
|
- 把 skill 安装到 `$CODEX_HOME/skills/boss-recruit-pipeline`
|
|
14
14
|
- 在 `$CODEX_HOME/boss-recruit-mcp/screening-config.json` 创建配置模板
|
|
15
|
+
- 在 `$CODEX_HOME/boss-recruit-mcp/agent-mcp-configs` 生成 Cursor/Trae/Claude Code/OpenClaw 的 MCP 模板
|
|
15
16
|
- 默认把筛选结果输出到用户桌面
|
|
16
17
|
|
|
18
|
+
如果你只想导出 MCP 模板,可执行:
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
boss-recruit-mcp mcp-config --client all
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
## 前置要求
|
|
18
25
|
|
|
19
26
|
- Chrome 需使用远程调试端口启动;推荐 `9222`,但也可以使用你已在运行的其他端口
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
1. 先检查 MCP 是否已安装 / 可调用;若未安装,优先执行:
|
|
24
24
|
- `npx @reconcrap/boss-recruit-mcp install`
|
|
25
|
+
- 若要给 Cursor / Trae / Claude Code / OpenClaw 快速接入,执行 `boss-recruit-mcp mcp-config --client all`,并把生成文件中的 `mcpServers` 合并到对应客户端配置
|
|
25
26
|
2. 若用户还未确认 Chrome 调试端口,必须先询问:
|
|
26
27
|
- 建议使用 `9222`
|
|
27
28
|
- 但也允许用户明确提供一个已在使用的其他远程调试端口
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
- Tool response 重点字段:
|
|
79
80
|
- `status`
|
|
80
81
|
- `required_confirmations`
|
|
82
|
+
- `pending_questions`
|
|
81
83
|
- `review.extracted_search_params`
|
|
82
84
|
- `review.current_search_params`
|
|
83
85
|
- `review.missing_fields`
|
|
@@ -157,8 +159,9 @@
|
|
|
157
159
|
6. 缺失项常见含义:
|
|
158
160
|
- `city`: 城市,如“杭州”
|
|
159
161
|
- `degree`: 学历,如“本科”“硕士及以上”
|
|
160
|
-
- `schools`:
|
|
161
|
-
- `
|
|
162
|
+
- `schools`: 学校标签,如“统招本科、双一流院校、985、211、qs100、qs500”
|
|
163
|
+
- `filter_recent_viewed`: 是否过滤近 14 天内查看过的人选
|
|
164
|
+
- `target_count`: 目标处理人数,如“10”;表示本轮需要处理多少位候选人,不表示必须有多少人通过
|
|
162
165
|
- `keyword`: 搜索关键词,如“AI infra”“推荐系统”
|
|
163
166
|
7. 若返回 `NEED_INPUT`:
|
|
164
167
|
- 不要只问一次就结束;
|
|
@@ -168,8 +171,10 @@
|
|
|
168
171
|
8. 若返回 `NEED_CONFIRMATION`:
|
|
169
172
|
- 询问用户是否确认 `proposed_keyword`;
|
|
170
173
|
- 同时也要让用户确认其他已提取参数里是否有误;
|
|
174
|
+
- 若 `required_confirmations` 或 `pending_questions` 里包含 `filter_recent_viewed`,必须明确补问:是否需要过滤近 14 天查看过的人选;
|
|
171
175
|
- 若确认,带 `confirmation.keyword_confirmed=true` 和 `keyword_value` 再次调用;
|
|
172
|
-
- 若用户修改关键词,传用户给的新词作为 `keyword_value`
|
|
176
|
+
- 若用户修改关键词,传用户给的新词作为 `keyword_value` 再次调用;
|
|
177
|
+
- 对“是否过滤近 14 天查看”这个问题,需把用户选择写入 `overrides.filter_recent_viewed=true/false` 再次调用。
|
|
173
178
|
9. 当仍有缺失参数但用户想直接开始时:
|
|
174
179
|
- 先明确告知默认值及风险;
|
|
175
180
|
- 必须得到用户明确确认“可以按默认值继续”后,才能继续执行。
|
|
@@ -178,7 +183,10 @@
|
|
|
178
183
|
- 缺失参数已补齐,或用户已明确接受默认值;
|
|
179
184
|
- `NEED_CONFIRMATION` 分支中的关键词也已确认。
|
|
180
185
|
11. 若返回 `COMPLETED`:
|
|
181
|
-
-
|
|
186
|
+
- 向用户返回摘要:目标处理人数、已处理、通过数、耗时、输出文件路径。
|
|
187
|
+
- 只要状态是 `COMPLETED`,就视为本轮任务完成;
|
|
188
|
+
- 不要因为 `passed_count < target_count` 就自动重跑;
|
|
189
|
+
- `target_count` 的含义是“处理人数目标”,不是“通过人数目标”。
|
|
182
190
|
12. 若返回 `FAILED`:
|
|
183
191
|
- 先提炼 `error.code`、`error.message`、`diagnostics`;
|
|
184
192
|
- 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
|
|
@@ -193,6 +201,9 @@
|
|
|
193
201
|
- 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
|
|
194
202
|
- 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
|
|
195
203
|
- 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
|
|
204
|
+
- 若用户输入 `qs50`、`qs200`、`qs500` 等任意 `QS数字` 学校标签,统一按 `<=100 -> qs100`、`>100 -> qs500` 处理;不要把原始 `QS200` 再传到底层搜索命令。
|
|
205
|
+
- 若用户没有明确提到“是否过滤近 14 天查看过的人选”,必须在参数确认阶段主动补问,不能静默默认开启或关闭。
|
|
206
|
+
- 若用户说“过滤近 14 天查看”“排除最近看过的”,映射为 `filter_recent_viewed=true`;若用户说“不过滤近 14 天查看”“保留最近看过的”,映射为 `filter_recent_viewed=false`。
|
|
196
207
|
- 不要把“用户没有继续回复”解释为“默认同意”;默认值只能在用户明确口头确认后使用。
|
|
197
208
|
- 参数确认对话里,优先采用这种结构:
|
|
198
209
|
- 已识别参数
|
|
@@ -213,7 +224,8 @@
|
|
|
213
224
|
- `已识别参数` 只放当前看起来可信的值,例如:
|
|
214
225
|
- 城市:杭州
|
|
215
226
|
- 学历:本科
|
|
216
|
-
-
|
|
227
|
+
- 学校标签:统招本科 / 双一流院校 / 985 / 211 / QS100 / QS500(按实际需求选择)
|
|
228
|
+
- 过滤近14天查看:需要 / 不需要
|
|
217
229
|
- 关键词:AI infra
|
|
218
230
|
- 目标人数:10
|
|
219
231
|
- `待确认 / 待修正` 要明确写出“识别值 -> 疑点 -> 需要用户给出的标准值”,例如:
|
|
@@ -227,7 +239,7 @@
|
|
|
227
239
|
- 若继续会使用哪些默认值
|
|
228
240
|
- 会导致搜索范围变宽、相关性下降或结果偏差增大
|
|
229
241
|
- `请用户回复` 要求用户一次性回复完整,优先使用这种收口方式:
|
|
230
|
-
- 请直接按“城市 / 学历 / 学校标签 / 关键词 / 目标人数”补充或修正
|
|
242
|
+
- 请直接按“城市 / 学历 / 学校标签 / 是否过滤近14天查看 / 关键词 / 目标人数”补充或修正
|
|
231
243
|
- 如果你接受默认值继续,请明确回复“确认按默认值继续”
|
|
232
244
|
- 若用户补充后仍有缺失,再发第二轮确认时继续复用同一结构,只保留:
|
|
233
245
|
- 已更新的参数
|
|
@@ -262,7 +274,7 @@
|
|
|
262
274
|
|
|
263
275
|
用户:
|
|
264
276
|
|
|
265
|
-
- “在 Boss 上找做过 AI infra 的候选人,必须发过 CCF-A 区论文,城市杭州,本科,学校 985/211/QS100
|
|
277
|
+
- “在 Boss 上找做过 AI infra 的候选人,必须发过 CCF-A 区论文,城市杭州,本科,学校 统招本科/双一流院校/985/211/QS100/QS500 中按需选择,目标 10 人”
|
|
266
278
|
|
|
267
279
|
期望行为:
|
|
268
280
|
|
|
@@ -285,5 +297,6 @@
|
|
|
285
297
|
- 校准缺失时,直接指导用户重新校准,不要建议复制或复用任何历史 calibration 文件。
|
|
286
298
|
- 若当前 agent 受 MCP 数量限制,明确告诉用户“本轮改走 CLI fallback”,但用户体验上仍保持同一套确认和状态输出。
|
|
287
299
|
- 新开 Chrome 后若检测到跳转登录,先提示用户手动登录并等待确认,再继续。
|
|
300
|
+
- 若返回 `COMPLETED`,不要把“通过人数不足”理解成“任务未完成”;除非用户明确要求“必须找到 N 个通过人选”,否则不要自动追加新一轮搜索。
|
|
288
301
|
- 若要使用默认值,必须写明“请确认是否按默认值继续”,不能模糊带过。
|
|
289
302
|
- 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
|
package/src/adapters.js
CHANGED
|
@@ -157,7 +157,7 @@ function parseSearchCount(output) {
|
|
|
157
157
|
function parseScreenSummary(output) {
|
|
158
158
|
const processed = output.match(/已处理:\s*(\d+)\s*人/);
|
|
159
159
|
const passed = output.match(/通过筛选:\s*(\d+)\s*人/);
|
|
160
|
-
const target = output.match(
|
|
160
|
+
const target = output.match(/目标(?:处理)?人数:\s*(\d+)\s*人/);
|
|
161
161
|
const csv = output.match(/结果已导出到:\s*(.+)/);
|
|
162
162
|
|
|
163
163
|
return {
|
|
@@ -299,6 +299,10 @@ export async function runSearchCli({ workspaceRoot, searchParams }) {
|
|
|
299
299
|
String(debugPort)
|
|
300
300
|
];
|
|
301
301
|
|
|
302
|
+
if (typeof searchParams.filter_recent_viewed === "boolean") {
|
|
303
|
+
args.push("--filter-recent-viewed", String(searchParams.filter_recent_viewed));
|
|
304
|
+
}
|
|
305
|
+
|
|
302
306
|
const result = await runProcess({
|
|
303
307
|
command: "node",
|
|
304
308
|
args,
|
package/src/cli.js
CHANGED
|
@@ -21,6 +21,10 @@ const calibrationScriptPath = path.join(
|
|
|
21
21
|
"calibrate-favorite-position-v2.cjs"
|
|
22
22
|
);
|
|
23
23
|
const bossUrl = "https://www.zhipin.com/web/chat/search";
|
|
24
|
+
const SUPPORTED_MCP_CLIENTS = ["generic", "cursor", "trae", "claudecode", "openclaw"];
|
|
25
|
+
const DEFAULT_MCP_SERVER_NAME = "boss-recruit";
|
|
26
|
+
const DEFAULT_MCP_COMMAND = "npx";
|
|
27
|
+
const DEFAULT_MCP_ARGS = ["-y", "@reconcrap/boss-recruit-mcp@latest", "start"];
|
|
24
28
|
|
|
25
29
|
function getCodexHome() {
|
|
26
30
|
return process.env.CODEX_HOME
|
|
@@ -70,6 +74,112 @@ function parseOptions(args) {
|
|
|
70
74
|
return options;
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
function parseJsonObjectOption(value, label) {
|
|
78
|
+
if (value === undefined || value === null || value === "") {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseJsonOption(value, label);
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
83
|
+
throw new Error(`${label} must be a JSON object`);
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseStringArrayOption(value, label) {
|
|
89
|
+
if (value === undefined || value === null || value === "") {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const parsed = parseJsonOption(value, label);
|
|
93
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
94
|
+
throw new Error(`${label} must be a JSON string array`);
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeMcpClientName(value) {
|
|
100
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
101
|
+
if (!raw) return "";
|
|
102
|
+
if (raw === "claude-code") return "claudecode";
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseMcpClientTargets(rawValue) {
|
|
107
|
+
if (!rawValue) return SUPPORTED_MCP_CLIENTS.slice();
|
|
108
|
+
const raw = String(rawValue).trim().toLowerCase();
|
|
109
|
+
if (!raw || raw === "all") {
|
|
110
|
+
return SUPPORTED_MCP_CLIENTS.slice();
|
|
111
|
+
}
|
|
112
|
+
const candidates = raw
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((item) => normalizeMcpClientName(item))
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
const unique = [...new Set(candidates)];
|
|
117
|
+
const invalid = unique.filter((item) => !SUPPORTED_MCP_CLIENTS.includes(item));
|
|
118
|
+
if (invalid.length) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unsupported --client value: ${invalid.join(", ")}. Supported: ${SUPPORTED_MCP_CLIENTS.join(", ")}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return unique;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getAgentConfigOutputDir(options = {}) {
|
|
127
|
+
if (typeof options["output-dir"] === "string" && options["output-dir"].trim()) {
|
|
128
|
+
return path.resolve(options["output-dir"]);
|
|
129
|
+
}
|
|
130
|
+
return path.join(getCodexHome(), "boss-recruit-mcp", "agent-mcp-configs");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildMcpLaunchConfig(options = {}) {
|
|
134
|
+
const command =
|
|
135
|
+
typeof options.command === "string" && options.command.trim()
|
|
136
|
+
? options.command.trim()
|
|
137
|
+
: DEFAULT_MCP_COMMAND;
|
|
138
|
+
const args = parseStringArrayOption(options["args-json"], "args-json");
|
|
139
|
+
const env = parseJsonObjectOption(options["env-json"], "env-json");
|
|
140
|
+
const launchArgs = args.length
|
|
141
|
+
? args
|
|
142
|
+
: command === "boss-recruit-mcp"
|
|
143
|
+
? ["start"]
|
|
144
|
+
: DEFAULT_MCP_ARGS.slice();
|
|
145
|
+
const launchConfig = {
|
|
146
|
+
command,
|
|
147
|
+
args: launchArgs
|
|
148
|
+
};
|
|
149
|
+
if (Object.keys(env).length > 0) {
|
|
150
|
+
launchConfig.env = env;
|
|
151
|
+
}
|
|
152
|
+
return launchConfig;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildMcpConfigFileContent(options = {}) {
|
|
156
|
+
const serverName =
|
|
157
|
+
typeof options["server-name"] === "string" && options["server-name"].trim()
|
|
158
|
+
? options["server-name"].trim()
|
|
159
|
+
: DEFAULT_MCP_SERVER_NAME;
|
|
160
|
+
return {
|
|
161
|
+
mcpServers: {
|
|
162
|
+
[serverName]: buildMcpLaunchConfig(options)
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeMcpConfigFiles(options = {}) {
|
|
168
|
+
const clients = parseMcpClientTargets(options.client);
|
|
169
|
+
const outputDir = getAgentConfigOutputDir(options);
|
|
170
|
+
ensureDir(outputDir);
|
|
171
|
+
const files = [];
|
|
172
|
+
|
|
173
|
+
for (const client of clients) {
|
|
174
|
+
const filePath = path.join(outputDir, `mcp.${client}.json`);
|
|
175
|
+
const content = buildMcpConfigFileContent(options);
|
|
176
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2), "utf8");
|
|
177
|
+
files.push({ client, file: filePath });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { outputDir, files };
|
|
181
|
+
}
|
|
182
|
+
|
|
73
183
|
function readTextFile(filePath, label) {
|
|
74
184
|
const resolved = path.resolve(String(filePath));
|
|
75
185
|
try {
|
|
@@ -411,6 +521,7 @@ function printHelp() {
|
|
|
411
521
|
console.log(" boss-recruit-mcp install Install Codex skill and initialize user config");
|
|
412
522
|
console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
|
|
413
523
|
console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
|
|
524
|
+
console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
|
|
414
525
|
console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
|
|
415
526
|
console.log(" boss-recruit-mcp calibrate Run favorite-button calibration and save favorite-calibration.json");
|
|
416
527
|
console.log(" boss-recruit-mcp launch-chrome Launch Chrome in remote-debugging mode, open Boss search, and check login state");
|
|
@@ -419,6 +530,11 @@ function printHelp() {
|
|
|
419
530
|
console.log("Run command:");
|
|
420
531
|
console.log(" boss-recruit-mcp run --instruction \"找杭州本科做过推荐系统的人\" [--confirmation-json '{...}'] [--overrides-json '{...}']");
|
|
421
532
|
console.log(" boss-recruit-mcp run --instruction-file request.txt [--confirmation-file confirmation.json] [--overrides-file overrides.json]");
|
|
533
|
+
console.log("");
|
|
534
|
+
console.log("MCP config command:");
|
|
535
|
+
console.log(" boss-recruit-mcp mcp-config --client cursor");
|
|
536
|
+
console.log(" boss-recruit-mcp mcp-config --client all --output-dir <dir>");
|
|
537
|
+
console.log(" boss-recruit-mcp mcp-config --client generic --command boss-recruit-mcp --args-json '[\"start\"]'");
|
|
422
538
|
}
|
|
423
539
|
|
|
424
540
|
function printPaths() {
|
|
@@ -432,23 +548,48 @@ function printPaths() {
|
|
|
432
548
|
console.log(`desktop_output_default=${getDesktopDir()}`);
|
|
433
549
|
}
|
|
434
550
|
|
|
551
|
+
function printMcpConfig(options = {}) {
|
|
552
|
+
const clients = parseMcpClientTargets(options.client);
|
|
553
|
+
if (clients.length === 1 && !options["output-dir"]) {
|
|
554
|
+
const config = buildMcpConfigFileContent(options);
|
|
555
|
+
printJson(config);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const result = writeMcpConfigFiles(options);
|
|
560
|
+
console.log(`MCP config templates exported to: ${result.outputDir}`);
|
|
561
|
+
for (const item of result.files) {
|
|
562
|
+
console.log(`- ${item.client}: ${item.file}`);
|
|
563
|
+
}
|
|
564
|
+
console.log("");
|
|
565
|
+
console.log("Tip:");
|
|
566
|
+
console.log("1. Choose the template file matching your AI client.");
|
|
567
|
+
console.log("2. Merge its mcpServers block into that client's MCP config.");
|
|
568
|
+
}
|
|
569
|
+
|
|
435
570
|
function installAll() {
|
|
436
571
|
const skillTarget = installSkill();
|
|
437
572
|
const configResult = ensureUserConfig();
|
|
573
|
+
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
438
574
|
console.log(`Skill installed to: ${skillTarget}`);
|
|
439
575
|
if (configResult.created) {
|
|
440
576
|
console.log(`Config template created at: ${configResult.path}`);
|
|
441
577
|
} else {
|
|
442
578
|
console.log(`Config already exists at: ${configResult.path}`);
|
|
443
579
|
}
|
|
580
|
+
console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
|
|
581
|
+
for (const item of mcpTemplateResult.files) {
|
|
582
|
+
console.log(`- ${item.client}: ${item.file}`);
|
|
583
|
+
}
|
|
444
584
|
console.log("");
|
|
445
585
|
console.log("Next steps:");
|
|
446
586
|
console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
|
|
447
|
-
console.log("2. Choose a
|
|
448
|
-
console.log("3.
|
|
449
|
-
console.log("4. Run `boss-recruit-mcp
|
|
450
|
-
console.log("5. Run `boss-recruit-mcp
|
|
451
|
-
console.log("6. Run `boss-recruit-mcp
|
|
587
|
+
console.log("2. Choose a client template from the exported MCP config files and merge it into your AI client config.");
|
|
588
|
+
console.log("3. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
|
|
589
|
+
console.log("4. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
|
|
590
|
+
console.log("5. Run `boss-recruit-mcp launch-chrome --port <your-port>`; if it reports the page redirected away from search, log in to Boss manually in that Chrome window.");
|
|
591
|
+
console.log("6. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
|
|
592
|
+
console.log("7. Run `boss-recruit-mcp start` or configure your MCP client to launch the command from the generated template.");
|
|
452
593
|
}
|
|
453
594
|
|
|
454
595
|
async function runPipelineOnce(options) {
|
|
@@ -504,6 +645,14 @@ switch (command) {
|
|
|
504
645
|
);
|
|
505
646
|
break;
|
|
506
647
|
}
|
|
648
|
+
case "mcp-config":
|
|
649
|
+
try {
|
|
650
|
+
printMcpConfig(options);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error(error.message || "Failed to generate MCP config template.");
|
|
653
|
+
process.exitCode = 1;
|
|
654
|
+
}
|
|
655
|
+
break;
|
|
507
656
|
case "doctor":
|
|
508
657
|
await printDoctor(options);
|
|
509
658
|
break;
|
package/src/index.js
CHANGED
package/src/parser.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
const SEARCH_SCHOOL_MAP = {
|
|
2
|
+
"统招": "统招本科",
|
|
3
|
+
"统招本科": "统招本科",
|
|
4
|
+
"统招本": "统招本科",
|
|
5
|
+
"全日制本科": "统招本科",
|
|
6
|
+
"双一流": "双一流院校",
|
|
7
|
+
"双一流院校": "双一流院校",
|
|
8
|
+
"双一流学校": "双一流院校",
|
|
2
9
|
"985": "985院校",
|
|
10
|
+
"985院校": "985院校",
|
|
3
11
|
"211": "211院校",
|
|
4
|
-
"
|
|
12
|
+
"211院校": "211院校",
|
|
13
|
+
"qs": "QS 100",
|
|
14
|
+
"qs100": "QS 100",
|
|
15
|
+
"qs500": "QS 500"
|
|
5
16
|
};
|
|
6
17
|
const KNOWN_SCHOOL_LABELS = new Set(Object.values(SEARCH_SCHOOL_MAP));
|
|
7
18
|
const DEFAULT_PARAM_VALUES = {
|
|
@@ -39,8 +50,12 @@ function normalizeSchoolLabel(value) {
|
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
const compact = raw.toLowerCase().replace(/\s+/g, "");
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const qsMatch = compact.match(/^qs(\d+)$/);
|
|
54
|
+
if (qsMatch) {
|
|
55
|
+
const rank = Number.parseInt(qsMatch[1], 10);
|
|
56
|
+
if (Number.isFinite(rank)) {
|
|
57
|
+
return rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100;
|
|
58
|
+
}
|
|
44
59
|
}
|
|
45
60
|
return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
|
|
46
61
|
}
|
|
@@ -82,6 +97,7 @@ function extractCity(text) {
|
|
|
82
97
|
function extractDegree(text) {
|
|
83
98
|
if (/(博士及以上|博士)/.test(text)) return "博士";
|
|
84
99
|
if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
|
|
100
|
+
if (/硕士/.test(text)) return "硕士";
|
|
85
101
|
if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
|
|
86
102
|
if (/本科/.test(text)) return "本科";
|
|
87
103
|
return null;
|
|
@@ -89,12 +105,47 @@ function extractDegree(text) {
|
|
|
89
105
|
|
|
90
106
|
function extractSchools(text) {
|
|
91
107
|
const schools = [];
|
|
108
|
+
if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
|
|
109
|
+
if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
|
|
92
110
|
if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
|
|
93
111
|
if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
|
|
94
|
-
|
|
112
|
+
const qsMatches = text.matchAll(/\bqs\s*(\d+)\b/ig);
|
|
113
|
+
for (const match of qsMatches) {
|
|
114
|
+
const rank = Number.parseInt(match[1], 10);
|
|
115
|
+
if (Number.isFinite(rank)) {
|
|
116
|
+
schools.push(rank > 100 ? SEARCH_SCHOOL_MAP.qs500 : SEARCH_SCHOOL_MAP.qs100);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
95
119
|
return uniqueList(schools);
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
function extractRecentViewedFilter(text) {
|
|
123
|
+
const negativePatterns = [
|
|
124
|
+
/(?:不|别|无需|不用|不要).{0,6}(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
125
|
+
/(?:保留|包含).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
126
|
+
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:不要|不用|无需|不需要|不必).{0,4}(?:过滤|排除|去掉|剔除)/i
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const pattern of negativePatterns) {
|
|
130
|
+
if (pattern.test(text)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const positivePatterns = [
|
|
136
|
+
/(?:过滤|排除|去掉|剔除).{0,8}(?:近?14天(?:内)?查看(?:过)?)/i,
|
|
137
|
+
/(?:近?14天(?:内)?查看(?:过)?).{0,8}(?:过滤|排除|去掉|剔除)/i
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const pattern of positivePatterns) {
|
|
141
|
+
if (pattern.test(text)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
function normalizeStringOverride(value) {
|
|
99
150
|
if (typeof value !== "string") return null;
|
|
100
151
|
const normalized = value.trim();
|
|
@@ -176,6 +227,10 @@ function sanitizeClause(clause) {
|
|
|
176
227
|
.trim();
|
|
177
228
|
}
|
|
178
229
|
|
|
230
|
+
function isCountPlanningClause(clause) {
|
|
231
|
+
return /(?:目标(?:筛选)?(?:人数|数量)?|至少筛选|筛选\s*\d+\s*位|输出\s*\d+\s*(?:位|个|个人选|个候选人)?|最终输出\s*\d+\s*(?:位|个|个人选|个候选人)?|处理\s*\d+\s*(?:位|人)|(?:浏览|拉取|抓取).*(?:至少\s*)?\d+\s*(?:位|个|个人选|个候选人)?|最匹配.*\d+\s*(?:位|个|个人选|个候选人)?)/i.test(clause);
|
|
232
|
+
}
|
|
233
|
+
|
|
179
234
|
function buildScreenCriteria(text, searchParams) {
|
|
180
235
|
const clauses = text
|
|
181
236
|
.split(/[,,。;;\n]/)
|
|
@@ -186,8 +241,9 @@ function buildScreenCriteria(text, searchParams) {
|
|
|
186
241
|
if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
|
|
187
242
|
if (/地点|城市/.test(clause)) return false;
|
|
188
243
|
if (/学历|本科|硕士|博士/.test(clause) && !/论文|项目|经验/.test(clause)) return false;
|
|
189
|
-
if (/985|211|qs\s*\d
|
|
190
|
-
if (
|
|
244
|
+
if (/985|211|qs\s*\d+|双一流|统招(?:本科)?|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
|
|
245
|
+
if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
|
|
246
|
+
if (isCountPlanningClause(clause)) return false;
|
|
191
247
|
return true;
|
|
192
248
|
});
|
|
193
249
|
|
|
@@ -345,6 +401,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
345
401
|
city: extractCity(text),
|
|
346
402
|
degree: extractDegree(text),
|
|
347
403
|
schools: extractSchools(text),
|
|
404
|
+
filter_recent_viewed: extractRecentViewedFilter(text),
|
|
348
405
|
keyword_explicit: extractKeywordExplicit(text),
|
|
349
406
|
keyword_auto: extractKeywordAuto(text),
|
|
350
407
|
target_count: extractTargetCount(text)
|
|
@@ -355,11 +412,15 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
355
412
|
const overrideDegree = normalizeStringOverride(overrides.degree);
|
|
356
413
|
const overrideSchools = normalizeSchoolsOverride(overrides.schools);
|
|
357
414
|
const overrideKeyword = normalizeStringOverride(overrides.keyword);
|
|
415
|
+
const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
|
|
416
|
+
? overrides.filter_recent_viewed
|
|
417
|
+
: null;
|
|
358
418
|
|
|
359
419
|
if (overrideCity) parsed.city = overrideCity;
|
|
360
420
|
if (overrideDegree) parsed.degree = overrideDegree;
|
|
361
421
|
if (overrideSchools && overrideSchools.length > 0) parsed.schools = overrideSchools;
|
|
362
422
|
if (overrideKeyword) parsed.keyword_override = overrideKeyword;
|
|
423
|
+
if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
|
|
363
424
|
|
|
364
425
|
if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
|
|
365
426
|
parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
|
|
@@ -371,6 +432,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
371
432
|
city: parsed.city,
|
|
372
433
|
degree: parsed.degree,
|
|
373
434
|
schools: parsed.schools,
|
|
435
|
+
filter_recent_viewed: parsed.filter_recent_viewed,
|
|
374
436
|
keyword: keywordResolution.keyword
|
|
375
437
|
};
|
|
376
438
|
|
|
@@ -397,6 +459,19 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
397
459
|
{ skipKeywordDefault }
|
|
398
460
|
);
|
|
399
461
|
const suspicious_fields = collectSuspiciousFields(searchParams, screenParams);
|
|
462
|
+
const needs_recent_viewed_filter_confirmation = searchParams.filter_recent_viewed === null;
|
|
463
|
+
const pending_questions = needs_recent_viewed_filter_confirmation
|
|
464
|
+
? [
|
|
465
|
+
{
|
|
466
|
+
field: "filter_recent_viewed",
|
|
467
|
+
question: "是否需要过滤近14天查看过的人选?",
|
|
468
|
+
options: [
|
|
469
|
+
{ label: "需要过滤", value: true },
|
|
470
|
+
{ label: "不过滤", value: false }
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
: [];
|
|
400
475
|
|
|
401
476
|
return {
|
|
402
477
|
parsed,
|
|
@@ -406,8 +481,10 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
406
481
|
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
407
482
|
suspicious_fields,
|
|
408
483
|
needs_keyword_confirmation: keywordResolution.needsConfirmation,
|
|
484
|
+
needs_recent_viewed_filter_confirmation,
|
|
409
485
|
needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
|
|
410
486
|
proposed_keyword: keywordResolution.proposedKeyword,
|
|
487
|
+
pending_questions,
|
|
411
488
|
default_preview: defaultPreview,
|
|
412
489
|
applied_defaults: appliedDefaults,
|
|
413
490
|
review: {
|
|
@@ -418,6 +495,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
418
495
|
missing_fields: missingBeforeDefaults,
|
|
419
496
|
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
420
497
|
suspicious_fields,
|
|
498
|
+
pending_questions,
|
|
421
499
|
default_preview: defaultPreview,
|
|
422
500
|
applied_defaults: appliedDefaults
|
|
423
501
|
}
|
package/src/pipeline.js
CHANGED
|
@@ -10,6 +10,9 @@ function buildRequiredConfirmations(parsedResult) {
|
|
|
10
10
|
if (parsedResult.needs_keyword_confirmation) {
|
|
11
11
|
confirmations.push("keyword");
|
|
12
12
|
}
|
|
13
|
+
if (parsedResult.needs_recent_viewed_filter_confirmation) {
|
|
14
|
+
confirmations.push("filter_recent_viewed");
|
|
15
|
+
}
|
|
13
16
|
if (parsedResult.has_unresolved_missing_fields) {
|
|
14
17
|
confirmations.push("missing_fields_or_defaults");
|
|
15
18
|
}
|
|
@@ -25,6 +28,7 @@ function buildNeedInputResponse(parsedResult) {
|
|
|
25
28
|
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
26
29
|
search_params: parsedResult.searchParams,
|
|
27
30
|
screen_params: parsedResult.screenParams,
|
|
31
|
+
pending_questions: parsedResult.pending_questions,
|
|
28
32
|
review: parsedResult.review,
|
|
29
33
|
error: {
|
|
30
34
|
code: "MISSING_REQUIRED_FIELDS",
|
|
@@ -44,6 +48,7 @@ function buildNeedConfirmationResponse(parsedResult) {
|
|
|
44
48
|
keyword: parsedResult.proposed_keyword || parsedResult.searchParams.keyword
|
|
45
49
|
},
|
|
46
50
|
screen_params: parsedResult.screenParams,
|
|
51
|
+
pending_questions: parsedResult.pending_questions,
|
|
47
52
|
review: parsedResult.review
|
|
48
53
|
};
|
|
49
54
|
}
|
|
@@ -146,7 +151,11 @@ export async function runRecruitPipeline({
|
|
|
146
151
|
return buildNeedInputResponse(parsed);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
if (
|
|
154
|
+
if (
|
|
155
|
+
parsed.needs_keyword_confirmation
|
|
156
|
+
|| parsed.needs_search_params_confirmation
|
|
157
|
+
|| parsed.needs_recent_viewed_filter_confirmation
|
|
158
|
+
) {
|
|
150
159
|
return buildNeedConfirmationResponse(parsed);
|
|
151
160
|
}
|
|
152
161
|
|
|
@@ -237,7 +246,10 @@ export async function runRecruitPipeline({
|
|
|
237
246
|
processed_count: summary.processed_count ?? null,
|
|
238
247
|
passed_count: summary.passed_count ?? null,
|
|
239
248
|
duration_sec: durationSec,
|
|
240
|
-
output_csv: summary.output_csv
|
|
241
|
-
|
|
249
|
+
output_csv: summary.output_csv,
|
|
250
|
+
completion_reason: "processed_target_reached",
|
|
251
|
+
target_count_semantics: "target_count means processed candidate count, not passed candidate count"
|
|
252
|
+
},
|
|
253
|
+
message: "流水线已完成。target_count 表示处理人数目标,而不是通过人数目标;即使通过人数小于 target_count,只要已处理达到目标人数,也应视为本轮完成。"
|
|
242
254
|
};
|
|
243
255
|
}
|
package/src/test-parser.js
CHANGED
|
@@ -10,6 +10,7 @@ function testNeedInput() {
|
|
|
10
10
|
|
|
11
11
|
assert.equal(r.needs_keyword_confirmation, true);
|
|
12
12
|
assert.equal(r.needs_search_params_confirmation, true);
|
|
13
|
+
assert.equal(r.needs_recent_viewed_filter_confirmation, true);
|
|
13
14
|
assert.equal(r.proposed_keyword?.toLowerCase(), "ai infra");
|
|
14
15
|
assert.deepEqual(
|
|
15
16
|
r.default_preview,
|
|
@@ -37,6 +38,7 @@ function testExampleExtraction() {
|
|
|
37
38
|
assert.equal(firstPass.searchParams.city, "杭州");
|
|
38
39
|
assert.equal(firstPass.searchParams.degree, "本科及以上");
|
|
39
40
|
assert.deepEqual(firstPass.searchParams.schools.sort(), ["211院校", "985院校", "QS 100"].sort());
|
|
41
|
+
assert.equal(firstPass.needs_recent_viewed_filter_confirmation, true);
|
|
40
42
|
assert.equal(firstPass.screenParams.target_count, 500);
|
|
41
43
|
|
|
42
44
|
const confirmed = parseRecruitInstruction({
|
|
@@ -46,12 +48,16 @@ function testExampleExtraction() {
|
|
|
46
48
|
keyword_value: "ai infra",
|
|
47
49
|
search_params_confirmed: true
|
|
48
50
|
},
|
|
49
|
-
overrides:
|
|
51
|
+
overrides: {
|
|
52
|
+
filter_recent_viewed: false
|
|
53
|
+
}
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
assert.equal(confirmed.needs_keyword_confirmation, false);
|
|
53
57
|
assert.equal(confirmed.needs_search_params_confirmation, false);
|
|
58
|
+
assert.equal(confirmed.needs_recent_viewed_filter_confirmation, false);
|
|
54
59
|
assert.equal(confirmed.searchParams.keyword, "ai infra");
|
|
60
|
+
assert.equal(confirmed.searchParams.filter_recent_viewed, false);
|
|
55
61
|
assert.equal(confirmed.missing_fields.length, 0);
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -74,11 +80,14 @@ function testStructuredInputAndCriteriaCleanup() {
|
|
|
74
80
|
keyword_value: "AI infra",
|
|
75
81
|
search_params_confirmed: true
|
|
76
82
|
},
|
|
77
|
-
overrides:
|
|
83
|
+
overrides: {
|
|
84
|
+
filter_recent_viewed: false
|
|
85
|
+
}
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
assert.equal(r.searchParams.city, "杭州");
|
|
81
89
|
assert.equal(r.searchParams.degree, "本科");
|
|
90
|
+
assert.equal(r.searchParams.filter_recent_viewed, false);
|
|
82
91
|
assert.equal(r.screenParams.target_count, 10);
|
|
83
92
|
assert.equal(
|
|
84
93
|
r.screenParams.criteria,
|
|
@@ -86,9 +95,9 @@ function testStructuredInputAndCriteriaCleanup() {
|
|
|
86
95
|
);
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
function
|
|
98
|
+
function testSchoolAliasesAndQsBuckets() {
|
|
90
99
|
const byInstruction = parseRecruitInstruction({
|
|
91
|
-
instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,学校要求 qs50、985
|
|
100
|
+
instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,学校要求 qs50、qs200、985、双一流、统招,过滤掉14天内查看过的人选,目标人数 20 人",
|
|
92
101
|
confirmation: {
|
|
93
102
|
keyword_confirmed: true,
|
|
94
103
|
keyword_value: "推荐系统",
|
|
@@ -97,7 +106,11 @@ function testQsVariantsNormalizeToQs100() {
|
|
|
97
106
|
overrides: null
|
|
98
107
|
});
|
|
99
108
|
|
|
100
|
-
assert.deepEqual(
|
|
109
|
+
assert.deepEqual(
|
|
110
|
+
byInstruction.searchParams.schools.sort(),
|
|
111
|
+
["985院校", "QS 100", "QS 500", "双一流院校", "统招本科"].sort()
|
|
112
|
+
);
|
|
113
|
+
assert.equal(byInstruction.searchParams.filter_recent_viewed, true);
|
|
101
114
|
|
|
102
115
|
const byOverride = parseRecruitInstruction({
|
|
103
116
|
instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,目标人数 20 人",
|
|
@@ -107,11 +120,42 @@ function testQsVariantsNormalizeToQs100() {
|
|
|
107
120
|
search_params_confirmed: true
|
|
108
121
|
},
|
|
109
122
|
overrides: {
|
|
110
|
-
schools: ["qs50", "qs500", "211"]
|
|
123
|
+
schools: ["qs50", "qs500", "211", "双一流学校", "统招本"],
|
|
124
|
+
filter_recent_viewed: false
|
|
111
125
|
}
|
|
112
126
|
});
|
|
113
127
|
|
|
114
|
-
assert.deepEqual(
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
byOverride.searchParams.schools.sort(),
|
|
130
|
+
["211院校", "QS 100", "QS 500", "双一流院校", "统招本科"].sort()
|
|
131
|
+
);
|
|
132
|
+
assert.equal(byOverride.searchParams.filter_recent_viewed, false);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function testPlanningClausesRemovedAndMasterDegreeParsed() {
|
|
136
|
+
const r = parseRecruitInstruction({
|
|
137
|
+
instruction:
|
|
138
|
+
"在 Boss 直聘上搜索候选人:城市杭州;学历硕士;学校标签:985、211、QS200;关键词:算法。请先尽可能多地浏览/拉取候选人(至少 50 个),再按硬性要求筛选:简历中明确出现 CCF-A 类会议或期刊论文。最终输出 5 个最匹配的人选。",
|
|
139
|
+
confirmation: {
|
|
140
|
+
keyword_confirmed: true,
|
|
141
|
+
keyword_value: "算法",
|
|
142
|
+
search_params_confirmed: true
|
|
143
|
+
},
|
|
144
|
+
overrides: {
|
|
145
|
+
schools: ["985院校", "211院校", "QS200"],
|
|
146
|
+
filter_recent_viewed: false,
|
|
147
|
+
target_count: 5
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert.equal(r.searchParams.degree, "硕士");
|
|
152
|
+
assert.deepEqual(r.searchParams.schools.sort(), ["985院校", "211院校", "QS 500"].sort());
|
|
153
|
+
assert.equal(r.searchParams.filter_recent_viewed, false);
|
|
154
|
+
assert.equal(r.screenParams.target_count, 5);
|
|
155
|
+
assert.equal(
|
|
156
|
+
r.screenParams.criteria,
|
|
157
|
+
"候选人需有算法相关经历;再按硬性要求筛选:简历中明确出现 CCF-A 类会议或期刊论文"
|
|
158
|
+
);
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
function testCitySanitizationAndConfirmationGate() {
|
|
@@ -124,6 +168,7 @@ function testCitySanitizationAndConfirmationGate() {
|
|
|
124
168
|
|
|
125
169
|
assert.equal(r.searchParams.city, "杭州");
|
|
126
170
|
assert.equal(r.needs_search_params_confirmation, true);
|
|
171
|
+
assert.equal(r.needs_recent_viewed_filter_confirmation, true);
|
|
127
172
|
assert.equal(r.suspicious_fields.length, 0);
|
|
128
173
|
}
|
|
129
174
|
|
|
@@ -142,6 +187,7 @@ function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
|
|
|
142
187
|
assert.equal(r.searchParams.city, null);
|
|
143
188
|
assert.equal(r.searchParams.degree, "不限");
|
|
144
189
|
assert.deepEqual(r.searchParams.schools, []);
|
|
190
|
+
assert.equal(r.needs_recent_viewed_filter_confirmation, true);
|
|
145
191
|
assert.equal(r.screenParams.target_count, 10);
|
|
146
192
|
assert.deepEqual(r.applied_defaults, {
|
|
147
193
|
city: "不限城市",
|
|
@@ -151,14 +197,53 @@ function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
|
|
|
151
197
|
});
|
|
152
198
|
}
|
|
153
199
|
|
|
200
|
+
function testRecentViewedFilterPromptAndNegativeOverride() {
|
|
201
|
+
const missingChoice = parseRecruitInstruction({
|
|
202
|
+
instruction: "帮我找杭州本科做过推荐系统的人,学校 985,目标人数 10 人",
|
|
203
|
+
confirmation: {
|
|
204
|
+
keyword_confirmed: true,
|
|
205
|
+
keyword_value: "推荐系统",
|
|
206
|
+
search_params_confirmed: true
|
|
207
|
+
},
|
|
208
|
+
overrides: null
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(missingChoice.needs_recent_viewed_filter_confirmation, true);
|
|
212
|
+
assert.deepEqual(missingChoice.pending_questions, [
|
|
213
|
+
{
|
|
214
|
+
field: "filter_recent_viewed",
|
|
215
|
+
question: "是否需要过滤近14天查看过的人选?",
|
|
216
|
+
options: [
|
|
217
|
+
{ label: "需要过滤", value: true },
|
|
218
|
+
{ label: "不过滤", value: false }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const explicitNo = parseRecruitInstruction({
|
|
224
|
+
instruction: "帮我找杭州本科做过推荐系统的人,学校 985,不过滤近14天查看过的人选,目标人数 10 人",
|
|
225
|
+
confirmation: {
|
|
226
|
+
keyword_confirmed: true,
|
|
227
|
+
keyword_value: "推荐系统",
|
|
228
|
+
search_params_confirmed: true
|
|
229
|
+
},
|
|
230
|
+
overrides: null
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
assert.equal(explicitNo.needs_recent_viewed_filter_confirmation, false);
|
|
234
|
+
assert.equal(explicitNo.searchParams.filter_recent_viewed, false);
|
|
235
|
+
}
|
|
236
|
+
|
|
154
237
|
function main() {
|
|
155
238
|
testNeedInput();
|
|
156
239
|
testExampleExtraction();
|
|
157
240
|
testMissingFieldsBatch();
|
|
158
241
|
testStructuredInputAndCriteriaCleanup();
|
|
159
|
-
|
|
242
|
+
testSchoolAliasesAndQsBuckets();
|
|
243
|
+
testPlanningClausesRemovedAndMasterDegreeParsed();
|
|
160
244
|
testCitySanitizationAndConfirmationGate();
|
|
161
245
|
testDefaultsCanOnlyApplyWhenExplicitlyRequested();
|
|
246
|
+
testRecentViewedFilterPromptAndNegativeOverride();
|
|
162
247
|
// eslint-disable-next-line no-console
|
|
163
248
|
console.log("parser tests passed");
|
|
164
249
|
}
|
|
@@ -1233,7 +1233,7 @@ async function main() {
|
|
|
1233
1233
|
while (processedCount < targetCount) {
|
|
1234
1234
|
console.log('');
|
|
1235
1235
|
console.log('----------------------------------------');
|
|
1236
|
-
console.log(`处理进度: ${processedCount}/${targetCount} 已通过 ${passedCandidates.length} 人 未确认收藏 ${uncertainFavoriteCount} 人`);
|
|
1236
|
+
console.log(`处理进度: 已处理 ${processedCount}/${targetCount}(目标处理人数) 已通过 ${passedCandidates.length} 人 未确认收藏 ${uncertainFavoriteCount} 人`);
|
|
1237
1237
|
|
|
1238
1238
|
const processedKeysArray = Array.from(processedCardKeys);
|
|
1239
1239
|
const findCardExpr = jsFindNextUnprocessedCard + '(' + currentCardIndex + ',' + JSON.stringify(processedKeysArray) + ')';
|
|
@@ -1606,7 +1606,7 @@ async function main() {
|
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
1608
|
if (processedCount >= targetCount * 3) {
|
|
1609
|
-
console.log('警告:
|
|
1609
|
+
console.log('警告: 已处理超过目标处理人数3倍,强制结束');
|
|
1610
1610
|
break;
|
|
1611
1611
|
}
|
|
1612
1612
|
}
|
|
@@ -1636,7 +1636,8 @@ async function main() {
|
|
|
1636
1636
|
console.log('处理结果:');
|
|
1637
1637
|
console.log(` 已处理: ${processedCount} 人`);
|
|
1638
1638
|
console.log(` 通过筛选: ${passedCandidates.length} 人`);
|
|
1639
|
-
console.log(`
|
|
1639
|
+
console.log(` 目标处理人数: ${targetCount} 人`);
|
|
1640
|
+
console.log(` 完成条件: 已处理人数达到目标处理人数;不要求通过人数达到该值`);
|
|
1640
1641
|
console.log('');
|
|
1641
1642
|
console.log('Done.');
|
|
1642
1643
|
}
|
|
@@ -149,17 +149,24 @@ export class BossSearcher {
|
|
|
149
149
|
const safeSchool = school.replace(/'/g, "\\'");
|
|
150
150
|
const result = await this.evaluate(
|
|
151
151
|
'(function() {' +
|
|
152
|
+
' const normalizeText = function(value) {' +
|
|
153
|
+
' return String(value || "").replace(/\\s+/g, "").trim();' +
|
|
154
|
+
' };' +
|
|
152
155
|
' const iframe = document.querySelector("iframe");' +
|
|
153
156
|
' if (!iframe || !iframe.contentWindow) return { error: "no iframe" };' +
|
|
154
157
|
' const doc = iframe.contentWindow.document;' +
|
|
155
|
-
' const schoolItems = doc.querySelectorAll(".school-item");' +
|
|
156
|
-
' const
|
|
157
|
-
' const
|
|
158
|
-
'
|
|
159
|
-
'
|
|
158
|
+
' const schoolItems = Array.from(doc.querySelectorAll(".school-item"));' +
|
|
159
|
+
' const targetSchool = normalizeText("' + safeSchool + '");' +
|
|
160
|
+
' const schoolItem = schoolItems.find(function(item) {' +
|
|
161
|
+
' const textNode = item.querySelector(".checkbox-text");' +
|
|
162
|
+
' return normalizeText(textNode ? textNode.textContent : item.textContent) === targetSchool;' +
|
|
163
|
+
' });' +
|
|
164
|
+
' if (schoolItem) {' +
|
|
165
|
+
' const label = schoolItem.querySelector("label.checkbox");' +
|
|
160
166
|
' if (label) {' +
|
|
161
|
-
'
|
|
162
|
-
' const
|
|
167
|
+
' const checkbox = schoolItem.querySelector(".checkbox-input");' +
|
|
168
|
+
' const isChecked = label.classList.contains("checked") || Boolean(checkbox && checkbox.checked);' +
|
|
169
|
+
' if (!isChecked) label.click();' +
|
|
163
170
|
' return { success: true, school: "' + safeSchool + '", checked: checkbox ? checkbox.checked : false };' +
|
|
164
171
|
' }' +
|
|
165
172
|
' }' +
|
|
@@ -618,6 +625,42 @@ export class BossSearcher {
|
|
|
618
625
|
}
|
|
619
626
|
}
|
|
620
627
|
|
|
628
|
+
async setRecentViewedFilter(enabled = true) {
|
|
629
|
+
console.log('🕒 设置近14天查看过滤: ' + (enabled ? '开启' : '关闭'));
|
|
630
|
+
try {
|
|
631
|
+
const result = await this.evaluate(
|
|
632
|
+
'(function() {' +
|
|
633
|
+
' const iframe = document.querySelector("iframe");' +
|
|
634
|
+
' if (!iframe || !iframe.contentWindow) return { error: "no iframe" };' +
|
|
635
|
+
' const doc = iframe.contentWindow.document;' +
|
|
636
|
+
' const normalizeText = function(value) {' +
|
|
637
|
+
' return String(value || "").replace(/\\s+/g, "").trim();' +
|
|
638
|
+
' };' +
|
|
639
|
+
' const targetLabel = doc.querySelector(\'label.checkbox.high_search_checkbox[ka="search_change_view_resume"]\')' +
|
|
640
|
+
' || Array.from(doc.querySelectorAll("label.checkbox.high_search_checkbox")).find(function(label) {' +
|
|
641
|
+
' const textNode = label.querySelector(".checkbox-text");' +
|
|
642
|
+
' return normalizeText(textNode ? textNode.textContent : label.textContent) === "过滤近14天查看";' +
|
|
643
|
+
' });' +
|
|
644
|
+
' if (!targetLabel) return { error: "recent viewed filter not found" };' +
|
|
645
|
+
' const checkbox = targetLabel.querySelector(".checkbox-input");' +
|
|
646
|
+
' const currentChecked = targetLabel.classList.contains("checked") || Boolean(checkbox && checkbox.checked);' +
|
|
647
|
+
' const desiredChecked = ' + (enabled ? 'true' : 'false') + ';' +
|
|
648
|
+
' if (currentChecked !== desiredChecked) {' +
|
|
649
|
+
' targetLabel.click();' +
|
|
650
|
+
' }' +
|
|
651
|
+
' const finalChecked = targetLabel.classList.contains("checked") || Boolean(checkbox && checkbox.checked);' +
|
|
652
|
+
' return { success: true, checked: finalChecked, changed: currentChecked !== desiredChecked };' +
|
|
653
|
+
'})()'
|
|
654
|
+
);
|
|
655
|
+
await this.sleep(1200);
|
|
656
|
+
console.log(' 近14天查看过滤: ' + (result && result.checked ? '✅ 已开启' : '⬜ 未开启'));
|
|
657
|
+
return result;
|
|
658
|
+
} catch (e) {
|
|
659
|
+
console.log(' 设置近14天查看过滤时出错:', e.message);
|
|
660
|
+
return { error: e.message };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
621
664
|
async getResults() {
|
|
622
665
|
console.log('📋 获取搜索结果...');
|
|
623
666
|
try {
|
|
@@ -54,6 +54,7 @@ class BossSearchCLI {
|
|
|
54
54
|
degree: '不限',
|
|
55
55
|
schools: [],
|
|
56
56
|
city: null,
|
|
57
|
+
filterRecentViewed: null,
|
|
57
58
|
port: 9222,
|
|
58
59
|
experience: '不限',
|
|
59
60
|
ageMin: null,
|
|
@@ -62,14 +63,49 @@ class BossSearchCLI {
|
|
|
62
63
|
|
|
63
64
|
const schoolMap = {
|
|
64
65
|
'211': '211院校',
|
|
66
|
+
'211院校': '211院校',
|
|
65
67
|
'985': '985院校',
|
|
68
|
+
'985院校': '985院校',
|
|
69
|
+
'qs': 'QS 100',
|
|
66
70
|
'qs100': 'QS 100',
|
|
67
71
|
'qs500': 'QS 500',
|
|
68
72
|
'双一流': '双一流院校',
|
|
73
|
+
'双一流院校': '双一流院校',
|
|
74
|
+
'双一流学校': '双一流院校',
|
|
69
75
|
'留学生': '留学生',
|
|
70
|
-
'统招': '统招本科'
|
|
76
|
+
'统招': '统招本科',
|
|
77
|
+
'统招本科': '统招本科',
|
|
78
|
+
'统招本': '统招本科',
|
|
79
|
+
'全日制本科': '统招本科'
|
|
71
80
|
};
|
|
72
81
|
|
|
82
|
+
function resolveQsSchool(normalizedSchool) {
|
|
83
|
+
const matched = normalizedSchool.match(/^qs(\d+)$/);
|
|
84
|
+
if (!matched) return null;
|
|
85
|
+
|
|
86
|
+
const rank = Number.parseInt(matched[1], 10);
|
|
87
|
+
if (!Number.isFinite(rank)) return null;
|
|
88
|
+
|
|
89
|
+
return rank > 100 ? 'QS 500' : 'QS 100';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeSchool(rawSchool) {
|
|
93
|
+
const raw = String(rawSchool || '').trim();
|
|
94
|
+
if (!raw) return raw;
|
|
95
|
+
const normalized = raw.toLowerCase().replace(/\s+/g, '');
|
|
96
|
+
const qsSchool = resolveQsSchool(normalized);
|
|
97
|
+
if (qsSchool) return qsSchool;
|
|
98
|
+
return schoolMap[normalized] || schoolMap[raw] || raw;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseBooleanArg(rawValue) {
|
|
102
|
+
const normalized = String(rawValue || '').trim().toLowerCase();
|
|
103
|
+
if (!normalized) return null;
|
|
104
|
+
if (['true', '1', 'yes', 'y', 'on', '是', '要', '需要', '过滤'].includes(normalized)) return true;
|
|
105
|
+
if (['false', '0', 'no', 'n', 'off', '否', '不要', '不需要', '不过滤'].includes(normalized)) return false;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
const argv = process.argv.slice(2);
|
|
74
110
|
for (let i = 0; i < argv.length; i++) {
|
|
75
111
|
const arg = argv[i];
|
|
@@ -78,13 +114,12 @@ class BossSearchCLI {
|
|
|
78
114
|
} else if (arg === '--degree' || arg === '-d') {
|
|
79
115
|
args.degree = argv[++i];
|
|
80
116
|
} else if (arg === '--schools' || arg === '-s') {
|
|
81
|
-
const schools = argv[++i].split(
|
|
82
|
-
args.schools = schools.map(
|
|
83
|
-
const normalized = s.trim().toLowerCase();
|
|
84
|
-
return schoolMap[normalized] || s;
|
|
85
|
-
});
|
|
117
|
+
const schools = String(argv[++i] || '').split(/[,,]/);
|
|
118
|
+
args.schools = Array.from(new Set(schools.map(normalizeSchool).filter(Boolean)));
|
|
86
119
|
} else if (arg === '--city' || arg === '-c') {
|
|
87
120
|
args.city = argv[++i];
|
|
121
|
+
} else if (arg === '--filter-recent-viewed') {
|
|
122
|
+
args.filterRecentViewed = parseBooleanArg(argv[++i]);
|
|
88
123
|
} else if (arg === '--port' || arg === '-p') {
|
|
89
124
|
const port = Number.parseInt(argv[++i], 10);
|
|
90
125
|
if (Number.isFinite(port) && port > 0) {
|
|
@@ -108,6 +143,9 @@ class BossSearchCLI {
|
|
|
108
143
|
if (config.schools.length > 0) {
|
|
109
144
|
console.log(' 院校要求:', config.schools.join(', '));
|
|
110
145
|
}
|
|
146
|
+
if (typeof config.filterRecentViewed === 'boolean') {
|
|
147
|
+
console.log(' 过滤近14天查看:', config.filterRecentViewed ? '是' : '否');
|
|
148
|
+
}
|
|
111
149
|
console.log('');
|
|
112
150
|
|
|
113
151
|
await this.searcher.sleep(500);
|
|
@@ -127,6 +165,11 @@ class BossSearchCLI {
|
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
await this.searcher.clickSearch();
|
|
168
|
+
|
|
169
|
+
if (config.filterRecentViewed) {
|
|
170
|
+
await this.searcher.setRecentViewedFilter(true);
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
await this.searcher.sleep(2000);
|
|
131
174
|
await this.searcher.getResults();
|
|
132
175
|
|