@reconcrap/boss-recommend-mcp 0.1.14 → 1.0.0
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 +9 -2
- package/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +29 -12
- package/src/adapters.js +45 -4
- package/src/cli.js +245 -48
- package/src/index.js +43 -0
- package/src/parser.js +45 -7
- package/src/pipeline.js +50 -9
- package/src/test-parser.js +66 -0
- package/src/test-pipeline.js +32 -0
- package/vendor/boss-recommend-search-cli/src/cli.js +27 -12
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ MCP 工具名:`run_recommend_pipeline`
|
|
|
25
25
|
- 学历支持单选与多选语义:如“本科及以上”会展开为 `本科/硕士/博士`;如“大专、本科”只勾选这两项
|
|
26
26
|
- 执行前会逐项确认筛选参数:学校标签 / 学历 / 性别 / 是否过滤近14天已看
|
|
27
27
|
- 页面就绪(已登录且在 recommend 页)后,会先提取岗位栏全部岗位并要求用户确认本次岗位;确认后先点击岗位,再执行 search/screen
|
|
28
|
+
- 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
|
|
28
29
|
- npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
29
30
|
- `post_action` 必须在每次完整运行开始时确认一次
|
|
30
31
|
- `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
|
|
@@ -64,6 +65,7 @@ BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列
|
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
|
|
68
|
+
npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "推荐页筛选985男生,近14天没有,有大模型平台经验,符合标准的收藏"
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
## 配置
|
|
@@ -77,9 +79,9 @@ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,
|
|
|
77
79
|
配置路径优先级:
|
|
78
80
|
|
|
79
81
|
1. `BOSS_RECOMMEND_SCREEN_CONFIG`
|
|
80
|
-
2. `<workspace>/config/screening-config.json
|
|
82
|
+
2. `<workspace>/config/screening-config.json`(优先;在受限权限环境推荐使用)
|
|
81
83
|
3. `<workspace>/boss-recommend-mcp/config/screening-config.json`
|
|
82
|
-
4. `~/.boss-recommend-mcp/screening-config.json
|
|
84
|
+
4. `~/.boss-recommend-mcp/screening-config.json`(当 workspace 不可写或无 workspace 时回退)
|
|
83
85
|
5. 兼容旧路径:`$CODEX_HOME/boss-recommend-mcp/screening-config.json`
|
|
84
86
|
|
|
85
87
|
注意:
|
|
@@ -127,14 +129,19 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
127
129
|
"confirmation": {
|
|
128
130
|
"filters_confirmed": true,
|
|
129
131
|
"school_tag_confirmed": true,
|
|
132
|
+
"school_tag_value": ["985", "211"],
|
|
130
133
|
"degree_confirmed": true,
|
|
134
|
+
"degree_value": ["本科", "硕士", "博士"],
|
|
131
135
|
"gender_confirmed": true,
|
|
136
|
+
"gender_value": "女",
|
|
132
137
|
"recent_not_view_confirmed": true,
|
|
138
|
+
"recent_not_view_value": "近14天没有",
|
|
133
139
|
"criteria_confirmed": true,
|
|
134
140
|
"target_count_confirmed": true,
|
|
135
141
|
"target_count_value": 20,
|
|
136
142
|
"post_action_confirmed": true,
|
|
137
143
|
"post_action_value": "greet",
|
|
144
|
+
"final_confirmed": true,
|
|
138
145
|
"job_confirmed": true,
|
|
139
146
|
"job_value": "算法工程师(视频/图像模型方向) _ 杭州",
|
|
140
147
|
"max_greet_count_confirmed": true,
|
package/package.json
CHANGED
|
@@ -51,7 +51,11 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
51
51
|
- 严禁在未询问用户的情况下自动把 `max_greet_count` 设为 `target_count` 或其他默认值
|
|
52
52
|
- 岗位(`job`)是否确定
|
|
53
53
|
- 必须先列出 recommend 页岗位栏里识别到的全部岗位,让用户明确选择
|
|
54
|
+
- 即使前序步骤已提取到 `job` 参数,执行前也必须再次展示岗位列表并让用户二次确认
|
|
54
55
|
- 用户确认后必须先点击该岗位,再开始 search 和 screen
|
|
56
|
+
- 正式开始 search/screen 前,必须做最后一轮“全参数总确认”
|
|
57
|
+
- 需要向用户复述并确认:岗位、school_tag、degree、gender、recent_not_view、criteria、target_count、post_action、max_greet_count
|
|
58
|
+
- 只有用户明确最终确认后才允许执行
|
|
55
59
|
|
|
56
60
|
`post_action` 的确认是**单次运行级别**的:
|
|
57
61
|
|
|
@@ -64,17 +68,22 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
64
68
|
- Tool name: `run_recommend_pipeline`
|
|
65
69
|
- Input:
|
|
66
70
|
- `instruction` (required)
|
|
67
|
-
- `confirmation`
|
|
68
|
-
- `filters_confirmed`
|
|
69
|
-
- `school_tag_confirmed`
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
71
|
+
- `confirmation`
|
|
72
|
+
- `filters_confirmed`
|
|
73
|
+
- `school_tag_confirmed`
|
|
74
|
+
- `school_tag_value`(建议回传最终确认值,避免二轮调用丢失)
|
|
75
|
+
- `degree_confirmed`
|
|
76
|
+
- `degree_value`
|
|
77
|
+
- `gender_confirmed`
|
|
78
|
+
- `gender_value`
|
|
79
|
+
- `recent_not_view_confirmed`
|
|
80
|
+
- `recent_not_view_value`
|
|
81
|
+
- `criteria_confirmed`
|
|
74
82
|
- `target_count_confirmed`
|
|
75
83
|
- `target_count_value` (integer, optional)
|
|
76
84
|
- `post_action_confirmed`
|
|
77
85
|
- `post_action_value` (`favorite|greet`)
|
|
86
|
+
- `final_confirmed`
|
|
78
87
|
- `job_confirmed`
|
|
79
88
|
- `job_value` (string)
|
|
80
89
|
- `max_greet_count_confirmed`
|
|
@@ -98,11 +107,17 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
98
107
|
- recommend-screen-cli 负责滚动推荐列表、打开详情、提取完整简历图、调用多模态模型判断,并按单次确认的 `post_action` 执行收藏或打招呼。
|
|
99
108
|
- 详情页处理完成后必须关闭详情页并确认已关闭。
|
|
100
109
|
|
|
101
|
-
## Fallback
|
|
102
|
-
|
|
103
|
-
如果 MCP 不可用,改用:
|
|
104
|
-
|
|
105
|
-
`boss-recommend-mcp run --instruction "..." [--confirmation-json '{...}'] [--overrides-json '{...}']`
|
|
110
|
+
## Fallback
|
|
111
|
+
|
|
112
|
+
如果 MCP 不可用,改用:
|
|
113
|
+
|
|
114
|
+
`npx -y @reconcrap/boss-recommend-mcp@latest run --instruction "..." [--confirmation-json '{...}'] [--overrides-json '{...}']`
|
|
115
|
+
|
|
116
|
+
禁止错误回退:
|
|
117
|
+
|
|
118
|
+
- 不能把 recommend 请求回退到 `boss-recruit-mcp` / `run_recruit_pipeline`
|
|
119
|
+
- 不能执行 `boss-recruit-mcp doctor` 作为 recommend 流程的环境检查
|
|
120
|
+
- 若检测到当前环境只有 recruit MCP,应先修复 recommend MCP 配置,再继续
|
|
106
121
|
|
|
107
122
|
CLI fallback 的状态机与 MCP 保持一致:
|
|
108
123
|
|
|
@@ -135,6 +150,8 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
135
150
|
当工具返回 `status=FAILED` 且 `error.code=PIPELINE_PREFLIGHT_FAILED` 时:
|
|
136
151
|
|
|
137
152
|
1. 若 `diagnostics.checks` 中 `screen_config` 失败,优先引导用户填写 `screening-config.json` 的 `baseUrl/apiKey/model`(必须让用户提供真实值,不可保留模板值)。
|
|
153
|
+
- 禁止 agent 自行代填或猜测示例值(如 `test-key` / `mock-key` / `https://example.com` / `gpt-4` 占位等)
|
|
154
|
+
- 必须逐项向用户确认 `baseUrl`、`apiKey`、`model` 后再写入
|
|
138
155
|
2. 优先查看 `diagnostics.auto_repair`,若有自动修复动作则先基于其结果继续执行或给出最小化补救提示。
|
|
139
156
|
3. 若自动修复后仍失败,再读取 `diagnostics.recovery.agent_prompt`,直接把这段提示词交给 AI agent 执行环境修复。
|
|
140
157
|
4. 若 `diagnostics.recovery.agent_prompt` 不存在,使用下面的兜底提示词(严格顺序,不可跳步):
|
package/src/adapters.js
CHANGED
|
@@ -126,6 +126,34 @@ function resolveScreenConfigCandidates(workspaceRoot) {
|
|
|
126
126
|
].filter(Boolean);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
function canWriteDirectory(targetDir) {
|
|
130
|
+
try {
|
|
131
|
+
ensureDir(targetDir);
|
|
132
|
+
fs.accessSync(targetDir, fs.constants.W_OK);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveWritableScreenConfigPath(workspaceRoot) {
|
|
140
|
+
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
141
|
+
const workspacePreferred = candidateMap.workspace_paths?.[0] || null;
|
|
142
|
+
if (candidateMap.env_path) {
|
|
143
|
+
return candidateMap.env_path;
|
|
144
|
+
}
|
|
145
|
+
if (workspacePreferred && canWriteDirectory(path.dirname(workspacePreferred))) {
|
|
146
|
+
return workspacePreferred;
|
|
147
|
+
}
|
|
148
|
+
if (candidateMap.user_path && canWriteDirectory(path.dirname(candidateMap.user_path))) {
|
|
149
|
+
return candidateMap.user_path;
|
|
150
|
+
}
|
|
151
|
+
if (workspacePreferred) {
|
|
152
|
+
return workspacePreferred;
|
|
153
|
+
}
|
|
154
|
+
return candidateMap.user_path || candidateMap.legacy_path;
|
|
155
|
+
}
|
|
156
|
+
|
|
129
157
|
function resolveScreenConfigPath(workspaceRoot) {
|
|
130
158
|
const candidateMap = buildScreenConfigCandidateMap(workspaceRoot);
|
|
131
159
|
if (candidateMap.env_path) {
|
|
@@ -135,9 +163,9 @@ function resolveScreenConfigPath(workspaceRoot) {
|
|
|
135
163
|
if (existingWorkspacePath) {
|
|
136
164
|
return existingWorkspacePath;
|
|
137
165
|
}
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
166
|
+
const writablePath = resolveWritableScreenConfigPath(workspaceRoot);
|
|
167
|
+
if (writablePath) {
|
|
168
|
+
return writablePath;
|
|
141
169
|
}
|
|
142
170
|
return candidateMap.legacy_path;
|
|
143
171
|
}
|
|
@@ -151,7 +179,7 @@ export function getScreenConfigResolution(workspaceRoot) {
|
|
|
151
179
|
candidate_paths,
|
|
152
180
|
workspace_root: path.resolve(String(workspaceRoot || process.cwd())),
|
|
153
181
|
workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
|
|
154
|
-
writable_path:
|
|
182
|
+
writable_path: resolveWritableScreenConfigPath(workspaceRoot),
|
|
155
183
|
legacy_path: candidateMap.legacy_path
|
|
156
184
|
};
|
|
157
185
|
}
|
|
@@ -860,6 +888,19 @@ export async function inspectBossRecommendPageState(port, options = {}) {
|
|
|
860
888
|
(tab) => typeof tab?.url === "string" && tab.url.includes("/web/chat/recommend")
|
|
861
889
|
);
|
|
862
890
|
if (exactTab) {
|
|
891
|
+
if (isBossLoginTab(exactTab)) {
|
|
892
|
+
return buildBossPageState({
|
|
893
|
+
ok: false,
|
|
894
|
+
state: "LOGIN_REQUIRED",
|
|
895
|
+
path: exactTab.url || bossLoginUrl,
|
|
896
|
+
current_url: exactTab.url || bossLoginUrl,
|
|
897
|
+
title: exactTab.title || null,
|
|
898
|
+
requires_login: true,
|
|
899
|
+
expected_url: expectedUrl,
|
|
900
|
+
login_url: bossLoginUrl,
|
|
901
|
+
message: "当前标签页虽在 recommend 路径,但检测到登录态页面特征,请先完成 Boss 登录。"
|
|
902
|
+
});
|
|
903
|
+
}
|
|
863
904
|
return buildBossPageState({
|
|
864
905
|
ok: true,
|
|
865
906
|
state: "RECOMMEND_READY",
|
package/src/cli.js
CHANGED
|
@@ -27,6 +27,8 @@ const supportedMcpClients = ["generic", "cursor", "trae", "claudecode", "opencla
|
|
|
27
27
|
const defaultMcpServerName = "boss-recommend";
|
|
28
28
|
const defaultMcpCommand = "npx";
|
|
29
29
|
const defaultMcpArgs = ["-y", "@reconcrap/boss-recommend-mcp@latest", "start"];
|
|
30
|
+
const recommendMcpPackageName = "@reconcrap/boss-recommend-mcp";
|
|
31
|
+
const recommendMcpBinaryName = "boss-recommend-mcp";
|
|
30
32
|
const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
|
|
31
33
|
const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
|
|
32
34
|
const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
|
|
@@ -83,7 +85,9 @@ function dedupePaths(items) {
|
|
|
83
85
|
const result = [];
|
|
84
86
|
const seen = new Set();
|
|
85
87
|
for (const item of items || []) {
|
|
86
|
-
const
|
|
88
|
+
const raw = String(item ?? "").trim();
|
|
89
|
+
if (!raw) continue;
|
|
90
|
+
const resolved = path.resolve(raw);
|
|
87
91
|
if (!resolved || seen.has(resolved)) continue;
|
|
88
92
|
seen.add(resolved);
|
|
89
93
|
result.push(resolved);
|
|
@@ -91,6 +95,32 @@ function dedupePaths(items) {
|
|
|
91
95
|
return result;
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
function dedupeLower(values = []) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const result = [];
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
const normalized = String(value || "").trim();
|
|
103
|
+
if (!normalized) continue;
|
|
104
|
+
const key = normalized.toLowerCase();
|
|
105
|
+
if (seen.has(key)) continue;
|
|
106
|
+
seen.add(key);
|
|
107
|
+
result.push(normalized);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function discoverAppDataDirsByPattern(baseDir, pattern) {
|
|
113
|
+
try {
|
|
114
|
+
if (!pathExists(baseDir)) return [];
|
|
115
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
116
|
+
return entries
|
|
117
|
+
.filter((entry) => entry.isDirectory() && pattern.test(entry.name))
|
|
118
|
+
.map((entry) => entry.name);
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
function getDesktopDir() {
|
|
95
125
|
return path.join(os.homedir(), "Desktop");
|
|
96
126
|
}
|
|
@@ -149,9 +179,29 @@ function parsePositivePort(raw) {
|
|
|
149
179
|
return Number.isFinite(port) && port > 0 ? port : null;
|
|
150
180
|
}
|
|
151
181
|
|
|
182
|
+
function isEphemeralWorkspaceRoot(rootPath) {
|
|
183
|
+
const normalized = path.resolve(String(rootPath || ""))
|
|
184
|
+
.replace(/\\/g, "/")
|
|
185
|
+
.toLowerCase();
|
|
186
|
+
return (
|
|
187
|
+
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
188
|
+
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
152
192
|
function getWorkspaceRoot(options) {
|
|
153
|
-
const
|
|
154
|
-
return path.resolve(
|
|
193
|
+
const fromOption = String(options["workspace-root"] || "").trim();
|
|
194
|
+
if (fromOption) return path.resolve(fromOption);
|
|
195
|
+
|
|
196
|
+
const fromEnv = String(process.env.BOSS_WORKSPACE_ROOT || "").trim();
|
|
197
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
198
|
+
|
|
199
|
+
const cwd = path.resolve(process.cwd());
|
|
200
|
+
const initCwd = String(process.env.INIT_CWD || "").trim();
|
|
201
|
+
if (isEphemeralWorkspaceRoot(cwd) && initCwd) {
|
|
202
|
+
return path.resolve(initCwd);
|
|
203
|
+
}
|
|
204
|
+
return cwd;
|
|
155
205
|
}
|
|
156
206
|
|
|
157
207
|
function readTextFile(filePath, label) {
|
|
@@ -306,10 +356,19 @@ function parseAgentTargets(rawValue) {
|
|
|
306
356
|
function getKnownExternalMcpConfigPathsByAgent() {
|
|
307
357
|
const home = os.homedir();
|
|
308
358
|
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
359
|
+
const traeDirNames = dedupeLower([
|
|
360
|
+
"Trae",
|
|
361
|
+
"Trae CN",
|
|
362
|
+
"TraeCN",
|
|
363
|
+
"trae-cn",
|
|
364
|
+
"trae_cn",
|
|
365
|
+
...discoverAppDataDirsByPattern(appData, /^trae(?:[\s\-_]?cn)?$/i)
|
|
366
|
+
]);
|
|
367
|
+
const traeConfigPaths = traeDirNames.map((dir) => path.join(appData, dir, "User", "mcp.json"));
|
|
309
368
|
return {
|
|
310
369
|
cursor: [path.join(appData, "Cursor", "User", "mcp.json"), path.join(home, ".cursor", "mcp.json")],
|
|
311
|
-
trae: [path.join(
|
|
312
|
-
"trae-cn": [path.join(
|
|
370
|
+
trae: [...traeConfigPaths, path.join(home, ".trae", "mcp.json"), path.join(home, ".trae-cn", "mcp.json")],
|
|
371
|
+
"trae-cn": [...traeConfigPaths, path.join(home, ".trae-cn", "mcp.json"), path.join(home, ".trae", "mcp.json")],
|
|
313
372
|
claude: [path.join(home, ".claude", "mcp.json")],
|
|
314
373
|
openclaw: [path.join(home, ".openclaw", "mcp.json")]
|
|
315
374
|
};
|
|
@@ -383,15 +442,76 @@ function installExternalMcpConfigs(options = {}) {
|
|
|
383
442
|
function getKnownExternalSkillBaseDirsByAgent() {
|
|
384
443
|
const home = os.homedir();
|
|
385
444
|
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
445
|
+
const traeDirNames = dedupeLower([
|
|
446
|
+
"Trae",
|
|
447
|
+
"Trae CN",
|
|
448
|
+
"TraeCN",
|
|
449
|
+
"trae-cn",
|
|
450
|
+
"trae_cn",
|
|
451
|
+
...discoverAppDataDirsByPattern(appData, /^trae(?:[\s\-_]?cn)?$/i)
|
|
452
|
+
]);
|
|
453
|
+
const traeSkillDirs = traeDirNames.map((dir) => path.join(appData, dir, "User", "skills"));
|
|
386
454
|
return {
|
|
387
455
|
cursor: [path.join(home, ".cursor", "skills"), path.join(appData, "Cursor", "User", "skills")],
|
|
388
|
-
trae: [path.join(home, ".trae", "skills"), path.join(
|
|
389
|
-
"trae-cn": [path.join(home, ".trae-cn", "skills"), path.join(
|
|
456
|
+
trae: [path.join(home, ".trae", "skills"), path.join(home, ".trae-cn", "skills"), ...traeSkillDirs],
|
|
457
|
+
"trae-cn": [path.join(home, ".trae-cn", "skills"), path.join(home, ".trae", "skills"), ...traeSkillDirs],
|
|
390
458
|
claude: [path.join(home, ".claude", "skills")],
|
|
391
459
|
openclaw: [path.join(home, ".openclaw", "skills"), path.join(appData, "OpenClaw", "User", "skills")]
|
|
392
460
|
};
|
|
393
461
|
}
|
|
394
462
|
|
|
463
|
+
function isRecommendMcpLaunchConfig(launchConfig) {
|
|
464
|
+
if (!launchConfig || typeof launchConfig !== "object") return false;
|
|
465
|
+
const command = String(launchConfig.command || "").toLowerCase();
|
|
466
|
+
const args = Array.isArray(launchConfig.args) ? launchConfig.args : [];
|
|
467
|
+
const joined = `${command} ${args.map((item) => String(item || "")).join(" ")}`.toLowerCase();
|
|
468
|
+
return (
|
|
469
|
+
joined.includes(recommendMcpPackageName.toLowerCase())
|
|
470
|
+
|| joined.includes(`${recommendMcpBinaryName} start`)
|
|
471
|
+
|| (command.endsWith(recommendMcpBinaryName) && args.includes("start"))
|
|
472
|
+
|| command === recommendMcpBinaryName
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function inspectMcpServerEntries(filePath) {
|
|
477
|
+
if (!pathExists(filePath)) {
|
|
478
|
+
return {
|
|
479
|
+
exists: false,
|
|
480
|
+
has_boss_recommend: false,
|
|
481
|
+
has_boss_recruit: false,
|
|
482
|
+
recommend_server_names: [],
|
|
483
|
+
recruit_server_names: []
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const parsed = readJsonObjectFileSafe(filePath);
|
|
487
|
+
const servers = parsed?.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers)
|
|
488
|
+
? parsed.mcpServers
|
|
489
|
+
: {};
|
|
490
|
+
const recommendNames = [];
|
|
491
|
+
const recruitNames = [];
|
|
492
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
493
|
+
const lowerName = String(name || "").toLowerCase();
|
|
494
|
+
if (isRecommendMcpLaunchConfig(config) || lowerName.includes("boss-recommend")) {
|
|
495
|
+
recommendNames.push(name);
|
|
496
|
+
}
|
|
497
|
+
const serialized = JSON.stringify(config || {}).toLowerCase();
|
|
498
|
+
if (
|
|
499
|
+
lowerName.includes("boss-recruit")
|
|
500
|
+
|| serialized.includes("@reconcrap/boss-recruit-mcp")
|
|
501
|
+
|| serialized.includes("boss-recruit-mcp")
|
|
502
|
+
) {
|
|
503
|
+
recruitNames.push(name);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
exists: true,
|
|
508
|
+
has_boss_recommend: recommendNames.length > 0,
|
|
509
|
+
has_boss_recruit: recruitNames.length > 0,
|
|
510
|
+
recommend_server_names: recommendNames,
|
|
511
|
+
recruit_server_names: recruitNames
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
395
515
|
function resolveExternalSkillBaseDirs(options = {}) {
|
|
396
516
|
const fromEnv = parsePathListFromEnv(process.env[externalSkillDirsEnv]);
|
|
397
517
|
const pathMap = getKnownExternalSkillBaseDirsByAgent();
|
|
@@ -449,17 +569,52 @@ function installSkill() {
|
|
|
449
569
|
return syncSkillAssets({ force: true }).targetDir;
|
|
450
570
|
}
|
|
451
571
|
|
|
452
|
-
function
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
template.outputDir = getDesktopDir();
|
|
458
|
-
template.debugPort = 9222;
|
|
459
|
-
fs.writeFileSync(targetPath, JSON.stringify(template, null, 2), "utf8");
|
|
460
|
-
return { path: targetPath, created: true };
|
|
572
|
+
function pathStartsWith(filePath, rootPath) {
|
|
573
|
+
const file = path.resolve(String(filePath || ""));
|
|
574
|
+
const root = path.resolve(String(rootPath || ""));
|
|
575
|
+
if (process.platform === "win32") {
|
|
576
|
+
return file.toLowerCase().startsWith(root.toLowerCase());
|
|
461
577
|
}
|
|
462
|
-
return
|
|
578
|
+
return file.startsWith(root);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function resolveCliConfigTarget(options = {}) {
|
|
582
|
+
const workspaceRoot = getWorkspaceRoot(options);
|
|
583
|
+
const resolution = getScreenConfigResolution(workspaceRoot);
|
|
584
|
+
const workspacePreferred = (resolution.candidate_paths || []).find((item) => pathStartsWith(item, workspaceRoot)) || null;
|
|
585
|
+
const configPath = resolution.writable_path || resolution.resolved_path || workspacePreferred || getUserConfigPath();
|
|
586
|
+
return {
|
|
587
|
+
workspaceRoot,
|
|
588
|
+
resolution,
|
|
589
|
+
configPath,
|
|
590
|
+
workspacePreferred
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function ensureUserConfig(options = {}) {
|
|
595
|
+
const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
|
|
596
|
+
const writeTargets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
|
|
597
|
+
let lastError = null;
|
|
598
|
+
for (const targetPath of writeTargets) {
|
|
599
|
+
try {
|
|
600
|
+
ensureDir(path.dirname(targetPath));
|
|
601
|
+
if (!fs.existsSync(targetPath)) {
|
|
602
|
+
const template = JSON.parse(fs.readFileSync(exampleConfigPath, "utf8"));
|
|
603
|
+
template.outputDir = getDesktopDir();
|
|
604
|
+
template.debugPort = 9222;
|
|
605
|
+
fs.writeFileSync(targetPath, JSON.stringify(template, null, 2), "utf8");
|
|
606
|
+
return { path: targetPath, created: true };
|
|
607
|
+
}
|
|
608
|
+
const stat = fs.statSync(targetPath);
|
|
609
|
+
if (stat.isFile()) {
|
|
610
|
+
return { path: targetPath, created: false };
|
|
611
|
+
}
|
|
612
|
+
lastError = new Error(`Config target is a directory and cannot be used as file: ${targetPath}`);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
lastError = error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
throw lastError || new Error("No writable target for screening-config.json");
|
|
463
618
|
}
|
|
464
619
|
|
|
465
620
|
function readJsonObjectFile(filePath) {
|
|
@@ -471,24 +626,57 @@ function readJsonObjectFile(filePath) {
|
|
|
471
626
|
return parsed;
|
|
472
627
|
}
|
|
473
628
|
|
|
474
|
-
function loadBestExistingUserConfig() {
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
629
|
+
function loadBestExistingUserConfig(options = {}) {
|
|
630
|
+
const { resolution, configPath, workspacePreferred } = resolveCliConfigTarget(options);
|
|
631
|
+
const candidates = dedupePaths([
|
|
632
|
+
...(resolution.candidate_paths || []),
|
|
633
|
+
configPath,
|
|
634
|
+
workspacePreferred,
|
|
635
|
+
getLegacyUserConfigPath()
|
|
636
|
+
]).filter(Boolean);
|
|
637
|
+
for (const candidate of candidates) {
|
|
638
|
+
if (fs.existsSync(candidate)) {
|
|
639
|
+
try {
|
|
640
|
+
const stat = fs.statSync(candidate);
|
|
641
|
+
if (!stat.isFile()) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
return { path: candidate, config: readJsonObjectFile(candidate) };
|
|
648
|
+
}
|
|
479
649
|
}
|
|
480
|
-
|
|
481
|
-
|
|
650
|
+
return { path: configPath, config: {} };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function writeConfigWithFallback(nextConfig, options = {}) {
|
|
654
|
+
const { configPath, workspacePreferred } = resolveCliConfigTarget(options);
|
|
655
|
+
const targets = dedupePaths([configPath, workspacePreferred]).filter(Boolean);
|
|
656
|
+
let lastError = null;
|
|
657
|
+
for (const target of targets) {
|
|
658
|
+
try {
|
|
659
|
+
ensureDir(path.dirname(target));
|
|
660
|
+
if (fs.existsSync(target)) {
|
|
661
|
+
const stat = fs.statSync(target);
|
|
662
|
+
if (!stat.isFile()) {
|
|
663
|
+
lastError = new Error(`Config target is a directory and cannot be used as file: ${target}`);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
fs.writeFileSync(target, JSON.stringify(nextConfig, null, 2), "utf8");
|
|
668
|
+
return target;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
lastError = error;
|
|
671
|
+
}
|
|
482
672
|
}
|
|
483
|
-
|
|
673
|
+
throw lastError || new Error("No writable target for screening-config.json");
|
|
484
674
|
}
|
|
485
675
|
|
|
486
|
-
function persistDebugPortSelection(port) {
|
|
487
|
-
const { config } = loadBestExistingUserConfig();
|
|
488
|
-
const configPath = getUserConfigPath();
|
|
676
|
+
function persistDebugPortSelection(port, options = {}) {
|
|
677
|
+
const { config } = loadBestExistingUserConfig(options);
|
|
489
678
|
config.debugPort = port;
|
|
490
|
-
|
|
491
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
679
|
+
const configPath = writeConfigWithFallback(config, options);
|
|
492
680
|
return { port, configPath };
|
|
493
681
|
}
|
|
494
682
|
|
|
@@ -498,7 +686,7 @@ function setDebugPort(options = {}) {
|
|
|
498
686
|
throw new Error("Missing required --port <number> for set-port.");
|
|
499
687
|
}
|
|
500
688
|
process.env.BOSS_RECOMMEND_CHROME_PORT = String(selected);
|
|
501
|
-
return persistDebugPortSelection(selected);
|
|
689
|
+
return persistDebugPortSelection(selected, options);
|
|
502
690
|
}
|
|
503
691
|
|
|
504
692
|
function setScreeningConfig(options = {}) {
|
|
@@ -509,8 +697,7 @@ function setScreeningConfig(options = {}) {
|
|
|
509
697
|
throw new Error("Missing required fields: --base-url, --api-key, --model");
|
|
510
698
|
}
|
|
511
699
|
|
|
512
|
-
const { config: existing } = loadBestExistingUserConfig();
|
|
513
|
-
const configPath = getUserConfigPath();
|
|
700
|
+
const { config: existing } = loadBestExistingUserConfig(options);
|
|
514
701
|
const nextConfig = {
|
|
515
702
|
...existing,
|
|
516
703
|
baseUrl,
|
|
@@ -530,8 +717,7 @@ function setScreeningConfig(options = {}) {
|
|
|
530
717
|
if (debugPort) {
|
|
531
718
|
nextConfig.debugPort = debugPort;
|
|
532
719
|
}
|
|
533
|
-
|
|
534
|
-
fs.writeFileSync(configPath, JSON.stringify(nextConfig, null, 2), "utf8");
|
|
720
|
+
const configPath = writeConfigWithFallback(nextConfig, options);
|
|
535
721
|
return { path: configPath, updated: true };
|
|
536
722
|
}
|
|
537
723
|
|
|
@@ -650,16 +836,21 @@ function inspectAgentIntegration(agentRaw) {
|
|
|
650
836
|
|
|
651
837
|
const mcpPathMap = getKnownExternalMcpConfigPathsByAgent();
|
|
652
838
|
const skillPathMap = getKnownExternalSkillBaseDirsByAgent();
|
|
653
|
-
const mcp_paths = dedupePaths(
|
|
654
|
-
|
|
839
|
+
const mcp_paths = dedupePaths([
|
|
840
|
+
...(mcpPathMap[agent] || []),
|
|
841
|
+
...parsePathListFromEnv(process.env[externalMcpTargetsEnv])
|
|
842
|
+
]);
|
|
843
|
+
const skill_bases = dedupePaths([
|
|
844
|
+
...(skillPathMap[agent] || []),
|
|
845
|
+
...parsePathListFromEnv(process.env[externalSkillDirsEnv])
|
|
846
|
+
]);
|
|
655
847
|
|
|
656
848
|
const mcp_checks = mcp_paths.map((mcpPath) => {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return { path: mcpPath, exists: true, has_boss_recommend: has };
|
|
849
|
+
const detail = inspectMcpServerEntries(mcpPath);
|
|
850
|
+
return {
|
|
851
|
+
path: mcpPath,
|
|
852
|
+
...detail
|
|
853
|
+
};
|
|
663
854
|
});
|
|
664
855
|
|
|
665
856
|
const hasRecommendIntent = (content) => /(recommend|推荐页|boss recommend|recommend page)/i.test(content);
|
|
@@ -736,6 +927,10 @@ async function printDoctor(options = {}) {
|
|
|
736
927
|
if (typeof options.agent === "string" && options.agent.trim()) {
|
|
737
928
|
agentIntegration = inspectAgentIntegration(options.agent.trim());
|
|
738
929
|
const agentMcpOk = agentIntegration.mcp_checks.some((item) => item.has_boss_recommend);
|
|
930
|
+
const agentRecruitOnly = (
|
|
931
|
+
!agentMcpOk
|
|
932
|
+
&& agentIntegration.mcp_checks.some((item) => item.has_boss_recruit)
|
|
933
|
+
);
|
|
739
934
|
const agentSkillOk = agentIntegration.skill_checks.some((item) => item.exists);
|
|
740
935
|
const agentRecruitRouteGuardOk = agentIntegration.skill_checks.every(
|
|
741
936
|
(item) => !item.recruit_skill_exists || item.recruit_route_guard
|
|
@@ -750,7 +945,9 @@ async function printDoctor(options = {}) {
|
|
|
750
945
|
path: agentIntegration.mcp_checks.map((item) => item.path).join(" | "),
|
|
751
946
|
message: agentMcpOk
|
|
752
947
|
? "目标 Agent MCP 配置已检测到 boss-recommend。"
|
|
753
|
-
:
|
|
948
|
+
: agentRecruitOnly
|
|
949
|
+
? "目标 Agent MCP 配置未检测到 boss-recommend,但检测到 boss-recruit(可能导致错误调用 recruit pipeline)。"
|
|
950
|
+
: "目标 Agent MCP 配置未检测到 boss-recommend。"
|
|
754
951
|
});
|
|
755
952
|
checks.push({
|
|
756
953
|
key: `agent_${agentIntegration.agent}_skill`,
|
|
@@ -821,8 +1018,8 @@ function printHelp() {
|
|
|
821
1018
|
console.log(" boss-recommend-mcp run Run the recommend pipeline once via CLI and print JSON");
|
|
822
1019
|
console.log(" boss-recommend-mcp install Install skill and MCP config templates (supports --agent trae-cn/cursor/...)");
|
|
823
1020
|
console.log(" boss-recommend-mcp install-skill Install only the Codex skill");
|
|
824
|
-
console.log(" boss-recommend-mcp init-config Create
|
|
825
|
-
console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model
|
|
1021
|
+
console.log(" boss-recommend-mcp init-config Create screening-config.json if missing (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
1022
|
+
console.log(" boss-recommend-mcp config set Write baseUrl/apiKey/model (prefer workspace config/, fallback ~/.boss-recommend-mcp)");
|
|
826
1023
|
console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
|
|
827
1024
|
console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
|
|
828
1025
|
console.log(" boss-recommend-mcp doctor Check config and runtime prerequisites (supports --agent trae-cn/cursor/...)");
|
|
@@ -890,7 +1087,7 @@ async function runPipelineOnce(options) {
|
|
|
890
1087
|
const explicitPort = parsePositivePort(options.port);
|
|
891
1088
|
if (explicitPort) {
|
|
892
1089
|
process.env.BOSS_RECOMMEND_CHROME_PORT = String(explicitPort);
|
|
893
|
-
persistDebugPortSelection(explicitPort);
|
|
1090
|
+
persistDebugPortSelection(explicitPort, options);
|
|
894
1091
|
}
|
|
895
1092
|
|
|
896
1093
|
const result = await runRecommendPipeline({
|
|
@@ -937,7 +1134,7 @@ switch (command) {
|
|
|
937
1134
|
console.log(`Skill installed to: ${installSkill()}`);
|
|
938
1135
|
break;
|
|
939
1136
|
case "init-config": {
|
|
940
|
-
const result = ensureUserConfig();
|
|
1137
|
+
const result = ensureUserConfig(options);
|
|
941
1138
|
console.log(result.created ? `Config template created at: ${result.path}` : `Config already exists at: ${result.path}`);
|
|
942
1139
|
break;
|
|
943
1140
|
}
|
package/src/index.js
CHANGED
|
@@ -46,9 +46,51 @@ function createToolSchema() {
|
|
|
46
46
|
properties: {
|
|
47
47
|
filters_confirmed: { type: "boolean" },
|
|
48
48
|
school_tag_confirmed: { type: "boolean" },
|
|
49
|
+
school_tag_value: {
|
|
50
|
+
oneOf: [
|
|
51
|
+
{
|
|
52
|
+
type: "string",
|
|
53
|
+
enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: "array",
|
|
57
|
+
items: {
|
|
58
|
+
type: "string",
|
|
59
|
+
enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
|
|
60
|
+
},
|
|
61
|
+
minItems: 1,
|
|
62
|
+
uniqueItems: true
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
49
66
|
degree_confirmed: { type: "boolean" },
|
|
67
|
+
degree_value: {
|
|
68
|
+
oneOf: [
|
|
69
|
+
{
|
|
70
|
+
type: "string",
|
|
71
|
+
enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "array",
|
|
75
|
+
items: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
|
|
78
|
+
},
|
|
79
|
+
minItems: 1,
|
|
80
|
+
uniqueItems: true
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
50
84
|
gender_confirmed: { type: "boolean" },
|
|
85
|
+
gender_value: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: ["不限", "男", "女"]
|
|
88
|
+
},
|
|
51
89
|
recent_not_view_confirmed: { type: "boolean" },
|
|
90
|
+
recent_not_view_value: {
|
|
91
|
+
type: "string",
|
|
92
|
+
enum: ["不限", "近14天没有"]
|
|
93
|
+
},
|
|
52
94
|
criteria_confirmed: { type: "boolean" },
|
|
53
95
|
target_count_confirmed: { type: "boolean" },
|
|
54
96
|
target_count_value: {
|
|
@@ -60,6 +102,7 @@ function createToolSchema() {
|
|
|
60
102
|
type: "string",
|
|
61
103
|
enum: ["favorite", "greet"]
|
|
62
104
|
},
|
|
105
|
+
final_confirmed: { type: "boolean" },
|
|
63
106
|
job_confirmed: { type: "boolean" },
|
|
64
107
|
job_value: { type: "string" },
|
|
65
108
|
max_greet_count_confirmed: { type: "boolean" },
|
package/src/parser.js
CHANGED
|
@@ -477,9 +477,13 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
477
477
|
const detectedDegrees = extractDegrees(text);
|
|
478
478
|
const schoolTagAudit = auditSchoolTagSelections(overrides?.school_tag);
|
|
479
479
|
const overrideSchoolTag = schoolTagAudit.valid.length > 0 ? schoolTagAudit.valid : null;
|
|
480
|
+
const confirmationSchoolTag = normalizeSchoolTagSelections(confirmation?.school_tag_value);
|
|
480
481
|
const overrideDegrees = normalizeDegreeSelections(overrides?.degree);
|
|
482
|
+
const confirmationDegrees = normalizeDegreeSelections(confirmation?.degree_value);
|
|
481
483
|
const overrideGender = normalizeGender(overrides?.gender);
|
|
484
|
+
const confirmationGender = normalizeGender(confirmation?.gender_value);
|
|
482
485
|
const overrideRecentNotView = normalizeRecentNotView(overrides?.recent_not_view);
|
|
486
|
+
const confirmationRecentNotView = normalizeRecentNotView(confirmation?.recent_not_view_value);
|
|
483
487
|
const overrideCriteria = overrides?.criteria;
|
|
484
488
|
const jobSelectionHint = normalizeText(overrides?.job || confirmation?.job_value || "");
|
|
485
489
|
|
|
@@ -487,16 +491,18 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
487
491
|
? sortSchoolTagSelections(detectedSchoolTags)
|
|
488
492
|
: ["不限"];
|
|
489
493
|
const searchParams = {
|
|
490
|
-
school_tag: overrideSchoolTag || inferredSchoolTag,
|
|
494
|
+
school_tag: overrideSchoolTag || confirmationSchoolTag || inferredSchoolTag,
|
|
491
495
|
degree: (
|
|
492
496
|
(Array.isArray(overrideDegrees) && overrideDegrees.length > 0
|
|
493
497
|
? overrideDegrees
|
|
498
|
+
: Array.isArray(confirmationDegrees) && confirmationDegrees.length > 0
|
|
499
|
+
? confirmationDegrees
|
|
494
500
|
: Array.isArray(detectedDegrees) && detectedDegrees.length > 0
|
|
495
501
|
? detectedDegrees
|
|
496
502
|
: ["不限"])
|
|
497
503
|
),
|
|
498
|
-
gender: overrideGender || extractGender(text) || "不限",
|
|
499
|
-
recent_not_view: overrideRecentNotView || extractRecentNotView(text) || "不限"
|
|
504
|
+
gender: overrideGender || confirmationGender || extractGender(text) || "不限",
|
|
505
|
+
recent_not_view: overrideRecentNotView || confirmationRecentNotView || extractRecentNotView(text) || "不限"
|
|
500
506
|
};
|
|
501
507
|
const screenParams = {
|
|
502
508
|
criteria: buildCriteria(text, overrideCriteria),
|
|
@@ -525,10 +531,42 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
525
531
|
invalidOverrideSchoolTags: schoolTagAudit.invalid,
|
|
526
532
|
maxGreetCountResolution
|
|
527
533
|
});
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
534
|
+
const hasSchoolTagSignal = Boolean(
|
|
535
|
+
(Array.isArray(overrideSchoolTag) && overrideSchoolTag.length > 0)
|
|
536
|
+
|| (Array.isArray(confirmationSchoolTag) && confirmationSchoolTag.length > 0)
|
|
537
|
+
|| (Array.isArray(detectedSchoolTags) && detectedSchoolTags.length > 0)
|
|
538
|
+
);
|
|
539
|
+
const hasDegreeSignal = Boolean(
|
|
540
|
+
(Array.isArray(overrideDegrees) && overrideDegrees.length > 0)
|
|
541
|
+
|| (Array.isArray(confirmationDegrees) && confirmationDegrees.length > 0)
|
|
542
|
+
|| (Array.isArray(detectedDegrees) && detectedDegrees.length > 0)
|
|
543
|
+
);
|
|
544
|
+
const hasGenderSignal = Boolean(
|
|
545
|
+
overrideGender
|
|
546
|
+
|| confirmationGender
|
|
547
|
+
|| extractGender(text)
|
|
548
|
+
);
|
|
549
|
+
const hasRecentNotViewSignal = Boolean(
|
|
550
|
+
overrideRecentNotView
|
|
551
|
+
|| confirmationRecentNotView
|
|
552
|
+
|| extractRecentNotView(text)
|
|
553
|
+
);
|
|
554
|
+
const needs_school_tag_confirmation = (
|
|
555
|
+
confirmation?.school_tag_confirmed !== true
|
|
556
|
+
|| !hasSchoolTagSignal
|
|
557
|
+
);
|
|
558
|
+
const needs_degree_confirmation = (
|
|
559
|
+
confirmation?.degree_confirmed !== true
|
|
560
|
+
|| !hasDegreeSignal
|
|
561
|
+
);
|
|
562
|
+
const needs_gender_confirmation = (
|
|
563
|
+
confirmation?.gender_confirmed !== true
|
|
564
|
+
|| !hasGenderSignal
|
|
565
|
+
);
|
|
566
|
+
const needs_recent_not_view_confirmation = (
|
|
567
|
+
confirmation?.recent_not_view_confirmed !== true
|
|
568
|
+
|| !hasRecentNotViewSignal
|
|
569
|
+
);
|
|
532
570
|
const needs_filters_confirmation = (
|
|
533
571
|
confirmation?.filters_confirmed !== true
|
|
534
572
|
|| needs_school_tag_confirmation
|
package/src/pipeline.js
CHANGED
|
@@ -19,13 +19,14 @@ function normalizeText(value) {
|
|
|
19
19
|
function normalizeJobTitle(value) {
|
|
20
20
|
const text = normalizeText(value);
|
|
21
21
|
if (!text) return "";
|
|
22
|
-
const
|
|
22
|
+
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
23
|
+
const strippedRange = byGap
|
|
23
24
|
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
24
25
|
.trim();
|
|
25
26
|
const strippedSingle = strippedRange
|
|
26
27
|
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
27
28
|
.trim();
|
|
28
|
-
return strippedSingle ||
|
|
29
|
+
return strippedSingle || byGap;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function normalizeJobOptions(jobOptions = []) {
|
|
@@ -299,8 +300,8 @@ function buildNeedInputResponse(parsedResult) {
|
|
|
299
300
|
};
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
function buildNeedConfirmationResponse(parsedResult) {
|
|
303
|
-
return {
|
|
303
|
+
function buildNeedConfirmationResponse(parsedResult) {
|
|
304
|
+
return {
|
|
304
305
|
status: "NEED_CONFIRMATION",
|
|
305
306
|
required_confirmations: buildRequiredConfirmations(parsedResult),
|
|
306
307
|
search_params: parsedResult.searchParams,
|
|
@@ -311,10 +312,22 @@ function buildNeedConfirmationResponse(parsedResult) {
|
|
|
311
312
|
max_greet_count: parsedResult.proposed_max_greet_count || parsedResult.screenParams.max_greet_count
|
|
312
313
|
},
|
|
313
314
|
pending_questions: parsedResult.pending_questions,
|
|
314
|
-
review: parsedResult.review
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
315
|
+
review: parsedResult.review
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildFinalReviewQuestion({ searchParams, screenParams, selectedJob }) {
|
|
320
|
+
return {
|
|
321
|
+
field: "final_review",
|
|
322
|
+
question: "开始执行搜索和筛选前,请最后确认全部参数(岗位/筛选条件/筛选 criteria/目标人数/post_action/max_greet_count)无误。",
|
|
323
|
+
value: {
|
|
324
|
+
job: selectedJob?.title || selectedJob?.label || selectedJob?.value || null,
|
|
325
|
+
search_params: searchParams,
|
|
326
|
+
screen_params: screenParams
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
318
331
|
function buildFailedResponse(code, message, extra = {}) {
|
|
319
332
|
return {
|
|
320
333
|
status: "FAILED",
|
|
@@ -445,7 +458,7 @@ export async function runRecommendPipeline(
|
|
|
445
458
|
"2) apiKey",
|
|
446
459
|
"3) model",
|
|
447
460
|
`配置文件路径:${screenConfigCheck.path}`,
|
|
448
|
-
"注意:不要使用模板占位符(例如 replace-with-openai-api-key
|
|
461
|
+
"注意:不要使用模板占位符(例如 replace-with-openai-api-key),也不要由 agent 自行猜测或代填示例值。必须向用户逐项确认真实可用值后再重试。"
|
|
449
462
|
].join("\n")
|
|
450
463
|
}
|
|
451
464
|
: undefined,
|
|
@@ -559,6 +572,34 @@ export async function runRecommendPipeline(
|
|
|
559
572
|
}
|
|
560
573
|
const selectedJob = selectedJobResolution.job;
|
|
561
574
|
const selectedJobToken = selectedJob.value || selectedJob.title || selectedJob.label;
|
|
575
|
+
if (confirmation?.final_confirmed !== true) {
|
|
576
|
+
const pendingQuestions = (parsed.pending_questions || []).filter((item) => item?.field !== "final_review");
|
|
577
|
+
pendingQuestions.push(buildFinalReviewQuestion({
|
|
578
|
+
searchParams: parsed.searchParams,
|
|
579
|
+
screenParams: {
|
|
580
|
+
...parsed.screenParams,
|
|
581
|
+
target_count: parsed.proposed_target_count ?? parsed.screenParams.target_count,
|
|
582
|
+
post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
|
|
583
|
+
max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
|
|
584
|
+
},
|
|
585
|
+
selectedJob
|
|
586
|
+
}));
|
|
587
|
+
return {
|
|
588
|
+
status: "NEED_CONFIRMATION",
|
|
589
|
+
required_confirmations: dedupe([...buildRequiredConfirmations(parsed), "final_review"]),
|
|
590
|
+
search_params: parsed.searchParams,
|
|
591
|
+
screen_params: {
|
|
592
|
+
...parsed.screenParams,
|
|
593
|
+
target_count: parsed.proposed_target_count ?? parsed.screenParams.target_count,
|
|
594
|
+
post_action: parsed.proposed_post_action || parsed.screenParams.post_action,
|
|
595
|
+
max_greet_count: parsed.proposed_max_greet_count || parsed.screenParams.max_greet_count
|
|
596
|
+
},
|
|
597
|
+
selected_job: selectedJob,
|
|
598
|
+
pending_questions: pendingQuestions,
|
|
599
|
+
review: parsed.review,
|
|
600
|
+
job_options: jobOptions
|
|
601
|
+
};
|
|
602
|
+
}
|
|
562
603
|
|
|
563
604
|
const searchResult = await searchCli({
|
|
564
605
|
workspaceRoot,
|
package/src/test-parser.js
CHANGED
|
@@ -67,6 +67,70 @@ function testConfirmedPostActionAndOverrides() {
|
|
|
67
67
|
assert.equal(result.needs_max_greet_count_confirmation, false);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation() {
|
|
71
|
+
const result = parseRecommendInstruction({
|
|
72
|
+
instruction: "通过boss推荐skill帮我找人",
|
|
73
|
+
confirmation: {
|
|
74
|
+
filters_confirmed: true,
|
|
75
|
+
school_tag_confirmed: true,
|
|
76
|
+
degree_confirmed: true,
|
|
77
|
+
gender_confirmed: true,
|
|
78
|
+
recent_not_view_confirmed: true,
|
|
79
|
+
criteria_confirmed: true,
|
|
80
|
+
target_count_confirmed: true,
|
|
81
|
+
post_action_confirmed: true,
|
|
82
|
+
post_action_value: "favorite"
|
|
83
|
+
},
|
|
84
|
+
overrides: {
|
|
85
|
+
criteria: "必须有至少3年工作经验,且做过算法"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.deepEqual(result.searchParams.school_tag, ["不限"]);
|
|
90
|
+
assert.deepEqual(result.searchParams.degree, ["不限"]);
|
|
91
|
+
assert.equal(result.searchParams.gender, "不限");
|
|
92
|
+
assert.equal(result.searchParams.recent_not_view, "不限");
|
|
93
|
+
assert.equal(result.needs_school_tag_confirmation, true);
|
|
94
|
+
assert.equal(result.needs_degree_confirmation, true);
|
|
95
|
+
assert.equal(result.needs_gender_confirmation, true);
|
|
96
|
+
assert.equal(result.needs_recent_not_view_confirmation, true);
|
|
97
|
+
assert.equal(result.needs_filters_confirmation, true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function testFilterConfirmedWithExplicitConfirmationValuesShouldNotFallbackToUnlimited() {
|
|
101
|
+
const result = parseRecommendInstruction({
|
|
102
|
+
instruction: "通过boss推荐skill帮我找人",
|
|
103
|
+
confirmation: {
|
|
104
|
+
filters_confirmed: true,
|
|
105
|
+
school_tag_confirmed: true,
|
|
106
|
+
school_tag_value: ["985", "211"],
|
|
107
|
+
degree_confirmed: true,
|
|
108
|
+
degree_value: ["大专", "本科", "硕士", "博士"],
|
|
109
|
+
gender_confirmed: true,
|
|
110
|
+
gender_value: "男",
|
|
111
|
+
recent_not_view_confirmed: true,
|
|
112
|
+
recent_not_view_value: "近14天没有",
|
|
113
|
+
criteria_confirmed: true,
|
|
114
|
+
target_count_confirmed: true,
|
|
115
|
+
target_count_value: 3,
|
|
116
|
+
post_action_confirmed: true,
|
|
117
|
+
post_action_value: "favorite"
|
|
118
|
+
},
|
|
119
|
+
overrides: {
|
|
120
|
+
criteria: "必须有至少3年工作经验,且做过算法"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
assert.deepEqual(result.searchParams.school_tag, ["985", "211"]);
|
|
125
|
+
assert.deepEqual(result.searchParams.degree, ["大专", "本科", "硕士", "博士"]);
|
|
126
|
+
assert.equal(result.searchParams.gender, "男");
|
|
127
|
+
assert.equal(result.searchParams.recent_not_view, "近14天没有");
|
|
128
|
+
assert.equal(result.needs_school_tag_confirmation, false);
|
|
129
|
+
assert.equal(result.needs_degree_confirmation, false);
|
|
130
|
+
assert.equal(result.needs_gender_confirmation, false);
|
|
131
|
+
assert.equal(result.needs_recent_not_view_confirmation, false);
|
|
132
|
+
}
|
|
133
|
+
|
|
70
134
|
function testMultipleSchoolTagsMarkedSuspicious() {
|
|
71
135
|
const result = parseRecommendInstruction({
|
|
72
136
|
instruction: "推荐页筛选985和211,有推荐系统经验",
|
|
@@ -351,6 +415,8 @@ function testMcpMentionShouldStayInCriteria() {
|
|
|
351
415
|
function main() {
|
|
352
416
|
testNeedConfirmationIncludesPostAction();
|
|
353
417
|
testConfirmedPostActionAndOverrides();
|
|
418
|
+
testFilterConfirmedWithoutExplicitValuesShouldRequireReconfirmation();
|
|
419
|
+
testFilterConfirmedWithExplicitConfirmationValuesShouldNotFallbackToUnlimited();
|
|
354
420
|
testMultipleSchoolTagsMarkedSuspicious();
|
|
355
421
|
testDegreeCanBeExtracted();
|
|
356
422
|
testDegreeAtOrAboveExpansion();
|
package/src/test-pipeline.js
CHANGED
|
@@ -30,6 +30,14 @@ function createJobListResult() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function createJobConfirmedConfirmation() {
|
|
33
|
+
return {
|
|
34
|
+
job_confirmed: true,
|
|
35
|
+
job_value: "数据分析实习生 _ 杭州",
|
|
36
|
+
final_confirmed: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createJobConfirmedWithoutFinalConfirmation() {
|
|
33
41
|
return {
|
|
34
42
|
job_confirmed: true,
|
|
35
43
|
job_value: "数据分析实习生 _ 杭州"
|
|
@@ -318,6 +326,29 @@ async function testNeedJobConfirmationGate() {
|
|
|
318
326
|
assert.equal(result.job_options.length, 2);
|
|
319
327
|
}
|
|
320
328
|
|
|
329
|
+
async function testNeedFinalReviewConfirmationGate() {
|
|
330
|
+
const result = await runRecommendPipeline(
|
|
331
|
+
{
|
|
332
|
+
workspaceRoot: process.cwd(),
|
|
333
|
+
instruction: "test",
|
|
334
|
+
confirmation: createJobConfirmedWithoutFinalConfirmation(),
|
|
335
|
+
overrides: {}
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
parseRecommendInstruction: () => createParsed(),
|
|
339
|
+
runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
|
|
340
|
+
ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: {} }),
|
|
341
|
+
listRecommendJobs: async () => createJobListResult(),
|
|
342
|
+
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
343
|
+
runRecommendScreenCli: async () => ({ ok: true, summary: {} })
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
assert.equal(result.status, "NEED_CONFIRMATION");
|
|
348
|
+
assert.equal(result.required_confirmations.includes("final_review"), true);
|
|
349
|
+
assert.equal(result.pending_questions.some((item) => item.field === "final_review"), true);
|
|
350
|
+
}
|
|
351
|
+
|
|
321
352
|
async function testLoginRequiredShouldReturnGuidance() {
|
|
322
353
|
const result = await runRecommendPipeline(
|
|
323
354
|
{
|
|
@@ -587,6 +618,7 @@ async function main() {
|
|
|
587
618
|
await testNeedMaxGreetCountConfirmationGate();
|
|
588
619
|
await testNeedInputGate();
|
|
589
620
|
await testNeedJobConfirmationGate();
|
|
621
|
+
await testNeedFinalReviewConfirmationGate();
|
|
590
622
|
await testCompletedPipeline();
|
|
591
623
|
await testSearchFailure();
|
|
592
624
|
await testLoginRequiredShouldReturnGuidance();
|
|
@@ -19,13 +19,14 @@ function normalizeText(value) {
|
|
|
19
19
|
function normalizeJobTitle(value) {
|
|
20
20
|
const text = normalizeText(value);
|
|
21
21
|
if (!text) return "";
|
|
22
|
-
const
|
|
22
|
+
const byGap = text.split(/\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
23
|
+
const strippedRange = byGap
|
|
23
24
|
.replace(/\s+\d+(?:\.\d+)?\s*(?:-|~|—|至)\s*\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)?$/u, "")
|
|
24
25
|
.trim();
|
|
25
26
|
const strippedSingle = strippedRange
|
|
26
27
|
.replace(/\s+\d+(?:\.\d+)?\s*(?:k|K|千|万|元\/天|元\/月|元\/年|K\/月|k\/月|万\/月|万\/年)$/u, "")
|
|
27
28
|
.trim();
|
|
28
|
-
return strippedSingle ||
|
|
29
|
+
return strippedSingle || byGap;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function parsePositiveInteger(raw) {
|
|
@@ -419,13 +420,14 @@ class RecommendSearchCli {
|
|
|
419
420
|
const normalizeTitle = (value) => {
|
|
420
421
|
const text = normalize(value);
|
|
421
422
|
if (!text) return '';
|
|
422
|
-
const
|
|
423
|
+
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
424
|
+
const strippedRange = byGap
|
|
423
425
|
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
424
426
|
.trim();
|
|
425
427
|
const strippedSingle = strippedRange
|
|
426
428
|
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
427
429
|
.trim();
|
|
428
|
-
return strippedSingle ||
|
|
430
|
+
return strippedSingle || byGap;
|
|
429
431
|
};
|
|
430
432
|
const isVisible = (el) => {
|
|
431
433
|
if (!el) return false;
|
|
@@ -437,9 +439,13 @@ class RecommendSearchCli {
|
|
|
437
439
|
return rect.width > 2 && rect.height > 2;
|
|
438
440
|
};
|
|
439
441
|
|
|
440
|
-
const items = Array.from(
|
|
441
|
-
|
|
442
|
-
|
|
442
|
+
const items = Array.from(doc.querySelectorAll([
|
|
443
|
+
'.ui-dropmenu-list .job-list .job-item',
|
|
444
|
+
'.job-selecter-options .job-list .job-item',
|
|
445
|
+
'.job-selector-options .job-list .job-item',
|
|
446
|
+
'.dropmenu-list .job-list .job-item',
|
|
447
|
+
'.job-list .job-item'
|
|
448
|
+
].join(',')));
|
|
443
449
|
const jobs = [];
|
|
444
450
|
const seen = new Set();
|
|
445
451
|
for (const item of items) {
|
|
@@ -481,9 +487,13 @@ class RecommendSearchCli {
|
|
|
481
487
|
const doc = frame.contentDocument;
|
|
482
488
|
const selectors = [
|
|
483
489
|
'.chat-job-select',
|
|
490
|
+
'.chat-job-selector',
|
|
484
491
|
'.job-selecter',
|
|
492
|
+
'.job-selector',
|
|
485
493
|
'.job-select-wrap',
|
|
486
494
|
'.job-select',
|
|
495
|
+
'.job-select-box',
|
|
496
|
+
'.job-wrap',
|
|
487
497
|
'.chat-job-name',
|
|
488
498
|
'.top-chat-search'
|
|
489
499
|
];
|
|
@@ -564,17 +574,22 @@ class RecommendSearchCli {
|
|
|
564
574
|
const normalizeTitle = (value) => {
|
|
565
575
|
const text = normalize(value);
|
|
566
576
|
if (!text) return '';
|
|
567
|
-
const
|
|
577
|
+
const byGap = text.split(/\\s{2,}/).map((item) => item.trim()).filter(Boolean)[0] || text;
|
|
578
|
+
const strippedRange = byGap
|
|
568
579
|
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:-|~|—|至)\\s*\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)?$/u, '')
|
|
569
580
|
.trim();
|
|
570
581
|
const strippedSingle = strippedRange
|
|
571
582
|
.replace(/\\s+\\d+(?:\\.\\d+)?\\s*(?:k|K|千|万|元\\/天|元\\/月|元\\/年|K\\/月|k\\/月|万\\/月|万\\/年)$/u, '')
|
|
572
583
|
.trim();
|
|
573
|
-
return strippedSingle ||
|
|
584
|
+
return strippedSingle || byGap;
|
|
574
585
|
};
|
|
575
|
-
const items = Array.from(
|
|
576
|
-
|
|
577
|
-
|
|
586
|
+
const items = Array.from(doc.querySelectorAll([
|
|
587
|
+
'.ui-dropmenu-list .job-list .job-item',
|
|
588
|
+
'.job-selecter-options .job-list .job-item',
|
|
589
|
+
'.job-selector-options .job-list .job-item',
|
|
590
|
+
'.dropmenu-list .job-list .job-item',
|
|
591
|
+
'.job-list .job-item'
|
|
592
|
+
].join(',')));
|
|
578
593
|
const target = items.find((item) => {
|
|
579
594
|
const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
|
|
580
595
|
const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
|