@reconcrap/boss-recruit-mcp 1.0.15 → 1.0.17
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/SKILL.md +2 -0
- package/src/adapters.js +118 -2
- package/src/cli.js +229 -40
- package/src/pipeline.js +123 -1
- package/src/test-pipeline.js +40 -0
package/README.md
CHANGED
|
@@ -25,10 +25,12 @@ npx @reconcrap/boss-recruit-mcp install
|
|
|
25
25
|
- 安装 Codex skill 到 `$CODEX_HOME/skills/boss-recruit-pipeline`
|
|
26
26
|
- 初始化用户配置到 `$CODEX_HOME/boss-recruit-mcp/screening-config.json`
|
|
27
27
|
- 生成通用 MCP 配置模板到 `$CODEX_HOME/boss-recruit-mcp/agent-mcp-configs`
|
|
28
|
+
- 自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
29
|
+
- 自动尝试把 skill 镜像到已检测到的外部 agent skills 目录
|
|
28
30
|
- 包内自带 `boss-search-cli` 与 `boss-screen-cli` 运行时文件,无需额外目录结构
|
|
29
31
|
- 不包含 `favorite-calibration.json`,首次使用前需要自行校准生成
|
|
30
32
|
|
|
31
|
-
## 跨 Agent 快速接入(Cursor / Trae / Claude Code / OpenClaw)
|
|
33
|
+
## 跨 Agent 快速接入(Cursor / Trae / trae-cn / Claude Code / OpenClaw)
|
|
32
34
|
|
|
33
35
|
生成 MCP 配置模板:
|
|
34
36
|
|
|
@@ -58,6 +60,7 @@ $CODEX_HOME/boss-recruit-mcp/agent-mcp-configs
|
|
|
58
60
|
boss-recruit-mcp mcp-config --client cursor
|
|
59
61
|
boss-recruit-mcp mcp-config --client claudecode
|
|
60
62
|
boss-recruit-mcp mcp-config --client trae
|
|
63
|
+
boss-recruit-mcp mcp-config --client trae-cn
|
|
61
64
|
boss-recruit-mcp mcp-config --client openclaw
|
|
62
65
|
boss-recruit-mcp mcp-config --client generic
|
|
63
66
|
```
|
|
@@ -73,6 +76,13 @@ boss-recruit-mcp mcp-config --client generic
|
|
|
73
76
|
boss-recruit-mcp mcp-config --client generic --command boss-recruit-mcp --args-json "[\"start\"]"
|
|
74
77
|
```
|
|
75
78
|
|
|
79
|
+
可选环境变量(用于跨 agent 自动配置):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
BOSS_RECRUIT_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
|
|
83
|
+
BOSS_RECRUIT_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
|
|
84
|
+
```
|
|
85
|
+
|
|
76
86
|
## 准备配置
|
|
77
87
|
|
|
78
88
|
1. 初始化后编辑用户配置文件:
|
|
@@ -224,6 +234,7 @@ boss-recruit-mcp doctor --port <port>
|
|
|
224
234
|
- 确认后自动执行:搜索 CLI -> 点击搜索 -> 勾选“过滤近14天查看”(按轮次规则) -> 筛选 CLI
|
|
225
235
|
- 返回摘要:目标数、已处理、通过数、耗时、输出 CSV
|
|
226
236
|
- 执行前会先做本地依赖预检查,若目录 / 入口 / 配置文件缺失则返回 `PIPELINE_PREFLIGHT_FAILED`
|
|
237
|
+
- preflight 会检查 Node.js 与 npm 依赖(`chrome-remote-interface` / `ws`);失败时会返回 `diagnostics.recovery`(含有序修复步骤与 `agent_prompt`)
|
|
227
238
|
- 若缺少 `favorite-calibration.json`,会返回 `CALIBRATION_REQUIRED`
|
|
228
239
|
- 若某轮搜索返回可筛选候选人但筛选 `processed_count` 非法或为 0,会先导出当前累计 CSV,再返回 `SCREEN_NO_PROGRESS`
|
|
229
240
|
- 若当前运行环境不允许启动子进程,会返回更明确的权限错误码而不是笼统失败
|
package/package.json
CHANGED
|
@@ -289,6 +289,8 @@ description: "Use when users ask to recruit candidates on Boss Zhipin via the bo
|
|
|
289
289
|
- 不要把底层 stderr 原样大段贴给用户,只提炼关键错误和下一步。
|
|
290
290
|
- 如果失败原因明显是环境问题,要直接说明不是用户输入有误。
|
|
291
291
|
- 如果工具已经返回 `diagnostics.checks`,优先基于这些检查项生成排障建议。
|
|
292
|
+
- 如果返回 `PIPELINE_PREFLIGHT_FAILED` 且含 `diagnostics.recovery.agent_prompt`,优先把该提示词直接交给 AI agent 自动修复依赖。
|
|
293
|
+
- 自动修复依赖必须严格串行:先 Node.js,再 npm 依赖;若涉及 Python / Pillow,先 Python 再 Pillow。
|
|
292
294
|
- 如果工具返回 `output_csv`,在摘要里给出路径,避免重复解释内部流程。
|
|
293
295
|
- 如果端口还没确认,必须先问用户“是否使用推荐的 `9222`,还是你已经有别的远程调试端口”,不能直接把 `9222` 当成已确认值。
|
|
294
296
|
- 用户确认端口后,先执行一次 `boss-recruit-mcp set-port --port <port>`,让后续 `doctor / launch-chrome / calibrate / run` 自动复用同一端口。
|
package/src/adapters.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import CDP from "chrome-remote-interface";
|
|
7
7
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
@@ -156,6 +156,117 @@ function runProcess({ command, args, cwd, timeoutMs }) {
|
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function runProcessSync({ command, args, cwd }) {
|
|
160
|
+
try {
|
|
161
|
+
const result = spawnSync(command, args, {
|
|
162
|
+
cwd,
|
|
163
|
+
windowsHide: true,
|
|
164
|
+
shell: false,
|
|
165
|
+
env: process.env,
|
|
166
|
+
encoding: "utf8"
|
|
167
|
+
});
|
|
168
|
+
const stdout = String(result.stdout || "").trim();
|
|
169
|
+
const stderr = String(result.stderr || "").trim();
|
|
170
|
+
return {
|
|
171
|
+
ok: result.status === 0,
|
|
172
|
+
status: result.status,
|
|
173
|
+
stdout,
|
|
174
|
+
stderr,
|
|
175
|
+
output: [stdout, stderr].filter(Boolean).join("\n").trim(),
|
|
176
|
+
error_code: result.error?.code || null,
|
|
177
|
+
error_message: result.error?.message || null
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
status: -1,
|
|
183
|
+
stdout: "",
|
|
184
|
+
stderr: "",
|
|
185
|
+
output: "",
|
|
186
|
+
error_code: error.code || "SPAWN_FAILED",
|
|
187
|
+
error_message: error.message || String(error)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseMajorVersion(raw) {
|
|
193
|
+
const match = String(raw || "").match(/v?(\d+)(?:\.\d+){0,2}/);
|
|
194
|
+
if (!match) return null;
|
|
195
|
+
const major = Number.parseInt(match[1], 10);
|
|
196
|
+
return Number.isFinite(major) ? major : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildNodeCommandCheck() {
|
|
200
|
+
const probe = runProcessSync({
|
|
201
|
+
command: "node",
|
|
202
|
+
args: ["--version"]
|
|
203
|
+
});
|
|
204
|
+
const major = parseMajorVersion(probe.output);
|
|
205
|
+
const versionOk = Number.isInteger(major) && major >= 18;
|
|
206
|
+
return {
|
|
207
|
+
key: "node_cli",
|
|
208
|
+
ok: probe.ok && versionOk,
|
|
209
|
+
path: "node --version",
|
|
210
|
+
message: probe.ok
|
|
211
|
+
? (versionOk
|
|
212
|
+
? `Node 命令可用 (${probe.output || "unknown version"})`
|
|
213
|
+
: `Node 版本过低 (${probe.output || "unknown version"}),要求 >= 18`)
|
|
214
|
+
: `未找到 node 命令,请先安装 Node.js >= 18。${probe.error_message ? ` (${probe.error_message})` : ""}`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildNodePackageCheck({ key, moduleName, cwd, missingMessage }) {
|
|
219
|
+
if (!cwd || !pathExists(cwd)) {
|
|
220
|
+
return {
|
|
221
|
+
key,
|
|
222
|
+
ok: false,
|
|
223
|
+
path: moduleName,
|
|
224
|
+
module: moduleName,
|
|
225
|
+
install_cwd: null,
|
|
226
|
+
message: missingMessage
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const probe = runProcessSync({
|
|
230
|
+
command: "node",
|
|
231
|
+
args: ["-e", `require.resolve(${JSON.stringify(moduleName)});`],
|
|
232
|
+
cwd
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
key,
|
|
236
|
+
ok: probe.ok,
|
|
237
|
+
path: moduleName,
|
|
238
|
+
module: moduleName,
|
|
239
|
+
install_cwd: cwd,
|
|
240
|
+
message: probe.ok
|
|
241
|
+
? `${moduleName} npm 依赖可用`
|
|
242
|
+
: `缺少 npm 依赖 ${moduleName},请在 boss-recruit-mcp 目录执行 npm install。`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildRuntimeDependencyChecks({ searchDir, screenDir }) {
|
|
247
|
+
return [
|
|
248
|
+
buildNodeCommandCheck(),
|
|
249
|
+
buildNodePackageCheck({
|
|
250
|
+
key: "npm_dep_chrome_remote_interface_search",
|
|
251
|
+
moduleName: "chrome-remote-interface",
|
|
252
|
+
cwd: searchDir,
|
|
253
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-search-cli 目录不存在。"
|
|
254
|
+
}),
|
|
255
|
+
buildNodePackageCheck({
|
|
256
|
+
key: "npm_dep_chrome_remote_interface_screen",
|
|
257
|
+
moduleName: "chrome-remote-interface",
|
|
258
|
+
cwd: screenDir,
|
|
259
|
+
missingMessage: "无法校验 chrome-remote-interface:boss-screen-cli 目录不存在。"
|
|
260
|
+
}),
|
|
261
|
+
buildNodePackageCheck({
|
|
262
|
+
key: "npm_dep_ws",
|
|
263
|
+
moduleName: "ws",
|
|
264
|
+
cwd: screenDir,
|
|
265
|
+
missingMessage: "无法校验 ws:boss-screen-cli 目录不存在。"
|
|
266
|
+
})
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
159
270
|
function parseSearchCount(output) {
|
|
160
271
|
const m = output.match(/找到\s*(\d+)\s*个候选人/);
|
|
161
272
|
if (!m) return null;
|
|
@@ -351,13 +462,18 @@ export function runPipelinePreflight(workspaceRoot) {
|
|
|
351
462
|
message: "favorite-calibration.json 不存在(可选,仅在旧页面回退点击时需要)"
|
|
352
463
|
}
|
|
353
464
|
];
|
|
465
|
+
checks.push(...buildRuntimeDependencyChecks({ searchDir, screenDir }));
|
|
354
466
|
|
|
355
467
|
const requiredCheckKeys = new Set([
|
|
356
468
|
"search_cli_dir",
|
|
357
469
|
"search_cli_entry",
|
|
358
470
|
"screen_cli_dir",
|
|
359
471
|
"screen_cli_entry",
|
|
360
|
-
"screen_config"
|
|
472
|
+
"screen_config",
|
|
473
|
+
"node_cli",
|
|
474
|
+
"npm_dep_chrome_remote_interface_search",
|
|
475
|
+
"npm_dep_chrome_remote_interface_screen",
|
|
476
|
+
"npm_dep_ws"
|
|
361
477
|
]);
|
|
362
478
|
|
|
363
479
|
return {
|
package/src/cli.js
CHANGED
|
@@ -28,14 +28,16 @@ const SUPPORTED_MCP_CLIENTS = ["generic", "cursor", "trae", "claudecode", "openc
|
|
|
28
28
|
const DEFAULT_MCP_SERVER_NAME = "boss-recruit";
|
|
29
29
|
const DEFAULT_MCP_COMMAND = "npx";
|
|
30
30
|
const DEFAULT_MCP_ARGS = ["-y", "@reconcrap/boss-recruit-mcp@latest", "start"];
|
|
31
|
-
const AUTO_SYNC_SKIP_COMMANDS = new Set([
|
|
32
|
-
"install",
|
|
33
|
-
"install-skill",
|
|
34
|
-
"where",
|
|
35
|
-
"help",
|
|
36
|
-
"--help",
|
|
37
|
-
"-h"
|
|
38
|
-
]);
|
|
31
|
+
const AUTO_SYNC_SKIP_COMMANDS = new Set([
|
|
32
|
+
"install",
|
|
33
|
+
"install-skill",
|
|
34
|
+
"where",
|
|
35
|
+
"help",
|
|
36
|
+
"--help",
|
|
37
|
+
"-h"
|
|
38
|
+
]);
|
|
39
|
+
const EXTERNAL_MCP_TARGETS_ENV = "BOSS_RECRUIT_MCP_CONFIG_TARGETS";
|
|
40
|
+
const EXTERNAL_SKILL_DIRS_ENV = "BOSS_RECRUIT_EXTERNAL_SKILL_DIRS";
|
|
39
41
|
|
|
40
42
|
function getPackageVersion() {
|
|
41
43
|
try {
|
|
@@ -58,13 +60,46 @@ function getCodexHome() {
|
|
|
58
60
|
: path.join(os.homedir(), ".codex");
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
function ensureDir(targetPath) {
|
|
62
|
-
fs.mkdirSync(targetPath, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
function ensureDir(targetPath) {
|
|
64
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pathExists(targetPath) {
|
|
68
|
+
try {
|
|
69
|
+
return fs.existsSync(targetPath);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readJsonObjectFileSafe(filePath) {
|
|
76
|
+
if (!pathExists(filePath)) return {};
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
79
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Fallback below.
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function dedupePaths(items) {
|
|
89
|
+
const result = [];
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (const item of items || []) {
|
|
92
|
+
const resolved = path.resolve(String(item || ""));
|
|
93
|
+
if (!resolved || seen.has(resolved)) continue;
|
|
94
|
+
seen.add(resolved);
|
|
95
|
+
result.push(resolved);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getDesktopDir() {
|
|
101
|
+
return path.join(os.homedir(), "Desktop");
|
|
102
|
+
}
|
|
68
103
|
|
|
69
104
|
function getUserConfigPath() {
|
|
70
105
|
return path.join(getCodexHome(), "boss-recruit-mcp", "screening-config.json");
|
|
@@ -146,12 +181,13 @@ function parseStringArrayOption(value, label) {
|
|
|
146
181
|
return parsed;
|
|
147
182
|
}
|
|
148
183
|
|
|
149
|
-
function normalizeMcpClientName(value) {
|
|
150
|
-
const raw = String(value || "").trim().toLowerCase();
|
|
151
|
-
if (!raw) return "";
|
|
152
|
-
if (raw === "claude-code") return "claudecode";
|
|
153
|
-
return
|
|
154
|
-
|
|
184
|
+
function normalizeMcpClientName(value) {
|
|
185
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
186
|
+
if (!raw) return "";
|
|
187
|
+
if (raw === "claude-code") return "claudecode";
|
|
188
|
+
if (raw === "trae-cn") return "trae";
|
|
189
|
+
return raw;
|
|
190
|
+
}
|
|
155
191
|
|
|
156
192
|
function parseMcpClientTargets(rawValue) {
|
|
157
193
|
if (!rawValue) return SUPPORTED_MCP_CLIENTS.slice();
|
|
@@ -214,7 +250,7 @@ function buildMcpConfigFileContent(options = {}) {
|
|
|
214
250
|
};
|
|
215
251
|
}
|
|
216
252
|
|
|
217
|
-
function writeMcpConfigFiles(options = {}) {
|
|
253
|
+
function writeMcpConfigFiles(options = {}) {
|
|
218
254
|
const clients = parseMcpClientTargets(options.client);
|
|
219
255
|
const outputDir = getAgentConfigOutputDir(options);
|
|
220
256
|
ensureDir(outputDir);
|
|
@@ -227,8 +263,142 @@ function writeMcpConfigFiles(options = {}) {
|
|
|
227
263
|
files.push({ client, file: filePath });
|
|
228
264
|
}
|
|
229
265
|
|
|
230
|
-
return { outputDir, files };
|
|
231
|
-
}
|
|
266
|
+
return { outputDir, files };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parsePathListFromEnv(raw) {
|
|
270
|
+
if (!raw) return [];
|
|
271
|
+
const text = String(raw).trim();
|
|
272
|
+
if (!text) return [];
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(text);
|
|
275
|
+
if (Array.isArray(parsed)) {
|
|
276
|
+
return dedupePaths(parsed.filter(Boolean));
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Fallback to delimiter split.
|
|
280
|
+
}
|
|
281
|
+
return dedupePaths(
|
|
282
|
+
text
|
|
283
|
+
.split(path.delimiter)
|
|
284
|
+
.map((item) => item.trim())
|
|
285
|
+
.filter(Boolean)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getKnownExternalMcpConfigPaths() {
|
|
290
|
+
const home = os.homedir();
|
|
291
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
292
|
+
return dedupePaths([
|
|
293
|
+
path.join(appData, "Cursor", "User", "mcp.json"),
|
|
294
|
+
path.join(appData, "Trae", "User", "mcp.json"),
|
|
295
|
+
path.join(appData, "Trae CN", "User", "mcp.json"),
|
|
296
|
+
path.join(home, ".trae", "mcp.json"),
|
|
297
|
+
path.join(home, ".trae-cn", "mcp.json"),
|
|
298
|
+
path.join(home, ".claude", "mcp.json"),
|
|
299
|
+
path.join(home, ".openclaw", "mcp.json")
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resolveExternalMcpConfigTargets() {
|
|
304
|
+
const fromEnv = parsePathListFromEnv(process.env[EXTERNAL_MCP_TARGETS_ENV]);
|
|
305
|
+
const known = getKnownExternalMcpConfigPaths().filter((filePath) => {
|
|
306
|
+
if (pathExists(filePath)) return true;
|
|
307
|
+
return pathExists(path.dirname(filePath));
|
|
308
|
+
});
|
|
309
|
+
return dedupePaths([...fromEnv, ...known]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
313
|
+
const nextConfig = buildMcpConfigFileContent(options);
|
|
314
|
+
const serverName = Object.keys(nextConfig.mcpServers || {})[0] || DEFAULT_MCP_SERVER_NAME;
|
|
315
|
+
const launchConfig = nextConfig.mcpServers?.[serverName] || buildMcpLaunchConfig(options);
|
|
316
|
+
const current = readJsonObjectFileSafe(filePath);
|
|
317
|
+
const existingServers =
|
|
318
|
+
current?.mcpServers && typeof current.mcpServers === "object" && !Array.isArray(current.mcpServers)
|
|
319
|
+
? current.mcpServers
|
|
320
|
+
: {};
|
|
321
|
+
const existingEntry = existingServers[serverName];
|
|
322
|
+
const merged = {
|
|
323
|
+
...current,
|
|
324
|
+
mcpServers: {
|
|
325
|
+
...existingServers,
|
|
326
|
+
[serverName]: launchConfig
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
ensureDir(path.dirname(filePath));
|
|
331
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
|
|
332
|
+
const updated = JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
|
|
333
|
+
return {
|
|
334
|
+
file: filePath,
|
|
335
|
+
server: serverName,
|
|
336
|
+
updated
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function installExternalMcpConfigs(options = {}) {
|
|
341
|
+
const targets = resolveExternalMcpConfigTargets();
|
|
342
|
+
const applied = [];
|
|
343
|
+
const skipped = [];
|
|
344
|
+
for (const target of targets) {
|
|
345
|
+
try {
|
|
346
|
+
const existed = pathExists(target);
|
|
347
|
+
const merged = mergeMcpServerConfigFile(target, options);
|
|
348
|
+
applied.push({
|
|
349
|
+
file: target,
|
|
350
|
+
server: merged.server,
|
|
351
|
+
created: !existed,
|
|
352
|
+
updated: merged.updated
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
skipped.push({
|
|
356
|
+
file: target,
|
|
357
|
+
reason: error.message
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { targets, applied, skipped };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getKnownExternalSkillBaseDirs() {
|
|
365
|
+
const home = os.homedir();
|
|
366
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
367
|
+
return dedupePaths([
|
|
368
|
+
path.join(home, ".cursor", "skills"),
|
|
369
|
+
path.join(home, ".trae", "skills"),
|
|
370
|
+
path.join(home, ".trae-cn", "skills"),
|
|
371
|
+
path.join(home, ".claude", "skills"),
|
|
372
|
+
path.join(home, ".openclaw", "skills"),
|
|
373
|
+
path.join(appData, "Cursor", "User", "skills"),
|
|
374
|
+
path.join(appData, "Trae", "User", "skills"),
|
|
375
|
+
path.join(appData, "Trae CN", "User", "skills"),
|
|
376
|
+
path.join(appData, "OpenClaw", "User", "skills")
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function resolveExternalSkillBaseDirs() {
|
|
381
|
+
const fromEnv = parsePathListFromEnv(process.env[EXTERNAL_SKILL_DIRS_ENV]);
|
|
382
|
+
const known = getKnownExternalSkillBaseDirs().filter((dirPath) => pathExists(dirPath));
|
|
383
|
+
return dedupePaths([...fromEnv, ...known]);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function mirrorSkillToExternalDirs() {
|
|
387
|
+
const baseDirs = resolveExternalSkillBaseDirs();
|
|
388
|
+
const mirrored = [];
|
|
389
|
+
const skipped = [];
|
|
390
|
+
for (const baseDir of baseDirs) {
|
|
391
|
+
try {
|
|
392
|
+
const targetDir = path.join(baseDir, skillName);
|
|
393
|
+
ensureDir(path.dirname(targetDir));
|
|
394
|
+
fs.cpSync(skillSourceDir, targetDir, { recursive: true, force: true });
|
|
395
|
+
mirrored.push({ base_dir: baseDir, target_dir: targetDir });
|
|
396
|
+
} catch (error) {
|
|
397
|
+
skipped.push({ base_dir: baseDir, reason: error.message });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { baseDirs, mirrored, skipped };
|
|
401
|
+
}
|
|
232
402
|
|
|
233
403
|
function readTextFile(filePath, label) {
|
|
234
404
|
const resolved = path.resolve(String(filePath));
|
|
@@ -909,7 +1079,7 @@ async function launchChrome(options) {
|
|
|
909
1079
|
};
|
|
910
1080
|
}
|
|
911
1081
|
|
|
912
|
-
function printHelp() {
|
|
1082
|
+
function printHelp() {
|
|
913
1083
|
console.log("boss-recruit-mcp");
|
|
914
1084
|
console.log("");
|
|
915
1085
|
console.log("Usage:");
|
|
@@ -920,7 +1090,7 @@ function printHelp() {
|
|
|
920
1090
|
console.log(" boss-recruit-mcp install-skill Install only the Codex skill");
|
|
921
1091
|
console.log(" boss-recruit-mcp init-config Create ~/.codex/boss-recruit-mcp/screening-config.json if missing");
|
|
922
1092
|
console.log(" boss-recruit-mcp set-port Persist preferred Chrome debug port to active screening-config");
|
|
923
|
-
console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
|
|
1093
|
+
console.log(" boss-recruit-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
|
|
924
1094
|
console.log(" boss-recruit-mcp doctor Check config, calibration, and runtime prerequisites");
|
|
925
1095
|
console.log(" boss-recruit-mcp calibrate Auto-open Boss search page, then run favorite-button calibration");
|
|
926
1096
|
console.log(" boss-recruit-mcp launch-chrome Reuse existing Chrome debug instance when possible; otherwise launch one, open Boss search, and check login state");
|
|
@@ -972,22 +1142,41 @@ function printMcpConfig(options = {}) {
|
|
|
972
1142
|
console.log("2. Merge its mcpServers block into that client's MCP config.");
|
|
973
1143
|
}
|
|
974
1144
|
|
|
975
|
-
function installAll() {
|
|
976
|
-
const skillTarget = installSkill();
|
|
977
|
-
const configResult = ensureUserConfig();
|
|
978
|
-
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
console.log(`Config
|
|
1145
|
+
function installAll() {
|
|
1146
|
+
const skillTarget = installSkill();
|
|
1147
|
+
const configResult = ensureUserConfig();
|
|
1148
|
+
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
1149
|
+
const externalMcpResult = installExternalMcpConfigs({});
|
|
1150
|
+
const externalSkillResult = mirrorSkillToExternalDirs();
|
|
1151
|
+
console.log(`Skill installed to: ${skillTarget}`);
|
|
1152
|
+
if (configResult.created) {
|
|
1153
|
+
console.log(`Config template created at: ${configResult.path}`);
|
|
1154
|
+
} else {
|
|
1155
|
+
console.log(`Config already exists at: ${configResult.path}`);
|
|
984
1156
|
}
|
|
985
1157
|
console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
|
|
986
|
-
for (const item of mcpTemplateResult.files) {
|
|
987
|
-
console.log(`- ${item.client}: ${item.file}`);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
|
|
1158
|
+
for (const item of mcpTemplateResult.files) {
|
|
1159
|
+
console.log(`- ${item.client}: ${item.file}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (externalMcpResult.targets.length > 0) {
|
|
1162
|
+
console.log(`Auto-configured external MCP files: ${externalMcpResult.applied.length}`);
|
|
1163
|
+
for (const item of externalMcpResult.applied) {
|
|
1164
|
+
const action = item.created ? "created" : item.updated ? "updated" : "unchanged";
|
|
1165
|
+
console.log(`- ${item.file} (${action})`);
|
|
1166
|
+
}
|
|
1167
|
+
} else {
|
|
1168
|
+
console.log("No external MCP config target detected. Set BOSS_RECRUIT_MCP_CONFIG_TARGETS to auto-configure custom agents.");
|
|
1169
|
+
}
|
|
1170
|
+
if (externalSkillResult.baseDirs.length > 0) {
|
|
1171
|
+
console.log(`Mirrored skill to external dirs: ${externalSkillResult.mirrored.length}`);
|
|
1172
|
+
for (const item of externalSkillResult.mirrored) {
|
|
1173
|
+
console.log(`- ${item.target_dir}`);
|
|
1174
|
+
}
|
|
1175
|
+
} else {
|
|
1176
|
+
console.log("No external skill dir detected. Set BOSS_RECRUIT_EXTERNAL_SKILL_DIRS to mirror skill for non-Codex agents.");
|
|
1177
|
+
}
|
|
1178
|
+
console.log("");
|
|
1179
|
+
console.log("Next steps:");
|
|
991
1180
|
console.log("1. Fill in baseUrl/apiKey/model in the config file above.");
|
|
992
1181
|
console.log("2. Choose a client template from the exported MCP config files and merge it into your AI client config.");
|
|
993
1182
|
console.log("3. Choose a Chrome remote-debugging port (9222 is recommended, but you can reuse an existing port).");
|
package/src/pipeline.js
CHANGED
|
@@ -8,6 +8,126 @@ import {
|
|
|
8
8
|
runScreenCli
|
|
9
9
|
} from "./adapters.js";
|
|
10
10
|
|
|
11
|
+
function dedupe(values = []) {
|
|
12
|
+
return [...new Set(values.filter(Boolean))];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function failedCheckSet(checks = []) {
|
|
16
|
+
const failed = checks
|
|
17
|
+
.filter((item) => item && item.ok === false && typeof item.key === "string")
|
|
18
|
+
.map((item) => item.key);
|
|
19
|
+
return new Set(failed);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function collectNpmInstallDirs(checks = [], workspaceRoot) {
|
|
23
|
+
const npmCheckKeys = new Set([
|
|
24
|
+
"npm_dep_chrome_remote_interface_search",
|
|
25
|
+
"npm_dep_chrome_remote_interface_screen",
|
|
26
|
+
"npm_dep_ws"
|
|
27
|
+
]);
|
|
28
|
+
const dirs = checks
|
|
29
|
+
.filter((item) => item && item.ok === false && npmCheckKeys.has(item.key))
|
|
30
|
+
.map((item) => item.install_cwd)
|
|
31
|
+
.filter((value) => typeof value === "string" && value.trim());
|
|
32
|
+
if (dirs.length > 0) return dedupe(dirs);
|
|
33
|
+
return workspaceRoot ? [workspaceRoot] : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildNpmInstallCommands(checks = [], workspaceRoot) {
|
|
37
|
+
const dirs = collectNpmInstallDirs(checks, workspaceRoot);
|
|
38
|
+
const commands = [];
|
|
39
|
+
for (const dir of dirs) {
|
|
40
|
+
const escaped = String(dir).replace(/'/g, "''");
|
|
41
|
+
commands.push(`Set-Location '${escaped}'`);
|
|
42
|
+
commands.push("npm install");
|
|
43
|
+
}
|
|
44
|
+
return commands;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatCommandBlock(commands = []) {
|
|
48
|
+
return commands.map((command) => `- ${command}`).join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildPreflightRecovery(checks = [], workspaceRoot) {
|
|
52
|
+
const failed = failedCheckSet(checks);
|
|
53
|
+
if (failed.size === 0) return null;
|
|
54
|
+
|
|
55
|
+
const needNode = failed.has("node_cli");
|
|
56
|
+
const needNpm = (
|
|
57
|
+
failed.has("npm_dep_chrome_remote_interface_search")
|
|
58
|
+
|| failed.has("npm_dep_chrome_remote_interface_screen")
|
|
59
|
+
|| failed.has("npm_dep_ws")
|
|
60
|
+
);
|
|
61
|
+
const needPython = failed.has("python_cli");
|
|
62
|
+
const needPillow = failed.has("python_pillow");
|
|
63
|
+
|
|
64
|
+
const ordered_steps = [];
|
|
65
|
+
if (needNode) {
|
|
66
|
+
ordered_steps.push({
|
|
67
|
+
id: "install_nodejs",
|
|
68
|
+
title: "安装 Node.js >= 18",
|
|
69
|
+
blocked_by: [],
|
|
70
|
+
commands: [
|
|
71
|
+
"winget install OpenJS.NodeJS.LTS",
|
|
72
|
+
"node --version"
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (needNpm) {
|
|
77
|
+
ordered_steps.push({
|
|
78
|
+
id: "install_npm_dependencies",
|
|
79
|
+
title: "安装 npm 依赖(chrome-remote-interface / ws)",
|
|
80
|
+
blocked_by: needNode ? ["install_nodejs"] : [],
|
|
81
|
+
commands: buildNpmInstallCommands(checks, workspaceRoot)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (needPython) {
|
|
85
|
+
ordered_steps.push({
|
|
86
|
+
id: "install_python",
|
|
87
|
+
title: "安装 Python(确保 python 命令可用)",
|
|
88
|
+
blocked_by: [],
|
|
89
|
+
commands: [
|
|
90
|
+
"winget install Python.Python.3.12",
|
|
91
|
+
"python --version"
|
|
92
|
+
]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (needPillow) {
|
|
96
|
+
ordered_steps.push({
|
|
97
|
+
id: "install_pillow",
|
|
98
|
+
title: "安装 Pillow",
|
|
99
|
+
blocked_by: needPython ? ["install_python"] : [],
|
|
100
|
+
commands: [
|
|
101
|
+
"python -m pip install --upgrade pip",
|
|
102
|
+
"python -m pip install pillow"
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const promptLines = [
|
|
108
|
+
"你是环境修复 agent。请先读取 diagnostics.checks,再严格按下面顺序执行,不要并行跳步:",
|
|
109
|
+
"1) node_cli 失败 -> 先安装 Node.js,未成功前禁止执行 npm install。",
|
|
110
|
+
"2) npm_dep_* 失败 -> 再安装 npm 依赖(chrome-remote-interface / ws)。",
|
|
111
|
+
"3) python_cli 失败 -> 安装 Python 并确保 python 命令可用。",
|
|
112
|
+
"4) python_pillow 失败 -> 最后安装 Pillow。",
|
|
113
|
+
"每一步完成后都重新运行 doctor,直到所有检查通过后再重试 run_recruit_pipeline。"
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
if (needNpm) {
|
|
117
|
+
const npmCommands = buildNpmInstallCommands(checks, workspaceRoot);
|
|
118
|
+
if (npmCommands.length > 0) {
|
|
119
|
+
promptLines.push("建议执行的 npm 命令:");
|
|
120
|
+
promptLines.push(formatCommandBlock(npmCommands));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
failed_check_keys: [...failed],
|
|
126
|
+
ordered_steps,
|
|
127
|
+
agent_prompt: promptLines.join("\n")
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
11
131
|
function buildRequiredConfirmations(parsedResult) {
|
|
12
132
|
const confirmations = [];
|
|
13
133
|
|
|
@@ -280,6 +400,7 @@ export async function runRecruitPipeline({
|
|
|
280
400
|
|
|
281
401
|
const preflight = runPreflight(workspaceRoot);
|
|
282
402
|
if (!preflight.ok) {
|
|
403
|
+
const recovery = buildPreflightRecovery(preflight.checks, workspaceRoot);
|
|
283
404
|
return buildFailedResponse(
|
|
284
405
|
"PIPELINE_PREFLIGHT_FAILED",
|
|
285
406
|
"招聘流水线运行前检查失败,请先修复缺失的本地依赖或配置文件。",
|
|
@@ -289,7 +410,8 @@ export async function runRecruitPipeline({
|
|
|
289
410
|
diagnostics: {
|
|
290
411
|
checks: preflight.checks,
|
|
291
412
|
debug_port: preflight.debug_port,
|
|
292
|
-
calibration_path: preflight.calibration_path
|
|
413
|
+
calibration_path: preflight.calibration_path,
|
|
414
|
+
recovery
|
|
293
415
|
}
|
|
294
416
|
}
|
|
295
417
|
);
|
package/src/test-pipeline.js
CHANGED
|
@@ -420,6 +420,45 @@ async function testNeedInputGateStillWorks() {
|
|
|
420
420
|
assert.equal(preflightCalled, false);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
async function testPreflightRecoveryPlanOrder() {
|
|
424
|
+
const tempDir = createTempDir("preflight-recovery");
|
|
425
|
+
const parsed = createParsed();
|
|
426
|
+
const deps = {
|
|
427
|
+
parseRecruitInstruction: () => parsed,
|
|
428
|
+
runPipelinePreflight: () => ({
|
|
429
|
+
ok: false,
|
|
430
|
+
debug_port: 9222,
|
|
431
|
+
calibration_path: null,
|
|
432
|
+
checks: [
|
|
433
|
+
{ key: "node_cli", ok: false },
|
|
434
|
+
{ key: "npm_dep_ws", ok: false, install_cwd: "C:/workspace/boss-recruit-mcp" }
|
|
435
|
+
]
|
|
436
|
+
}),
|
|
437
|
+
ensureBossSearchPageReady: async () => ({ ok: true, state: "SEARCH_READY", debug_port: 9222, page_state: {} }),
|
|
438
|
+
runSearchCli: async () => buildSearchOk(0),
|
|
439
|
+
runScreenCli: async () => buildScreenOk({ processedCount: 0, passedCount: 0, outputCsv: null })
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const result = await runRecruitPipeline(
|
|
443
|
+
{
|
|
444
|
+
workspaceRoot: tempDir,
|
|
445
|
+
instruction: "test",
|
|
446
|
+
confirmation: {},
|
|
447
|
+
overrides: {}
|
|
448
|
+
},
|
|
449
|
+
deps
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
assert.equal(result.status, "FAILED");
|
|
453
|
+
assert.equal(result.error.code, "PIPELINE_PREFLIGHT_FAILED");
|
|
454
|
+
assert.deepEqual(
|
|
455
|
+
result.diagnostics.recovery.ordered_steps.map((item) => item.id),
|
|
456
|
+
["install_nodejs", "install_npm_dependencies"]
|
|
457
|
+
);
|
|
458
|
+
assert.deepEqual(result.diagnostics.recovery.ordered_steps[1].blocked_by, ["install_nodejs"]);
|
|
459
|
+
assert.equal(result.diagnostics.recovery.agent_prompt.includes("不要并行跳步"), true);
|
|
460
|
+
}
|
|
461
|
+
|
|
423
462
|
async function main() {
|
|
424
463
|
await testSearchExhaustedCompletesAndMergesCsv();
|
|
425
464
|
await testSearchExhaustedByTipNodataEvenWhenCandidateCountPositive();
|
|
@@ -427,6 +466,7 @@ async function main() {
|
|
|
427
466
|
await testScreenNoProgressWithZeroProcessedExportsBeforeFail();
|
|
428
467
|
await testScreenNoProgressWithInvalidProcessedAndNoCsv();
|
|
429
468
|
await testNeedInputGateStillWorks();
|
|
469
|
+
await testPreflightRecoveryPlanOrder();
|
|
430
470
|
// eslint-disable-next-line no-console
|
|
431
471
|
console.log("pipeline tests passed");
|
|
432
472
|
}
|