@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 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
- - 学校标签只支持 `985` / `211` / `qs100`;如果输入 `qs50`、`qs200`、`qs500` 等其他 `QS数字`,会统一按 `qs100` 处理
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 由语义自动抽取、或搜索参数仍未被用户明确确认,返回 `NEED_CONFIRMATION`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -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`: 学校标签,如“985、211、qs100”
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数字` 学校标签,统一按 `qs100` 处理;不要把原始 `QS200` 再传到底层搜索命令。
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
- - 学校标签:985 / 211 / QS100
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,目标 10 人”
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 Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
448
- console.log("3. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
449
- console.log("4. 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.");
450
- console.log("5. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
451
- console.log("6. Run `boss-recruit-mcp start` or configure your MCP client to launch `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
@@ -49,6 +49,7 @@ function createToolSchema() {
49
49
  properties: {
50
50
  city: { type: "string" },
51
51
  degree: { type: "string" },
52
+ filter_recent_viewed: { type: "boolean" },
52
53
  schools: {
53
54
  anyOf: [
54
55
  { type: "array", items: { type: "string" } },
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
- "qs100": "QS 100"
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
- if (/^qs\d+$/.test(compact)) {
43
- return SEARCH_SCHOOL_MAP.qs100;
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
- if (/\bqs\s*\d+\b/i.test(text)) schools.push(SEARCH_SCHOOL_MAP["qs100"]);
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+|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
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 (parsed.needs_keyword_confirmation || parsed.needs_search_params_confirmation) {
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
 
@@ -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: null
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: null
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 testQsVariantsNormalizeToQs100() {
98
+ function testSchoolAliasesAndQsBuckets() {
90
99
  const byInstruction = parseRecruitInstruction({
91
- instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,学校要求 qs50、985,目标人数 20 人",
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(byInstruction.searchParams.schools.sort(), ["985院校", "QS 100"].sort());
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(byOverride.searchParams.schools.sort(), ["211院校", "QS 100"].sort());
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 100"].sort());
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
- testQsVariantsNormalizeToQs100();
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 targetTexts = ["统招本科", "双一流院校", "211院校", "985院校", "留学生", "QS 100", "QS 500"];' +
157
- ' const targetIdx = targetTexts.indexOf("' + safeSchool + '");' +
158
- ' if (targetIdx >= 0 && schoolItems[targetIdx]) {' +
159
- ' const label = schoolItems[targetIdx].querySelector("label.checkbox");' +
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
- ' label.click();' +
162
- ' const checkbox = schoolItems[targetIdx].querySelector(".checkbox-input");' +
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
- if (/^qs\d+$/.test(normalized)) {
77
- return 'QS 100';
78
- }
79
- return schoolMap[normalized] || raw;
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