@reconcrap/boss-recruit-mcp 1.0.8 → 1.0.9

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
@@ -97,6 +97,7 @@ $CODEX_HOME/boss-recruit-mcp/screening-config.json
97
97
  - `openaiOrganization`
98
98
  - `openaiProject`
99
99
  - `debugPort` 可选,默认 `9222`
100
+ - 端口优先级:`--port > BOSS_RECRUIT_CHROME_PORT > screening-config.json.debugPort > 9222`
100
101
  - `calibrationFile` 可选;不填时默认使用 `$CODEX_HOME/boss-recruit-mcp/favorite-calibration.json`
101
102
  - `outputDir` 可选;不填时默认输出到用户桌面
102
103
  - 学校标签支持 `统招本科` / `双一流院校` / `985` / `211` / `qs100` / `qs500`;如果输入 `qs50`、`qs200`、`qs500` 等其他 `QS数字`,会按 `<=100 -> qs100`、`>100 -> qs500` 归一
@@ -140,7 +141,15 @@ boss-recruit-mcp run --instruction-file request.txt --confirmation-file confirma
140
141
 
141
142
  ## Chrome 与校准
142
143
 
143
- 先确认你要使用的 Chrome 远程调试端口。推荐 `9222`,但如果你已经有一个正在运行的远程调试 Chrome,也可以继续使用那个端口。确认端口后,再执行下面的命令。
144
+ 先确认你要使用的 Chrome 远程调试端口。推荐 `9222`,但如果你已经有一个正在运行的远程调试 Chrome,也可以继续使用那个端口。
145
+
146
+ 建议先固化一次端口(后续命令自动沿用):
147
+
148
+ ```bash
149
+ boss-recruit-mcp set-port --port <port>
150
+ ```
151
+
152
+ 确认端口后,再执行下面的命令。
144
153
 
145
154
  执行校准(会自动尝试打开 Boss 搜索页):
146
155
 
@@ -179,6 +188,8 @@ $CODEX_HOME/boss-recruit-mcp/favorite-calibration.json
179
188
  也可以用下面的命令检查依赖、配置和校准文件:
180
189
 
181
190
  ```bash
191
+ boss-recruit-mcp doctor
192
+ # 或显式指定
182
193
  boss-recruit-mcp doctor --port <port>
183
194
  ```
184
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recruit-mcp",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Unified MCP pipeline for boss-search-cli and boss-screen-cli",
5
5
  "keywords": [
6
6
  "boss",
@@ -35,6 +35,7 @@ boss-recruit-mcp mcp-config --client all
35
35
  - 参数确认尽量复用统一模板:`已识别参数` / `待确认或待修正` / `缺失参数` / `默认值提醒` / `请用户回复`。
36
36
  - 在正式执行前,必须单独让用户确认筛选 `criteria`(尤其学历/学校/论文等硬性条件)无误,不能只确认关键词和搜索参数。
37
37
  - 端口未确认时,必须先询问用户是否使用推荐的 `9222`,或提供一个已有的其他远程调试端口,不能直接默认 `9222`。
38
+ - 用户确认端口后,先执行 `boss-recruit-mcp set-port --port <port>`,让后续 `doctor / launch-chrome / calibrate / run` 自动复用同一端口。
38
39
  - 任何需要打开 Chrome 的动作前,先检查调试端口是否已有可用实例;端口可连时必须复用,不要再新开一个 9222 实例。
39
40
  - 若页面未停留在 Boss search(例如跳到登录页或首页),必须提示用户先手动登录 Boss,再继续。
40
41
  - 如果识别结果里出现明显脏值或可疑字段,例如“杭州筛选做过”,必须要求用户改成标准值后再继续。
@@ -27,16 +27,18 @@
27
27
  - 建议使用 `9222`
28
28
  - 但也允许用户明确提供一个已在使用的其他远程调试端口
29
29
  3. 在用户确认端口前,不要直接假设 `9222` 并执行任何依赖端口的命令。
30
- 4. 端口确认后,先检查依赖与端口状态:
31
- - `boss-recruit-mcp doctor --port <port>`
32
- 5. 任何“准备打开 Chrome”的动作前,必须先判断该端口是否已有可用实例:
30
+ 4. 用户确认端口后,先执行一次端口固化,确保后续动作自动沿用同一端口:
31
+ - `boss-recruit-mcp set-port --port <port>`
32
+ 5. 再检查依赖与端口状态:
33
+ - `boss-recruit-mcp doctor`(或显式 `boss-recruit-mcp doctor --port <port>`)
34
+ 6. 任何“准备打开 Chrome”的动作前,必须先判断该端口是否已有可用实例:
33
35
  - 若调试端口可连,禁止再新开 Chrome;直接复用现有实例,并确保页面在 `https://www.zhipin.com/web/chat/search`
34
36
  - 仅当调试端口不可连时,才执行 `boss-recruit-mcp launch-chrome --port <port>`
35
- 6. 若执行 `launch-chrome` 后页面没有停留在 `https://www.zhipin.com/web/chat/search`:
37
+ 7. 若执行 `launch-chrome` 后页面没有停留在 `https://www.zhipin.com/web/chat/search`:
36
38
  - 若仍在 search 页面,可继续;
37
39
  - 若跳转到登录页、首页或其他 Boss 页面,视为“需要重新登录”;
38
40
  - 必须明确提示用户手动登录 Boss,并等待用户回复“已登录/可以继续”后,才能继续后续动作。
39
- 7. 只有在以上条件满足后,才继续调用流水线。
41
+ 8. 只有在以上条件满足后,才继续调用流水线。
40
42
 
41
43
  ## Calibration Requirement
42
44
 
@@ -278,6 +280,7 @@
278
280
  - 如果工具已经返回 `diagnostics.checks`,优先基于这些检查项生成排障建议。
279
281
  - 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
280
282
  - 如果端口还没确认,必须先问用户“是否使用推荐的 `9222`,还是你已经有别的远程调试端口”,不能直接把 `9222` 当成已确认值。
283
+ - 用户确认端口后,先执行一次 `boss-recruit-mcp set-port --port <port>`,让后续 `doctor / launch-chrome / calibrate / run` 自动复用同一端口。
281
284
  - 如果需要打开 Chrome,优先帮用户执行而不是只给命令。
282
285
  - 如果新打开的 Chrome 页面跳离了 search 页面,必须判断为“需要登录”,提示用户手动登录后再继续。
283
286
 
@@ -290,12 +293,13 @@
290
293
  期望行为:
291
294
 
292
295
  1. 先询问用户是否使用推荐的 Chrome 调试端口 `9222`,或提供一个已有的其他端口。
293
- 2. 用户确认端口后,启动对应端口的调试 Chrome 并打开 Boss 搜索页面。
294
- 3. 先检查校准文件是否存在;若不存在,提醒用户按步骤完成校准。
295
- 4. 环境就绪后再首次调用流水线。
296
- 5. 若 keyword 被自动提取为 `AI infra`,先让用户确认。
297
- 6. 确认后再次调用。
298
- 7. 成功则返回通过人数与 CSV 路径;失败则按错误类型给出下一步。
296
+ 2. 用户确认端口后,先执行 `boss-recruit-mcp set-port --port <port>` 固化端口。
297
+ 3. 启动对应端口的调试 Chrome 并打开 Boss 搜索页面。
298
+ 4. 先检查校准文件是否存在;若不存在,提醒用户按步骤完成校准。
299
+ 5. 环境就绪后再首次调用流水线。
300
+ 6. 若 keyword 被自动提取为 `AI infra`,先让用户确认。
301
+ 7. 确认后再次调用。
302
+ 8. 成功则返回通过人数与 CSV 路径;失败则按错误类型给出下一步。
299
303
 
300
304
  ## Response Style
301
305
 
package/src/adapters.js CHANGED
@@ -49,6 +49,11 @@ function pathExists(targetPath) {
49
49
  }
50
50
  }
51
51
 
52
+ function parsePositiveInteger(raw) {
53
+ const value = Number.parseInt(String(raw || ""), 10);
54
+ return Number.isFinite(value) && value > 0 ? value : null;
55
+ }
56
+
52
57
  function resolveSearchCliDir(workspaceRoot) {
53
58
  const localDir = path.join(workspaceRoot, "boss-search-cli");
54
59
  if (pathExists(localDir)) {
@@ -190,35 +195,43 @@ function loadScreenConfig(configPath) {
190
195
  }
191
196
  }
192
197
 
193
- function resolveDebugPort(config) {
194
- const fromEnv = Number.parseInt(process.env.BOSS_RECRUIT_CHROME_PORT || "", 10);
195
- if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
196
- const fromConfig = Number.parseInt(String(config?.debugPort || ""), 10);
197
- if (Number.isFinite(fromConfig) && fromConfig > 0) return fromConfig;
198
- return 9222;
198
+ function readScreenConfigJson(configPath) {
199
+ if (!pathExists(configPath)) return null;
200
+ try {
201
+ const raw = fs.readFileSync(configPath, "utf8");
202
+ const parsed = JSON.parse(raw);
203
+ return parsed && typeof parsed === "object" ? parsed : null;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+
209
+ function resolveDebugPortFromConfigPath(configPath) {
210
+ const parsed = readScreenConfigJson(configPath);
211
+ const fromConfig = parsePositiveInteger(parsed?.debugPort);
212
+ if (fromConfig) return fromConfig;
213
+ return null;
199
214
  }
200
215
 
201
216
  function resolveWorkspaceDebugPort(workspaceRoot) {
217
+ const fromEnv = parsePositiveInteger(process.env.BOSS_RECRUIT_CHROME_PORT);
218
+ if (fromEnv) return fromEnv;
219
+
202
220
  const configPath = resolveScreenConfigPath(workspaceRoot);
203
- if (pathExists(configPath)) {
204
- const loaded = loadScreenConfig(configPath);
205
- if (loaded.ok) {
206
- return resolveDebugPort(loaded.config);
207
- }
208
- }
209
- return resolveDebugPort(null);
221
+ const fromConfig = resolveDebugPortFromConfigPath(configPath);
222
+ if (fromConfig) return fromConfig;
223
+
224
+ return 9222;
210
225
  }
211
226
 
212
227
  export function runPipelinePreflight(workspaceRoot) {
213
228
  const searchDir = resolveSearchCliDir(workspaceRoot);
214
229
  const screenDir = resolveScreenCliDir(workspaceRoot);
215
230
  const screenConfigPath = resolveScreenConfigPath(workspaceRoot);
216
- const loaded = pathExists(screenConfigPath) ? loadScreenConfig(screenConfigPath) : null;
217
- const debugPort = loaded?.ok ? resolveDebugPort(loaded.config) : resolveDebugPort(null);
218
- const calibrationPath = loaded?.ok
219
- ? (loaded.config.calibrationFile
220
- ? path.resolve(path.dirname(screenConfigPath), loaded.config.calibrationFile)
221
- : getUserCalibrationPath())
231
+ const rawConfig = readScreenConfigJson(screenConfigPath);
232
+ const debugPort = resolveWorkspaceDebugPort(workspaceRoot);
233
+ const calibrationPath = rawConfig?.calibrationFile
234
+ ? path.resolve(path.dirname(screenConfigPath), rawConfig.calibrationFile)
222
235
  : getUserCalibrationPath();
223
236
  const checks = [
224
237
  {
@@ -354,7 +367,7 @@ export async function runScreenCli({ workspaceRoot, screenParams }) {
354
367
  const calibration = loaded.config.calibrationFile
355
368
  ? path.resolve(configBaseDir, loaded.config.calibrationFile)
356
369
  : getUserCalibrationPath();
357
- const debugPort = resolveDebugPort(loaded.config);
370
+ const debugPort = resolveWorkspaceDebugPort(workspaceRoot);
358
371
 
359
372
  const outputName = `筛选结果_${Date.now()}.csv`;
360
373
  let outputPath = outputName;
package/src/cli.js CHANGED
@@ -284,6 +284,99 @@ function getWorkspaceRoot(options) {
284
284
  return path.resolve(String(raw));
285
285
  }
286
286
 
287
+ function parsePositivePort(raw) {
288
+ const port = Number.parseInt(String(raw || ""), 10);
289
+ return Number.isFinite(port) && port > 0 ? port : null;
290
+ }
291
+
292
+ function getActiveScreenConfigPath(workspaceRoot) {
293
+ const preflight = runPipelinePreflight(workspaceRoot);
294
+ const screenConfigCheck = preflight.checks.find((item) => item.key === "screen_config");
295
+ if (screenConfigCheck?.path) {
296
+ return path.resolve(screenConfigCheck.path);
297
+ }
298
+ return getUserConfigPath();
299
+ }
300
+
301
+ function readJsonObjectFile(filePath) {
302
+ const raw = fs.readFileSync(filePath, "utf8");
303
+ const parsed = JSON.parse(raw);
304
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
305
+ throw new Error("Config content must be a JSON object");
306
+ }
307
+ return parsed;
308
+ }
309
+
310
+ function readDebugPortFromConfigPath(configPath) {
311
+ try {
312
+ if (!fs.existsSync(configPath)) return null;
313
+ const parsed = readJsonObjectFile(configPath);
314
+ return parsePositivePort(parsed.debugPort);
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ function persistDebugPortSelection(port, options = {}) {
321
+ const workspaceRoot = getWorkspaceRoot(options);
322
+ const configPath = getActiveScreenConfigPath(workspaceRoot);
323
+ const existed = fs.existsSync(configPath);
324
+ let config = {};
325
+
326
+ if (existed) {
327
+ config = readJsonObjectFile(configPath);
328
+ }
329
+
330
+ config.debugPort = port;
331
+ ensureDir(path.dirname(configPath));
332
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
333
+
334
+ return {
335
+ port,
336
+ configPath,
337
+ existed
338
+ };
339
+ }
340
+
341
+ function applyExplicitPortSelection(options = {}, extras = {}) {
342
+ const selected = parsePositivePort(options.port);
343
+ if (!selected) return null;
344
+
345
+ process.env.BOSS_RECRUIT_CHROME_PORT = String(selected);
346
+ if (!extras.persist) return { port: selected, persisted: false };
347
+
348
+ try {
349
+ const persisted = persistDebugPortSelection(selected, options);
350
+ return {
351
+ port: selected,
352
+ persisted: true,
353
+ configPath: persisted.configPath
354
+ };
355
+ } catch (error) {
356
+ return {
357
+ port: selected,
358
+ persisted: false,
359
+ error: error.message
360
+ };
361
+ }
362
+ }
363
+
364
+ function setDebugPort(options = {}) {
365
+ const selected = parsePositivePort(options.port);
366
+ if (!selected) {
367
+ throw new Error("Missing required --port <number> for set-port.");
368
+ }
369
+
370
+ process.env.BOSS_RECRUIT_CHROME_PORT = String(selected);
371
+ const result = persistDebugPortSelection(selected, options);
372
+
373
+ return {
374
+ port: selected,
375
+ configPath: result.configPath,
376
+ existed: result.existed
377
+ };
378
+ }
379
+
287
380
  function printJson(value) {
288
381
  console.log(JSON.stringify(value, null, 2));
289
382
  }
@@ -421,9 +514,18 @@ function hasModule(moduleName) {
421
514
  }
422
515
 
423
516
  function getDebugPort(options = {}) {
424
- const raw = options.port || process.env.BOSS_RECRUIT_CHROME_PORT || "9222";
425
- const port = Number.parseInt(String(raw), 10);
426
- return Number.isFinite(port) && port > 0 ? port : 9222;
517
+ const fromOption = parsePositivePort(options.port);
518
+ if (fromOption) return fromOption;
519
+
520
+ const fromEnv = parsePositivePort(process.env.BOSS_RECRUIT_CHROME_PORT);
521
+ if (fromEnv) return fromEnv;
522
+
523
+ const workspaceRoot = getWorkspaceRoot(options);
524
+ const configPath = getActiveScreenConfigPath(workspaceRoot);
525
+ const fromConfig = readDebugPortFromConfigPath(configPath);
526
+ if (fromConfig) return fromConfig;
527
+
528
+ return 9222;
427
529
  }
428
530
 
429
531
  function getCalibrationTimeoutMs(options = {}) {
@@ -481,8 +583,10 @@ function ensureUserConfig() {
481
583
  }
482
584
 
483
585
  async function printDoctor(options) {
586
+ applyExplicitPortSelection(options, { persist: true });
484
587
  const port = getDebugPort(options);
485
- const checks = runPipelinePreflight(process.cwd()).checks.slice();
588
+ const workspaceRoot = getWorkspaceRoot(options);
589
+ const checks = runPipelinePreflight(workspaceRoot).checks.slice();
486
590
  const pageState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
487
591
  const userConfigPath = getUserConfigPath();
488
592
  checks.push({
@@ -523,6 +627,7 @@ async function printDoctor(options) {
523
627
  }
524
628
 
525
629
  async function calibrate(options) {
630
+ applyExplicitPortSelection(options, { persist: true });
526
631
  const port = getDebugPort(options);
527
632
  const output = options.output ? path.resolve(String(options.output)) : getUserCalibrationPath();
528
633
  const timeoutMs = getCalibrationTimeoutMs(options);
@@ -587,6 +692,7 @@ async function calibrate(options) {
587
692
  }
588
693
 
589
694
  async function launchChrome(options) {
695
+ applyExplicitPortSelection(options, { persist: true });
590
696
  const port = getDebugPort(options);
591
697
  const initialState = await inspectBossPageState(port, { timeoutMs: 2000, pollMs: 500 });
592
698
  let usedExistingInstance = initialState.state !== "DEBUG_PORT_UNREACHABLE";
@@ -692,6 +798,7 @@ function printHelp() {
692
798
  console.log(" boss-recruit-mcp install Install Codex skill and initialize user config");
693
799
  console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
694
800
  console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
801
+ console.log(" boss-recruit-mcp set-port Persist preferred Chrome debug port to active screening-config");
695
802
  console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
696
803
  console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
697
804
  console.log(" boss-recruit-mcp calibrate Auto-open Boss search page, then run favorite-button calibration");
@@ -705,6 +812,9 @@ function printHelp() {
705
812
  console.log("Calibration command:");
706
813
  console.log(" boss-recruit-mcp calibrate --port 9222 [--timeout-ms 60000] [--output <path>]");
707
814
  console.log("");
815
+ console.log("Port command:");
816
+ console.log(" boss-recruit-mcp set-port --port 19222");
817
+ console.log("");
708
818
  console.log("MCP config command:");
709
819
  console.log(" boss-recruit-mcp mcp-config --client cursor");
710
820
  console.log(" boss-recruit-mcp mcp-config --client all --output-dir <dir>");
@@ -760,13 +870,15 @@ function installAll() {
760
870
  console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
761
871
  console.log("2. Choose a client template from the exported MCP config files and merge it into your AI client config.");
762
872
  console.log("3. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
763
- console.log("4. Run `boss-recruit-mcp doctor --port <your-port>` to verify config, calibration, and runtime prerequisites.");
764
- 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.");
765
- console.log("6. Run `boss-recruit-mcp calibrate --port <your-port>` to generate favorite-calibration.json for this environment.");
766
- console.log("7. Run `boss-recruit-mcp start` or configure your MCP client to launch the command from the generated template.");
873
+ console.log("4. Run `boss-recruit-mcp set-port --port <your-port>` once to persist your chosen port for all later commands.");
874
+ console.log("5. Run `boss-recruit-mcp doctor` (or `boss-recruit-mcp doctor --port <your-port>`) to verify config, calibration, and runtime prerequisites.");
875
+ console.log("6. Run `boss-recruit-mcp launch-chrome` (or `--port <your-port>`); if it reports the page redirected away from search, log in to Boss manually in that Chrome window.");
876
+ console.log("7. Run `boss-recruit-mcp calibrate` (or `--port <your-port>`) to generate favorite-calibration.json for this environment.");
877
+ console.log("8. Run `boss-recruit-mcp start` or configure your MCP client to launch the command from the generated template.");
767
878
  }
768
879
 
769
880
  async function runPipelineOnce(options) {
881
+ applyExplicitPortSelection(options, { persist: true });
770
882
  const instruction = getRunInstruction(options);
771
883
  const confirmation = getRunConfirmation(options);
772
884
  const overrides = getRunOverrides(options);
@@ -819,6 +931,18 @@ switch (command) {
819
931
  );
820
932
  break;
821
933
  }
934
+ case "set-port": {
935
+ try {
936
+ const result = setDebugPort(options);
937
+ console.log(`Preferred debug port saved: ${result.port}`);
938
+ console.log(`Updated config: ${result.configPath}`);
939
+ console.log("Port priority for runtime commands: --port > BOSS_RECRUIT_CHROME_PORT > screening-config.json.debugPort > 9222");
940
+ } catch (error) {
941
+ console.error(error.message || "Failed to persist debug port.");
942
+ process.exitCode = 1;
943
+ }
944
+ break;
945
+ }
822
946
  case "mcp-config":
823
947
  try {
824
948
  printMcpConfig(options);
@@ -6,14 +6,70 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const readline = require('readline');
8
8
  const { spawn, spawnSync } = require('child_process');
9
+ const DEFAULT_DEBUG_PORT = 9222;
9
10
 
10
- const args = process.argv.slice(2).reduce((acc, arg, i, arr) => {
11
- if (arg.startsWith('--')) {
12
- const key = arg.slice(2);
13
- acc[key] = arr[i + 1] && !arr[i + 1].startsWith('--') ? arr[i + 1] : true;
11
+ function parsePositiveInteger(raw) {
12
+ const value = Number.parseInt(String(raw || ''), 10);
13
+ if (Number.isFinite(value) && value > 0) {
14
+ return value;
14
15
  }
15
- return acc;
16
- }, {});
16
+ return null;
17
+ }
18
+
19
+ function resolveDebugPort({ explicitPort = null, configPort = null } = {}) {
20
+ const fromExplicit = parsePositiveInteger(explicitPort);
21
+ if (fromExplicit) return fromExplicit;
22
+ const fromEnv = parsePositiveInteger(process.env.BOSS_RECRUIT_CHROME_PORT);
23
+ if (fromEnv) return fromEnv;
24
+ const fromConfig = parsePositiveInteger(configPort);
25
+ if (fromConfig) return fromConfig;
26
+ return DEFAULT_DEBUG_PORT;
27
+ }
28
+
29
+ function parseCliArgs(argv) {
30
+ const parsed = {};
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const token = argv[i];
33
+
34
+ if (token === '-h') {
35
+ parsed.help = true;
36
+ continue;
37
+ }
38
+ if (token === '-p') {
39
+ const next = argv[i + 1];
40
+ if (next && !next.startsWith('-')) {
41
+ parsed.port = next;
42
+ i += 1;
43
+ } else {
44
+ parsed.port = true;
45
+ }
46
+ continue;
47
+ }
48
+ if (!token.startsWith('--')) {
49
+ continue;
50
+ }
51
+
52
+ const eqIndex = token.indexOf('=');
53
+ if (eqIndex > 2) {
54
+ const key = token.slice(2, eqIndex);
55
+ const value = token.slice(eqIndex + 1);
56
+ parsed[key] = value || true;
57
+ continue;
58
+ }
59
+
60
+ const key = token.slice(2);
61
+ const next = argv[i + 1];
62
+ if (next && !next.startsWith('-')) {
63
+ parsed[key] = next;
64
+ i += 1;
65
+ } else {
66
+ parsed[key] = true;
67
+ }
68
+ }
69
+ return parsed;
70
+ }
71
+
72
+ const args = parseCliArgs(process.argv.slice(2));
17
73
 
18
74
  let baseUrl = args.baseurl || args.baseUrl || null;
19
75
  let apiKey = args.apikey || args.apiKey || null;
@@ -25,10 +81,7 @@ let targetCount = Number.parseInt(args.target || args.targetCount || '', 10);
25
81
  if (!Number.isFinite(targetCount) || targetCount <= 0) {
26
82
  targetCount = null;
27
83
  }
28
- let debugPort = Number.parseInt(args.port || '9222', 10);
29
- if (!Number.isFinite(debugPort) || debugPort <= 0) {
30
- debugPort = 9222;
31
- }
84
+ let debugPort = resolveDebugPort({ explicitPort: args.port });
32
85
  let configFile = args.config ? path.resolve(String(args.config)) : path.resolve(process.cwd(), 'favorite-calibration.json');
33
86
  let outputCsv = args.output || `筛选结果_${Date.now()}.csv`;
34
87
  const bossSearchUrl = 'https://www.zhipin.com/web/chat/search';
@@ -36,16 +89,21 @@ const calibrationScriptPath = path.join(__dirname, 'calibrate-favorite-position-
36
89
  const MAX_RESUME_TEXT_CHARS = 12000;
37
90
  const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1';
38
91
 
92
+ const startupDiscovered = discoverInstalledBossRecruitResources();
93
+ applyDiscoveredResources(startupDiscovered);
39
94
  applyOpenAIEnvironmentDefaults();
40
95
 
41
- if (args.help || args.h) {
96
+ if (args.help) {
42
97
  printUsage();
43
98
  process.exit(0);
44
99
  }
45
100
 
46
101
  function printUsage() {
47
102
  const scriptName = path.basename(process.argv[1] || 'boss-screen-cli.cjs');
48
- console.log(`Usage: node ${scriptName} --criteria <criteria> --targetCount <n> [--baseurl <url>] [--apikey <key>] [--model <model>] [--openai-organization <org_id>] [--openai-project <project_id>] [--port <9222>] [--config <favorite-calibration.json>] [--output <csv>]`);
103
+ console.log(`Usage: node ${scriptName} --criteria <criteria> --targetCount <n> [--baseurl <url>] [--apikey <key>] [--model <model>] [--openai-organization <org_id>] [--openai-project <project_id>] [--port <number>] [--config <favorite-calibration.json>] [--output <csv>]`);
104
+ console.log(` -p, --port <number> Chrome调试端口(默认: ${debugPort})`);
105
+ console.log(' -h, --help 显示帮助');
106
+ console.log(' 端口优先级: --port > BOSS_RECRUIT_CHROME_PORT > screening-config.json.debugPort > 9222');
49
107
  console.log('Tip: run without parameters to enter step-by-step interactive mode.');
50
108
  }
51
109
 
@@ -207,10 +265,10 @@ function applyDiscoveredResources(discovered) {
207
265
  openaiProject = discovered.config.openaiProject.trim();
208
266
  }
209
267
 
210
- const configPort = Number.parseInt(String(discovered.config.debugPort || ''), 10);
211
- if (!args.port && Number.isFinite(configPort) && configPort > 0) {
212
- debugPort = configPort;
213
- }
268
+ debugPort = resolveDebugPort({
269
+ explicitPort: args.port,
270
+ configPort: discovered.config.debugPort
271
+ });
214
272
 
215
273
  if (!args.output && typeof discovered.config.outputDir === 'string' && discovered.config.outputDir.trim()) {
216
274
  const outputDir = discovered.config.outputDir.trim();
@@ -1,11 +1,74 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
2
7
  import { BossSearcher } from './boss-searcher.js';
3
8
 
9
+ const DEFAULT_DEBUG_PORT = 9222;
10
+
11
+ function parsePositiveInteger(raw) {
12
+ const value = Number.parseInt(String(raw || ''), 10);
13
+ if (Number.isFinite(value) && value > 0) {
14
+ return value;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function getCodexHome() {
20
+ return process.env.CODEX_HOME
21
+ ? path.resolve(process.env.CODEX_HOME)
22
+ : path.join(os.homedir(), '.codex');
23
+ }
24
+
25
+ function collectConfigCandidates() {
26
+ const currentFilePath = fileURLToPath(import.meta.url);
27
+ const scriptDir = path.dirname(currentFilePath);
28
+
29
+ return [
30
+ process.env.BOSS_RECRUIT_SCREEN_CONFIG
31
+ ? path.resolve(process.env.BOSS_RECRUIT_SCREEN_CONFIG)
32
+ : null,
33
+ path.join(getCodexHome(), 'boss-recruit-mcp', 'screening-config.json'),
34
+ path.resolve(process.cwd(), 'boss-recruit-mcp', 'config', 'screening-config.json'),
35
+ path.resolve(process.cwd(), 'config', 'screening-config.json'),
36
+ path.resolve(scriptDir, '..', '..', '..', 'config', 'screening-config.json')
37
+ ].filter(Boolean);
38
+ }
39
+
40
+ function resolvePortFromConfig() {
41
+ for (const configPath of collectConfigCandidates()) {
42
+ try {
43
+ if (!fs.existsSync(configPath)) continue;
44
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
45
+ const port = parsePositiveInteger(parsed?.debugPort);
46
+ if (port) return port;
47
+ } catch {
48
+ continue;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function resolveDebugPort(explicitPort) {
55
+ if (explicitPort) return explicitPort;
56
+
57
+ const envPort = parsePositiveInteger(process.env.BOSS_RECRUIT_CHROME_PORT);
58
+ if (envPort) return envPort;
59
+
60
+ const configPort = resolvePortFromConfig();
61
+ if (configPort) return configPort;
62
+
63
+ return DEFAULT_DEBUG_PORT;
64
+ }
65
+
4
66
  class BossSearchCLI {
5
67
  constructor() {
6
68
  const args = this.parseArgs();
69
+ args.port = resolveDebugPort(args.port);
7
70
  this.args = args;
8
- this.searcher = new BossSearcher(args.port);
71
+ this.searcher = args.help ? null : new BossSearcher(args.port);
9
72
  }
10
73
 
11
74
  ensureStep(result, label) {
@@ -18,6 +81,11 @@ class BossSearchCLI {
18
81
  }
19
82
 
20
83
  async run() {
84
+ if (this.args.help) {
85
+ this.printHelp();
86
+ return;
87
+ }
88
+
21
89
  console.log('========================================');
22
90
  console.log(' Boss直聘搜索自动化工具');
23
91
  console.log('========================================\n');
@@ -62,6 +130,24 @@ class BossSearchCLI {
62
130
  }
63
131
  }
64
132
 
133
+ printHelp() {
134
+ console.log('Boss直聘搜索自动化工具');
135
+ console.log('');
136
+ console.log('用法:');
137
+ console.log(' node src/cli.js [options]');
138
+ console.log('');
139
+ console.log('选项:');
140
+ console.log(' -k, --keywords <text> 搜索关键词');
141
+ console.log(' -d, --degree <text> 学历要求(默认: 不限)');
142
+ console.log(' -s, --schools <list> 院校要求,支持逗号分隔');
143
+ console.log(' -c, --city <text> 城市');
144
+ console.log(' --filter-recent-viewed <bool> 过滤近14天查看');
145
+ console.log(` -p, --port <number> Chrome调试端口(默认: ${this.args.port})`);
146
+ console.log(' -h, --help 显示帮助');
147
+ console.log('');
148
+ console.log('端口优先级: --port > BOSS_RECRUIT_CHROME_PORT > screening-config.json.debugPort > 9222');
149
+ }
150
+
65
151
  parseArgs() {
66
152
  const args = {
67
153
  keywords: '',
@@ -69,7 +155,8 @@ class BossSearchCLI {
69
155
  schools: [],
70
156
  city: null,
71
157
  filterRecentViewed: null,
72
- port: 9222,
158
+ port: null,
159
+ help: false,
73
160
  experience: '不限',
74
161
  ageMin: null,
75
162
  ageMax: null
@@ -135,14 +222,16 @@ class BossSearchCLI {
135
222
  } else if (arg === '--filter-recent-viewed') {
136
223
  args.filterRecentViewed = parseBooleanArg(argv[++i]);
137
224
  } else if (arg === '--port' || arg === '-p') {
138
- const port = Number.parseInt(argv[++i], 10);
139
- if (Number.isFinite(port) && port > 0) {
225
+ const port = parsePositiveInteger(argv[++i]);
226
+ if (port) {
140
227
  args.port = port;
141
228
  }
229
+ } else if (arg === '--help' || arg === '-h') {
230
+ args.help = true;
142
231
  }
143
232
  }
144
233
 
145
- if (!args.keywords) {
234
+ if (!args.help && !args.keywords) {
146
235
  console.log('⚠️ 未指定搜索关键词,使用默认值');
147
236
  args.keywords = '算法工程师';
148
237
  }