@reconcrap/boss-recommend-mcp 0.1.9 → 0.1.11
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 +7 -4
- package/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +2 -2
- package/src/adapters.js +102 -8
- package/src/cli.js +8 -3
- package/src/index.js +20 -5
- package/src/parser.js +46 -15
- package/src/pipeline.js +2 -1
- package/src/test-parser.js +17 -5
- package/src/test-pipeline.js +4 -2
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +12 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ MCP 工具名:`run_recommend_pipeline`
|
|
|
20
20
|
|
|
21
21
|
- 页面目标固定为 `https://www.zhipin.com/web/chat/recommend`
|
|
22
22
|
- 支持推荐页原生筛选:学校标签 / 学历 / 性别 / 近14天没有
|
|
23
|
+
- 学校标签支持多选语义:如“985、211”会同时勾选这两项
|
|
23
24
|
- 学历支持单选与多选语义:如“本科及以上”会展开为 `本科/硕士/博士`;如“大专、本科”只勾选这两项
|
|
24
25
|
- 执行前会逐项确认筛选参数:学校标签 / 学历 / 性别 / 是否过滤近14天已看
|
|
25
26
|
- npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
|
|
@@ -72,14 +73,16 @@ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,
|
|
|
72
73
|
配置路径优先级:
|
|
73
74
|
|
|
74
75
|
1. `BOSS_RECOMMEND_SCREEN_CONFIG`
|
|
75
|
-
2. `<workspace>/
|
|
76
|
-
3.
|
|
77
|
-
4.
|
|
76
|
+
2. `<workspace>/config/screening-config.json`
|
|
77
|
+
3. `<workspace>/boss-recommend-mcp/config/screening-config.json`
|
|
78
|
+
4. `~/.boss-recommend-mcp/screening-config.json`(若可写)
|
|
79
|
+
5. 兼容旧路径:`$CODEX_HOME/boss-recommend-mcp/screening-config.json`
|
|
78
80
|
|
|
79
81
|
注意:
|
|
80
82
|
|
|
81
83
|
- `install` 阶段不会自动创建 `screening-config.json`
|
|
82
84
|
- 首次运行若缺配置,会由 doctor / pipeline 明确提示用户填写 `baseUrl`、`apiKey`、`model`
|
|
85
|
+
- 在 `npx` 临时目录(如 `AppData\\Local\\npm-cache\\_npx\\...`)执行时,不会再把该临时目录当作 `screening-config.json` 目标路径
|
|
83
86
|
|
|
84
87
|
配置样例见:
|
|
85
88
|
|
|
@@ -132,7 +135,7 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
|
|
|
132
135
|
"max_greet_count_value": 10
|
|
133
136
|
},
|
|
134
137
|
"overrides": {
|
|
135
|
-
"school_tag": "211",
|
|
138
|
+
"school_tag": ["985", "211"],
|
|
136
139
|
"degree": ["本科", "硕士", "博士"],
|
|
137
140
|
"gender": "女",
|
|
138
141
|
"recent_not_view": "近14天没有",
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
35
35
|
|
|
36
36
|
在真正执行前,必须先确认:
|
|
37
37
|
|
|
38
|
-
- 学校标签(`school_tag
|
|
38
|
+
- 学校标签(`school_tag`,支持多选)
|
|
39
39
|
- 学历(`degree`)
|
|
40
40
|
- 性别(`gender`)
|
|
41
41
|
- 是否过滤近14天已看(`recent_not_view`)
|
|
@@ -72,7 +72,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
|
|
|
72
72
|
- `max_greet_count_confirmed`
|
|
73
73
|
- `max_greet_count_value` (integer)
|
|
74
74
|
- `overrides`
|
|
75
|
-
|
|
75
|
+
- `school_tag`(可传单值或数组,如 `["985","211"]`)
|
|
76
76
|
- `degree`(可传单值或数组;如“本科及以上”应展开为 `["本科","硕士","博士"]`)
|
|
77
77
|
- `gender`
|
|
78
78
|
- `recent_not_view`
|
package/src/adapters.js
CHANGED
|
@@ -7,8 +7,10 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
8
8
|
const packagedMcpDir = path.resolve(path.dirname(currentFilePath), "..");
|
|
9
9
|
const bossRecommendUrl = "https://www.zhipin.com/web/chat/recommend";
|
|
10
|
+
const bossLoginUrl = "https://www.zhipin.com/web/user/?ka=bticket";
|
|
10
11
|
const chromeOnboardingUrlPattern = /^chrome:\/\/(welcome|intro|newtab|signin|history-sync|settings\/syncSetup)/i;
|
|
11
|
-
const bossLoginUrlPattern = /zhipin\.com\/web\/user|passport\.zhipin\.com/i;
|
|
12
|
+
const bossLoginUrlPattern = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
|
|
13
|
+
const bossLoginTitlePattern = /登录|signin|扫码登录|BOSS直聘登录/i;
|
|
12
14
|
const screenConfigTemplateDefaults = {
|
|
13
15
|
baseUrl: "https://api.openai.com/v1",
|
|
14
16
|
apiKey: "replace-with-openai-api-key",
|
|
@@ -58,6 +60,9 @@ function parsePositiveInteger(raw) {
|
|
|
58
60
|
|
|
59
61
|
function resolveWorkspaceConfigCandidates(workspaceRoot) {
|
|
60
62
|
const root = path.resolve(String(workspaceRoot || process.cwd()));
|
|
63
|
+
if (isEphemeralNpxWorkspaceRoot(root)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
61
66
|
const directPath = path.join(root, "config", "screening-config.json");
|
|
62
67
|
const nestedPath = path.join(root, "boss-recommend-mcp", "config", "screening-config.json");
|
|
63
68
|
const candidates = [directPath];
|
|
@@ -76,6 +81,30 @@ function serializeDegreeSelection(value) {
|
|
|
76
81
|
return normalized || "不限";
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
function serializeSchoolTagSelection(value) {
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
87
|
+
if (!normalized.length) return "不限";
|
|
88
|
+
if (normalized.includes("不限")) {
|
|
89
|
+
return normalized.length === 1
|
|
90
|
+
? "不限"
|
|
91
|
+
: normalized.filter((item) => item !== "不限").join(",");
|
|
92
|
+
}
|
|
93
|
+
return normalized.join(",");
|
|
94
|
+
}
|
|
95
|
+
const normalized = String(value || "").trim();
|
|
96
|
+
return normalized || "不限";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isEphemeralNpxWorkspaceRoot(workspaceRoot) {
|
|
100
|
+
const root = path.resolve(String(workspaceRoot || ""));
|
|
101
|
+
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
102
|
+
return (
|
|
103
|
+
normalized.includes("/appdata/local/npm-cache/_npx/")
|
|
104
|
+
|| normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
function buildScreenConfigCandidateMap(workspaceRoot) {
|
|
80
109
|
return {
|
|
81
110
|
env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
|
|
@@ -106,7 +135,7 @@ function resolveScreenConfigPath(workspaceRoot) {
|
|
|
106
135
|
if (existingWorkspacePath) {
|
|
107
136
|
return existingWorkspacePath;
|
|
108
137
|
}
|
|
109
|
-
// 默认固定写入/读取 Agent
|
|
138
|
+
// 默认固定写入/读取 Agent 无关路径,避免落到 npx 临时目录或 .codex 旧路径。
|
|
110
139
|
if (candidateMap.user_path) {
|
|
111
140
|
return candidateMap.user_path;
|
|
112
141
|
}
|
|
@@ -120,6 +149,8 @@ export function getScreenConfigResolution(workspaceRoot) {
|
|
|
120
149
|
return {
|
|
121
150
|
resolved_path,
|
|
122
151
|
candidate_paths,
|
|
152
|
+
workspace_root: path.resolve(String(workspaceRoot || process.cwd())),
|
|
153
|
+
workspace_ephemeral: isEphemeralNpxWorkspaceRoot(workspaceRoot),
|
|
123
154
|
writable_path: candidateMap.user_path,
|
|
124
155
|
legacy_path: candidateMap.legacy_path
|
|
125
156
|
};
|
|
@@ -782,6 +813,16 @@ function findChromeOnboardingUrl(tabs) {
|
|
|
782
813
|
return null;
|
|
783
814
|
}
|
|
784
815
|
|
|
816
|
+
function isBossLoginTab(tab) {
|
|
817
|
+
const url = String(tab?.url || "");
|
|
818
|
+
const title = String(tab?.title || "");
|
|
819
|
+
return (
|
|
820
|
+
url === bossLoginUrl
|
|
821
|
+
|| bossLoginUrlPattern.test(url)
|
|
822
|
+
|| bossLoginTitlePattern.test(title)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
785
826
|
export async function inspectBossRecommendPageState(port, options = {}) {
|
|
786
827
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
|
|
787
828
|
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
|
|
@@ -809,6 +850,21 @@ export async function inspectBossRecommendPageState(port, options = {}) {
|
|
|
809
850
|
});
|
|
810
851
|
}
|
|
811
852
|
|
|
853
|
+
const loginTab = tabs.find((tab) => isBossLoginTab(tab));
|
|
854
|
+
if (loginTab) {
|
|
855
|
+
return buildBossPageState({
|
|
856
|
+
ok: false,
|
|
857
|
+
state: "LOGIN_REQUIRED",
|
|
858
|
+
path: loginTab.url || bossLoginUrl,
|
|
859
|
+
current_url: loginTab.url || bossLoginUrl,
|
|
860
|
+
title: loginTab.title || null,
|
|
861
|
+
requires_login: true,
|
|
862
|
+
expected_url: expectedUrl,
|
|
863
|
+
login_url: bossLoginUrl,
|
|
864
|
+
message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
812
868
|
const bossTab = tabs.find(
|
|
813
869
|
(tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
|
|
814
870
|
);
|
|
@@ -946,6 +1002,17 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
946
1002
|
}
|
|
947
1003
|
|
|
948
1004
|
let launchAttempt = null;
|
|
1005
|
+
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1006
|
+
return {
|
|
1007
|
+
ok: false,
|
|
1008
|
+
debug_port: debugPort,
|
|
1009
|
+
state: pageState.state,
|
|
1010
|
+
page_state: {
|
|
1011
|
+
...pageState,
|
|
1012
|
+
launch_attempt: launchAttempt
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
949
1016
|
if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
|
|
950
1017
|
launchAttempt = launchChromeWithDebugPort(debugPort);
|
|
951
1018
|
if (launchAttempt.ok) {
|
|
@@ -954,6 +1021,17 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
954
1021
|
timeoutMs: inspectTimeoutMs,
|
|
955
1022
|
pollMs
|
|
956
1023
|
});
|
|
1024
|
+
if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
|
|
1025
|
+
return {
|
|
1026
|
+
ok: false,
|
|
1027
|
+
debug_port: debugPort,
|
|
1028
|
+
state: pageState.state,
|
|
1029
|
+
page_state: {
|
|
1030
|
+
...pageState,
|
|
1031
|
+
launch_attempt: launchAttempt
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
957
1035
|
if (pageState.state === "RECOMMEND_READY") {
|
|
958
1036
|
const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
|
|
959
1037
|
return {
|
|
@@ -980,7 +1058,11 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
|
|
|
980
1058
|
}
|
|
981
1059
|
|
|
982
1060
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
983
|
-
if (
|
|
1061
|
+
if (
|
|
1062
|
+
pageState.state === "DEBUG_PORT_UNREACHABLE"
|
|
1063
|
+
|| pageState.state === "LOGIN_REQUIRED"
|
|
1064
|
+
|| pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
|
|
1065
|
+
) {
|
|
984
1066
|
break;
|
|
985
1067
|
}
|
|
986
1068
|
await openBossRecommendTab(debugPort);
|
|
@@ -1031,7 +1113,7 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
|
|
|
1031
1113
|
const args = [
|
|
1032
1114
|
cliPath,
|
|
1033
1115
|
"--school-tag",
|
|
1034
|
-
searchParams.school_tag,
|
|
1116
|
+
serializeSchoolTagSelection(searchParams.school_tag),
|
|
1035
1117
|
"--degree",
|
|
1036
1118
|
serializeDegreeSelection(searchParams.degree),
|
|
1037
1119
|
"--gender",
|
|
@@ -1047,14 +1129,20 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
|
|
|
1047
1129
|
cwd: searchDir,
|
|
1048
1130
|
timeoutMs: 180000
|
|
1049
1131
|
});
|
|
1050
|
-
const structured = parseJsonOutput(result.stdout);
|
|
1132
|
+
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1133
|
+
const missingOutputError = result.code === 0 && !structured
|
|
1134
|
+
? {
|
|
1135
|
+
code: "RECOMMEND_SEARCH_NO_OUTPUT",
|
|
1136
|
+
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1137
|
+
}
|
|
1138
|
+
: null;
|
|
1051
1139
|
return {
|
|
1052
1140
|
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
1053
1141
|
stdout: result.stdout,
|
|
1054
1142
|
stderr: result.stderr,
|
|
1055
1143
|
structured,
|
|
1056
1144
|
summary: structured?.result || null,
|
|
1057
|
-
error: structured?.error || (
|
|
1145
|
+
error: structured?.error || missingOutputError || (
|
|
1058
1146
|
result.code === 0
|
|
1059
1147
|
? null
|
|
1060
1148
|
: {
|
|
@@ -1146,14 +1234,20 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams }) {
|
|
|
1146
1234
|
cwd: screenDir,
|
|
1147
1235
|
timeoutMs: 60 * 60 * 1000
|
|
1148
1236
|
});
|
|
1149
|
-
const structured = parseJsonOutput(result.stdout);
|
|
1237
|
+
const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
|
|
1238
|
+
const missingOutputError = result.code === 0 && !structured
|
|
1239
|
+
? {
|
|
1240
|
+
code: "RECOMMEND_SCREEN_NO_OUTPUT",
|
|
1241
|
+
message: "推荐页筛选命令执行结束但未返回可解析结果。"
|
|
1242
|
+
}
|
|
1243
|
+
: null;
|
|
1150
1244
|
return {
|
|
1151
1245
|
ok: result.code === 0 && structured?.status === "COMPLETED",
|
|
1152
1246
|
stdout: result.stdout,
|
|
1153
1247
|
stderr: result.stderr,
|
|
1154
1248
|
structured,
|
|
1155
1249
|
summary: structured?.result || null,
|
|
1156
|
-
error: structured?.error || (
|
|
1250
|
+
error: structured?.error || missingOutputError || (
|
|
1157
1251
|
result.code === 0
|
|
1158
1252
|
? null
|
|
1159
1253
|
: {
|
package/src/cli.js
CHANGED
|
@@ -687,13 +687,18 @@ async function printDoctor(options = {}) {
|
|
|
687
687
|
const checks = preflight.checks.slice();
|
|
688
688
|
const configResolution = getScreenConfigResolution(workspaceRoot);
|
|
689
689
|
const pageState = await inspectBossRecommendPageState(port, { timeoutMs: 2000, pollMs: 500 });
|
|
690
|
-
const
|
|
690
|
+
const resolvedConfigPath = configResolution.resolved_path || configResolution.writable_path;
|
|
691
|
+
const userConfigExists = (
|
|
692
|
+
(resolvedConfigPath && fs.existsSync(resolvedConfigPath))
|
|
693
|
+
|| fs.existsSync(configResolution.writable_path)
|
|
694
|
+
|| fs.existsSync(configResolution.legacy_path)
|
|
695
|
+
);
|
|
691
696
|
checks.push({
|
|
692
697
|
key: "user_config",
|
|
693
698
|
ok: userConfigExists,
|
|
694
|
-
path:
|
|
699
|
+
path: resolvedConfigPath,
|
|
695
700
|
message: userConfigExists
|
|
696
|
-
?
|
|
701
|
+
? `检测到配置文件(resolved_path):${resolvedConfigPath}`
|
|
697
702
|
: "用户配置不存在(可通过 `boss-recommend-mcp init-config` 创建模板,或 `boss-recommend-mcp config set` 写入真实值)"
|
|
698
703
|
});
|
|
699
704
|
checks.push({
|
package/src/index.js
CHANGED
|
@@ -72,8 +72,21 @@ function createToolSchema() {
|
|
|
72
72
|
type: "object",
|
|
73
73
|
properties: {
|
|
74
74
|
school_tag: {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
oneOf: [
|
|
76
|
+
{
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "array",
|
|
82
|
+
items: {
|
|
83
|
+
type: "string",
|
|
84
|
+
enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
|
|
85
|
+
},
|
|
86
|
+
minItems: 1,
|
|
87
|
+
uniqueItems: true
|
|
88
|
+
}
|
|
89
|
+
]
|
|
77
90
|
},
|
|
78
91
|
degree: {
|
|
79
92
|
oneOf: [
|
|
@@ -222,9 +235,11 @@ async function handleRequest(message, workspaceRoot) {
|
|
|
222
235
|
|
|
223
236
|
export function startServer() {
|
|
224
237
|
const envRoot = process.env.BOSS_WORKSPACE_ROOT;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
const workspaceRoot = envRoot
|
|
239
|
+
? path.resolve(envRoot)
|
|
240
|
+
: process.env.INIT_CWD
|
|
241
|
+
? path.resolve(process.env.INIT_CWD)
|
|
242
|
+
: path.resolve(process.cwd());
|
|
228
243
|
let buffer = Buffer.alloc(0);
|
|
229
244
|
let framing = FRAMING_UNKNOWN;
|
|
230
245
|
|
package/src/parser.js
CHANGED
|
@@ -118,6 +118,44 @@ function normalizeSchoolTag(value) {
|
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function sortSchoolTagSelections(values) {
|
|
122
|
+
const order = new Map(SCHOOL_TAG_OPTIONS.map((item, index) => [item, index]));
|
|
123
|
+
const unique = Array.from(
|
|
124
|
+
new Set((values || []).map((item) => normalizeSchoolTag(item)).filter(Boolean))
|
|
125
|
+
);
|
|
126
|
+
if (!unique.length) return [];
|
|
127
|
+
if (unique.includes("不限")) {
|
|
128
|
+
return unique.length === 1
|
|
129
|
+
? ["不限"]
|
|
130
|
+
: unique.filter((item) => item !== "不限").sort((left, right) => order.get(left) - order.get(right));
|
|
131
|
+
}
|
|
132
|
+
return unique.sort((left, right) => order.get(left) - order.get(right));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeSchoolTagSelections(input) {
|
|
136
|
+
if (Array.isArray(input)) {
|
|
137
|
+
const normalized = sortSchoolTagSelections(input);
|
|
138
|
+
return normalized.length ? normalized : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const text = normalizeText(input);
|
|
142
|
+
if (!text) return null;
|
|
143
|
+
if (text === "不限") return ["不限"];
|
|
144
|
+
const selected = [];
|
|
145
|
+
const chunks = text.split(/[,,、/|]/).map((item) => normalizeSchoolTag(item)).filter(Boolean);
|
|
146
|
+
selected.push(...chunks);
|
|
147
|
+
for (const label of SCHOOL_TAG_OPTIONS) {
|
|
148
|
+
if (label === "不限") continue;
|
|
149
|
+
if (text.includes(label)) {
|
|
150
|
+
selected.push(label);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const normalized = sortSchoolTagSelections(selected);
|
|
154
|
+
if (normalized.length > 0) return normalized;
|
|
155
|
+
const single = normalizeSchoolTag(text);
|
|
156
|
+
return single ? [single] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
121
159
|
function normalizeDegree(value) {
|
|
122
160
|
const normalized = normalizeText(value);
|
|
123
161
|
if (!normalized) return null;
|
|
@@ -386,30 +424,23 @@ function resolveMaxGreetCount({ instruction, confirmation, overrides, postAction
|
|
|
386
424
|
}
|
|
387
425
|
|
|
388
426
|
function collectSuspiciousFields({ detectedSchoolTags }) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
suspicious.push({
|
|
392
|
-
field: "school_tag",
|
|
393
|
-
value: detectedSchoolTags,
|
|
394
|
-
reason: "推荐页学校标签当前是单选,指令里同时提到了多个学校标签,请确认最终要应用哪一个。"
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
return suspicious;
|
|
427
|
+
void detectedSchoolTags;
|
|
428
|
+
return [];
|
|
398
429
|
}
|
|
399
430
|
|
|
400
431
|
export function parseRecommendInstruction({ instruction, confirmation, overrides }) {
|
|
401
432
|
const text = normalizeText(instruction);
|
|
402
433
|
const detectedSchoolTags = extractSchoolTags(text);
|
|
403
434
|
const detectedDegrees = extractDegrees(text);
|
|
404
|
-
const overrideSchoolTag =
|
|
435
|
+
const overrideSchoolTag = normalizeSchoolTagSelections(overrides?.school_tag);
|
|
405
436
|
const overrideDegrees = normalizeDegreeSelections(overrides?.degree);
|
|
406
437
|
const overrideGender = normalizeGender(overrides?.gender);
|
|
407
438
|
const overrideRecentNotView = normalizeRecentNotView(overrides?.recent_not_view);
|
|
408
439
|
const overrideCriteria = overrides?.criteria;
|
|
409
440
|
|
|
410
|
-
const inferredSchoolTag = detectedSchoolTags.length >
|
|
411
|
-
?
|
|
412
|
-
:
|
|
441
|
+
const inferredSchoolTag = detectedSchoolTags.length > 0
|
|
442
|
+
? sortSchoolTagSelections(detectedSchoolTags)
|
|
443
|
+
: ["不限"];
|
|
413
444
|
const searchParams = {
|
|
414
445
|
school_tag: overrideSchoolTag || inferredSchoolTag,
|
|
415
446
|
degree: (
|
|
@@ -465,8 +496,8 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
|
|
|
465
496
|
|
|
466
497
|
if (needs_school_tag_confirmation) {
|
|
467
498
|
const schoolTagQuestion = detectedSchoolTags.length > 1
|
|
468
|
-
?
|
|
469
|
-
: "
|
|
499
|
+
? `检测到学校标签:${detectedSchoolTags.join(" / ")}。请确认学校标签筛选(可多选)。`
|
|
500
|
+
: "请确认学校标签筛选(可多选)。";
|
|
470
501
|
pending_questions.push({
|
|
471
502
|
field: "school_tag",
|
|
472
503
|
question: schoolTagQuestion,
|
package/src/pipeline.js
CHANGED
|
@@ -208,6 +208,7 @@ function buildFailedResponse(code, message, extra = {}) {
|
|
|
208
208
|
|
|
209
209
|
function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
210
210
|
const expectedUrl = pageState?.expected_url || "https://www.zhipin.com/web/chat/recommend";
|
|
211
|
+
const loginUrl = pageState?.login_url || "https://www.zhipin.com/web/user/?ka=bticket";
|
|
211
212
|
const currentUrl = pageState?.current_url || null;
|
|
212
213
|
const state = pageState?.state || "UNKNOWN";
|
|
213
214
|
const isPortIssue = state === "DEBUG_PORT_UNREACHABLE";
|
|
@@ -224,7 +225,7 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
|
|
|
224
225
|
? `2) 若端口不可连接,请用远程调试方式启动 Chrome(示例:chrome.exe --remote-debugging-port=${debugPort})。`
|
|
225
226
|
: "2) 确认端口可连接且浏览器窗口保持打开。",
|
|
226
227
|
needsLogin
|
|
227
|
-
?
|
|
228
|
+
? `3) 当前检测到 Boss 未登录,请先打开并完成登录:${loginUrl}`
|
|
228
229
|
: "3) 如 Boss 登录态失效,请先重新登录。",
|
|
229
230
|
`4) 登录完成后先导航并停留在推荐页:${expectedUrl}`,
|
|
230
231
|
"5) 完成后回复“已就绪”,我会继续执行并优先自动导航到推荐页。"
|
package/src/test-parser.js
CHANGED
|
@@ -8,7 +8,7 @@ function testNeedConfirmationIncludesPostAction() {
|
|
|
8
8
|
overrides: null
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
assert.
|
|
11
|
+
assert.deepEqual(result.searchParams.school_tag, ["985"]);
|
|
12
12
|
assert.deepEqual(result.searchParams.degree, ["不限"]);
|
|
13
13
|
assert.equal(result.searchParams.gender, "男");
|
|
14
14
|
assert.equal(result.searchParams.recent_not_view, "近14天没有");
|
|
@@ -48,7 +48,7 @@ function testConfirmedPostActionAndOverrides() {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
assert.
|
|
51
|
+
assert.deepEqual(result.searchParams.school_tag, ["211"]);
|
|
52
52
|
assert.deepEqual(result.searchParams.degree, ["本科"]);
|
|
53
53
|
assert.equal(result.searchParams.gender, "女");
|
|
54
54
|
assert.equal(result.searchParams.recent_not_view, "近14天没有");
|
|
@@ -83,10 +83,9 @@ function testMultipleSchoolTagsMarkedSuspicious() {
|
|
|
83
83
|
overrides: null
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
assert.
|
|
86
|
+
assert.deepEqual(result.searchParams.school_tag, ["985", "211"]);
|
|
87
87
|
assert.deepEqual(result.searchParams.degree, ["不限"]);
|
|
88
|
-
assert.equal(result.suspicious_fields.length,
|
|
89
|
-
assert.equal(result.suspicious_fields[0].field, "school_tag");
|
|
88
|
+
assert.equal(result.suspicious_fields.length, 0);
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
function testDegreeCanBeExtracted() {
|
|
@@ -131,6 +130,18 @@ function testDegreeOverrideCanBeArray() {
|
|
|
131
130
|
assert.deepEqual(result.searchParams.degree, ["大专", "本科"]);
|
|
132
131
|
}
|
|
133
132
|
|
|
133
|
+
function testSchoolTagOverrideCanBeArray() {
|
|
134
|
+
const result = parseRecommendInstruction({
|
|
135
|
+
instruction: "推荐页筛选985候选人,有算法经验",
|
|
136
|
+
confirmation: null,
|
|
137
|
+
overrides: {
|
|
138
|
+
school_tag: ["985", "211"]
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.deepEqual(result.searchParams.school_tag, ["985", "211"]);
|
|
143
|
+
}
|
|
144
|
+
|
|
134
145
|
function testCriteriaCanBeProvidedViaOverrides() {
|
|
135
146
|
const result = parseRecommendInstruction({
|
|
136
147
|
instruction: "推荐页筛选211女生",
|
|
@@ -281,6 +292,7 @@ function main() {
|
|
|
281
292
|
testDegreeAtOrAboveExpansion();
|
|
282
293
|
testDegreeExplicitListOnly();
|
|
283
294
|
testDegreeOverrideCanBeArray();
|
|
295
|
+
testSchoolTagOverrideCanBeArray();
|
|
284
296
|
testCriteriaCanBeProvidedViaOverrides();
|
|
285
297
|
testMissingCriteriaTriggersNeedInput();
|
|
286
298
|
testMcpMentionShouldStayInCriteria();
|
package/src/test-pipeline.js
CHANGED
|
@@ -4,7 +4,7 @@ import { runRecommendPipeline } from "./pipeline.js";
|
|
|
4
4
|
function createParsed(overrides = {}) {
|
|
5
5
|
return {
|
|
6
6
|
searchParams: {
|
|
7
|
-
school_tag: "985",
|
|
7
|
+
school_tag: ["985"],
|
|
8
8
|
degree: ["本科"],
|
|
9
9
|
gender: "男",
|
|
10
10
|
recent_not_view: "近14天没有"
|
|
@@ -267,7 +267,8 @@ async function testLoginRequiredShouldReturnGuidance() {
|
|
|
267
267
|
page_state: {
|
|
268
268
|
state: "LOGIN_REQUIRED",
|
|
269
269
|
expected_url: "https://www.zhipin.com/web/chat/recommend",
|
|
270
|
-
current_url: "https://www.zhipin.com/web/geek/job"
|
|
270
|
+
current_url: "https://www.zhipin.com/web/geek/job",
|
|
271
|
+
login_url: "https://www.zhipin.com/web/user/?ka=bticket"
|
|
271
272
|
}
|
|
272
273
|
}),
|
|
273
274
|
runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
|
|
@@ -281,6 +282,7 @@ async function testLoginRequiredShouldReturnGuidance() {
|
|
|
281
282
|
assert.equal(result.guidance.debug_port, 9555);
|
|
282
283
|
assert.equal(result.guidance.expected_url, "https://www.zhipin.com/web/chat/recommend");
|
|
283
284
|
assert.equal(result.guidance.agent_prompt.includes("9555"), true);
|
|
285
|
+
assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
|
|
284
286
|
assert.equal(result.guidance.agent_prompt.includes("已就绪"), true);
|
|
285
287
|
}
|
|
286
288
|
|
|
@@ -1559,6 +1559,18 @@ class RecommendScreenCli {
|
|
|
1559
1559
|
|
|
1560
1560
|
await this.connect();
|
|
1561
1561
|
try {
|
|
1562
|
+
const startupDetailState = await this.getDetailClosedState();
|
|
1563
|
+
if (!startupDetailState?.closed) {
|
|
1564
|
+
log("[恢复] 检测到详情页处于打开状态,先尝试关闭后再继续筛选");
|
|
1565
|
+
const startupClosed = await this.closeDetailPage(4);
|
|
1566
|
+
if (!startupClosed) {
|
|
1567
|
+
throw this.buildError("DETAIL_CLOSE_FAILED_AT_START", "启动时未能关闭遗留详情页");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const startupListReady = await this.waitForListReady(18);
|
|
1571
|
+
if (!startupListReady) {
|
|
1572
|
+
throw this.buildError("RECOMMEND_PAGE_NOT_READY", "推荐列表未就绪(可能仍停留在详情页)");
|
|
1573
|
+
}
|
|
1562
1574
|
const initialList = await this.evaluate(jsGetListState);
|
|
1563
1575
|
if (!initialList?.ok) {
|
|
1564
1576
|
throw this.buildError("RECOMMEND_PAGE_NOT_READY", initialList?.error || "推荐列表不可用");
|