@reconcrap/boss-recruit-mcp 1.0.6 → 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 +53 -5
- package/package.json +1 -1
- package/skills/boss-recruit-pipeline/README.md +7 -0
- package/skills/boss-recruit-pipeline/SKILL.md +14 -6
- package/src/adapters.js +4 -0
- package/src/cli.js +154 -5
- package/src/index.js +1 -0
- package/src/parser.js +78 -5
- package/src/pipeline.js +10 -1
- package/src/test-parser.js +69 -9
- package/vendor/boss-search-cli/src/boss-searcher.js +50 -7
- package/vendor/boss-search-cli/src/cli.js +44 -7
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,7 +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` 可选;不填时默认输出到用户桌面
|
|
44
|
-
-
|
|
90
|
+
- 学校标签支持 `统招本科` / `双一流院校` / `985` / `211` / `qs100` / `qs500`;如果输入 `qs50`、`qs200`、`qs500` 等其他 `QS数字`,会按 `<=100 -> qs100`、`>100 -> qs500` 归一
|
|
45
91
|
|
|
46
92
|
## 运行
|
|
47
93
|
|
|
@@ -56,13 +102,13 @@ boss-recruit-mcp start
|
|
|
56
102
|
如果当前 AI agent 无法添加新的 MCP、MCP 数量受限,或者只支持 shell/命令执行,也可以直接调用同一后端的 CLI fallback:
|
|
57
103
|
|
|
58
104
|
```bash
|
|
59
|
-
boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,城市杭州,本科,学校 985/211/QS100,目标 10
|
|
105
|
+
boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,城市杭州,本科,学校 985/211/QS100,目标 10 人,过滤近14天查看过的人选"
|
|
60
106
|
```
|
|
61
107
|
|
|
62
108
|
也支持通过 JSON 传确认信息与覆盖参数:
|
|
63
109
|
|
|
64
110
|
```bash
|
|
65
|
-
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}"
|
|
66
112
|
```
|
|
67
113
|
|
|
68
114
|
如果命令行中放长文本不方便,改用文件:
|
|
@@ -133,6 +179,7 @@ boss-recruit-mcp doctor --port <port>
|
|
|
133
179
|
"use_default_for_missing": false
|
|
134
180
|
},
|
|
135
181
|
"overrides": {
|
|
182
|
+
"filter_recent_viewed": true,
|
|
136
183
|
"target_count": 500
|
|
137
184
|
}
|
|
138
185
|
}
|
|
@@ -141,11 +188,12 @@ boss-recruit-mcp doctor --port <port>
|
|
|
141
188
|
## 行为说明
|
|
142
189
|
|
|
143
190
|
- 若缺 `city/degree/schools/keyword/target_count`,返回 `NEED_INPUT`
|
|
144
|
-
- 若 keyword
|
|
191
|
+
- 若 keyword 由语义自动抽取、搜索参数仍未被用户明确确认,或用户未说明是否过滤近 14 天查看过的人选,返回 `NEED_CONFIRMATION`
|
|
145
192
|
- 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
|
|
193
|
+
- 若用户没提“是否过滤近14天查看”,会在 `pending_questions` 里返回该问题,调用方应先补问再继续
|
|
146
194
|
- 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
|
|
147
195
|
- `target_count` 表示“目标处理人数”,不是“目标通过人数”;状态一旦是 `COMPLETED`,就表示本轮已完成,不应因通过人数不足而自动重跑
|
|
148
|
-
- 确认后自动执行:搜索 CLI -> 筛选 CLI
|
|
196
|
+
- 确认后自动执行:搜索 CLI -> 点击搜索 -> 勾选“过滤近14天查看”(如启用) -> 筛选 CLI
|
|
149
197
|
- 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
|
|
150
198
|
- 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
|
|
151
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,7 +159,8 @@
|
|
|
157
159
|
6. 缺失项常见含义:
|
|
158
160
|
- `city`: 城市,如“杭州”
|
|
159
161
|
- `degree`: 学历,如“本科”“硕士及以上”
|
|
160
|
-
- `schools`:
|
|
162
|
+
- `schools`: 学校标签,如“统招本科、双一流院校、985、211、qs100、qs500”
|
|
163
|
+
- `filter_recent_viewed`: 是否过滤近 14 天内查看过的人选
|
|
161
164
|
- `target_count`: 目标处理人数,如“10”;表示本轮需要处理多少位候选人,不表示必须有多少人通过
|
|
162
165
|
- `keyword`: 搜索关键词,如“AI infra”“推荐系统”
|
|
163
166
|
7. 若返回 `NEED_INPUT`:
|
|
@@ -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
|
- 必须得到用户明确确认“可以按默认值继续”后,才能继续执行。
|
|
@@ -196,7 +201,9 @@
|
|
|
196
201
|
- 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
|
|
197
202
|
- 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
|
|
198
203
|
- 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
|
|
199
|
-
- 若用户输入 `qs50`、`qs200`、`qs500` 等任意 `QS数字` 学校标签,统一按
|
|
204
|
+
- 若用户输入 `qs50`、`qs200`、`qs500` 等任意 `QS数字` 学校标签,统一按 `<=100 -> qs100`、`>100 -> qs500` 处理;不要把原始 `QS200` 再传到底层搜索命令。
|
|
205
|
+
- 若用户没有明确提到“是否过滤近 14 天查看过的人选”,必须在参数确认阶段主动补问,不能静默默认开启或关闭。
|
|
206
|
+
- 若用户说“过滤近 14 天查看”“排除最近看过的”,映射为 `filter_recent_viewed=true`;若用户说“不过滤近 14 天查看”“保留最近看过的”,映射为 `filter_recent_viewed=false`。
|
|
200
207
|
- 不要把“用户没有继续回复”解释为“默认同意”;默认值只能在用户明确口头确认后使用。
|
|
201
208
|
- 参数确认对话里,优先采用这种结构:
|
|
202
209
|
- 已识别参数
|
|
@@ -217,7 +224,8 @@
|
|
|
217
224
|
- `已识别参数` 只放当前看起来可信的值,例如:
|
|
218
225
|
- 城市:杭州
|
|
219
226
|
- 学历:本科
|
|
220
|
-
-
|
|
227
|
+
- 学校标签:统招本科 / 双一流院校 / 985 / 211 / QS100 / QS500(按实际需求选择)
|
|
228
|
+
- 过滤近14天查看:需要 / 不需要
|
|
221
229
|
- 关键词:AI infra
|
|
222
230
|
- 目标人数:10
|
|
223
231
|
- `待确认 / 待修正` 要明确写出“识别值 -> 疑点 -> 需要用户给出的标准值”,例如:
|
|
@@ -231,7 +239,7 @@
|
|
|
231
239
|
- 若继续会使用哪些默认值
|
|
232
240
|
- 会导致搜索范围变宽、相关性下降或结果偏差增大
|
|
233
241
|
- `请用户回复` 要求用户一次性回复完整,优先使用这种收口方式:
|
|
234
|
-
- 请直接按“城市 / 学历 / 学校标签 / 关键词 / 目标人数”补充或修正
|
|
242
|
+
- 请直接按“城市 / 学历 / 学校标签 / 是否过滤近14天查看 / 关键词 / 目标人数”补充或修正
|
|
235
243
|
- 如果你接受默认值继续,请明确回复“确认按默认值继续”
|
|
236
244
|
- 若用户补充后仍有缺失,再发第二轮确认时继续复用同一结构,只保留:
|
|
237
245
|
- 已更新的参数
|
|
@@ -266,7 +274,7 @@
|
|
|
266
274
|
|
|
267
275
|
用户:
|
|
268
276
|
|
|
269
|
-
- “在 Boss 上找做过 AI infra 的候选人,必须发过 CCF-A 区论文,城市杭州,本科,学校 985/211/QS100
|
|
277
|
+
- “在 Boss 上找做过 AI infra 的候选人,必须发过 CCF-A 区论文,城市杭州,本科,学校 统招本科/双一流院校/985/211/QS100/QS500 中按需选择,目标 10 人”
|
|
270
278
|
|
|
271
279
|
期望行为:
|
|
272
280
|
|
package/src/adapters.js
CHANGED
|
@@ -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
|
}
|
|
@@ -90,12 +105,47 @@ function extractDegree(text) {
|
|
|
90
105
|
|
|
91
106
|
function extractSchools(text) {
|
|
92
107
|
const schools = [];
|
|
108
|
+
if (/统招(?:本科)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["统招"]);
|
|
109
|
+
if (/双一流(?:院校|学校)?/.test(text)) schools.push(SEARCH_SCHOOL_MAP["双一流"]);
|
|
93
110
|
if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
|
|
94
111
|
if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
|
|
95
|
-
|
|
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
|
+
}
|
|
96
119
|
return uniqueList(schools);
|
|
97
120
|
}
|
|
98
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
|
+
|
|
99
149
|
function normalizeStringOverride(value) {
|
|
100
150
|
if (typeof value !== "string") return null;
|
|
101
151
|
const normalized = value.trim();
|
|
@@ -191,7 +241,8 @@ function buildScreenCriteria(text, searchParams) {
|
|
|
191
241
|
if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
|
|
192
242
|
if (/地点|城市/.test(clause)) return false;
|
|
193
243
|
if (/学历|本科|硕士|博士/.test(clause) && !/论文|项目|经验/.test(clause)) return false;
|
|
194
|
-
if (/985|211|qs\s*\d
|
|
244
|
+
if (/985|211|qs\s*\d+|双一流|统招(?:本科)?|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
|
|
245
|
+
if (/近?14天(?:内)?查看(?:过)?|过滤近14天查看/.test(clause)) return false;
|
|
195
246
|
if (isCountPlanningClause(clause)) return false;
|
|
196
247
|
return true;
|
|
197
248
|
});
|
|
@@ -350,6 +401,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
350
401
|
city: extractCity(text),
|
|
351
402
|
degree: extractDegree(text),
|
|
352
403
|
schools: extractSchools(text),
|
|
404
|
+
filter_recent_viewed: extractRecentViewedFilter(text),
|
|
353
405
|
keyword_explicit: extractKeywordExplicit(text),
|
|
354
406
|
keyword_auto: extractKeywordAuto(text),
|
|
355
407
|
target_count: extractTargetCount(text)
|
|
@@ -360,11 +412,15 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
360
412
|
const overrideDegree = normalizeStringOverride(overrides.degree);
|
|
361
413
|
const overrideSchools = normalizeSchoolsOverride(overrides.schools);
|
|
362
414
|
const overrideKeyword = normalizeStringOverride(overrides.keyword);
|
|
415
|
+
const overrideRecentViewed = typeof overrides.filter_recent_viewed === "boolean"
|
|
416
|
+
? overrides.filter_recent_viewed
|
|
417
|
+
: null;
|
|
363
418
|
|
|
364
419
|
if (overrideCity) parsed.city = overrideCity;
|
|
365
420
|
if (overrideDegree) parsed.degree = overrideDegree;
|
|
366
421
|
if (overrideSchools && overrideSchools.length > 0) parsed.schools = overrideSchools;
|
|
367
422
|
if (overrideKeyword) parsed.keyword_override = overrideKeyword;
|
|
423
|
+
if (overrideRecentViewed !== null) parsed.filter_recent_viewed = overrideRecentViewed;
|
|
368
424
|
|
|
369
425
|
if (Number.isFinite(overrides.target_count) && overrides.target_count > 0) {
|
|
370
426
|
parsed.target_count = Number.parseInt(String(overrides.target_count), 10);
|
|
@@ -376,6 +432,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
376
432
|
city: parsed.city,
|
|
377
433
|
degree: parsed.degree,
|
|
378
434
|
schools: parsed.schools,
|
|
435
|
+
filter_recent_viewed: parsed.filter_recent_viewed,
|
|
379
436
|
keyword: keywordResolution.keyword
|
|
380
437
|
};
|
|
381
438
|
|
|
@@ -402,6 +459,19 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
402
459
|
{ skipKeywordDefault }
|
|
403
460
|
);
|
|
404
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
|
+
: [];
|
|
405
475
|
|
|
406
476
|
return {
|
|
407
477
|
parsed,
|
|
@@ -411,8 +481,10 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
411
481
|
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
412
482
|
suspicious_fields,
|
|
413
483
|
needs_keyword_confirmation: keywordResolution.needsConfirmation,
|
|
484
|
+
needs_recent_viewed_filter_confirmation,
|
|
414
485
|
needs_search_params_confirmation: confirmation?.search_params_confirmed !== true,
|
|
415
486
|
proposed_keyword: keywordResolution.proposedKeyword,
|
|
487
|
+
pending_questions,
|
|
416
488
|
default_preview: defaultPreview,
|
|
417
489
|
applied_defaults: appliedDefaults,
|
|
418
490
|
review: {
|
|
@@ -423,6 +495,7 @@ export function parseRecruitInstruction({ instruction, confirmation, overrides }
|
|
|
423
495
|
missing_fields: missingBeforeDefaults,
|
|
424
496
|
has_unresolved_missing_fields: missingBeforeDefaults.length > 0 && !useDefaultForMissing,
|
|
425
497
|
suspicious_fields,
|
|
498
|
+
pending_questions,
|
|
426
499
|
default_preview: defaultPreview,
|
|
427
500
|
applied_defaults: appliedDefaults
|
|
428
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
|
|
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,16 @@ 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);
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
function testPlanningClausesRemovedAndMasterDegreeParsed() {
|
|
@@ -125,12 +143,14 @@ function testPlanningClausesRemovedAndMasterDegreeParsed() {
|
|
|
125
143
|
},
|
|
126
144
|
overrides: {
|
|
127
145
|
schools: ["985院校", "211院校", "QS200"],
|
|
146
|
+
filter_recent_viewed: false,
|
|
128
147
|
target_count: 5
|
|
129
148
|
}
|
|
130
149
|
});
|
|
131
150
|
|
|
132
151
|
assert.equal(r.searchParams.degree, "硕士");
|
|
133
|
-
assert.deepEqual(r.searchParams.schools.sort(), ["985院校", "211院校", "QS
|
|
152
|
+
assert.deepEqual(r.searchParams.schools.sort(), ["985院校", "211院校", "QS 500"].sort());
|
|
153
|
+
assert.equal(r.searchParams.filter_recent_viewed, false);
|
|
134
154
|
assert.equal(r.screenParams.target_count, 5);
|
|
135
155
|
assert.equal(
|
|
136
156
|
r.screenParams.criteria,
|
|
@@ -148,6 +168,7 @@ function testCitySanitizationAndConfirmationGate() {
|
|
|
148
168
|
|
|
149
169
|
assert.equal(r.searchParams.city, "杭州");
|
|
150
170
|
assert.equal(r.needs_search_params_confirmation, true);
|
|
171
|
+
assert.equal(r.needs_recent_viewed_filter_confirmation, true);
|
|
151
172
|
assert.equal(r.suspicious_fields.length, 0);
|
|
152
173
|
}
|
|
153
174
|
|
|
@@ -166,6 +187,7 @@ function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
|
|
|
166
187
|
assert.equal(r.searchParams.city, null);
|
|
167
188
|
assert.equal(r.searchParams.degree, "不限");
|
|
168
189
|
assert.deepEqual(r.searchParams.schools, []);
|
|
190
|
+
assert.equal(r.needs_recent_viewed_filter_confirmation, true);
|
|
169
191
|
assert.equal(r.screenParams.target_count, 10);
|
|
170
192
|
assert.deepEqual(r.applied_defaults, {
|
|
171
193
|
city: "不限城市",
|
|
@@ -175,15 +197,53 @@ function testDefaultsCanOnlyApplyWhenExplicitlyRequested() {
|
|
|
175
197
|
});
|
|
176
198
|
}
|
|
177
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
|
+
|
|
178
237
|
function main() {
|
|
179
238
|
testNeedInput();
|
|
180
239
|
testExampleExtraction();
|
|
181
240
|
testMissingFieldsBatch();
|
|
182
241
|
testStructuredInputAndCriteriaCleanup();
|
|
183
|
-
|
|
242
|
+
testSchoolAliasesAndQsBuckets();
|
|
184
243
|
testPlanningClausesRemovedAndMasterDegreeParsed();
|
|
185
244
|
testCitySanitizationAndConfirmationGate();
|
|
186
245
|
testDefaultsCanOnlyApplyWhenExplicitlyRequested();
|
|
246
|
+
testRecentViewedFilterPromptAndNegativeOverride();
|
|
187
247
|
// eslint-disable-next-line no-console
|
|
188
248
|
console.log("parser tests passed");
|
|
189
249
|
}
|
|
@@ -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,21 +63,47 @@ 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',
|
|
71
|
+
'qs500': 'QS 500',
|
|
67
72
|
'双一流': '双一流院校',
|
|
73
|
+
'双一流院校': '双一流院校',
|
|
74
|
+
'双一流学校': '双一流院校',
|
|
68
75
|
'留学生': '留学生',
|
|
69
|
-
'统招': '统招本科'
|
|
76
|
+
'统招': '统招本科',
|
|
77
|
+
'统招本科': '统招本科',
|
|
78
|
+
'统招本': '统招本科',
|
|
79
|
+
'全日制本科': '统招本科'
|
|
70
80
|
};
|
|
71
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
|
+
|
|
72
92
|
function normalizeSchool(rawSchool) {
|
|
73
93
|
const raw = String(rawSchool || '').trim();
|
|
74
94
|
if (!raw) return raw;
|
|
75
95
|
const normalized = raw.toLowerCase().replace(/\s+/g, '');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
const argv = process.argv.slice(2);
|
|
@@ -87,10 +114,12 @@ class BossSearchCLI {
|
|
|
87
114
|
} else if (arg === '--degree' || arg === '-d') {
|
|
88
115
|
args.degree = argv[++i];
|
|
89
116
|
} else if (arg === '--schools' || arg === '-s') {
|
|
90
|
-
const schools = argv[++i].split(
|
|
91
|
-
args.schools = schools.map(normalizeSchool);
|
|
117
|
+
const schools = String(argv[++i] || '').split(/[,,]/);
|
|
118
|
+
args.schools = Array.from(new Set(schools.map(normalizeSchool).filter(Boolean)));
|
|
92
119
|
} else if (arg === '--city' || arg === '-c') {
|
|
93
120
|
args.city = argv[++i];
|
|
121
|
+
} else if (arg === '--filter-recent-viewed') {
|
|
122
|
+
args.filterRecentViewed = parseBooleanArg(argv[++i]);
|
|
94
123
|
} else if (arg === '--port' || arg === '-p') {
|
|
95
124
|
const port = Number.parseInt(argv[++i], 10);
|
|
96
125
|
if (Number.isFinite(port) && port > 0) {
|
|
@@ -114,6 +143,9 @@ class BossSearchCLI {
|
|
|
114
143
|
if (config.schools.length > 0) {
|
|
115
144
|
console.log(' 院校要求:', config.schools.join(', '));
|
|
116
145
|
}
|
|
146
|
+
if (typeof config.filterRecentViewed === 'boolean') {
|
|
147
|
+
console.log(' 过滤近14天查看:', config.filterRecentViewed ? '是' : '否');
|
|
148
|
+
}
|
|
117
149
|
console.log('');
|
|
118
150
|
|
|
119
151
|
await this.searcher.sleep(500);
|
|
@@ -133,6 +165,11 @@ class BossSearchCLI {
|
|
|
133
165
|
}
|
|
134
166
|
|
|
135
167
|
await this.searcher.clickSearch();
|
|
168
|
+
|
|
169
|
+
if (config.filterRecentViewed) {
|
|
170
|
+
await this.searcher.setRecentViewedFilter(true);
|
|
171
|
+
}
|
|
172
|
+
|
|
136
173
|
await this.searcher.sleep(2000);
|
|
137
174
|
await this.searcher.getResults();
|
|
138
175
|
|