@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 +12 -1
- package/package.json +1 -1
- package/skills/boss-recruit-pipeline/README.md +1 -0
- package/skills/boss-recruit-pipeline/SKILL.md +15 -11
- package/src/adapters.js +33 -20
- package/src/cli.js +132 -8
- package/vendor/boss-screen-cli/boss-screen-cli.cjs +74 -16
- package/vendor/boss-search-cli/src/cli.js +94 -5
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
|
@@ -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
|
|
32
|
-
5.
|
|
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
|
-
|
|
37
|
+
7. 若执行 `launch-chrome` 后页面没有停留在 `https://www.zhipin.com/web/chat/search`:
|
|
36
38
|
- 若仍在 search 页面,可继续;
|
|
37
39
|
- 若跳转到登录页、首页或其他 Boss 页面,视为“需要重新登录”;
|
|
38
40
|
- 必须明确提示用户手动登录 Boss,并等待用户回复“已登录/可以继续”后,才能继续后续动作。
|
|
39
|
-
|
|
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.
|
|
294
|
-
3.
|
|
295
|
-
4.
|
|
296
|
-
5.
|
|
297
|
-
6.
|
|
298
|
-
7.
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
217
|
-
const debugPort =
|
|
218
|
-
const calibrationPath =
|
|
219
|
-
? (
|
|
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 =
|
|
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
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
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
|
|
764
|
-
console.log("5. Run `boss-recruit-mcp
|
|
765
|
-
console.log("6. Run `boss-recruit-mcp
|
|
766
|
-
console.log("7. Run `boss-recruit-mcp
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 <
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
debugPort
|
|
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:
|
|
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 =
|
|
139
|
-
if (
|
|
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
|
}
|