@reconcrap/boss-recommend-mcp 0.1.0 → 0.1.2
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 +16 -1
- package/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +11 -2
- package/src/adapters.js +11 -0
- package/src/cli.js +185 -1
- package/src/index.js +21 -0
- package/src/parser.js +165 -3
- package/src/pipeline.js +8 -0
- package/src/test-parser.js +90 -0
- package/src/test-pipeline.js +36 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +126 -2
- package/vendor/boss-recommend-search-cli/src/cli.js +222 -12
package/README.md
CHANGED
|
@@ -19,7 +19,10 @@ MCP 工具名:`run_recommend_pipeline`
|
|
|
19
19
|
## 设计特点
|
|
20
20
|
|
|
21
21
|
- 页面目标固定为 `https://www.zhipin.com/web/chat/recommend`
|
|
22
|
-
- 支持推荐页原生筛选:学校标签 / 性别 / 近14天没有
|
|
22
|
+
- 支持推荐页原生筛选:学校标签 / 学历 / 性别 / 近14天没有
|
|
23
|
+
- 学历支持单选与多选语义:如“本科及以上”会展开为 `本科/硕士/博士`;如“大专、本科”只勾选这两项
|
|
24
|
+
- 执行前会逐项确认筛选参数:学校标签 / 学历 / 性别 / 是否过滤近14天已看
|
|
25
|
+
- npm 全局安装后会自动执行 install:生成 Codex skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
23
26
|
- `post_action` 必须在每次完整运行开始时确认一次
|
|
24
27
|
- `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
|
|
25
28
|
- 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
|
|
@@ -41,6 +44,13 @@ npm install
|
|
|
41
44
|
node src/cli.js start
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
可选环境变量(用于跨 agent 自动配置):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
|
|
51
|
+
BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
|
|
52
|
+
```
|
|
53
|
+
|
|
44
54
|
或使用 CLI fallback:
|
|
45
55
|
|
|
46
56
|
```bash
|
|
@@ -92,6 +102,10 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
92
102
|
"instruction": "推荐页筛选211女生,近14天没有,有 AI Agent 经验,符合标准的直接沟通",
|
|
93
103
|
"confirmation": {
|
|
94
104
|
"filters_confirmed": true,
|
|
105
|
+
"school_tag_confirmed": true,
|
|
106
|
+
"degree_confirmed": true,
|
|
107
|
+
"gender_confirmed": true,
|
|
108
|
+
"recent_not_view_confirmed": true,
|
|
95
109
|
"criteria_confirmed": true,
|
|
96
110
|
"target_count_confirmed": true,
|
|
97
111
|
"target_count_value": 20,
|
|
@@ -102,6 +116,7 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
102
116
|
},
|
|
103
117
|
"overrides": {
|
|
104
118
|
"school_tag": "211",
|
|
119
|
+
"degree": ["本科", "硕士", "博士"],
|
|
105
120
|
"gender": "女",
|
|
106
121
|
"recent_not_view": "近14天没有",
|
|
107
122
|
"criteria": "候选人需要有 AI Agent 或 MCP 工具开发经验",
|
package/package.json
CHANGED
|
@@ -22,7 +22,10 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
22
22
|
|
|
23
23
|
在真正执行前,必须先确认:
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- 学校标签(`school_tag`)
|
|
26
|
+
- 学历(`degree`)
|
|
27
|
+
- 性别(`gender`)
|
|
28
|
+
- 是否过滤近14天已看(`recent_not_view`)
|
|
26
29
|
- screening criteria 是否正确
|
|
27
30
|
- `target_count`(目标筛选人数)是否需要设置(可不设上限)
|
|
28
31
|
- `post_action` 是否确定为 `favorite` 或 `greet`
|
|
@@ -41,6 +44,10 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
41
44
|
- `instruction` (required)
|
|
42
45
|
- `confirmation`
|
|
43
46
|
- `filters_confirmed`
|
|
47
|
+
- `school_tag_confirmed`
|
|
48
|
+
- `degree_confirmed`
|
|
49
|
+
- `gender_confirmed`
|
|
50
|
+
- `recent_not_view_confirmed`
|
|
44
51
|
- `criteria_confirmed`
|
|
45
52
|
- `target_count_confirmed`
|
|
46
53
|
- `target_count_value` (integer, optional)
|
|
@@ -50,6 +57,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
50
57
|
- `max_greet_count_value` (integer)
|
|
51
58
|
- `overrides`
|
|
52
59
|
- `school_tag`
|
|
60
|
+
- `degree`(可传单值或数组;如“本科及以上”应展开为 `["本科","硕士","博士"]`)
|
|
53
61
|
- `gender`
|
|
54
62
|
- `recent_not_view`
|
|
55
63
|
- `criteria`
|
|
@@ -90,5 +98,6 @@ CLI fallback 的状态机与 MCP 保持一致:
|
|
|
90
98
|
|
|
91
99
|
- 用结构化中文输出
|
|
92
100
|
- 先给用户确认卡片,再正式执行
|
|
101
|
+
- 对 `school_tag/degree/gender/recent_not_view` 必须逐项提问并逐项确认,不可合并成一句“filters已确认”
|
|
93
102
|
- 不要跳过 `post_action` 的首轮确认
|
|
94
|
-
- 不要把 recommend 流程说成 search 流程
|
|
103
|
+
- 不要把 recommend 流程说成 search 流程
|
package/src/adapters.js
CHANGED
|
@@ -36,6 +36,15 @@ function parsePositiveInteger(raw) {
|
|
|
36
36
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function serializeDegreeSelection(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
42
|
+
return normalized.length ? normalized.join(",") : "不限";
|
|
43
|
+
}
|
|
44
|
+
const normalized = String(value || "").trim();
|
|
45
|
+
return normalized || "不限";
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
function resolveScreenConfigPath(workspaceRoot) {
|
|
40
49
|
const envConfigPath = process.env.BOSS_RECOMMEND_SCREEN_CONFIG
|
|
41
50
|
? path.resolve(process.env.BOSS_RECOMMEND_SCREEN_CONFIG)
|
|
@@ -506,6 +515,8 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
|
|
|
506
515
|
cliPath,
|
|
507
516
|
"--school-tag",
|
|
508
517
|
searchParams.school_tag,
|
|
518
|
+
"--degree",
|
|
519
|
+
serializeDegreeSelection(searchParams.degree),
|
|
509
520
|
"--gender",
|
|
510
521
|
searchParams.gender,
|
|
511
522
|
"--recent-not-view",
|
package/src/cli.js
CHANGED
|
@@ -27,6 +27,8 @@ const defaultMcpServerName = "boss-recommend";
|
|
|
27
27
|
const defaultMcpCommand = "npx";
|
|
28
28
|
const defaultMcpArgs = ["-y", "@reconcrap/boss-recommend-mcp@latest", "start"];
|
|
29
29
|
const autoSyncSkipCommands = new Set(["install", "install-skill", "where", "help", "--help", "-h"]);
|
|
30
|
+
const externalMcpTargetsEnv = "BOSS_RECOMMEND_MCP_CONFIG_TARGETS";
|
|
31
|
+
const externalSkillDirsEnv = "BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS";
|
|
30
32
|
|
|
31
33
|
function getPackageVersion() {
|
|
32
34
|
try {
|
|
@@ -49,6 +51,39 @@ function ensureDir(targetPath) {
|
|
|
49
51
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function pathExists(targetPath) {
|
|
55
|
+
try {
|
|
56
|
+
return fs.existsSync(targetPath);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readJsonObjectFileSafe(filePath) {
|
|
63
|
+
if (!pathExists(filePath)) return {};
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
66
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
67
|
+
return parsed;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Fallback below.
|
|
71
|
+
}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function dedupePaths(items) {
|
|
76
|
+
const result = [];
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const item of items || []) {
|
|
79
|
+
const resolved = path.resolve(String(item || ""));
|
|
80
|
+
if (!resolved || seen.has(resolved)) continue;
|
|
81
|
+
seen.add(resolved);
|
|
82
|
+
result.push(resolved);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
function getDesktopDir() {
|
|
53
88
|
return path.join(os.homedir(), "Desktop");
|
|
54
89
|
}
|
|
@@ -154,6 +189,7 @@ function normalizeMcpClientName(value) {
|
|
|
154
189
|
const raw = String(value || "").trim().toLowerCase();
|
|
155
190
|
if (!raw) return "";
|
|
156
191
|
if (raw === "claude-code") return "claudecode";
|
|
192
|
+
if (raw === "trae-cn") return "trae";
|
|
157
193
|
return raw;
|
|
158
194
|
}
|
|
159
195
|
|
|
@@ -219,6 +255,135 @@ function writeMcpConfigFiles(options = {}) {
|
|
|
219
255
|
return { outputDir, files };
|
|
220
256
|
}
|
|
221
257
|
|
|
258
|
+
function parsePathListFromEnv(raw) {
|
|
259
|
+
if (!raw) return [];
|
|
260
|
+
const text = String(raw).trim();
|
|
261
|
+
if (!text) return [];
|
|
262
|
+
try {
|
|
263
|
+
const parsed = JSON.parse(text);
|
|
264
|
+
if (Array.isArray(parsed)) {
|
|
265
|
+
return dedupePaths(parsed.filter(Boolean));
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// Fallback to delimiter split.
|
|
269
|
+
}
|
|
270
|
+
return dedupePaths(text.split(path.delimiter).map((item) => item.trim()).filter(Boolean));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getKnownExternalMcpConfigPaths() {
|
|
274
|
+
const home = os.homedir();
|
|
275
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
276
|
+
return dedupePaths([
|
|
277
|
+
path.join(appData, "Cursor", "User", "mcp.json"),
|
|
278
|
+
path.join(appData, "Trae", "User", "mcp.json"),
|
|
279
|
+
path.join(appData, "Trae CN", "User", "mcp.json"),
|
|
280
|
+
path.join(home, ".trae", "mcp.json"),
|
|
281
|
+
path.join(home, ".trae-cn", "mcp.json"),
|
|
282
|
+
path.join(home, ".claude", "mcp.json"),
|
|
283
|
+
path.join(home, ".openclaw", "mcp.json")
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resolveExternalMcpConfigTargets() {
|
|
288
|
+
const fromEnv = parsePathListFromEnv(process.env[externalMcpTargetsEnv]);
|
|
289
|
+
const known = getKnownExternalMcpConfigPaths().filter((filePath) => {
|
|
290
|
+
if (pathExists(filePath)) return true;
|
|
291
|
+
return pathExists(path.dirname(filePath));
|
|
292
|
+
});
|
|
293
|
+
return dedupePaths([...fromEnv, ...known]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function mergeMcpServerConfigFile(filePath, options = {}) {
|
|
297
|
+
const nextConfig = buildMcpConfigFileContent(options);
|
|
298
|
+
const serverName = Object.keys(nextConfig.mcpServers || {})[0] || defaultMcpServerName;
|
|
299
|
+
const launchConfig = nextConfig.mcpServers?.[serverName] || buildMcpLaunchConfig(options);
|
|
300
|
+
const current = readJsonObjectFileSafe(filePath);
|
|
301
|
+
const existingServers =
|
|
302
|
+
current?.mcpServers && typeof current.mcpServers === "object" && !Array.isArray(current.mcpServers)
|
|
303
|
+
? current.mcpServers
|
|
304
|
+
: {};
|
|
305
|
+
const existingEntry = existingServers[serverName];
|
|
306
|
+
const merged = {
|
|
307
|
+
...current,
|
|
308
|
+
mcpServers: {
|
|
309
|
+
...existingServers,
|
|
310
|
+
[serverName]: launchConfig
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
ensureDir(path.dirname(filePath));
|
|
315
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf8");
|
|
316
|
+
const updated = JSON.stringify(existingEntry || null) !== JSON.stringify(launchConfig);
|
|
317
|
+
return {
|
|
318
|
+
file: filePath,
|
|
319
|
+
server: serverName,
|
|
320
|
+
updated
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function installExternalMcpConfigs(options = {}) {
|
|
325
|
+
const targets = resolveExternalMcpConfigTargets();
|
|
326
|
+
const applied = [];
|
|
327
|
+
const skipped = [];
|
|
328
|
+
for (const target of targets) {
|
|
329
|
+
try {
|
|
330
|
+
const existed = pathExists(target);
|
|
331
|
+
const merged = mergeMcpServerConfigFile(target, options);
|
|
332
|
+
applied.push({
|
|
333
|
+
file: target,
|
|
334
|
+
server: merged.server,
|
|
335
|
+
created: !existed,
|
|
336
|
+
updated: merged.updated
|
|
337
|
+
});
|
|
338
|
+
} catch (error) {
|
|
339
|
+
skipped.push({
|
|
340
|
+
file: target,
|
|
341
|
+
reason: error.message
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { targets, applied, skipped };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getKnownExternalSkillBaseDirs() {
|
|
349
|
+
const home = os.homedir();
|
|
350
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
351
|
+
return dedupePaths([
|
|
352
|
+
path.join(home, ".cursor", "skills"),
|
|
353
|
+
path.join(home, ".trae", "skills"),
|
|
354
|
+
path.join(home, ".trae-cn", "skills"),
|
|
355
|
+
path.join(home, ".claude", "skills"),
|
|
356
|
+
path.join(home, ".openclaw", "skills"),
|
|
357
|
+
path.join(appData, "Cursor", "User", "skills"),
|
|
358
|
+
path.join(appData, "Trae", "User", "skills"),
|
|
359
|
+
path.join(appData, "Trae CN", "User", "skills"),
|
|
360
|
+
path.join(appData, "OpenClaw", "User", "skills")
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveExternalSkillBaseDirs() {
|
|
365
|
+
const fromEnv = parsePathListFromEnv(process.env[externalSkillDirsEnv]);
|
|
366
|
+
const known = getKnownExternalSkillBaseDirs().filter((dirPath) => pathExists(dirPath));
|
|
367
|
+
return dedupePaths([...fromEnv, ...known]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function mirrorSkillToExternalDirs() {
|
|
371
|
+
const baseDirs = resolveExternalSkillBaseDirs();
|
|
372
|
+
const mirrored = [];
|
|
373
|
+
const skipped = [];
|
|
374
|
+
for (const baseDir of baseDirs) {
|
|
375
|
+
try {
|
|
376
|
+
const targetDir = path.join(baseDir, skillName);
|
|
377
|
+
ensureDir(path.dirname(targetDir));
|
|
378
|
+
fs.cpSync(skillSourceDir, targetDir, { recursive: true, force: true });
|
|
379
|
+
mirrored.push({ base_dir: baseDir, target_dir: targetDir });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
skipped.push({ base_dir: baseDir, reason: error.message });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { baseDirs, mirrored, skipped };
|
|
385
|
+
}
|
|
386
|
+
|
|
222
387
|
function syncSkillAssets(options = {}) {
|
|
223
388
|
const force = options.force === true;
|
|
224
389
|
const targetDir = getSkillTargetDir();
|
|
@@ -421,7 +586,7 @@ function printHelp() {
|
|
|
421
586
|
console.log(" boss-recommend-mcp install-skill Install only the Codex skill");
|
|
422
587
|
console.log(" boss-recommend-mcp init-config Create ~/.codex/boss-recommend-mcp/screening-config.json if missing");
|
|
423
588
|
console.log(" boss-recommend-mcp set-port Persist preferred Chrome debug port to screening-config.json");
|
|
424
|
-
console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae/Claude Code/OpenClaw");
|
|
589
|
+
console.log(" boss-recommend-mcp mcp-config Generate MCP config JSON for Cursor/Trae(含 trae-cn)/Claude Code/OpenClaw");
|
|
425
590
|
console.log(" boss-recommend-mcp doctor Check config and runtime prerequisites");
|
|
426
591
|
console.log(" boss-recommend-mcp launch-chrome Launch or reuse Chrome debug instance and open Boss recommend page");
|
|
427
592
|
console.log(" boss-recommend-mcp where Print installed package, skill, and config paths");
|
|
@@ -447,12 +612,31 @@ function installAll() {
|
|
|
447
612
|
const skillTarget = installSkill();
|
|
448
613
|
const configResult = ensureUserConfig();
|
|
449
614
|
const mcpTemplateResult = writeMcpConfigFiles({ client: "all" });
|
|
615
|
+
const externalMcpResult = installExternalMcpConfigs({});
|
|
616
|
+
const externalSkillResult = mirrorSkillToExternalDirs();
|
|
450
617
|
console.log(`Skill installed to: ${skillTarget}`);
|
|
451
618
|
console.log(configResult.created ? `Config template created at: ${configResult.path}` : `Config already exists at: ${configResult.path}`);
|
|
452
619
|
console.log(`MCP config templates exported to: ${mcpTemplateResult.outputDir}`);
|
|
453
620
|
for (const item of mcpTemplateResult.files) {
|
|
454
621
|
console.log(`- ${item.client}: ${item.file}`);
|
|
455
622
|
}
|
|
623
|
+
if (externalMcpResult.targets.length > 0) {
|
|
624
|
+
console.log(`Auto-configured external MCP files: ${externalMcpResult.applied.length}`);
|
|
625
|
+
for (const item of externalMcpResult.applied) {
|
|
626
|
+
const action = item.created ? "created" : item.updated ? "updated" : "unchanged";
|
|
627
|
+
console.log(`- ${item.file} (${action})`);
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
console.log("No external MCP config target detected. Set BOSS_RECOMMEND_MCP_CONFIG_TARGETS to auto-configure custom agents.");
|
|
631
|
+
}
|
|
632
|
+
if (externalSkillResult.baseDirs.length > 0) {
|
|
633
|
+
console.log(`Mirrored skill to external dirs: ${externalSkillResult.mirrored.length}`);
|
|
634
|
+
for (const item of externalSkillResult.mirrored) {
|
|
635
|
+
console.log(`- ${item.target_dir}`);
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
console.log("No external skill dir detected. Set BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS to mirror skill for non-Codex agents.");
|
|
639
|
+
}
|
|
456
640
|
}
|
|
457
641
|
|
|
458
642
|
async function runPipelineOnce(options) {
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,10 @@ function createToolSchema() {
|
|
|
38
38
|
type: "object",
|
|
39
39
|
properties: {
|
|
40
40
|
filters_confirmed: { type: "boolean" },
|
|
41
|
+
school_tag_confirmed: { type: "boolean" },
|
|
42
|
+
degree_confirmed: { type: "boolean" },
|
|
43
|
+
gender_confirmed: { type: "boolean" },
|
|
44
|
+
recent_not_view_confirmed: { type: "boolean" },
|
|
41
45
|
criteria_confirmed: { type: "boolean" },
|
|
42
46
|
target_count_confirmed: { type: "boolean" },
|
|
43
47
|
target_count_value: {
|
|
@@ -64,6 +68,23 @@ function createToolSchema() {
|
|
|
64
68
|
type: "string",
|
|
65
69
|
enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
|
|
66
70
|
},
|
|
71
|
+
degree: {
|
|
72
|
+
oneOf: [
|
|
73
|
+
{
|
|
74
|
+
type: "string",
|
|
75
|
+
enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "array",
|
|
79
|
+
items: {
|
|
80
|
+
type: "string",
|
|
81
|
+
enum: ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"]
|
|
82
|
+
},
|
|
83
|
+
minItems: 1,
|
|
84
|
+
uniqueItems: true
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
67
88
|
gender: {
|
|
68
89
|
type: "string",
|
|
69
90
|
enum: ["不限", "男", "女"]
|
package/src/parser.js
CHANGED
|
@@ -7,6 +7,25 @@ const SCHOOL_TAG_OPTIONS = [
|
|
|
7
7
|
"国内外名校",
|
|
8
8
|
"公办本科"
|
|
9
9
|
];
|
|
10
|
+
const DEGREE_OPTIONS = [
|
|
11
|
+
"不限",
|
|
12
|
+
"初中及以下",
|
|
13
|
+
"中专/中技",
|
|
14
|
+
"高中",
|
|
15
|
+
"大专",
|
|
16
|
+
"本科",
|
|
17
|
+
"硕士",
|
|
18
|
+
"博士"
|
|
19
|
+
];
|
|
20
|
+
const DEGREE_ORDER = [
|
|
21
|
+
"初中及以下",
|
|
22
|
+
"中专/中技",
|
|
23
|
+
"高中",
|
|
24
|
+
"大专",
|
|
25
|
+
"本科",
|
|
26
|
+
"硕士",
|
|
27
|
+
"博士"
|
|
28
|
+
];
|
|
10
29
|
const GENDER_OPTIONS = ["不限", "男", "女"];
|
|
11
30
|
const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
|
|
12
31
|
const POST_ACTION_OPTIONS = ["favorite", "greet"];
|
|
@@ -30,6 +49,15 @@ const SCHOOL_TAG_PATTERNS = [
|
|
|
30
49
|
{ label: "国内外名校", pattern: /国内外名校|名校/i },
|
|
31
50
|
{ label: "公办本科", pattern: /公办本科/i }
|
|
32
51
|
];
|
|
52
|
+
const DEGREE_PATTERNS = [
|
|
53
|
+
{ label: "初中及以下", pattern: /初中及以下|初中以下/i },
|
|
54
|
+
{ label: "中专/中技", pattern: /中专\s*\/\s*中技|中专中技|中专|中技/i },
|
|
55
|
+
{ label: "高中", pattern: /(?:学历|教育|要求)?[^。;;\n]{0,8}高中/i },
|
|
56
|
+
{ label: "大专", pattern: /(?:学历|教育|要求)?[^。;;\n]{0,8}(?:大专|专科)/i },
|
|
57
|
+
{ label: "本科", pattern: /(?:学历|教育|要求)?[^。;;\n]{0,8}(?:本科|学士)/i },
|
|
58
|
+
{ label: "硕士", pattern: /(?:学历|教育|要求)?[^。;;\n]{0,8}(?:硕士|研究生)/i },
|
|
59
|
+
{ label: "博士", pattern: /(?:学历|教育|要求)?[^。;;\n]{0,8}博士/i }
|
|
60
|
+
];
|
|
33
61
|
const GENDER_PATTERNS = [
|
|
34
62
|
{ label: "男", pattern: /(?:性别|候选人|人选)?[^。;;\n]{0,8}(?:男生|男性|男)/i },
|
|
35
63
|
{ label: "女", pattern: /(?:性别|候选人|人选)?[^。;;\n]{0,8}(?:女生|女性|女)/i }
|
|
@@ -57,6 +85,7 @@ const MAX_GREET_COUNT_PATTERNS = [
|
|
|
57
85
|
];
|
|
58
86
|
const FILTER_CLAUSE_PATTERNS = [
|
|
59
87
|
/学校标签|院校标签|985|211|双一流|留学|国内外名校|公办本科/i,
|
|
88
|
+
/学历|学位|教育|初中及以下|中专|中技|高中|大专|专科|本科|硕士|研究生|博士/i,
|
|
60
89
|
/性别|男生|女生|男性|女性|男\b|女\b/i,
|
|
61
90
|
/近?14天(?:内)?没有|近?14天(?:内)?没看过|近?14天(?:内)?未查看|过滤[^。;;\n]{0,12}14天|排除[^。;;\n]{0,12}14天/i,
|
|
62
91
|
/目标(?:处理|筛选)?(?:人数|数量)?|至少(?:处理|筛选)|(?:处理|筛选)\s*\d+\s*(?:位|人)/i,
|
|
@@ -89,6 +118,75 @@ function normalizeSchoolTag(value) {
|
|
|
89
118
|
return null;
|
|
90
119
|
}
|
|
91
120
|
|
|
121
|
+
function normalizeDegree(value) {
|
|
122
|
+
const normalized = normalizeText(value);
|
|
123
|
+
if (!normalized) return null;
|
|
124
|
+
if (normalized === "专科") return "大专";
|
|
125
|
+
if (normalized === "研究生") return "硕士";
|
|
126
|
+
if (normalized === "中专" || normalized === "中技" || normalized === "中专中技") return "中专/中技";
|
|
127
|
+
return DEGREE_OPTIONS.includes(normalized) ? normalized : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sortDegreeSelections(values) {
|
|
131
|
+
return uniqueList(values).sort((left, right) => {
|
|
132
|
+
const leftIndex = DEGREE_ORDER.indexOf(left);
|
|
133
|
+
const rightIndex = DEGREE_ORDER.indexOf(right);
|
|
134
|
+
return leftIndex - rightIndex;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function expandDegreeAtOrAbove(value) {
|
|
139
|
+
const normalized = normalizeDegree(value);
|
|
140
|
+
if (!normalized || normalized === "不限") return [];
|
|
141
|
+
const startIndex = DEGREE_ORDER.indexOf(normalized);
|
|
142
|
+
if (startIndex === -1) return [];
|
|
143
|
+
return DEGREE_ORDER.slice(startIndex);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseDegreeSelectionsFromText(text) {
|
|
147
|
+
const normalizedText = normalizeText(text);
|
|
148
|
+
if (!normalizedText) return [];
|
|
149
|
+
if (/(?:学历|学位|教育)[^。;;\n]{0,6}不限|不限[^。;;\n]{0,6}(?:学历|学位|教育)/i.test(normalizedText)) {
|
|
150
|
+
return ["不限"];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const selected = [];
|
|
154
|
+
const atOrAbovePattern = /(初中及以下|中专\/中技|中专中技|中专|中技|高中|大专|专科|本科|硕士|研究生|博士)\s*(?:及|或)?以上/g;
|
|
155
|
+
let match;
|
|
156
|
+
while ((match = atOrAbovePattern.exec(normalizedText)) !== null) {
|
|
157
|
+
selected.push(...expandDegreeAtOrAbove(match[1]));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const { label, pattern } of DEGREE_PATTERNS) {
|
|
161
|
+
if (pattern.test(normalizedText)) {
|
|
162
|
+
selected.push(label);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return sortDegreeSelections(selected);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeDegreeSelections(input) {
|
|
169
|
+
if (Array.isArray(input)) {
|
|
170
|
+
const normalized = sortDegreeSelections(input.map((item) => normalizeDegree(item)).filter(Boolean));
|
|
171
|
+
if (!normalized.length) return null;
|
|
172
|
+
return normalized.includes("不限") ? ["不限"] : normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const text = normalizeText(input);
|
|
176
|
+
if (!text) return null;
|
|
177
|
+
if (text.includes("以上")) {
|
|
178
|
+
const fromText = parseDegreeSelectionsFromText(text);
|
|
179
|
+
if (fromText.length) return fromText;
|
|
180
|
+
}
|
|
181
|
+
const parts = text.split(/[,,、/|]/).map((item) => normalizeDegree(item)).filter(Boolean);
|
|
182
|
+
if (parts.length) {
|
|
183
|
+
const normalized = sortDegreeSelections(parts);
|
|
184
|
+
return normalized.includes("不限") ? ["不限"] : normalized;
|
|
185
|
+
}
|
|
186
|
+
const single = normalizeDegree(text);
|
|
187
|
+
return single ? [single] : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
92
190
|
function normalizeGender(value) {
|
|
93
191
|
const normalized = normalizeText(value);
|
|
94
192
|
if (!normalized) return null;
|
|
@@ -142,6 +240,10 @@ function extractGender(text) {
|
|
|
142
240
|
return null;
|
|
143
241
|
}
|
|
144
242
|
|
|
243
|
+
function extractDegrees(text) {
|
|
244
|
+
return parseDegreeSelectionsFromText(text);
|
|
245
|
+
}
|
|
246
|
+
|
|
145
247
|
function extractRecentNotView(text) {
|
|
146
248
|
for (const pattern of RECENT_NOT_VIEW_NEGATIVE_PATTERNS) {
|
|
147
249
|
if (pattern.test(text)) {
|
|
@@ -298,13 +400,22 @@ function collectSuspiciousFields({ detectedSchoolTags }) {
|
|
|
298
400
|
export function parseRecommendInstruction({ instruction, confirmation, overrides }) {
|
|
299
401
|
const text = normalizeText(instruction);
|
|
300
402
|
const detectedSchoolTags = extractSchoolTags(text);
|
|
403
|
+
const detectedDegrees = extractDegrees(text);
|
|
301
404
|
const overrideSchoolTag = normalizeSchoolTag(overrides?.school_tag);
|
|
405
|
+
const overrideDegrees = normalizeDegreeSelections(overrides?.degree);
|
|
302
406
|
const overrideGender = normalizeGender(overrides?.gender);
|
|
303
407
|
const overrideRecentNotView = normalizeRecentNotView(overrides?.recent_not_view);
|
|
304
408
|
const overrideCriteria = overrides?.criteria;
|
|
305
409
|
|
|
306
410
|
const searchParams = {
|
|
307
411
|
school_tag: overrideSchoolTag || detectedSchoolTags[0] || "不限",
|
|
412
|
+
degree: (
|
|
413
|
+
(Array.isArray(overrideDegrees) && overrideDegrees.length > 0
|
|
414
|
+
? overrideDegrees
|
|
415
|
+
: Array.isArray(detectedDegrees) && detectedDegrees.length > 0
|
|
416
|
+
? detectedDegrees
|
|
417
|
+
: ["不限"])
|
|
418
|
+
),
|
|
308
419
|
gender: overrideGender || extractGender(text) || "不限",
|
|
309
420
|
recent_not_view: overrideRecentNotView || extractRecentNotView(text) || "不限"
|
|
310
421
|
};
|
|
@@ -332,17 +443,63 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
332
443
|
}
|
|
333
444
|
|
|
334
445
|
const suspicious_fields = collectSuspiciousFields({ detectedSchoolTags });
|
|
335
|
-
const
|
|
446
|
+
const needs_school_tag_confirmation = confirmation?.school_tag_confirmed !== true;
|
|
447
|
+
const needs_degree_confirmation = confirmation?.degree_confirmed !== true;
|
|
448
|
+
const needs_gender_confirmation = confirmation?.gender_confirmed !== true;
|
|
449
|
+
const needs_recent_not_view_confirmation = confirmation?.recent_not_view_confirmed !== true;
|
|
450
|
+
const needs_filters_confirmation = (
|
|
451
|
+
confirmation?.filters_confirmed !== true
|
|
452
|
+
|| needs_school_tag_confirmation
|
|
453
|
+
|| needs_degree_confirmation
|
|
454
|
+
|| needs_gender_confirmation
|
|
455
|
+
|| needs_recent_not_view_confirmation
|
|
456
|
+
);
|
|
336
457
|
const needs_criteria_confirmation = confirmation?.criteria_confirmed !== true;
|
|
337
458
|
const needs_target_count_confirmation = targetCountResolution.needs_target_count_confirmation;
|
|
338
459
|
const needs_post_action_confirmation = postActionResolution.needs_post_action_confirmation;
|
|
339
460
|
const needs_max_greet_count_confirmation = maxGreetCountResolution.needs_max_greet_count_confirmation;
|
|
340
461
|
const pending_questions = [];
|
|
341
462
|
|
|
342
|
-
if (
|
|
463
|
+
if (needs_school_tag_confirmation) {
|
|
464
|
+
pending_questions.push({
|
|
465
|
+
field: "school_tag",
|
|
466
|
+
question: "请确认学校标签筛选。",
|
|
467
|
+
value: searchParams.school_tag,
|
|
468
|
+
options: SCHOOL_TAG_OPTIONS
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (needs_degree_confirmation) {
|
|
473
|
+
pending_questions.push({
|
|
474
|
+
field: "degree",
|
|
475
|
+
question: "请确认学历筛选(可多选)。",
|
|
476
|
+
value: searchParams.degree,
|
|
477
|
+
options: DEGREE_OPTIONS
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (needs_gender_confirmation) {
|
|
482
|
+
pending_questions.push({
|
|
483
|
+
field: "gender",
|
|
484
|
+
question: "请确认性别筛选。",
|
|
485
|
+
value: searchParams.gender,
|
|
486
|
+
options: GENDER_OPTIONS
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (needs_recent_not_view_confirmation) {
|
|
491
|
+
pending_questions.push({
|
|
492
|
+
field: "recent_not_view",
|
|
493
|
+
question: "请确认是否过滤近14天内已看过的人选。",
|
|
494
|
+
value: searchParams.recent_not_view,
|
|
495
|
+
options: RECENT_NOT_VIEW_OPTIONS
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (needs_filters_confirmation && pending_questions.every((item) => item.field !== "filters")) {
|
|
343
500
|
pending_questions.push({
|
|
344
501
|
field: "filters",
|
|
345
|
-
question: "
|
|
502
|
+
question: "请确认以上推荐页筛选项整体无误。",
|
|
346
503
|
value: searchParams
|
|
347
504
|
});
|
|
348
505
|
}
|
|
@@ -389,6 +546,10 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
389
546
|
missing_fields,
|
|
390
547
|
suspicious_fields,
|
|
391
548
|
needs_filters_confirmation,
|
|
549
|
+
needs_school_tag_confirmation,
|
|
550
|
+
needs_degree_confirmation,
|
|
551
|
+
needs_gender_confirmation,
|
|
552
|
+
needs_recent_not_view_confirmation,
|
|
392
553
|
needs_criteria_confirmation,
|
|
393
554
|
needs_target_count_confirmation,
|
|
394
555
|
needs_post_action_confirmation,
|
|
@@ -415,6 +576,7 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
415
576
|
}
|
|
416
577
|
|
|
417
578
|
export {
|
|
579
|
+
DEGREE_OPTIONS,
|
|
418
580
|
GENDER_OPTIONS,
|
|
419
581
|
POST_ACTION_LABELS,
|
|
420
582
|
POST_ACTION_OPTIONS,
|
package/src/pipeline.js
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
function buildRequiredConfirmations(parsedResult) {
|
|
10
10
|
const confirmations = [];
|
|
11
11
|
if (parsedResult.needs_filters_confirmation) confirmations.push("filters");
|
|
12
|
+
if (parsedResult.needs_school_tag_confirmation) confirmations.push("school_tag");
|
|
13
|
+
if (parsedResult.needs_degree_confirmation) confirmations.push("degree");
|
|
14
|
+
if (parsedResult.needs_gender_confirmation) confirmations.push("gender");
|
|
15
|
+
if (parsedResult.needs_recent_not_view_confirmation) confirmations.push("recent_not_view");
|
|
12
16
|
if (parsedResult.needs_criteria_confirmation) confirmations.push("criteria");
|
|
13
17
|
if (parsedResult.needs_target_count_confirmation) confirmations.push("target_count");
|
|
14
18
|
if (parsedResult.needs_post_action_confirmation) confirmations.push("post_action");
|
|
@@ -89,6 +93,10 @@ export async function runRecommendPipeline(
|
|
|
89
93
|
|
|
90
94
|
if (
|
|
91
95
|
parsed.needs_filters_confirmation
|
|
96
|
+
|| parsed.needs_school_tag_confirmation
|
|
97
|
+
|| parsed.needs_degree_confirmation
|
|
98
|
+
|| parsed.needs_gender_confirmation
|
|
99
|
+
|| parsed.needs_recent_not_view_confirmation
|
|
92
100
|
|| parsed.needs_criteria_confirmation
|
|
93
101
|
|| parsed.needs_target_count_confirmation
|
|
94
102
|
|| parsed.needs_post_action_confirmation
|