@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 +26 -0
- package/package.json +1 -1
- package/skills/boss-recruit-pipeline/README.md +2 -0
- package/skills/boss-recruit-pipeline/SKILL.md +43 -3
- package/src/cli.js +246 -8
- package/src/parser.js +5 -2
- package/src/test-parser.js +29 -0
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
|
@@ -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
|
-
-
|
|
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.
|
|
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:
|
|
329
|
+
ok: pageState.state !== "DEBUG_PORT_UNREACHABLE",
|
|
154
330
|
path: `http://localhost:${port}`,
|
|
155
|
-
message:
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
});
|
package/src/test-parser.js
CHANGED
|
@@ -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
|