@reconcrap/boss-recruit-mcp 1.0.3 → 1.0.5

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
@@ -50,6 +50,29 @@ boss-recruit-mcp start
50
50
 
51
51
  该服务通过 stdio 与 MCP client 通信。
52
52
 
53
+ ## CLI Fallback
54
+
55
+ 如果当前 AI agent 无法添加新的 MCP、MCP 数量受限,或者只支持 shell/命令执行,也可以直接调用同一后端的 CLI fallback:
56
+
57
+ ```bash
58
+ boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人,城市杭州,本科,学校 985/211/QS100,目标 10 人"
59
+ ```
60
+
61
+ 也支持通过 JSON 传确认信息与覆盖参数:
62
+
63
+ ```bash
64
+ boss-recruit-mcp run --instruction "在 Boss 上找做过推荐系统的人" --confirmation-json "{\"keyword_confirmed\":true,\"keyword_value\":\"推荐系统\",\"search_params_confirmed\":true}" --overrides-json "{\"city\":\"杭州\",\"degree\":\"本科\",\"schools\":[\"985\",\"211\",\"qs100\"],\"target_count\":10}"
65
+ ```
66
+
67
+ 如果命令行中放长文本不方便,改用文件:
68
+
69
+ ```bash
70
+ boss-recruit-mcp run --instruction-file request.txt --confirmation-file confirmation.json --overrides-file overrides.json
71
+ ```
72
+
73
+ `run` 的输出是 JSON,状态字段与 MCP 工具一致:`NEED_INPUT` / `NEED_CONFIRMATION` / `COMPLETED` / `FAILED`。
74
+ 只要命令成功产出结构化结果,即使状态是 `FAILED` 也会继续输出 JSON,便于 AI agent 直接解析;只有 CLI 参数错误或未处理异常时才会返回非零退出码。
75
+
53
76
  ## Chrome 与校准
54
77
 
55
78
  先确认你要使用的 Chrome 远程调试端口。推荐 `9222`,但如果你已经有一个正在运行的远程调试 Chrome,也可以继续使用那个端口。确认端口后,再执行下面的命令。
@@ -60,6 +83,9 @@ boss-recruit-mcp start
60
83
  boss-recruit-mcp launch-chrome --port <port>
61
84
  ```
62
85
 
86
+ `launch-chrome` 会自动为该端口创建独立的 Chrome profile 目录,避免复用已有 Chrome 实例导致调试端口未生效。
87
+ 命令还会检查新打开的 Boss 页面是否仍停留在 `search` 页面;如果跳转到了登录页或其他页面,说明需要用户先手动登录 Boss。
88
+
63
89
  然后执行校准:
64
90
 
65
91
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -23,9 +23,11 @@ npx @reconcrap/boss-recruit-mcp install
23
23
 
24
24
  ## 运行注意事项
25
25
 
26
+ - 默认优先走 MCP;如果当前 agent 无法再添加 MCP,也可以改用 `boss-recruit-mcp run` 作为 CLI fallback。
26
27
  - 正式开始前,必须先做一轮参数确认,分开展示已识别参数、待确认参数、缺失参数。
27
28
  - 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
28
29
  - 端口未确认时,必须先询问用户是否使用推荐的 `9222`,或提供一个已有的其他远程调试端口,不能直接默认 `9222`。
30
+ - 新打开 Chrome 实例后,要检查页面是否仍停留在 Boss search;如果跳转到其他页面,必须提示用户先手动登录 Boss,再继续。
29
31
  - 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
30
32
  - 如果缺少 `favorite-calibration.json`,必须指导用户在当前环境重新校准,不能搜索或复制历史遗留校准文件来顶替。
31
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
 
@@ -81,6 +85,28 @@
81
85
  - `review.default_preview`
82
86
  - `review.applied_defaults`
83
87
 
88
+ ## Backend Selection
89
+
90
+ - 默认执行路径:优先使用 MCP 工具 `run_recruit_pipeline`。
91
+ - 如果当前 AI agent 无法添加新的 MCP、MCP 数量受限、或当前会话拿不到该工具:
92
+ - 直接切换到 CLI fallback;
93
+ - 不要放弃流程,也不要要求用户手动把所有步骤重新翻译一遍。
94
+ - CLI fallback 必须调用与 MCP 相同后端的命令:
95
+ - `boss-recruit-mcp run --instruction "<user request>"`
96
+ - 如果命令行里直接放长文本不稳定,改用文件输入:
97
+ - `boss-recruit-mcp run --instruction-file <path>`
98
+ - 如果需要传确认信息或覆盖参数,使用这些参数:
99
+ - `--confirmation-json '<json>'`
100
+ - `--confirmation-file <path>`
101
+ - `--overrides-json '<json>'`
102
+ - `--overrides-file <path>`
103
+ - CLI fallback 的输出是 JSON,字段语义与 MCP 返回保持一致;优先按同一套状态机处理:
104
+ - `NEED_INPUT`
105
+ - `NEED_CONFIRMATION`
106
+ - `COMPLETED`
107
+ - `FAILED`
108
+ - 当 CLI fallback 可用时,不要再自行拼接搜索 CLI / 筛选 CLI 的底层命令来重建业务逻辑;优先继续复用 `boss-recruit-mcp run`。
109
+
84
110
  ## Confirmation First
85
111
 
86
112
  - 在任何一次正式搜索 / 筛选开始前,必须先单开一轮“参数确认对话”,不能在首轮解析后直接开跑。
@@ -99,19 +125,30 @@
99
125
 
100
126
  1. 收到招聘指令后,先做 setup 检查:
101
127
  - MCP 是否可用
128
+ - 若 MCP 不可用,CLI fallback 是否可用
102
129
  - Chrome 调试端口是否已确认
103
130
  - Chrome 是否已启动到 Boss 搜索页面
131
+ - 如果刚启动了新的 Chrome 实例,是否仍停留在 Boss search 页面而未跳转登录
104
132
  - `favorite-calibration.json` 是否存在
105
133
  2. 若缺少依赖或 MCP 未启动:
106
134
  - 自动安装依赖并帮助用户启动 MCP;
107
135
  - 优先使用 `npx @reconcrap/boss-recruit-mcp install`
108
- - 然后使用用户的 MCP 配置启动 `boss-recruit-mcp`
136
+ - 然后优先尝试使用用户的 MCP 配置启动 `boss-recruit-mcp`
137
+ - 如果当前 agent 因平台限制无法配置 MCP,切换到 CLI fallback
109
138
  3. 若缺少校准文件:
110
139
  - 明确提示用户先完成校准,不要直接调用流水线;
111
140
  - 明确给出校准步骤与命令;
112
141
  - 明确指出期望生成到哪个路径;
113
142
  - 不要在本机搜索并复用其他 `favorite-calibration.json`。
114
- 4. 只有当以上条件满足时,才首次调用 `run_recruit_pipeline`(只传 `instruction`),用于“解析”,不是立刻执行最终搜索结论。
143
+ 4. `launch-chrome` 后发现页面没有停留在 search,而是跳到了登录页、首页或其他页面:
144
+ - 明确告诉用户“当前需要手动登录 Boss”;
145
+ - 明确要求用户在新打开的 Chrome 窗口中完成登录;
146
+ - 等用户回复“已登录,可以继续”后,再继续下一步;
147
+ - 不要在用户未确认登录完成前直接执行搜索、校准或流水线。
148
+ 5. 只有当以上条件满足时,才首次进入流水线解析:
149
+ - 若 MCP 可用,调用 `run_recruit_pipeline`(只传 `instruction`)
150
+ - 若 MCP 不可用,调用 `boss-recruit-mcp run --instruction ...`
151
+ - 这一步用于“解析”,不是立刻执行最终搜索结论。
115
152
  5. 拿到首次解析结果后,先进入单独的“参数确认对话”:
116
153
  - 列出当前已提取到的参数;
117
154
  - 单独标出需要用户确认的参数;
@@ -219,6 +256,7 @@
219
256
  - 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
220
257
  - 如果端口还没确认,必须先问用户“是否使用推荐的 `9222`,还是你已经有别的远程调试端口”,不能直接把 `9222` 当成已确认值。
221
258
  - 如果需要打开 Chrome,优先帮用户执行而不是只给命令。
259
+ - 如果新打开的 Chrome 页面跳离了 search 页面,必须判断为“需要登录”,提示用户手动登录后再继续。
222
260
 
223
261
  ## Example
224
262
 
@@ -245,5 +283,7 @@
245
283
  - 参数确认阶段尽量复用统一模板,减少自由表述带来的漏项。
246
284
  - 端口未确认时,用“推荐值 + 可选其他端口”的话术,不要直接替用户决定。
247
285
  - 校准缺失时,直接指导用户重新校准,不要建议复制或复用任何历史 calibration 文件。
286
+ - 若当前 agent 受 MCP 数量限制,明确告诉用户“本轮改走 CLI fallback”,但用户体验上仍保持同一套确认和状态输出。
287
+ - 新开 Chrome 后若检测到跳转登录,先提示用户手动登录并等待确认,再继续。
248
288
  - 若要使用默认值,必须写明“请确认是否按默认值继续”,不能模糊带过。
249
289
  - 若运行失败,优先给用户“现在卡在哪一步 + 怎么继续”。
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { startServer } from "./index.js";
9
9
  import { runPipelinePreflight } from "./adapters.js";
10
+ import { runRecruitPipeline } from "./pipeline.js";
10
11
 
11
12
  const require = createRequire(import.meta.url);
12
13
  const currentFilePath = fileURLToPath(import.meta.url);
@@ -43,6 +44,15 @@ function getUserCalibrationPath() {
43
44
  return path.join(getCodexHome(), "boss-recruit-mcp", "favorite-calibration.json");
44
45
  }
45
46
 
47
+ function getChromeUserDataDir(port, options = {}) {
48
+ const rawProvided = options.userDataDir ?? options["user-data-dir"];
49
+ const provided = typeof rawProvided === "string" ? rawProvided.trim() : "";
50
+ const basePath = provided || path.join(getCodexHome(), "boss-recruit-mcp", `chrome-profile-${port}`);
51
+ const targetPath = path.resolve(basePath);
52
+ ensureDir(targetPath);
53
+ return targetPath;
54
+ }
55
+
46
56
  function parseOptions(args) {
47
57
  const options = {};
48
58
  for (let i = 0; i < args.length; i++) {
@@ -60,6 +70,171 @@ function parseOptions(args) {
60
70
  return options;
61
71
  }
62
72
 
73
+ function readTextFile(filePath, label) {
74
+ const resolved = path.resolve(String(filePath));
75
+ try {
76
+ return fs.readFileSync(resolved, "utf8");
77
+ } catch (error) {
78
+ throw new Error(`Failed to read ${label} file: ${resolved}. ${error.message}`);
79
+ }
80
+ }
81
+
82
+ function parseJsonOption(value, label) {
83
+ if (value === undefined || value === null || value === "") {
84
+ return undefined;
85
+ }
86
+
87
+ try {
88
+ return JSON.parse(String(value));
89
+ } catch (error) {
90
+ throw new Error(`Invalid ${label} JSON: ${error.message}`);
91
+ }
92
+ }
93
+
94
+ function getRunInstruction(options) {
95
+ if (typeof options.instruction === "string" && options.instruction.trim()) {
96
+ return options.instruction.trim();
97
+ }
98
+
99
+ const instructionFile = options["instruction-file"];
100
+ if (typeof instructionFile === "string" && instructionFile.trim()) {
101
+ return readTextFile(instructionFile, "instruction").trim();
102
+ }
103
+
104
+ throw new Error("Missing required --instruction or --instruction-file");
105
+ }
106
+
107
+ function getRunConfirmation(options) {
108
+ if (typeof options["confirmation-file"] === "string" && options["confirmation-file"].trim()) {
109
+ return parseJsonOption(
110
+ readTextFile(options["confirmation-file"], "confirmation"),
111
+ "confirmation"
112
+ );
113
+ }
114
+
115
+ return parseJsonOption(options["confirmation-json"], "confirmation");
116
+ }
117
+
118
+ function getRunOverrides(options) {
119
+ if (typeof options["overrides-file"] === "string" && options["overrides-file"].trim()) {
120
+ return parseJsonOption(
121
+ readTextFile(options["overrides-file"], "overrides"),
122
+ "overrides"
123
+ );
124
+ }
125
+
126
+ return parseJsonOption(options["overrides-json"], "overrides");
127
+ }
128
+
129
+ function getWorkspaceRoot(options) {
130
+ const raw = options["workspace-root"] || process.env.BOSS_WORKSPACE_ROOT || process.cwd();
131
+ return path.resolve(String(raw));
132
+ }
133
+
134
+ function printJson(value) {
135
+ console.log(JSON.stringify(value, null, 2));
136
+ }
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
+
63
238
  function hasModule(moduleName) {
64
239
  try {
65
240
  require.resolve(moduleName);
@@ -120,9 +295,10 @@ function ensureUserConfig() {
120
295
  return { path: targetPath, created: false };
121
296
  }
122
297
 
123
- function printDoctor(options) {
298
+ async function printDoctor(options) {
124
299
  const port = getDebugPort(options);
125
300
  const checks = runPipelinePreflight(process.cwd()).checks.slice();
301
+ const pageState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
126
302
  const userConfigPath = getUserConfigPath();
127
303
  checks.push({
128
304
  key: "user_config",
@@ -150,10 +326,14 @@ function printDoctor(options) {
150
326
  });
151
327
  checks.push({
152
328
  key: "chrome_debug_port",
153
- ok: true,
329
+ ok: pageState.state !== "DEBUG_PORT_UNREACHABLE",
154
330
  path: `http://localhost:${port}`,
155
- message: `建议使用 Chrome 调试端口 ${port}`
331
+ message:
332
+ pageState.state === "DEBUG_PORT_UNREACHABLE"
333
+ ? `无法连接 Chrome 调试端口 ${port}`
334
+ : `Chrome 调试端口 ${port} 可连接`
156
335
  });
336
+ checks.push(pageState);
157
337
  console.log(JSON.stringify({ ok: checks.every((item) => item.ok), port, checks }, null, 2));
158
338
  }
159
339
 
@@ -176,7 +356,7 @@ async function calibrate(options) {
176
356
  process.exitCode = code;
177
357
  }
178
358
 
179
- function launchChrome(options) {
359
+ async function launchChrome(options) {
180
360
  const chromePath = getChromeExecutable();
181
361
  if (!chromePath) {
182
362
  console.error("Chrome executable not found. Set BOSS_RECRUIT_CHROME_PATH or install Google Chrome.");
@@ -184,8 +364,10 @@ function launchChrome(options) {
184
364
  return;
185
365
  }
186
366
  const port = getDebugPort(options);
367
+ const userDataDir = getChromeUserDataDir(port, options);
187
368
  const args = [
188
369
  `--remote-debugging-port=${port}`,
370
+ `--user-data-dir=${userDataDir}`,
189
371
  "--new-window",
190
372
  bossUrl
191
373
  ];
@@ -196,7 +378,27 @@ function launchChrome(options) {
196
378
  });
197
379
  child.unref();
198
380
  console.log(`Chrome launched with remote debugging port ${port}`);
381
+ console.log(`User data dir: ${userDataDir}`);
199
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
+ }
200
402
  }
201
403
 
202
404
  function printHelp() {
@@ -205,13 +407,18 @@ function printHelp() {
205
407
  console.log("Usage:");
206
408
  console.log(" boss-recruit-mcp Start the MCP server");
207
409
  console.log(" boss-recruit-mcp start Start the MCP server");
410
+ console.log(" boss-recruit-mcp run Run the pipeline once via CLI and print JSON");
208
411
  console.log(" boss-recruit-mcp install Install Codex skill and initialize user config");
209
412
  console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
210
413
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
211
414
  console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
212
415
  console.log(" boss-recruit-mcp calibrate Run favorite-button calibration and save favorite-calibration.json");
213
- 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");
214
417
  console.log(" boss-recruit-mcp where Print installed package, skill, and config paths");
418
+ console.log("");
419
+ console.log("Run command:");
420
+ console.log(" boss-recruit-mcp run --instruction \"找杭州本科做过推荐系统的人\" [--confirmation-json '{...}'] [--overrides-json '{...}']");
421
+ console.log(" boss-recruit-mcp run --instruction-file request.txt [--confirmation-file confirmation.json] [--overrides-file overrides.json]");
215
422
  }
216
423
 
217
424
  function printPaths() {
@@ -239,11 +446,27 @@ function installAll() {
239
446
  console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
240
447
  console.log("2. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
241
448
  console.log("3. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
242
- 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.");
243
450
  console.log("5. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
244
451
  console.log("6. Run `boss-recruit-mcp start` or configure your MCP client to launch `boss-recruit-mcp`.");
245
452
  }
246
453
 
454
+ async function runPipelineOnce(options) {
455
+ const instruction = getRunInstruction(options);
456
+ const confirmation = getRunConfirmation(options);
457
+ const overrides = getRunOverrides(options);
458
+ const workspaceRoot = getWorkspaceRoot(options);
459
+
460
+ const result = await runRecruitPipeline({
461
+ workspaceRoot,
462
+ instruction,
463
+ confirmation,
464
+ overrides
465
+ });
466
+
467
+ printJson(result);
468
+ }
469
+
247
470
  const command = process.argv[2] || "start";
248
471
  const options = parseOptions(process.argv.slice(3));
249
472
 
@@ -251,6 +474,21 @@ switch (command) {
251
474
  case "start":
252
475
  startServer();
253
476
  break;
477
+ case "run":
478
+ try {
479
+ await runPipelineOnce(options);
480
+ } catch (error) {
481
+ printJson({
482
+ status: "FAILED",
483
+ error: {
484
+ code: "INVALID_CLI_INPUT",
485
+ message: error.message || "Invalid CLI input",
486
+ retryable: false
487
+ }
488
+ });
489
+ process.exitCode = 1;
490
+ }
491
+ break;
254
492
  case "install":
255
493
  installAll();
256
494
  break;
@@ -267,13 +505,13 @@ switch (command) {
267
505
  break;
268
506
  }
269
507
  case "doctor":
270
- printDoctor(options);
508
+ await printDoctor(options);
271
509
  break;
272
510
  case "calibrate":
273
511
  await calibrate(options);
274
512
  break;
275
513
  case "launch-chrome":
276
- launchChrome(options);
514
+ await launchChrome(options);
277
515
  break;
278
516
  case "where":
279
517
  printPaths();
package/src/parser.js CHANGED
@@ -39,6 +39,9 @@ function normalizeSchoolLabel(value) {
39
39
  }
40
40
 
41
41
  const compact = raw.toLowerCase().replace(/\s+/g, "");
42
+ if (/^qs\d+$/.test(compact)) {
43
+ return SEARCH_SCHOOL_MAP.qs100;
44
+ }
42
45
  return SEARCH_SCHOOL_MAP[compact] || SEARCH_SCHOOL_MAP[raw] || raw;
43
46
  }
44
47
 
@@ -88,7 +91,7 @@ function extractSchools(text) {
88
91
  const schools = [];
89
92
  if (/(^|[^0-9])985([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["985"]);
90
93
  if (/(^|[^0-9])211([^0-9]|$)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["211"]);
91
- if (/(qs\s*100|QS\s*100|qs100|QS100)/.test(text)) schools.push(SEARCH_SCHOOL_MAP["qs100"]);
94
+ if (/\bqs\s*\d+\b/i.test(text)) schools.push(SEARCH_SCHOOL_MAP["qs100"]);
92
95
  return uniqueList(schools);
93
96
  }
94
97
 
@@ -183,7 +186,7 @@ function buildScreenCriteria(text, searchParams) {
183
186
  if (/搜索关键词|关键词|keyword/i.test(clause)) return false;
184
187
  if (/地点|城市/.test(clause)) return false;
185
188
  if (/学历|本科|硕士|博士/.test(clause) && !/论文|项目|经验/.test(clause)) return false;
186
- if (/985|211|qs\s*100|QS\s*100|院校/.test(clause) && !/论文|经验|项目/.test(clause)) return false;
189
+ if (/985|211|qs\s*\d+|院校/i.test(clause) && !/论文|经验|项目/.test(clause)) return false;
187
190
  if (/至少筛选|目标人数|目标数量|筛选\d+位/.test(clause)) return false;
188
191
  return true;
189
192
  });
@@ -86,6 +86,34 @@ function testStructuredInputAndCriteriaCleanup() {
86
86
  );
87
87
  }
88
88
 
89
+ function testQsVariantsNormalizeToQs100() {
90
+ const byInstruction = parseRecruitInstruction({
91
+ instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,学校要求 qs50、985,目标人数 20 人",
92
+ confirmation: {
93
+ keyword_confirmed: true,
94
+ keyword_value: "推荐系统",
95
+ search_params_confirmed: true
96
+ },
97
+ overrides: null
98
+ });
99
+
100
+ assert.deepEqual(byInstruction.searchParams.schools.sort(), ["985院校", "QS 100"].sort());
101
+
102
+ const byOverride = parseRecruitInstruction({
103
+ instruction: "帮我找做过推荐系统的人,城市杭州,学历本科,目标人数 20 人",
104
+ confirmation: {
105
+ keyword_confirmed: true,
106
+ keyword_value: "推荐系统",
107
+ search_params_confirmed: true
108
+ },
109
+ overrides: {
110
+ schools: ["qs50", "qs500", "211"]
111
+ }
112
+ });
113
+
114
+ assert.deepEqual(byOverride.searchParams.schools.sort(), ["211院校", "QS 100"].sort());
115
+ }
116
+
89
117
  function testCitySanitizationAndConfirmationGate() {
90
118
  const r = parseRecruitInstruction({
91
119
  instruction:
@@ -128,6 +156,7 @@ function main() {
128
156
  testExampleExtraction();
129
157
  testMissingFieldsBatch();
130
158
  testStructuredInputAndCriteriaCleanup();
159
+ testQsVariantsNormalizeToQs100();
131
160
  testCitySanitizationAndConfirmationGate();
132
161
  testDefaultsCanOnlyApplyWhenExplicitlyRequested();
133
162
  // eslint-disable-next-line no-console