@reconcrap/boss-recommend-mcp 1.0.7 → 1.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -27,6 +27,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
27
27
  - 当用户请求中出现 “recommend / 推荐页 / boss recommend / recommend page” 语义时,只能走 `boss-recommend-pipeline`
28
28
  - 当用户请求中出现 “search / 搜索页 / boss search / search page” 语义时,不能走本技能,必须转交 `boss-recruit-pipeline` 并调用 `run_recruit_pipeline`
29
29
  - 不要调用 `boss-recruit-pipeline`,也不要调用 `run_recruit_pipeline`,除非上一条(search/搜索页语义)命中
30
+ - 当 recommend 流程返回任何错误(包括 `JOB_TRIGGER_NOT_FOUND` / `NO_RECOMMEND_IFRAME` / `BOSS_LOGIN_REQUIRED`)时,禁止把 recommend 请求降级到 recruit 流程;必须先修复 recommend 的页面就绪或登录态问题
30
31
 
31
32
  路由示例(中英文都要兼容):
32
33
 
package/src/index.js CHANGED
@@ -33,7 +33,7 @@ function createJsonRpcError(id, code, message) {
33
33
  function createToolSchema() {
34
34
  return {
35
35
  name: TOOL_NAME,
36
- description: "Boss 推荐页流水线:解析推荐筛选指令、确认岗位/筛选条件/post_action(greet 时确认 max_greet_count),先点击确认岗位,再执行 recommend filter 与 recommend screen 并返回摘要。",
36
+ description: "Boss 推荐页流水线:解析推荐筛选指令、确认岗位/筛选条件/post_action(greet 时确认 max_greet_count),先点击确认岗位,再执行 recommend filter 与 recommend screen 并返回摘要;也支持通过 instruction 直接发送“暂停 / 停止 / 继续”控制流程状态。",
37
37
  inputSchema: {
38
38
  type: "object",
39
39
  properties: {
package/src/pipeline.js CHANGED
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
1
3
  import path from "node:path";
2
4
  import { parseRecommendInstruction } from "./parser.js";
3
5
  import {
@@ -379,6 +381,122 @@ function buildChromeSetupGuidance({ debugPort, pageState }) {
379
381
  };
380
382
  }
381
383
 
384
+ const PIPELINE_CONTROL_STATE_RUNNING = "running";
385
+ const PIPELINE_CONTROL_STATE_PAUSED = "paused";
386
+ const PIPELINE_CONTROL_STATE_STOPPED = "stopped";
387
+ const PIPELINE_CONTROL_ACTION_PAUSE = "pause";
388
+ const PIPELINE_CONTROL_ACTION_STOP = "stop";
389
+ const PIPELINE_CONTROL_ACTION_CONTINUE = "continue";
390
+
391
+ function getStateHome() {
392
+ return process.env.BOSS_RECOMMEND_HOME
393
+ ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
394
+ : path.join(os.homedir(), ".boss-recommend-mcp");
395
+ }
396
+
397
+ function getPipelineControlStatePath() {
398
+ return path.join(getStateHome(), "pipeline-control-state.json");
399
+ }
400
+
401
+ function normalizePipelineControlState(value) {
402
+ const normalized = normalizeText(value).toLowerCase();
403
+ if (normalized === PIPELINE_CONTROL_STATE_PAUSED) return PIPELINE_CONTROL_STATE_PAUSED;
404
+ if (normalized === PIPELINE_CONTROL_STATE_STOPPED) return PIPELINE_CONTROL_STATE_STOPPED;
405
+ return PIPELINE_CONTROL_STATE_RUNNING;
406
+ }
407
+
408
+ function readPipelineControlFile() {
409
+ const filePath = getPipelineControlStatePath();
410
+ try {
411
+ if (!fs.existsSync(filePath)) {
412
+ return null;
413
+ }
414
+ const raw = fs.readFileSync(filePath, "utf8");
415
+ return JSON.parse(raw);
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+
421
+ function defaultGetPipelineControlState() {
422
+ const payload = readPipelineControlFile();
423
+ const state = normalizePipelineControlState(payload?.state);
424
+ return {
425
+ state,
426
+ action: normalizeText(payload?.action || "") || null,
427
+ updated_at: normalizeText(payload?.updated_at || "") || null
428
+ };
429
+ }
430
+
431
+ function defaultSetPipelineControlState(nextState, metadata = {}) {
432
+ const state = normalizePipelineControlState(nextState);
433
+ const payload = {
434
+ state,
435
+ action: normalizeText(metadata?.action || "") || null,
436
+ updated_at: new Date().toISOString()
437
+ };
438
+ const filePath = getPipelineControlStatePath();
439
+ try {
440
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
441
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
442
+ } catch {
443
+ // Keep in-memory response even if persistence fails.
444
+ }
445
+ return payload;
446
+ }
447
+
448
+ function formatPipelineControlState(statePayload) {
449
+ const normalized = statePayload && typeof statePayload === "object"
450
+ ? statePayload
451
+ : {};
452
+ return {
453
+ state: normalizePipelineControlState(normalized.state),
454
+ action: normalizeText(normalized.action || "") || null,
455
+ updated_at: normalizeText(normalized.updated_at || "") || null
456
+ };
457
+ }
458
+
459
+ function normalizeControlInstructionText(instruction) {
460
+ return normalizeText(instruction).replace(/[。!!??;;,,]+$/g, "").trim().toLowerCase();
461
+ }
462
+
463
+ function detectPipelineControlAction(instruction) {
464
+ const text = normalizeControlInstructionText(instruction);
465
+ if (!text) return null;
466
+
467
+ if (/^(?:请|麻烦|帮我)?\s*(?:先)?\s*(?:暂停|暂停一下|先暂停|pause)\s*$/.test(text)) {
468
+ return PIPELINE_CONTROL_ACTION_PAUSE;
469
+ }
470
+ if (/^(?:请|麻烦|帮我)?\s*(?:先)?\s*(?:停止|终止|结束|stop)\s*$/.test(text)) {
471
+ return PIPELINE_CONTROL_ACTION_STOP;
472
+ }
473
+ if (/^(?:请|麻烦|帮我)?\s*(?:继续|恢复|resume|continue)(?:执行)?\s*$/.test(text)) {
474
+ return PIPELINE_CONTROL_ACTION_CONTINUE;
475
+ }
476
+ return null;
477
+ }
478
+
479
+ function buildPipelinePausedResponse(controlState) {
480
+ return {
481
+ status: "PAUSED",
482
+ error: {
483
+ code: "PIPELINE_PAUSED",
484
+ message: "Recommend 流水线当前处于暂停状态。请先发送“继续”恢复,或发送“停止”结束暂停。",
485
+ retryable: true
486
+ },
487
+ required_user_action: "continue_or_stop_pipeline",
488
+ control_state: formatPipelineControlState(controlState)
489
+ };
490
+ }
491
+
492
+ function buildPipelineControlCommandResponse(status, message, controlState) {
493
+ return {
494
+ status,
495
+ message,
496
+ control_state: formatPipelineControlState(controlState)
497
+ };
498
+ }
499
+
382
500
  const defaultDependencies = {
383
501
  attemptPipelineAutoRepair,
384
502
  parseRecommendInstruction,
@@ -386,7 +504,9 @@ const defaultDependencies = {
386
504
  listRecommendJobs,
387
505
  runPipelinePreflight,
388
506
  runRecommendSearchCli,
389
- runRecommendScreenCli
507
+ runRecommendScreenCli,
508
+ getPipelineControlState: defaultGetPipelineControlState,
509
+ setPipelineControlState: defaultSetPipelineControlState
390
510
  };
391
511
 
392
512
  export async function runRecommendPipeline(
@@ -402,10 +522,82 @@ export async function runRecommendPipeline(
402
522
  listRecommendJobs: listJobs,
403
523
  runPipelinePreflight: runPreflight,
404
524
  runRecommendSearchCli: searchCli,
405
- runRecommendScreenCli: screenCli
525
+ runRecommendScreenCli: screenCli,
526
+ getPipelineControlState: getControlState,
527
+ setPipelineControlState: setControlState
406
528
  } = resolvedDependencies;
407
- const startedAt = Date.now();
408
- const parsed = parseInstruction({ instruction, confirmation, overrides });
529
+
530
+ const controlAction = detectPipelineControlAction(instruction);
531
+ const currentControlState = formatPipelineControlState(
532
+ typeof getControlState === "function"
533
+ ? getControlState(workspaceRoot)
534
+ : defaultGetPipelineControlState()
535
+ );
536
+
537
+ if (controlAction === PIPELINE_CONTROL_ACTION_PAUSE) {
538
+ const pausedState = formatPipelineControlState(
539
+ typeof setControlState === "function"
540
+ ? setControlState(PIPELINE_CONTROL_STATE_PAUSED, { action: PIPELINE_CONTROL_ACTION_PAUSE })
541
+ : defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_PAUSED, { action: PIPELINE_CONTROL_ACTION_PAUSE })
542
+ );
543
+ return buildPipelineControlCommandResponse(
544
+ "PAUSED",
545
+ "已暂停 Recommend 流水线。后续如需恢复请发送“继续”,如需终止暂停请发送“停止”。",
546
+ pausedState
547
+ );
548
+ }
549
+
550
+ if (controlAction === PIPELINE_CONTROL_ACTION_STOP) {
551
+ const stoppedState = formatPipelineControlState(
552
+ typeof setControlState === "function"
553
+ ? setControlState(PIPELINE_CONTROL_STATE_STOPPED, { action: PIPELINE_CONTROL_ACTION_STOP })
554
+ : defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_STOPPED, { action: PIPELINE_CONTROL_ACTION_STOP })
555
+ );
556
+ return buildPipelineControlCommandResponse(
557
+ "STOPPED",
558
+ "已停止 Recommend 流水线。后续可直接发送新的筛选指令重新开始。",
559
+ stoppedState
560
+ );
561
+ }
562
+
563
+ if (controlAction === PIPELINE_CONTROL_ACTION_CONTINUE) {
564
+ if (currentControlState.state !== PIPELINE_CONTROL_STATE_PAUSED) {
565
+ return buildFailedResponse(
566
+ "PIPELINE_CONTINUE_REQUIRES_PAUSED",
567
+ "当前流程未处于暂停状态,无法执行“继续”。请先发送“暂停”,或直接发送新的筛选指令。",
568
+ {
569
+ required_user_action: "pause_before_continue",
570
+ control_state: currentControlState
571
+ }
572
+ );
573
+ }
574
+ const resumedState = formatPipelineControlState(
575
+ typeof setControlState === "function"
576
+ ? setControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: PIPELINE_CONTROL_ACTION_CONTINUE })
577
+ : defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: PIPELINE_CONTROL_ACTION_CONTINUE })
578
+ );
579
+ return buildPipelineControlCommandResponse(
580
+ "RESUMED",
581
+ "已恢复 Recommend 流水线。请继续发送筛选指令,我会按确认流程继续执行。",
582
+ resumedState
583
+ );
584
+ }
585
+
586
+ if (currentControlState.state === PIPELINE_CONTROL_STATE_PAUSED) {
587
+ return buildPipelinePausedResponse(currentControlState);
588
+ }
589
+
590
+ if (currentControlState.state !== PIPELINE_CONTROL_STATE_RUNNING) {
591
+ const nextActiveState = formatPipelineControlState(
592
+ typeof setControlState === "function"
593
+ ? setControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: "run" })
594
+ : defaultSetPipelineControlState(PIPELINE_CONTROL_STATE_RUNNING, { action: "run" })
595
+ );
596
+ Object.assign(currentControlState, nextActiveState);
597
+ }
598
+
599
+ const startedAt = Date.now();
600
+ const parsed = parseInstruction({ instruction, confirmation, overrides });
409
601
 
410
602
  if (parsed.missing_fields.length > 0) {
411
603
  return buildNeedInputResponse(parsed);
@@ -534,6 +726,54 @@ export async function runRecommendPipeline(
534
726
  port: preflight.debug_port
535
727
  });
536
728
  if (!jobListResult.ok) {
729
+ const jobListErrorCode = String(jobListResult.error?.code || "");
730
+ const jobListErrorMessage = String(jobListResult.error?.message || "");
731
+ const pageReadinessFailure = (
732
+ jobListErrorCode === "JOB_TRIGGER_NOT_FOUND"
733
+ || jobListErrorCode === "NO_RECOMMEND_IFRAME"
734
+ || jobListErrorCode === "LOGIN_REQUIRED"
735
+ || jobListErrorMessage.includes("JOB_TRIGGER_NOT_FOUND")
736
+ || jobListErrorMessage.includes("NO_RECOMMEND_IFRAME")
737
+ || jobListErrorMessage.includes("LOGIN_REQUIRED")
738
+ );
739
+ if (pageReadinessFailure) {
740
+ const recheck = await ensureRecommendPageReady(workspaceRoot, {
741
+ port: preflight.debug_port
742
+ });
743
+ const loginRelated = new Set(["LOGIN_REQUIRED", "LOGIN_REQUIRED_AFTER_REDIRECT"]);
744
+ const connectivityRelated = new Set(["DEBUG_PORT_UNREACHABLE"]);
745
+ const guidance = buildChromeSetupGuidance({
746
+ debugPort: preflight.debug_port,
747
+ pageState: recheck.page_state
748
+ });
749
+ if (!recheck.ok || loginRelated.has(recheck.state) || connectivityRelated.has(recheck.state)) {
750
+ return buildFailedResponse(
751
+ connectivityRelated.has(recheck.state)
752
+ ? "BOSS_CHROME_NOT_CONNECTED"
753
+ : loginRelated.has(recheck.state)
754
+ ? "BOSS_LOGIN_REQUIRED"
755
+ : "BOSS_RECOMMEND_PAGE_NOT_READY",
756
+ loginRelated.has(recheck.state)
757
+ ? `检测到当前 Boss 处于未登录状态,请先登录后再继续。登录页:https://www.zhipin.com/web/user/?ka=bticket`
758
+ : connectivityRelated.has(recheck.state)
759
+ ? `读取岗位列表前需要先连接到端口 ${preflight.debug_port} 的 Chrome 远程调试实例。`
760
+ : `读取岗位列表前,请先在端口 ${preflight.debug_port} 的 Chrome 停留在 Boss recommend 页面。`,
761
+ {
762
+ search_params: parsed.searchParams,
763
+ screen_params: parsed.screenParams,
764
+ required_user_action: "prepare_boss_recommend_page",
765
+ guidance,
766
+ diagnostics: {
767
+ debug_port: preflight.debug_port,
768
+ page_state: recheck.page_state,
769
+ stdout: jobListResult.stdout?.slice(-1000),
770
+ stderr: jobListResult.stderr?.slice(-1000),
771
+ result: jobListResult.structured || null
772
+ }
773
+ }
774
+ );
775
+ }
776
+ }
537
777
  return buildFailedResponse(
538
778
  jobListResult.error?.code || "RECOMMEND_JOB_LIST_FAILED",
539
779
  jobListResult.error?.message || "读取推荐岗位列表失败,无法开始筛选。",
@@ -78,6 +78,152 @@ function createParsed(overrides = {}) {
78
78
  };
79
79
  }
80
80
 
81
+ function createControlStateStore(initialState = "running") {
82
+ let state = {
83
+ state: initialState,
84
+ action: null,
85
+ updated_at: null
86
+ };
87
+ return {
88
+ getPipelineControlState: () => ({ ...state }),
89
+ setPipelineControlState: (nextState, metadata = {}) => {
90
+ state = {
91
+ state: nextState,
92
+ action: metadata.action || null,
93
+ updated_at: "2026-01-01T00:00:00.000Z"
94
+ };
95
+ return { ...state };
96
+ },
97
+ snapshot: () => ({ ...state })
98
+ };
99
+ }
100
+
101
+ async function testPauseCommandShouldPausePipeline() {
102
+ const controlStore = createControlStateStore("running");
103
+ let parseCalled = false;
104
+ const result = await runRecommendPipeline(
105
+ {
106
+ workspaceRoot: process.cwd(),
107
+ instruction: "暂停",
108
+ confirmation: {},
109
+ overrides: {}
110
+ },
111
+ {
112
+ getPipelineControlState: controlStore.getPipelineControlState,
113
+ setPipelineControlState: controlStore.setPipelineControlState,
114
+ parseRecommendInstruction: () => {
115
+ parseCalled = true;
116
+ return createParsed();
117
+ }
118
+ }
119
+ );
120
+
121
+ assert.equal(result.status, "PAUSED");
122
+ assert.equal(result.control_state.state, "paused");
123
+ assert.equal(parseCalled, false);
124
+ }
125
+
126
+ async function testContinueCommandRequiresPausedState() {
127
+ const controlStore = createControlStateStore("running");
128
+ const result = await runRecommendPipeline(
129
+ {
130
+ workspaceRoot: process.cwd(),
131
+ instruction: "继续",
132
+ confirmation: {},
133
+ overrides: {}
134
+ },
135
+ {
136
+ getPipelineControlState: controlStore.getPipelineControlState,
137
+ setPipelineControlState: controlStore.setPipelineControlState
138
+ }
139
+ );
140
+
141
+ assert.equal(result.status, "FAILED");
142
+ assert.equal(result.error.code, "PIPELINE_CONTINUE_REQUIRES_PAUSED");
143
+ }
144
+
145
+ async function testContinueCommandCanResumeFromPausedState() {
146
+ const controlStore = createControlStateStore("running");
147
+ await runRecommendPipeline(
148
+ {
149
+ workspaceRoot: process.cwd(),
150
+ instruction: "暂停",
151
+ confirmation: {},
152
+ overrides: {}
153
+ },
154
+ {
155
+ getPipelineControlState: controlStore.getPipelineControlState,
156
+ setPipelineControlState: controlStore.setPipelineControlState
157
+ }
158
+ );
159
+
160
+ const resumed = await runRecommendPipeline(
161
+ {
162
+ workspaceRoot: process.cwd(),
163
+ instruction: "继续",
164
+ confirmation: {},
165
+ overrides: {}
166
+ },
167
+ {
168
+ getPipelineControlState: controlStore.getPipelineControlState,
169
+ setPipelineControlState: controlStore.setPipelineControlState
170
+ }
171
+ );
172
+
173
+ assert.equal(resumed.status, "RESUMED");
174
+ assert.equal(resumed.control_state.state, "running");
175
+ }
176
+
177
+ async function testPipelineShouldBlockWhenPaused() {
178
+ const controlStore = createControlStateStore("paused");
179
+ let parseCalled = false;
180
+ let preflightCalled = false;
181
+ const result = await runRecommendPipeline(
182
+ {
183
+ workspaceRoot: process.cwd(),
184
+ instruction: "推荐页筛选985男生",
185
+ confirmation: {},
186
+ overrides: {}
187
+ },
188
+ {
189
+ getPipelineControlState: controlStore.getPipelineControlState,
190
+ setPipelineControlState: controlStore.setPipelineControlState,
191
+ parseRecommendInstruction: () => {
192
+ parseCalled = true;
193
+ return createParsed();
194
+ },
195
+ runPipelinePreflight: () => {
196
+ preflightCalled = true;
197
+ return { ok: true, checks: [], debug_port: 9222 };
198
+ }
199
+ }
200
+ );
201
+
202
+ assert.equal(result.status, "PAUSED");
203
+ assert.equal(result.error.code, "PIPELINE_PAUSED");
204
+ assert.equal(parseCalled, false);
205
+ assert.equal(preflightCalled, false);
206
+ }
207
+
208
+ async function testStopCommandShouldExitPausedState() {
209
+ const controlStore = createControlStateStore("paused");
210
+ const result = await runRecommendPipeline(
211
+ {
212
+ workspaceRoot: process.cwd(),
213
+ instruction: "停止",
214
+ confirmation: {},
215
+ overrides: {}
216
+ },
217
+ {
218
+ getPipelineControlState: controlStore.getPipelineControlState,
219
+ setPipelineControlState: controlStore.setPipelineControlState
220
+ }
221
+ );
222
+
223
+ assert.equal(result.status, "STOPPED");
224
+ assert.equal(result.control_state.state, "stopped");
225
+ }
226
+
81
227
  async function testNeedConfirmationGate() {
82
228
  let preflightCalled = false;
83
229
  const result = await runRecommendPipeline(
@@ -344,6 +490,50 @@ async function testSearchNoIframeWithLoginShouldReturnLoginRequired() {
344
490
  assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
345
491
  }
346
492
 
493
+ async function testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin() {
494
+ const result = await runRecommendPipeline(
495
+ {
496
+ workspaceRoot: process.cwd(),
497
+ instruction: "test",
498
+ confirmation: {},
499
+ overrides: {}
500
+ },
501
+ {
502
+ parseRecommendInstruction: () => createParsed(),
503
+ runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
504
+ ensureBossRecommendPageReady: async () => ({
505
+ ok: false,
506
+ debug_port: 9222,
507
+ state: "LOGIN_REQUIRED",
508
+ page_state: {
509
+ state: "LOGIN_REQUIRED",
510
+ expected_url: "https://www.zhipin.com/web/chat/recommend",
511
+ current_url: "https://www.zhipin.com/web/user/?ka=bticket",
512
+ login_url: "https://www.zhipin.com/web/user/?ka=bticket"
513
+ }
514
+ }),
515
+ listRecommendJobs: async () => ({
516
+ ok: false,
517
+ stdout: "",
518
+ stderr: "",
519
+ structured: null,
520
+ jobs: [],
521
+ error: {
522
+ code: "JOB_TRIGGER_NOT_FOUND",
523
+ message: "JOB_TRIGGER_NOT_FOUND"
524
+ }
525
+ }),
526
+ runRecommendSearchCli: async () => ({ ok: true, summary: {} }),
527
+ runRecommendScreenCli: async () => ({ ok: true, summary: {} })
528
+ }
529
+ );
530
+
531
+ assert.equal(result.status, "FAILED");
532
+ assert.equal(result.error.code, "BOSS_LOGIN_REQUIRED");
533
+ assert.equal(result.required_user_action, "prepare_boss_recommend_page");
534
+ assert.equal(result.guidance.agent_prompt.includes("https://www.zhipin.com/web/user/?ka=bticket"), true);
535
+ }
536
+
347
537
  async function testNeedJobConfirmationGate() {
348
538
  const result = await runRecommendPipeline(
349
539
  {
@@ -711,6 +901,11 @@ async function testScreenConfigRecoveryStepShouldBeFirst() {
711
901
  }
712
902
 
713
903
  async function main() {
904
+ await testPauseCommandShouldPausePipeline();
905
+ await testContinueCommandRequiresPausedState();
906
+ await testContinueCommandCanResumeFromPausedState();
907
+ await testPipelineShouldBlockWhenPaused();
908
+ await testStopCommandShouldExitPausedState();
714
909
  await testNeedConfirmationGate();
715
910
  await testNeedSchoolTagConfirmationGate();
716
911
  await testNeedTargetCountConfirmationGate();
@@ -721,6 +916,7 @@ async function main() {
721
916
  await testCompletedPipeline();
722
917
  await testSearchFailure();
723
918
  await testSearchNoIframeWithLoginShouldReturnLoginRequired();
919
+ await testJobTriggerNotFoundShouldMapToLoginRequiredWhenRecheckShowsLogin();
724
920
  await testLoginRequiredShouldReturnGuidance();
725
921
  await testDebugPortUnreachableShouldReturnConnectionCode();
726
922
  await testPreflightRecoveryPlanOrder();
@@ -10,10 +10,10 @@ const BOSS_LOGIN_URL = "https://www.zhipin.com/web/user/?ka=bticket";
10
10
  const BOSS_LOGIN_URL_PATTERN = /(?:zhipin\.com\/web\/user(?:\/|\?|$)|passport\.zhipin\.com)/i;
11
11
  const BOSS_LOGIN_TITLE_PATTERN = /登录|signin|扫码登录|BOSS直聘登录/i;
12
12
  const SCHOOL_TAG_OPTIONS = ["不限", "985", "211", "双一流院校", "留学", "国内外名校", "公办本科"];
13
- const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
14
- const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
15
- const GENDER_OPTIONS = ["不限", "男", "女"];
16
- const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
13
+ const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
14
+ const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
15
+ const GENDER_OPTIONS = ["不限", "男", "女"];
16
+ const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
17
17
 
18
18
  function normalizeText(value) {
19
19
  return String(value || "").replace(/\s+/g, " ").trim();
@@ -107,7 +107,7 @@ function expandDegreeAtOrAbove(value) {
107
107
  return DEGREE_ORDER.slice(index);
108
108
  }
109
109
 
110
- function parseDegreeSelection(raw) {
110
+ function parseDegreeSelection(raw) {
111
111
  const text = normalizeText(raw);
112
112
  if (!text) return null;
113
113
  if (text === "不限") return ["不限"];
@@ -132,9 +132,9 @@ function parseDegreeSelection(raw) {
132
132
  }
133
133
  }
134
134
 
135
- const normalized = sortDegreeSelection(selected);
136
- return normalized.length ? normalized : null;
137
- }
135
+ const normalized = sortDegreeSelection(selected);
136
+ return normalized.length ? normalized : null;
137
+ }
138
138
 
139
139
  function parseArgs(argv) {
140
140
  const args = {
@@ -296,14 +296,14 @@ function generateBezierPath(start, end, steps = 18) {
296
296
  return path;
297
297
  }
298
298
 
299
- class RecommendSearchCli {
300
- constructor(args) {
301
- this.args = args;
302
- this.client = null;
303
- this.Runtime = null;
304
- this.Input = null;
305
- this.target = null;
306
- }
299
+ class RecommendSearchCli {
300
+ constructor(args) {
301
+ this.args = args;
302
+ this.client = null;
303
+ this.Runtime = null;
304
+ this.Input = null;
305
+ this.target = null;
306
+ }
307
307
 
308
308
  async connect() {
309
309
  const targets = await CDP.List({ port: this.args.port });
@@ -792,7 +792,7 @@ class RecommendSearchCli {
792
792
  })()`);
793
793
  }
794
794
 
795
- async openFilterPanel() {
795
+ async openFilterPanel() {
796
796
  if (await this.isFilterPanelVisible()) return;
797
797
  let lastError = 'FILTER_PANEL_UNAVAILABLE';
798
798
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -821,8 +821,8 @@ class RecommendSearchCli {
821
821
  lastError = fallback?.error || lastError;
822
822
  }
823
823
  }
824
- throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
825
- }
824
+ throw new Error(lastError === 'FILTER_TRIGGER_NOT_FOUND' ? lastError : 'FILTER_PANEL_UNAVAILABLE');
825
+ }
826
826
 
827
827
  async closeFilterPanel() {
828
828
  if (!(await this.isFilterPanelVisible())) {
@@ -1480,10 +1480,10 @@ class RecommendSearchCli {
1480
1480
  if (!Array.isArray(this.args.schoolTag) || this.args.schoolTag.length === 0) {
1481
1481
  throw new Error("INVALID_SCHOOL_TAG_INPUT");
1482
1482
  }
1483
- if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
1484
- throw new Error("INVALID_DEGREE_INPUT");
1485
- }
1486
-
1483
+ if (!Array.isArray(this.args.degree) || this.args.degree.length === 0) {
1484
+ throw new Error("INVALID_DEGREE_INPUT");
1485
+ }
1486
+
1487
1487
  await this.connect();
1488
1488
  try {
1489
1489
  const frameState = await this.getFrameState();