@reconcrap/boss-recruit-mcp 1.0.4 → 1.0.6

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
@@ -41,6 +41,7 @@ $CODEX_HOME/boss-recruit-mcp/screening-config.json
41
41
  - `debugPort` 可选,默认 `9222`
42
42
  - `calibrationFile` 可选;不填时默认使用 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
43
43
  - `outputDir` 可选;不填时默认输出到用户桌面
44
+ - 学校标签只支持 `985` / `211` / `qs100`;如果输入 `qs50`、`qs200`、`qs500` 等其他 `QS数字`,会统一按 `qs100` 处理
44
45
 
45
46
  ## 运行
46
47
 
@@ -84,6 +85,7 @@ boss-recruit-mcp launch-chrome --port <port>
84
85
  ```
85
86
 
86
87
  `launch-chrome` 会自动为该端口创建独立的 Chrome profile 目录,避免复用已有 Chrome 实例导致调试端口未生效。
88
+ 命令还会检查新打开的 Boss 页面是否仍停留在 `search` 页面;如果跳转到了登录页或其他页面,说明需要用户先手动登录 Boss。
87
89
 
88
90
  然后执行校准:
89
91
 
@@ -142,6 +144,7 @@ boss-recruit-mcp doctor --port <port>
142
144
  - 若 keyword 由语义自动抽取、或搜索参数仍未被用户明确确认,返回 `NEED_CONFIRMATION`
143
145
  - 正式执行前应先单独做一轮参数确认,把已识别参数、待确认项、缺失项、默认值风险分开给用户确认
144
146
  - 用户未补齐缺失参数时,只有在明确同意默认值及其质量风险后,才允许继续
147
+ - `target_count` 表示“目标处理人数”,不是“目标通过人数”;状态一旦是 `COMPLETED`,就表示本轮已完成,不应因通过人数不足而自动重跑
145
148
  - 确认后自动执行:搜索 CLI -> 筛选 CLI
146
149
  - 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
147
150
  - 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -27,6 +27,7 @@ npx @reconcrap/boss-recruit-mcp install
27
27
  - 正式开始前,必须先做一轮参数确认,分开展示已识别参数、待确认参数、缺失参数。
28
28
  - 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
29
29
  - 端口未确认时,必须先询问用户是否使用推荐的 `9222`,或提供一个已有的其他远程调试端口,不能直接默认 `9222`。
30
+ - 新打开 Chrome 实例后,要检查页面是否仍停留在 Boss search;如果跳转到其他页面,必须提示用户先手动登录 Boss,再继续。
30
31
  - 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
31
32
  - 如果缺少 `favorite-calibration.json`,必须指导用户在当前环境重新校准,不能搜索或复制历史遗留校准文件来顶替。
32
33
  - 若缺失参数仍未补齐,只能在用户明确确认接受默认值和质量风险后继续,不能静默按默认执行。
@@ -31,7 +31,11 @@
31
31
  5. 若 Chrome 未以远程调试模式启动,优先帮助用户启动:
32
32
  - `boss-recruit-mcp launch-chrome --port <port>`
33
33
  - 打开页面:`https://www.zhipin.com/web/chat/search`
34
- 6. 只有在以上条件满足后,才继续调用流水线。
34
+ 6. 启动新的 Chrome debugging 实例后,必须检查页面是否仍停留在 `https://www.zhipin.com/web/chat/search`:
35
+ - 若仍在 search 页面,可继续;
36
+ - 若跳转到登录页、首页或其他 Boss 页面,视为“需要重新登录”;
37
+ - 必须明确提示用户手动登录 Boss,并等待用户回复“已登录/可以继续”后,才能继续后续动作。
38
+ 7. 只有在以上条件满足后,才继续调用流水线。
35
39
 
36
40
  ## Calibration Requirement
37
41
 
@@ -124,6 +128,7 @@
124
128
  - 若 MCP 不可用,CLI fallback 是否可用
125
129
  - Chrome 调试端口是否已确认
126
130
  - Chrome 是否已启动到 Boss 搜索页面
131
+ - 如果刚启动了新的 Chrome 实例,是否仍停留在 Boss search 页面而未跳转登录
127
132
  - `favorite-calibration.json` 是否存在
128
133
  2. 若缺少依赖或 MCP 未启动:
129
134
  - 自动安装依赖并帮助用户启动 MCP;
@@ -135,7 +140,12 @@
135
140
  - 明确给出校准步骤与命令;
136
141
  - 明确指出期望生成到哪个路径;
137
142
  - 不要在本机搜索并复用其他 `favorite-calibration.json`。
138
- 4. 只有当以上条件满足时,才首次进入流水线解析:
143
+ 4. 若 `launch-chrome` 后发现页面没有停留在 search,而是跳到了登录页、首页或其他页面:
144
+ - 明确告诉用户“当前需要手动登录 Boss”;
145
+ - 明确要求用户在新打开的 Chrome 窗口中完成登录;
146
+ - 等用户回复“已登录,可以继续”后,再继续下一步;
147
+ - 不要在用户未确认登录完成前直接执行搜索、校准或流水线。
148
+ 5. 只有当以上条件满足时,才首次进入流水线解析:
139
149
  - 若 MCP 可用,调用 `run_recruit_pipeline`(只传 `instruction`)
140
150
  - 若 MCP 不可用,调用 `boss-recruit-mcp run --instruction ...`
141
151
  - 这一步用于“解析”,不是立刻执行最终搜索结论。
@@ -148,7 +158,7 @@
148
158
  - `city`: 城市,如“杭州”
149
159
  - `degree`: 学历,如“本科”“硕士及以上”
150
160
  - `schools`: 学校标签,如“985、211、qs100”
151
- - `target_count`: 目标筛选人数,如“10
161
+ - `target_count`: 目标处理人数,如“10”;表示本轮需要处理多少位候选人,不表示必须有多少人通过
152
162
  - `keyword`: 搜索关键词,如“AI infra”“推荐系统”
153
163
  7. 若返回 `NEED_INPUT`:
154
164
  - 不要只问一次就结束;
@@ -168,7 +178,10 @@
168
178
  - 缺失参数已补齐,或用户已明确接受默认值;
169
179
  - `NEED_CONFIRMATION` 分支中的关键词也已确认。
170
180
  11. 若返回 `COMPLETED`:
171
- - 向用户返回摘要:目标数、已处理、通过数、耗时、输出文件路径。
181
+ - 向用户返回摘要:目标处理人数、已处理、通过数、耗时、输出文件路径。
182
+ - 只要状态是 `COMPLETED`,就视为本轮任务完成;
183
+ - 不要因为 `passed_count < target_count` 就自动重跑;
184
+ - `target_count` 的含义是“处理人数目标”,不是“通过人数目标”。
172
185
  12. 若返回 `FAILED`:
173
186
  - 先提炼 `error.code`、`error.message`、`diagnostics`;
174
187
  - 如果是 `PIPELINE_PREFLIGHT_FAILED`,明确指出缺失的本地目录 / 文件;
@@ -183,6 +196,7 @@
183
196
  - 当用户提到“做过 AI infra / 推荐系统 / 搜索 / 广告 / 多模态”等经历,但没有显式写“关键词”,默认允许流水线先自动抽取,再走确认分支。
184
197
  - 当用户附带筛选要求(如“必须发表过 CCF-A 区论文”“有开源项目”“带过团队”),这些要求应该保留在 `criteria` 中,不应被误当作搜索过滤条件。
185
198
  - 若参数提取结果出现明显噪声、截断、短语串接、非标准枚举值,优先视为“识别不可靠”,要求用户确认,不要为了推进流程直接采用。
199
+ - 若用户输入 `qs50`、`qs200`、`qs500` 等任意 `QS数字` 学校标签,统一按 `qs100` 处理;不要把原始 `QS200` 再传到底层搜索命令。
186
200
  - 不要把“用户没有继续回复”解释为“默认同意”;默认值只能在用户明确口头确认后使用。
187
201
  - 参数确认对话里,优先采用这种结构:
188
202
  - 已识别参数
@@ -246,6 +260,7 @@
246
260
  - 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
247
261
  - 如果端口还没确认,必须先问用户“是否使用推荐的 `9222`,还是你已经有别的远程调试端口”,不能直接把 `9222` 当成已确认值。
248
262
  - 如果需要打开 Chrome,优先帮用户执行而不是只给命令。
263
+ - 如果新打开的 Chrome 页面跳离了 search 页面,必须判断为“需要登录”,提示用户手动登录后再继续。
249
264
 
250
265
  ## Example
251
266
 
@@ -273,5 +288,7 @@
273
288
  - 端口未确认时,用“推荐值 + 可选其他端口”的话术,不要直接替用户决定。
274
289
  - 校准缺失时,直接指导用户重新校准,不要建议复制或复用任何历史 calibration 文件。
275
290
  - 若当前 agent 受 MCP 数量限制,明确告诉用户“本轮改走 CLI fallback”,但用户体验上仍保持同一套确认和状态输出。
291
+ - 新开 Chrome 后若检测到跳转登录,先提示用户手动登录并等待确认,再继续。
292
+ - 若返回 `COMPLETED`,不要把“通过人数不足”理解成“任务未完成”;除非用户明确要求“必须找到 N 个通过人选”,否则不要自动追加新一轮搜索。
276
293
  - 若要使用默认值,必须写明“请确认是否按默认值继续”,不能模糊带过。
277
294
  - 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
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(/目标人数:\s*(\d+)\s*人/);
160
+ const target = output.match(/目标(?:处理)?人数:\s*(\d+)\s*人/);
161
161
  const csv = output.match(/结果已导出到:\s*(.+)/);
162
162
 
163
163
  return {
package/src/cli.js CHANGED
@@ -135,6 +135,106 @@ function printJson(value) {
135
135
  console.log(JSON.stringify(value, null, 2));
136
136
  }
137
137
 
138
+ function sleep(ms) {
139
+ return new Promise((resolve) => setTimeout(resolve, ms));
140
+ }
141
+
142
+ async function listChromeTabs(port) {
143
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`);
144
+ if (!response.ok) {
145
+ throw new Error(`DevTools endpoint returned ${response.status}`);
146
+ }
147
+ const data = await response.json();
148
+ return Array.isArray(data) ? data : [];
149
+ }
150
+
151
+ function buildBossPageState(payload) {
152
+ return {
153
+ key: "boss_page_state",
154
+ ...payload
155
+ };
156
+ }
157
+
158
+ async function inspectBossPageState(port, options = {}) {
159
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
160
+ const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
161
+ const expectedUrl = options.expectedUrl || bossUrl;
162
+ const deadline = Date.now() + timeoutMs;
163
+ let lastError = null;
164
+ let lastTabs = [];
165
+
166
+ while (Date.now() < deadline) {
167
+ try {
168
+ const tabs = await listChromeTabs(port);
169
+ lastTabs = tabs;
170
+
171
+ const exactSearchTab = tabs.find(
172
+ (tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/search")
173
+ );
174
+ if (exactSearchTab) {
175
+ return buildBossPageState({
176
+ ok: true,
177
+ state: "SEARCH_READY",
178
+ path: exactSearchTab.url,
179
+ current_url: exactSearchTab.url,
180
+ title: exactSearchTab.title || null,
181
+ requires_login: false,
182
+ message: "Boss 搜索页已打开,且当前仍停留在 search 页面。"
183
+ });
184
+ }
185
+
186
+ const bossTab = tabs.find(
187
+ (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
188
+ );
189
+ if (bossTab) {
190
+ return buildBossPageState({
191
+ ok: false,
192
+ state: "LOGIN_REQUIRED",
193
+ path: bossTab.url,
194
+ current_url: bossTab.url,
195
+ title: bossTab.title || null,
196
+ requires_login: true,
197
+ expected_url: expectedUrl,
198
+ message: "Boss 页面没有停留在 search 页面,通常表示需要重新登录。请用户手动登录 Boss 后再继续。"
199
+ });
200
+ }
201
+ } catch (error) {
202
+ lastError = error;
203
+ }
204
+
205
+ await sleep(pollMs);
206
+ }
207
+
208
+ if (lastError) {
209
+ return buildBossPageState({
210
+ ok: false,
211
+ state: "DEBUG_PORT_UNREACHABLE",
212
+ path: `http://127.0.0.1:${port}`,
213
+ current_url: null,
214
+ title: null,
215
+ requires_login: false,
216
+ expected_url: expectedUrl,
217
+ message: `无法连接到 Chrome DevTools 端口 ${port}。请确认 Chrome 已以远程调试模式启动。`,
218
+ error: lastError.message
219
+ });
220
+ }
221
+
222
+ return buildBossPageState({
223
+ ok: false,
224
+ state: "BOSS_TAB_NOT_FOUND",
225
+ path: expectedUrl,
226
+ current_url: null,
227
+ title: null,
228
+ requires_login: false,
229
+ expected_url: expectedUrl,
230
+ message: "未检测到 Boss 页面标签页。请确认 Chrome 已打开 Boss 搜索页。",
231
+ sample_urls: lastTabs
232
+ .map((tab) => tab?.url)
233
+ .filter(Boolean)
234
+ .slice(0, 5)
235
+ });
236
+ }
237
+
138
238
  function hasModule(moduleName) {
139
239
  try {
140
240
  require.resolve(moduleName);
@@ -195,9 +295,10 @@ function ensureUserConfig() {
195
295
  return { path: targetPath, created: false };
196
296
  }
197
297
 
198
- function printDoctor(options) {
298
+ async function printDoctor(options) {
199
299
  const port = getDebugPort(options);
200
300
  const checks = runPipelinePreflight(process.cwd()).checks.slice();
301
+ const pageState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
201
302
  const userConfigPath = getUserConfigPath();
202
303
  checks.push({
203
304
  key: "user_config",
@@ -225,10 +326,14 @@ function printDoctor(options) {
225
326
  });
226
327
  checks.push({
227
328
  key: "chrome_debug_port",
228
- ok: true,
329
+ ok: pageState.state !== "DEBUG_PORT_UNREACHABLE",
229
330
  path: `http://localhost:${port}`,
230
- message: `建议使用 Chrome 调试端口 ${port}`
331
+ message:
332
+ pageState.state === "DEBUG_PORT_UNREACHABLE"
333
+ ? `无法连接 Chrome 调试端口 ${port}`
334
+ : `Chrome 调试端口 ${port} 可连接`
231
335
  });
336
+ checks.push(pageState);
232
337
  console.log(JSON.stringify({ ok: checks.every((item) => item.ok), port, checks }, null, 2));
233
338
  }
234
339
 
@@ -251,7 +356,7 @@ async function calibrate(options) {
251
356
  process.exitCode = code;
252
357
  }
253
358
 
254
- function launchChrome(options) {
359
+ async function launchChrome(options) {
255
360
  const chromePath = getChromeExecutable();
256
361
  if (!chromePath) {
257
362
  console.error("Chrome executable not found. Set BOSS_RECRUIT_CHROME_PATH or install Google Chrome.");
@@ -275,6 +380,25 @@ function launchChrome(options) {
275
380
  console.log(`Chrome launched with remote debugging port ${port}`);
276
381
  console.log(`User data dir: ${userDataDir}`);
277
382
  console.log(`URL: ${bossUrl}`);
383
+
384
+ const pageState = await inspectBossPageState(port, { timeoutMs: 12000, pollMs: 1000 });
385
+ if (pageState.state === "SEARCH_READY") {
386
+ console.log("Boss search page is ready.");
387
+ console.log(`Current URL: ${pageState.current_url}`);
388
+ return;
389
+ }
390
+
391
+ if (pageState.state === "LOGIN_REQUIRED") {
392
+ console.log("Boss page redirected away from search. Manual login is required.");
393
+ console.log(`Current URL: ${pageState.current_url}`);
394
+ console.log("Please log in to Boss manually in the opened Chrome window, then tell the AI agent to continue.");
395
+ return;
396
+ }
397
+
398
+ console.log(pageState.message);
399
+ if (pageState.current_url) {
400
+ console.log(`Current URL: ${pageState.current_url}`);
401
+ }
278
402
  }
279
403
 
280
404
  function printHelp() {
@@ -289,7 +413,7 @@ function printHelp() {
289
413
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
290
414
  console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
291
415
  console.log(" boss-recruit-mcp calibrate Run favorite-button calibration and save favorite-calibration.json");
292
- console.log(" boss-recruit-mcp launch-chrome Launch Chrome in remote-debugging mode and open Boss search");
416
+ console.log(" boss-recruit-mcp launch-chrome Launch Chrome in remote-debugging mode, open Boss search, and check login state");
293
417
  console.log(" boss-recruit-mcp where Print installed package, skill, and config paths");
294
418
  console.log("");
295
419
  console.log("Run command:");
@@ -322,7 +446,7 @@ function installAll() {
322
446
  console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
323
447
  console.log("2. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
324
448
  console.log("3. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
325
- console.log("4. Run `boss-recruit-mcp launch-chrome --port <your-port>` and log in to Boss if needed.");
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.");
326
450
  console.log("5. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
327
451
  console.log("6. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
328
452
  }
@@ -381,13 +505,13 @@ switch (command) {
381
505
  break;
382
506
  }
383
507
  case "doctor":
384
- printDoctor(options);
508
+ await printDoctor(options);
385
509
  break;
386
510
  case "calibrate":
387
511
  await calibrate(options);
388
512
  break;
389
513
  case "launch-chrome":
390
- launchChrome(options);
514
+ await launchChrome(options);
391
515
  break;
392
516
  case "where":
393
517
  printPaths();
package/src/parser.js CHANGED
@@ -82,6 +82,7 @@ function extractCity(text) {
82
82
  function extractDegree(text) {
83
83
  if (/(博士及以上|博士)/.test(text)) return "博士";
84
84
  if (/(硕士及以上|硕士以上)/.test(text)) return "硕士及以上";
85
+ if (/硕士/.test(text)) return "硕士";
85
86
  if (/(本科及以上|本科以上)/.test(text)) return "本科及以上";
86
87
  if (/本科/.test(text)) return "本科";
87
88
  return null;
@@ -176,6 +177,10 @@ function sanitizeClause(clause) {
176
177
  .trim();
177
178
  }
178
179
 
180
+ function isCountPlanningClause(clause) {
181
+ return /(?:目标(?:筛选)?(?:人数|数量)?|至少筛选|筛选\s*\d+\s*位|输出\s*\d+\s*(?:位|个|个人选|个候选人)?|最终输出\s*\d+\s*(?:位|个|个人选|个候选人)?|处理\s*\d+\s*(?:位|人)|(?:浏览|拉取|抓取).*(?:至少\s*)?\d+\s*(?:位|个|个人选|个候选人)?|最匹配.*\d+\s*(?:位|个|个人选|个候选人)?)/i.test(clause);
182
+ }
183
+
179
184
  function buildScreenCriteria(text, searchParams) {
180
185
  const clauses = text
181
186
  .split(/[,,。;;\n]/)
@@ -187,7 +192,7 @@ function buildScreenCriteria(text, searchParams) {
187
192
  if (/地点|城市/.test(clause)) return false;
188
193
  if (/学历|本科|硕士|博士/.test(clause) && !/论文|项目|经验/.test(clause)) return false;
189
194
  if (/985|211|qs\s*\d+|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
190
- if (/至少筛选|目标人数|目标数量|筛选\d+位/.test(clause)) return false;
195
+ if (isCountPlanningClause(clause)) return false;
191
196
  return true;
192
197
  });
193
198
 
package/src/pipeline.js CHANGED
@@ -237,7 +237,10 @@ export async function runRecruitPipeline({
237
237
  processed_count: summary.processed_count ?? null,
238
238
  passed_count: summary.passed_count ?? null,
239
239
  duration_sec: durationSec,
240
- output_csv: summary.output_csv
241
- }
240
+ output_csv: summary.output_csv,
241
+ completion_reason: "processed_target_reached",
242
+ target_count_semantics: "target_count means processed candidate count, not passed candidate count"
243
+ },
244
+ message: "流水线已完成。target_count 表示处理人数目标,而不是通过人数目标;即使通过人数小于 target_count,只要已处理达到目标人数,也应视为本轮完成。"
242
245
  };
243
246
  }
@@ -114,6 +114,30 @@ function testQsVariantsNormalizeToQs100() {
114
114
  assert.deepEqual(byOverride.searchParams.schools.sort(), ["211院校", "QS 100"].sort());
115
115
  }
116
116
 
117
+ function testPlanningClausesRemovedAndMasterDegreeParsed() {
118
+ const r = parseRecruitInstruction({
119
+ instruction:
120
+ "在 Boss 直聘上搜索候选人:城市杭州;学历硕士;学校标签:985、211、QS200;关键词:算法。请先尽可能多地浏览/拉取候选人(至少 50 个),再按硬性要求筛选:简历中明确出现 CCF-A 类会议或期刊论文。最终输出 5 个最匹配的人选。",
121
+ confirmation: {
122
+ keyword_confirmed: true,
123
+ keyword_value: "算法",
124
+ search_params_confirmed: true
125
+ },
126
+ overrides: {
127
+ schools: ["985院校", "211院校", "QS200"],
128
+ target_count: 5
129
+ }
130
+ });
131
+
132
+ assert.equal(r.searchParams.degree, "硕士");
133
+ assert.deepEqual(r.searchParams.schools.sort(), ["985院校", "211院校", "QS 100"].sort());
134
+ assert.equal(r.screenParams.target_count, 5);
135
+ assert.equal(
136
+ r.screenParams.criteria,
137
+ "候选人需有算法相关经历;再按硬性要求筛选:简历中明确出现 CCF-A 类会议或期刊论文"
138
+ );
139
+ }
140
+
117
141
  function testCitySanitizationAndConfirmationGate() {
118
142
  const r = parseRecruitInstruction({
119
143
  instruction:
@@ -157,6 +181,7 @@ function main() {
157
181
  testMissingFieldsBatch();
158
182
  testStructuredInputAndCriteriaCleanup();
159
183
  testQsVariantsNormalizeToQs100();
184
+ testPlanningClausesRemovedAndMasterDegreeParsed();
160
185
  testCitySanitizationAndConfirmationGate();
161
186
  testDefaultsCanOnlyApplyWhenExplicitlyRequested();
162
187
  // eslint-disable-next-line no-console
@@ -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('警告: 已处理超过目标数量3倍,强制结束');
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(` 目标人数: ${targetCount} 人`);
1639
+ console.log(` 目标处理人数: ${targetCount} 人`);
1640
+ console.log(` 完成条件: 已处理人数达到目标处理人数;不要求通过人数达到该值`);
1640
1641
  console.log('');
1641
1642
  console.log('Done.');
1642
1643
  }
@@ -64,12 +64,21 @@ class BossSearchCLI {
64
64
  '211': '211院校',
65
65
  '985': '985院校',
66
66
  'qs100': 'QS 100',
67
- 'qs500': 'QS 500',
68
67
  '双一流': '双一流院校',
69
68
  '留学生': '留学生',
70
69
  '统招': '统招本科'
71
70
  };
72
71
 
72
+ function normalizeSchool(rawSchool) {
73
+ const raw = String(rawSchool || '').trim();
74
+ if (!raw) return raw;
75
+ const normalized = raw.toLowerCase().replace(/\s+/g, '');
76
+ if (/^qs\d+$/.test(normalized)) {
77
+ return 'QS 100';
78
+ }
79
+ return schoolMap[normalized] || raw;
80
+ }
81
+
73
82
  const argv = process.argv.slice(2);
74
83
  for (let i = 0; i < argv.length; i++) {
75
84
  const arg = argv[i];
@@ -79,10 +88,7 @@ class BossSearchCLI {
79
88
  args.degree = argv[++i];
80
89
  } else if (arg === '--schools' || arg === '-s') {
81
90
  const schools = argv[++i].split(',');
82
- args.schools = schools.map(function(s) {
83
- const normalized = s.trim().toLowerCase();
84
- return schoolMap[normalized] || s;
85
- });
91
+ args.schools = schools.map(normalizeSchool);
86
92
  } else if (arg === '--city' || arg === '-c') {
87
93
  args.city = argv[++i];
88
94
  } else if (arg === '--port' || arg === '-p') {