@reconcrap/boss-recommend-mcp 0.1.9 → 0.1.10

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 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,9 +73,10 @@ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,
72
73
  配置路径优先级:
73
74
 
74
75
  1. `BOSS_RECOMMEND_SCREEN_CONFIG`
75
- 2. `<workspace>/boss-recommend-mcp/config/screening-config.json`
76
- 3. `~/.boss-recommend-mcp/screening-config.json`
77
- 4. 兼容旧路径:`$CODEX_HOME/boss-recommend-mcp/screening-config.json`
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
 
@@ -132,7 +134,7 @@ node src/cli.js run --instruction-file request.txt --confirmation-file confirmat
132
134
  "max_greet_count_value": 10
133
135
  },
134
136
  "overrides": {
135
- "school_tag": "211",
137
+ "school_tag": ["985", "211"],
136
138
  "degree": ["本科", "硕士", "博士"],
137
139
  "gender": "女",
138
140
  "recent_not_view": "近14天没有",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -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
- - `school_tag`
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",
@@ -76,6 +78,33 @@ function serializeDegreeSelection(value) {
76
78
  return normalized || "不限";
77
79
  }
78
80
 
81
+ function serializeSchoolTagSelection(value) {
82
+ if (Array.isArray(value)) {
83
+ const normalized = value.map((item) => String(item || "").trim()).filter(Boolean);
84
+ if (!normalized.length) return "不限";
85
+ if (normalized.includes("不限")) {
86
+ return normalized.length === 1
87
+ ? "不限"
88
+ : normalized.filter((item) => item !== "不限").join(",");
89
+ }
90
+ return normalized.join(",");
91
+ }
92
+ const normalized = String(value || "").trim();
93
+ return normalized || "不限";
94
+ }
95
+
96
+ function canWriteConfigPath(filePath) {
97
+ if (!filePath) return false;
98
+ try {
99
+ const dirPath = path.dirname(filePath);
100
+ ensureDir(dirPath);
101
+ fs.accessSync(dirPath, fs.constants.W_OK);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
79
108
  function buildScreenConfigCandidateMap(workspaceRoot) {
80
109
  return {
81
110
  env_path: process.env.BOSS_RECOMMEND_SCREEN_CONFIG
@@ -106,6 +135,13 @@ function resolveScreenConfigPath(workspaceRoot) {
106
135
  if (existingWorkspacePath) {
107
136
  return existingWorkspacePath;
108
137
  }
138
+ if (canWriteConfigPath(candidateMap.user_path)) {
139
+ return candidateMap.user_path;
140
+ }
141
+ const writableWorkspacePath = candidateMap.workspace_paths.find((item) => canWriteConfigPath(item));
142
+ if (writableWorkspacePath) {
143
+ return writableWorkspacePath;
144
+ }
109
145
  // 默认固定写入/读取 Agent 无关路径,避免意外落到 .codex 旧路径。
110
146
  if (candidateMap.user_path) {
111
147
  return candidateMap.user_path;
@@ -782,6 +818,16 @@ function findChromeOnboardingUrl(tabs) {
782
818
  return null;
783
819
  }
784
820
 
821
+ function isBossLoginTab(tab) {
822
+ const url = String(tab?.url || "");
823
+ const title = String(tab?.title || "");
824
+ return (
825
+ url === bossLoginUrl
826
+ || bossLoginUrlPattern.test(url)
827
+ || bossLoginTitlePattern.test(title)
828
+ );
829
+ }
830
+
785
831
  export async function inspectBossRecommendPageState(port, options = {}) {
786
832
  const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 6000;
787
833
  const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 1000;
@@ -809,6 +855,21 @@ export async function inspectBossRecommendPageState(port, options = {}) {
809
855
  });
810
856
  }
811
857
 
858
+ const loginTab = tabs.find((tab) => isBossLoginTab(tab));
859
+ if (loginTab) {
860
+ return buildBossPageState({
861
+ ok: false,
862
+ state: "LOGIN_REQUIRED",
863
+ path: loginTab.url || bossLoginUrl,
864
+ current_url: loginTab.url || bossLoginUrl,
865
+ title: loginTab.title || null,
866
+ requires_login: true,
867
+ expected_url: expectedUrl,
868
+ login_url: bossLoginUrl,
869
+ message: "Boss 页面未登录,需先完成登录后再进入 recommend 页面。"
870
+ });
871
+ }
872
+
812
873
  const bossTab = tabs.find(
813
874
  (tab) => typeof tab?.url === "string" && tab.url.includes("zhipin.com")
814
875
  );
@@ -946,6 +1007,17 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
946
1007
  }
947
1008
 
948
1009
  let launchAttempt = null;
1010
+ if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1011
+ return {
1012
+ ok: false,
1013
+ debug_port: debugPort,
1014
+ state: pageState.state,
1015
+ page_state: {
1016
+ ...pageState,
1017
+ launch_attempt: launchAttempt
1018
+ }
1019
+ };
1020
+ }
949
1021
  if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
950
1022
  launchAttempt = launchChromeWithDebugPort(debugPort);
951
1023
  if (launchAttempt.ok) {
@@ -954,6 +1026,17 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
954
1026
  timeoutMs: inspectTimeoutMs,
955
1027
  pollMs
956
1028
  });
1029
+ if (pageState.state === "LOGIN_REQUIRED" || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT") {
1030
+ return {
1031
+ ok: false,
1032
+ debug_port: debugPort,
1033
+ state: pageState.state,
1034
+ page_state: {
1035
+ ...pageState,
1036
+ launch_attempt: launchAttempt
1037
+ }
1038
+ };
1039
+ }
957
1040
  if (pageState.state === "RECOMMEND_READY") {
958
1041
  const stableState = await verifyRecommendPageStable(debugPort, { settleMs, pollMs });
959
1042
  return {
@@ -980,7 +1063,11 @@ export async function ensureBossRecommendPageReady(workspaceRoot, options = {})
980
1063
  }
981
1064
 
982
1065
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
983
- if (pageState.state === "DEBUG_PORT_UNREACHABLE") {
1066
+ if (
1067
+ pageState.state === "DEBUG_PORT_UNREACHABLE"
1068
+ || pageState.state === "LOGIN_REQUIRED"
1069
+ || pageState.state === "LOGIN_REQUIRED_AFTER_REDIRECT"
1070
+ ) {
984
1071
  break;
985
1072
  }
986
1073
  await openBossRecommendTab(debugPort);
@@ -1031,7 +1118,7 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
1031
1118
  const args = [
1032
1119
  cliPath,
1033
1120
  "--school-tag",
1034
- searchParams.school_tag,
1121
+ serializeSchoolTagSelection(searchParams.school_tag),
1035
1122
  "--degree",
1036
1123
  serializeDegreeSelection(searchParams.degree),
1037
1124
  "--gender",
@@ -1047,14 +1134,20 @@ export async function runRecommendSearchCli({ workspaceRoot, searchParams }) {
1047
1134
  cwd: searchDir,
1048
1135
  timeoutMs: 180000
1049
1136
  });
1050
- const structured = parseJsonOutput(result.stdout);
1137
+ const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1138
+ const missingOutputError = result.code === 0 && !structured
1139
+ ? {
1140
+ code: "RECOMMEND_SEARCH_NO_OUTPUT",
1141
+ message: "推荐页筛选命令执行结束但未返回可解析结果。"
1142
+ }
1143
+ : null;
1051
1144
  return {
1052
1145
  ok: result.code === 0 && structured?.status === "COMPLETED",
1053
1146
  stdout: result.stdout,
1054
1147
  stderr: result.stderr,
1055
1148
  structured,
1056
1149
  summary: structured?.result || null,
1057
- error: structured?.error || (
1150
+ error: structured?.error || missingOutputError || (
1058
1151
  result.code === 0
1059
1152
  ? null
1060
1153
  : {
@@ -1146,14 +1239,20 @@ export async function runRecommendScreenCli({ workspaceRoot, screenParams }) {
1146
1239
  cwd: screenDir,
1147
1240
  timeoutMs: 60 * 60 * 1000
1148
1241
  });
1149
- const structured = parseJsonOutput(result.stdout);
1242
+ const structured = parseJsonOutput(result.stdout) || parseJsonOutput(result.stderr);
1243
+ const missingOutputError = result.code === 0 && !structured
1244
+ ? {
1245
+ code: "RECOMMEND_SCREEN_NO_OUTPUT",
1246
+ message: "推荐页筛选命令执行结束但未返回可解析结果。"
1247
+ }
1248
+ : null;
1150
1249
  return {
1151
1250
  ok: result.code === 0 && structured?.status === "COMPLETED",
1152
1251
  stdout: result.stdout,
1153
1252
  stderr: result.stderr,
1154
1253
  structured,
1155
1254
  summary: structured?.result || null,
1156
- error: structured?.error || (
1255
+ error: structured?.error || missingOutputError || (
1157
1256
  result.code === 0
1158
1257
  ? null
1159
1258
  : {
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 userConfigExists = fs.existsSync(configResolution.writable_path) || fs.existsSync(configResolution.legacy_path);
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: configResolution.writable_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
- type: "string",
76
- enum: ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"]
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 thisFile = fileURLToPath(import.meta.url);
226
- const mcpRoot = path.resolve(path.dirname(thisFile), "..");
227
- const workspaceRoot = envRoot ? path.resolve(envRoot) : path.resolve(mcpRoot, "..");
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
- const suspicious = [];
390
- if (detectedSchoolTags.length > 1) {
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 = normalizeSchoolTag(overrides?.school_tag);
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 > 1
411
- ? "不限"
412
- : detectedSchoolTags[0] || "不限";
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
- ? `检测到多个学校标签(${detectedSchoolTags.join(" / ")})。推荐页学校标签为单选,请确认本次最终选择。`
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
- ? "3) 当前检测到 Boss 未登录,请先在该 Chrome 实例完成登录。"
228
+ ? `3) 当前检测到 Boss 未登录,请先打开并完成登录:${loginUrl}`
228
229
  : "3) 如 Boss 登录态失效,请先重新登录。",
229
230
  `4) 登录完成后先导航并停留在推荐页:${expectedUrl}`,
230
231
  "5) 完成后回复“已就绪”,我会继续执行并优先自动导航到推荐页。"
@@ -8,7 +8,7 @@ function testNeedConfirmationIncludesPostAction() {
8
8
  overrides: null
9
9
  });
10
10
 
11
- assert.equal(result.searchParams.school_tag, "985");
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.equal(result.searchParams.school_tag, "211");
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.equal(result.searchParams.school_tag, "不限");
86
+ assert.deepEqual(result.searchParams.school_tag, ["985", "211"]);
87
87
  assert.deepEqual(result.searchParams.degree, ["不限"]);
88
- assert.equal(result.suspicious_fields.length, 1);
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();
@@ -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 || "推荐列表不可用");