@reconcrap/boss-recommend-mcp 1.1.12 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.1.12",
3
+ "version": "1.2.0",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
@@ -43,7 +43,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
43
43
 
44
44
  阶段 A(页面就绪前,禁止问岗位):
45
45
 
46
- - 页面范围(`page_scope`)必须先确认:`recommend`(推荐)或 `featured`(精选)
46
+ - 页面范围(`page_scope`)必须先确认:`recommend`(推荐)/ `latest`(最新)/ `featured`(精选)
47
47
  - 即使 instruction 里已出现“推荐/精选”关键词,也必须显式二次确认 `page_confirmed=true` 与 `page_value`
48
48
  - 学校标签(`school_tag`,支持多选)
49
49
  - 若输入混合了有效与无效选项(如 `985,211,qs100`),必须忽略无效项并保留有效项;不要直接回退到“不限”
@@ -97,7 +97,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
97
97
  - `instruction` (required)
98
98
  - `confirmation`
99
99
  - `page_confirmed`
100
- - `page_value` (`recommend|featured`)
100
+ - `page_value` (`recommend|latest|featured`)
101
101
  - `filters_confirmed`
102
102
  - `school_tag_confirmed`
103
103
  - `school_tag_value`(建议回传最终确认值,避免二轮调用丢失)
@@ -118,7 +118,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
118
118
  - `max_greet_count_confirmed`
119
119
  - `max_greet_count_value` (integer)
120
120
  - `overrides`
121
- - `page_scope` (`recommend|featured`)
121
+ - `page_scope` (`recommend|latest|featured`)
122
122
  - `school_tag`(可传单值或数组,如 `["985","211"]`)
123
123
  - `degree`(可传单值或数组;如“本科及以上”应展开为 `["本科","硕士","博士"]`)
124
124
  - `gender`
@@ -161,6 +161,7 @@ description: "Use when users ask to run Boss recommend-page filtering and screen
161
161
  - recommend-search-cli 只负责应用推荐页筛选项。
162
162
  - 若 `page_scope=featured`:必须严格按 `search -> 切换精选tab(data-status=3) -> screen` 顺序执行。
163
163
  - 若 `page_scope=featured` 且校准文件缺失:必须先触发 `boss-recommend-mcp calibrate` 自动校准,成功后再继续。
164
+ - 若 `page_scope=latest`:执行路径与 `recommend` 一致(search + screen),但 screen 列表选择器应按最新 tab 结构读取(data-status=1)。
164
165
  - recommend-screen-cli 负责滚动推荐列表、打开详情、提取完整简历图、调用多模态模型判断,并按单次确认的 `post_action` 执行收藏或打招呼。
165
166
  - 精选页收藏仅允许“校准坐标 + 模拟点击”,成功判定仅看 network `add/del` 信号。
166
167
  - 详情页处理完成后必须关闭详情页并确认已关闭。
package/src/adapters.js CHANGED
@@ -20,6 +20,7 @@ const screenConfigTemplateDefaults = {
20
20
  const DEFAULT_RECOMMEND_SCREEN_TIMEOUT_MS = 24 * 60 * 60 * 1000;
21
21
  const PAGE_SCOPE_TO_TAB_STATUS = {
22
22
  recommend: "0",
23
+ latest: "1",
23
24
  featured: "3"
24
25
  };
25
26
 
@@ -76,6 +77,7 @@ function normalizePageScope(value) {
76
77
  const normalized = normalizeText(value).toLowerCase();
77
78
  if (!normalized) return null;
78
79
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
80
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
79
81
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
80
82
  return null;
81
83
  }
@@ -1568,10 +1570,12 @@ function buildRecommendTabStateExpression() {
1568
1570
  const activeTab = tabs.find((item) => item.active && item.status) || null;
1569
1571
  const featuredCount = doc.querySelectorAll('li.geek-info-card').length;
1570
1572
  const recommendCount = doc.querySelectorAll('ul.card-list > li.card-item').length;
1573
+ const latestCount = doc.querySelectorAll('.candidate-card-wrap .card-inner[data-geek], .candidate-card-wrap [data-geek]').length;
1571
1574
  let inferredStatus = activeTab?.status || null;
1572
1575
  if (!inferredStatus) {
1573
- if (featuredCount > 0 && recommendCount === 0) inferredStatus = '3';
1574
- else if (recommendCount > 0 && featuredCount === 0) inferredStatus = '0';
1576
+ if (featuredCount > 0 && recommendCount === 0 && latestCount === 0) inferredStatus = '3';
1577
+ else if (latestCount > 0 && featuredCount === 0 && recommendCount === 0) inferredStatus = '1';
1578
+ else if (recommendCount > 0 && featuredCount === 0 && latestCount === 0) inferredStatus = '0';
1575
1579
  }
1576
1580
  return {
1577
1581
  ok: true,
@@ -1579,7 +1583,8 @@ function buildRecommendTabStateExpression() {
1579
1583
  tabs,
1580
1584
  layout: {
1581
1585
  featured_count: featuredCount,
1582
- recommend_count: recommendCount
1586
+ recommend_count: recommendCount,
1587
+ latest_count: latestCount
1583
1588
  }
1584
1589
  };
1585
1590
  })()`;
@@ -1958,25 +1963,34 @@ function buildRecommendRefreshStateExpression() {
1958
1963
  const finishedWrap = Array.from(doc.querySelectorAll('.finished-wrap')).find((el) => isVisible(el)) || null;
1959
1964
  const refreshButton = Array.from(doc.querySelectorAll('.finished-wrap .btn.btn-refresh, .finished-wrap .btn-refresh, .no-data-refresh .btn-refresh'))
1960
1965
  .find((el) => isVisible(el)) || null;
1961
- const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
1962
- const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
1963
- const finishedText = finishedWrap ? String(finishedWrap.textContent || '').replace(/\\s+/g, ' ').trim() : '';
1964
- const buttonText = refreshButton ? String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() : '';
1965
- return {
1966
- ok: true,
1966
+ const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
1967
+ const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
1968
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap .card-inner[data-geek], .candidate-card-wrap [data-geek]'));
1969
+ const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
1970
+ const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
1971
+ const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
1972
+ const inferredStatus = activeStatus
1973
+ || (latestCards.length > 0 && candidateCards.length === 0 ? '1' : candidateCards.length > 0 ? '0' : '');
1974
+ const effectiveCandidates = inferredStatus === '1' ? latestCards : candidateCards;
1975
+ const finishedText = finishedWrap ? String(finishedWrap.textContent || '').replace(/\\s+/g, ' ').trim() : '';
1976
+ const buttonText = refreshButton ? String(refreshButton.textContent || '').replace(/\\s+/g, ' ').trim() : '';
1977
+ return {
1978
+ ok: true,
1967
1979
  frame_url: (() => {
1968
1980
  try { return String(frame.contentWindow.location.href || ''); } catch { return ''; }
1969
1981
  })(),
1970
- finished_wrap_visible: Boolean(finishedWrap),
1971
- finished_wrap_text: finishedText || null,
1972
- refresh_button_visible: Boolean(refreshButton),
1973
- refresh_button_text: buttonText || null,
1974
- candidate_count: candidateCards.length,
1975
- total_card_count: cards.length,
1976
- list_ready: candidateCards.length > 0
1977
- };
1978
- })()`;
1979
- }
1982
+ finished_wrap_visible: Boolean(finishedWrap),
1983
+ finished_wrap_text: finishedText || null,
1984
+ refresh_button_visible: Boolean(refreshButton),
1985
+ refresh_button_text: buttonText || null,
1986
+ candidate_count: effectiveCandidates.length,
1987
+ recommend_candidate_count: candidateCards.length,
1988
+ latest_candidate_count: latestCards.length,
1989
+ total_card_count: Math.max(cards.length, latestCards.length),
1990
+ list_ready: effectiveCandidates.length > 0
1991
+ };
1992
+ })()`;
1993
+ }
1980
1994
 
1981
1995
  function buildRecommendRefreshClickExpression() {
1982
1996
  return `(() => {
package/src/cli.js CHANGED
@@ -187,6 +187,7 @@ function normalizePageScope(value) {
187
187
  const normalized = String(value || "").trim().toLowerCase();
188
188
  if (!normalized) return null;
189
189
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
190
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
190
191
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
191
192
  return null;
192
193
  }
package/src/index.js CHANGED
@@ -151,7 +151,7 @@ function createRunInputSchema() {
151
151
  page_confirmed: { type: "boolean" },
152
152
  page_value: {
153
153
  type: "string",
154
- enum: ["recommend", "featured"]
154
+ enum: ["recommend", "featured", "latest"]
155
155
  },
156
156
  filters_confirmed: { type: "boolean" },
157
157
  school_tag_confirmed: { type: "boolean" },
@@ -227,7 +227,7 @@ function createRunInputSchema() {
227
227
  properties: {
228
228
  page_scope: {
229
229
  type: "string",
230
- enum: ["recommend", "featured"]
230
+ enum: ["recommend", "featured", "latest"]
231
231
  },
232
232
  school_tag: {
233
233
  oneOf: [
package/src/parser.js CHANGED
@@ -34,10 +34,11 @@ const POST_ACTION_LABELS = {
34
34
  greet: "直接沟通",
35
35
  none: "什么也不做"
36
36
  };
37
- const PAGE_SCOPE_OPTIONS = ["recommend", "featured"];
37
+ const PAGE_SCOPE_OPTIONS = ["recommend", "featured", "latest"];
38
38
  const PAGE_SCOPE_LABELS = {
39
39
  recommend: "推荐",
40
- featured: "精选"
40
+ featured: "精选",
41
+ latest: "最新"
41
42
  };
42
43
  const LEADING_NOISE_PATTERNS = [
43
44
  /^使用boss-recommend-pipeline skills/i,
@@ -103,6 +104,7 @@ const META_CLAUSE_PATTERNS = [
103
104
  /帮我|请|运行|skill/i
104
105
  ];
105
106
  const FEATURED_SCOPE_PATTERN = /(?:精选牛人|精选页|精选页面|精选tab|精选标签|tab[^。;;\n]{0,6}精选|精选)/i;
107
+ const LATEST_SCOPE_PATTERN = /(?:最新页|最新页面|最新tab|最新标签|tab[^。;;\n]{0,6}最新|最新)/i;
106
108
  const RECOMMEND_SCOPE_PATTERN = /(?:推荐页|推荐页面|推荐tab|推荐标签|tab[^。;;\n]{0,6}推荐|推荐)/i;
107
109
 
108
110
  function normalizeText(input) {
@@ -278,11 +280,13 @@ function normalizePageScope(value) {
278
280
  if (!normalized) return null;
279
281
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
280
282
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
283
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
281
284
  return PAGE_SCOPE_OPTIONS.includes(normalized) ? normalized : null;
282
285
  }
283
286
 
284
287
  function extractPageScope(text) {
285
288
  if (FEATURED_SCOPE_PATTERN.test(text)) return "featured";
289
+ if (LATEST_SCOPE_PATTERN.test(text)) return "latest";
286
290
  if (RECOMMEND_SCOPE_PATTERN.test(text)) return "recommend";
287
291
  return null;
288
292
  }
@@ -612,11 +616,12 @@ export function parseRecommendInstruction({ instruction, confirmation, overrides
612
616
  if (needs_page_confirmation) {
613
617
  pending_questions.push({
614
618
  field: "page_scope",
615
- question: "请确认本次在推荐里的哪个页面执行筛选:推荐 精选。",
619
+ question: "请确认本次在推荐里的哪个页面执行筛选:推荐 / 精选 / 最新。",
616
620
  value: pageScopeResolution.proposed_page_scope,
617
621
  options: [
618
622
  { label: PAGE_SCOPE_LABELS.recommend, value: "recommend" },
619
- { label: PAGE_SCOPE_LABELS.featured, value: "featured" }
623
+ { label: PAGE_SCOPE_LABELS.featured, value: "featured" },
624
+ { label: PAGE_SCOPE_LABELS.latest, value: "latest" }
620
625
  ]
621
626
  });
622
627
  }
package/src/pipeline.js CHANGED
@@ -18,14 +18,17 @@ const FORCED_RECENT_NOT_VIEW_ON_SCREEN_RECOVERY = "近14天没有";
18
18
  const MAX_SCREEN_AUTO_RECOVERY_ATTEMPTS = 5;
19
19
  const PAGE_SCOPE_TO_TAB_STATUS = {
20
20
  recommend: "0",
21
+ latest: "1",
21
22
  featured: "3"
22
23
  };
23
24
  const TAB_STATUS_TO_PAGE_SCOPE = {
24
25
  "0": "recommend",
26
+ "1": "latest",
25
27
  "3": "featured"
26
28
  };
27
29
  const PAGE_SCOPE_LABELS = {
28
30
  recommend: "推荐",
31
+ latest: "最新",
29
32
  featured: "精选"
30
33
  };
31
34
 
@@ -41,6 +44,7 @@ function normalizePageScope(value) {
41
44
  const normalized = normalizeText(value).toLowerCase();
42
45
  if (!normalized) return null;
43
46
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
47
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
44
48
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
45
49
  return null;
46
50
  }
@@ -892,7 +896,7 @@ export async function runRecommendPipeline(
892
896
  };
893
897
 
894
898
  const ensureSelectedPageTab = async () => {
895
- if (selectedPage !== "featured") {
899
+ if (selectedPage === "recommend") {
896
900
  activeTabStatus = selectedTabStatus;
897
901
  return {
898
902
  ok: true,
@@ -1073,8 +1077,8 @@ export async function runRecommendPipeline(
1073
1077
  ensurePipelineNotAborted(runtimeHooks.signal);
1074
1078
  runtimeHooks.setStage(
1075
1079
  "screen",
1076
- selectedPage === "featured"
1077
- ? "search 完成,已切换到精选 tab,开始执行 recommend screen。"
1080
+ selectedPage !== "recommend"
1081
+ ? `search 完成,已切换到${PAGE_SCOPE_LABELS[selectedPage]} tab,开始执行 recommend screen。`
1078
1082
  : "search 完成,开始执行 recommend screen。"
1079
1083
  );
1080
1084
  } else {
@@ -177,6 +177,13 @@ function testPreflightRecommendShouldKeepFavoriteCalibrationOptional() {
177
177
  assert.equal(check.optional, true);
178
178
  }
179
179
 
180
+ function testPreflightLatestShouldKeepFavoriteCalibrationOptional() {
181
+ const preflight = runPipelinePreflight(process.cwd(), { pageScope: "latest" });
182
+ const check = (preflight.checks || []).find((item) => item?.key === "favorite_calibration");
183
+ assert.equal(Boolean(check), true);
184
+ assert.equal(check.optional, true);
185
+ }
186
+
180
187
  async function testEnsureFeaturedCalibrationReadyShouldAutoCalibrate() {
181
188
  const previousHome = process.env.BOSS_RECOMMEND_HOME;
182
189
  const previousCodexHome = process.env.CODEX_HOME;
@@ -282,6 +289,43 @@ async function testSearchCliShouldPassPageScopeArgument() {
282
289
  }
283
290
  }
284
291
 
292
+ async function testSearchCliShouldPassLatestPageScopeWithoutCalibration() {
293
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-search-page-scope-latest-"));
294
+ const cliDir = path.join(workspaceRoot, "boss-recommend-search-cli", "src");
295
+ fs.mkdirSync(cliDir, { recursive: true });
296
+ const cliPath = path.join(cliDir, "cli.js");
297
+ fs.writeFileSync(
298
+ cliPath,
299
+ [
300
+ "#!/usr/bin/env node",
301
+ "console.log(JSON.stringify({ status: 'COMPLETED', result: { argv: process.argv.slice(2) } }));"
302
+ ].join("\n"),
303
+ "utf8"
304
+ );
305
+
306
+ try {
307
+ const result = await runRecommendSearchCli({
308
+ workspaceRoot,
309
+ searchParams: {
310
+ school_tag: ["不限"],
311
+ degree: ["不限"],
312
+ gender: "不限",
313
+ recent_not_view: "不限"
314
+ },
315
+ selectedJob: null,
316
+ pageScope: "latest"
317
+ });
318
+ assert.equal(result.ok, true);
319
+ const argv = result.summary?.argv || [];
320
+ const pageScopeIndex = argv.indexOf("--page-scope");
321
+ assert.equal(pageScopeIndex >= 0, true);
322
+ assert.equal(argv[pageScopeIndex + 1], "latest");
323
+ assert.equal(argv.includes("--calibration"), false);
324
+ } finally {
325
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
326
+ }
327
+ }
328
+
285
329
  async function testScreenCliShouldPassPageScopeArgument() {
286
330
  const previousHome = process.env.BOSS_RECOMMEND_HOME;
287
331
  const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-scope-home-"));
@@ -342,6 +386,66 @@ async function testScreenCliShouldPassPageScopeArgument() {
342
386
  }
343
387
  }
344
388
 
389
+ async function testScreenCliShouldPassLatestPageScopeArgument() {
390
+ const previousHome = process.env.BOSS_RECOMMEND_HOME;
391
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-scope-latest-home-"));
392
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-page-scope-latest-workspace-"));
393
+ const cliDir = path.join(workspaceRoot, "boss-recommend-screen-cli");
394
+ fs.mkdirSync(cliDir, { recursive: true });
395
+ const cliPath = path.join(cliDir, "boss-recommend-screen-cli.cjs");
396
+ fs.writeFileSync(
397
+ cliPath,
398
+ [
399
+ "#!/usr/bin/env node",
400
+ "console.log(JSON.stringify({",
401
+ " status: 'COMPLETED',",
402
+ " result: {",
403
+ " processed_count: 0,",
404
+ " passed_count: 0,",
405
+ " skipped_count: 0,",
406
+ " argv: process.argv.slice(2),",
407
+ " resume_source: 'image_fallback',",
408
+ " active_tab_status: '1'",
409
+ " }",
410
+ "}));"
411
+ ].join("\n"),
412
+ "utf8"
413
+ );
414
+
415
+ process.env.BOSS_RECOMMEND_HOME = tempHome;
416
+ fs.writeFileSync(path.join(tempHome, "screening-config.json"), JSON.stringify({
417
+ baseUrl: "https://api.openai.com/v1",
418
+ apiKey: "sk-valid-test",
419
+ model: "gpt-4.1-mini"
420
+ }, null, 2));
421
+
422
+ try {
423
+ const result = await runRecommendScreenCli({
424
+ workspaceRoot,
425
+ screenParams: {
426
+ criteria: "有 MCP 经验",
427
+ target_count: null,
428
+ post_action: "none",
429
+ max_greet_count: null
430
+ },
431
+ pageScope: "latest"
432
+ });
433
+ assert.equal(result.ok, true);
434
+ const argv = result.summary?.argv || [];
435
+ const pageScopeIndex = argv.indexOf("--page-scope");
436
+ assert.equal(pageScopeIndex >= 0, true);
437
+ assert.equal(argv[pageScopeIndex + 1], "latest");
438
+ } finally {
439
+ if (previousHome === undefined) {
440
+ delete process.env.BOSS_RECOMMEND_HOME;
441
+ } else {
442
+ process.env.BOSS_RECOMMEND_HOME = previousHome;
443
+ }
444
+ fs.rmSync(tempHome, { recursive: true, force: true });
445
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
446
+ }
447
+ }
448
+
345
449
  async function main() {
346
450
  await testRunProcessHeartbeatAndOutput();
347
451
  await testRunProcessAbortSignal();
@@ -353,9 +457,12 @@ async function main() {
353
457
  testPreflightShouldCheckSharpInsteadOfPython();
354
458
  testPreflightFeaturedShouldRequireFavoriteCalibration();
355
459
  testPreflightRecommendShouldKeepFavoriteCalibrationOptional();
460
+ testPreflightLatestShouldKeepFavoriteCalibrationOptional();
356
461
  await testEnsureFeaturedCalibrationReadyShouldAutoCalibrate();
357
462
  await testSearchCliShouldPassPageScopeArgument();
463
+ await testSearchCliShouldPassLatestPageScopeWithoutCalibration();
358
464
  await testScreenCliShouldPassPageScopeArgument();
465
+ await testScreenCliShouldPassLatestPageScopeArgument();
359
466
  console.log("adapters runtime tests passed");
360
467
  }
361
468
 
@@ -488,6 +488,21 @@ function testFeaturedKeywordShouldProposeFeaturedPageScope() {
488
488
  assert.equal(result.pending_questions.some((item) => item.field === "page_scope"), true);
489
489
  }
490
490
 
491
+ function testLatestKeywordShouldProposeLatestPageScope() {
492
+ const result = parseRecommendInstruction({
493
+ instruction: "在推荐页最新里筛选候选人,有 Agent 经验,符合标准收藏",
494
+ confirmation: null,
495
+ overrides: null
496
+ });
497
+
498
+ assert.equal(result.proposed_page_scope, "latest");
499
+ assert.equal(result.needs_page_confirmation, true);
500
+ const pageQuestion = result.pending_questions.find((item) => item.field === "page_scope");
501
+ assert.equal(Boolean(pageQuestion), true);
502
+ assert.equal(Array.isArray(pageQuestion.options), true);
503
+ assert.equal(pageQuestion.options.some((item) => item.value === "latest"), true);
504
+ }
505
+
491
506
  function testConfirmedPageScopeShouldBeResolved() {
492
507
  const result = parseRecommendInstruction({
493
508
  instruction: "在推荐页筛选候选人,有 Agent 经验,符合标准收藏",
@@ -543,6 +558,7 @@ function main() {
543
558
  testPostActionNoneCanBeConfirmed();
544
559
  testJobSelectionHintCanComeFromOverrides();
545
560
  testFeaturedKeywordShouldProposeFeaturedPageScope();
561
+ testLatestKeywordShouldProposeLatestPageScope();
546
562
  testConfirmedPageScopeShouldBeResolved();
547
563
  testPageScopeOverrideShouldNotBypassConfirmation();
548
564
  console.log("parser tests passed");
@@ -1066,7 +1066,7 @@ async function testFeaturedPipelineShouldRunSearchThenSwitchTabThenScreen() {
1066
1066
  active_status: "0",
1067
1067
  tab_state: { active_status: "0" }
1068
1068
  }),
1069
- switchRecommendTab: async ({ target_status }) => {
1069
+ switchRecommendTab: async (_workspaceRoot, { target_status }) => {
1070
1070
  calls.push({ type: "switch", target_status });
1071
1071
  return {
1072
1072
  ok: true,
@@ -1102,6 +1102,82 @@ async function testFeaturedPipelineShouldRunSearchThenSwitchTabThenScreen() {
1102
1102
  assert.equal(result.result.resume_source, "network");
1103
1103
  }
1104
1104
 
1105
+ async function testLatestPipelineShouldRunSearchThenSwitchTabThenScreen() {
1106
+ const calls = [];
1107
+ const result = await runRecommendPipeline(
1108
+ {
1109
+ workspaceRoot: process.cwd(),
1110
+ instruction: "test",
1111
+ confirmation: {
1112
+ ...createJobConfirmedConfirmation(),
1113
+ page_confirmed: true,
1114
+ page_value: "latest"
1115
+ },
1116
+ overrides: {
1117
+ page_scope: "latest"
1118
+ }
1119
+ },
1120
+ {
1121
+ parseRecommendInstruction: () => createParsed({
1122
+ page_scope: "latest",
1123
+ proposed_page_scope: "latest"
1124
+ }),
1125
+ runPipelinePreflight: () => ({ ok: true, checks: [], debug_port: 9222 }),
1126
+ ensureBossRecommendPageReady: async () => ({ ok: true, state: "RECOMMEND_READY", page_state: { state: "RECOMMEND_READY" } }),
1127
+ listRecommendJobs: async () => createJobListResult(),
1128
+ runRecommendSearchCli: async ({ pageScope }) => {
1129
+ calls.push({ type: "search", pageScope });
1130
+ return {
1131
+ ok: true,
1132
+ summary: {
1133
+ candidate_count: 8,
1134
+ applied_filters: { degree: ["本科"] },
1135
+ page_state: { state: "RECOMMEND_READY" }
1136
+ }
1137
+ };
1138
+ },
1139
+ readRecommendTabState: async () => ({
1140
+ ok: true,
1141
+ active_status: "0",
1142
+ tab_state: { active_status: "0" }
1143
+ }),
1144
+ switchRecommendTab: async (_workspaceRoot, { target_status }) => {
1145
+ calls.push({ type: "switch", target_status });
1146
+ return {
1147
+ ok: true,
1148
+ state: "TAB_SWITCHED",
1149
+ active_status: "1",
1150
+ tab_state: { active_status: "1" }
1151
+ };
1152
+ },
1153
+ runRecommendScreenCli: async ({ pageScope }) => {
1154
+ calls.push({ type: "screen", pageScope });
1155
+ return {
1156
+ ok: true,
1157
+ summary: {
1158
+ processed_count: 8,
1159
+ passed_count: 3,
1160
+ skipped_count: 5,
1161
+ output_csv: "C:/temp/result.csv",
1162
+ completion_reason: "page_exhausted",
1163
+ active_tab_status: "1",
1164
+ resume_source: "image_fallback"
1165
+ }
1166
+ };
1167
+ }
1168
+ }
1169
+ );
1170
+
1171
+ assert.equal(result.status, "COMPLETED");
1172
+ assert.deepEqual(calls.map((item) => item.type), ["search", "switch", "screen"]);
1173
+ assert.equal(calls[0].pageScope, "latest");
1174
+ assert.equal(calls[1].target_status, "1");
1175
+ assert.equal(calls[2].pageScope, "latest");
1176
+ assert.equal(result.result.selected_page, "latest");
1177
+ assert.equal(result.result.active_tab_status, "1");
1178
+ assert.equal(result.result.resume_source, "image_fallback");
1179
+ }
1180
+
1105
1181
  async function testFeaturedMissingCalibrationShouldAutoCalibrateThenContinue() {
1106
1182
  const calls = [];
1107
1183
  let preflightCallCount = 0;
@@ -1855,6 +1931,7 @@ async function main() {
1855
1931
  await testNeedMaxGreetCountConfirmationGate();
1856
1932
  await testNeedInputGate();
1857
1933
  await testFeaturedPipelineShouldRunSearchThenSwitchTabThenScreen();
1934
+ await testLatestPipelineShouldRunSearchThenSwitchTabThenScreen();
1858
1935
  await testFeaturedMissingCalibrationShouldAutoCalibrateThenContinue();
1859
1936
  await testFeaturedCalibrationFailureShouldReturnCalibrationRequired();
1860
1937
  await testFeaturedTabSwitchFailureShouldReturnRetryableError();
@@ -15,6 +15,7 @@ const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
15
15
  const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
16
16
  const PAGE_SCOPE_TAB_STATUS = {
17
17
  recommend: "0",
18
+ latest: "1",
18
19
  featured: "3"
19
20
  };
20
21
 
@@ -79,6 +80,7 @@ function normalizePageScope(value) {
79
80
  const normalized = normalizeText(value).toLowerCase();
80
81
  if (!normalized) return null;
81
82
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
83
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
82
84
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
83
85
  return null;
84
86
  }
@@ -774,6 +776,7 @@ function buildListCandidatesExpr(processedKeys) {
774
776
  const processed = new Set(processedKeys || []);
775
777
  const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
776
778
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
779
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
777
780
  const textOf = (el) => String(el ? el.textContent : '').replace(/\s+/g, ' ').trim();
778
781
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
779
782
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
@@ -838,19 +841,71 @@ function buildListCandidatesExpr(processedKeys) {
838
841
  layout: 'featured'
839
842
  };
840
843
  }).filter(Boolean);
844
+ const latestCandidates = latestCards.map((card, index) => {
845
+ const inner = card.querySelector('.card-inner[data-geek]') || card.querySelector('[data-geek]');
846
+ if (!inner) return null;
847
+ const geekId = inner.getAttribute('data-geek');
848
+ if (!geekId) return null;
849
+ const rect = card.getBoundingClientRect();
850
+ const nameEl = card.querySelector('.name, .name-wrap .name, .name-wrap');
851
+ const tags = Array.from(card.querySelectorAll('.base-info span, .edu-wrap span, .desc span, .tag-wrap span, .tag-item'))
852
+ .map((item) => textOf(item))
853
+ .filter(Boolean);
854
+ const latestWork = card.querySelector('.timeline-wrap.work-exps .timeline-item');
855
+ const workSpans = latestWork
856
+ ? Array.from(latestWork.querySelectorAll('.join-text-wrap.content span')).map((item) => textOf(item)).filter(Boolean)
857
+ : [];
858
+ return {
859
+ found: true,
860
+ index,
861
+ key: geekId,
862
+ geek_id: geekId,
863
+ name: textOf(nameEl),
864
+ school: tags[0] || '',
865
+ major: tags[1] || '',
866
+ degree: tags[2] || '',
867
+ last_company: workSpans[0] || '',
868
+ last_position: workSpans[1] || '',
869
+ x: frameRect.left + rect.left + Math.min(Math.max(rect.width / 2, 80), Math.max(rect.width - 40, 80)),
870
+ y: frameRect.top + rect.top + Math.min(Math.max(rect.height / 2, 24), Math.max(rect.height - 12, 24)),
871
+ width: rect.width,
872
+ height: rect.height,
873
+ layout: 'latest'
874
+ };
875
+ }).filter(Boolean);
841
876
  const inferredStatus = activeStatus
842
- || (featuredCandidates.length > 0 && recommendCandidates.length === 0 ? '3' : recommendCandidates.length > 0 ? '0' : '');
877
+ || (
878
+ featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
879
+ ? '3'
880
+ : latestCandidates.length > 0 && recommendCandidates.length === 0 && featuredCandidates.length === 0
881
+ ? '1'
882
+ : recommendCandidates.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
883
+ ? '0'
884
+ : ''
885
+ );
843
886
  const activeLayout = inferredStatus === '3'
844
887
  ? 'featured'
845
- : inferredStatus === '0'
846
- ? 'recommend'
847
- : (featuredCandidates.length > 0 && recommendCandidates.length === 0 ? 'featured' : 'recommend');
848
- const candidates = activeLayout === 'featured' ? featuredCandidates : recommendCandidates;
888
+ : inferredStatus === '1'
889
+ ? 'latest'
890
+ : inferredStatus === '0'
891
+ ? 'recommend'
892
+ : (
893
+ featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
894
+ ? 'featured'
895
+ : latestCandidates.length > 0 && featuredCandidates.length === 0 && recommendCandidates.length === 0
896
+ ? 'latest'
897
+ : 'recommend'
898
+ );
899
+ const candidates = activeLayout === 'featured'
900
+ ? featuredCandidates
901
+ : activeLayout === 'latest'
902
+ ? latestCandidates
903
+ : recommendCandidates;
849
904
  return {
850
905
  ok: true,
851
906
  candidates: candidates.filter((candidate) => !processed.has(candidate.key)),
852
907
  candidate_count: candidates.length,
853
- total_cards: activeLayout === 'featured' ? featuredCards.length : cards.length,
908
+ total_cards: activeLayout === 'featured' ? featuredCards.length : activeLayout === 'latest' ? latestCards.length : cards.length,
854
909
  active_tab_status: inferredStatus || null,
855
910
  layout: activeLayout
856
911
  };
@@ -871,16 +926,28 @@ const jsGetListState = `(() => {
871
926
  const candidateCards = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
872
927
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
873
928
  const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
929
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
930
+ const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
874
931
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
875
932
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
876
933
  const activeTabStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
877
934
  const inferredStatus = activeTabStatus
878
- || (featuredCandidates.length > 0 && candidateCards.length === 0 ? '3' : candidateCards.length > 0 ? '0' : '');
935
+ || (
936
+ featuredCandidates.length > 0 && candidateCards.length === 0 && latestCandidates.length === 0
937
+ ? '3'
938
+ : latestCandidates.length > 0 && candidateCards.length === 0 && featuredCandidates.length === 0
939
+ ? '1'
940
+ : candidateCards.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
941
+ ? '0'
942
+ : ''
943
+ );
879
944
  const effectiveCount = inferredStatus === '3'
880
945
  ? featuredCandidates.length
881
- : inferredStatus === '0'
882
- ? candidateCards.length
883
- : Math.max(candidateCards.length, featuredCandidates.length);
946
+ : inferredStatus === '1'
947
+ ? latestCandidates.length
948
+ : inferredStatus === '0'
949
+ ? candidateCards.length
950
+ : Math.max(candidateCards.length, featuredCandidates.length, latestCandidates.length);
884
951
  return {
885
952
  ok: true,
886
953
  scrollTop: body ? body.scrollTop : 0,
@@ -898,7 +965,8 @@ const jsGetListState = `(() => {
898
965
  candidateCount: effectiveCount,
899
966
  recommendCandidateCount: candidateCards.length,
900
967
  featuredCandidateCount: featuredCandidates.length,
901
- totalCards: Math.max(cards.length, featuredCards.length),
968
+ latestCandidateCount: latestCandidates.length,
969
+ totalCards: Math.max(cards.length, featuredCards.length, latestCards.length),
902
970
  activeTabStatus: inferredStatus || null
903
971
  };
904
972
  })()`;
@@ -914,12 +982,21 @@ const jsScrollList = `(() => {
914
982
  const body = doc.body;
915
983
  const recommendCards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item')).filter((card) => card.querySelector('.card-inner[data-geekid]'));
916
984
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card')).filter((card) => card.querySelector('a[data-geekid]'));
985
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap')).filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
917
986
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
918
987
  const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
919
988
  const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
920
989
  const inferredStatus = activeStatus
921
- || (featuredCards.length > 0 && recommendCards.length === 0 ? '3' : recommendCards.length > 0 ? '0' : '');
922
- const activeCards = inferredStatus === '3' ? featuredCards : recommendCards;
990
+ || (
991
+ featuredCards.length > 0 && recommendCards.length === 0 && latestCards.length === 0
992
+ ? '3'
993
+ : latestCards.length > 0 && recommendCards.length === 0 && featuredCards.length === 0
994
+ ? '1'
995
+ : recommendCards.length > 0 && featuredCards.length === 0 && latestCards.length === 0
996
+ ? '0'
997
+ : ''
998
+ );
999
+ const activeCards = inferredStatus === '3' ? featuredCards : inferredStatus === '1' ? latestCards : recommendCards;
923
1000
  const lastCard = activeCards[activeCards.length - 1];
924
1001
  const before = {
925
1002
  scrollTop: body ? body.scrollTop : 0,
@@ -3361,11 +3438,11 @@ async function main() {
3361
3438
  const args = parseArgs(process.argv.slice(2));
3362
3439
  if (args.help) {
3363
3440
  console.log(JSON.stringify({
3364
- status: "COMPLETED",
3365
- result: {
3366
- usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --page-scope recommend|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
3367
- }
3368
- }));
3441
+ status: "COMPLETED",
3442
+ result: {
3443
+ usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
3444
+ }
3445
+ }));
3369
3446
  return;
3370
3447
  }
3371
3448
 
@@ -702,6 +702,21 @@ function testParseArgsShouldSupportFeaturedAliasesAndInlinePort() {
702
702
  assert.equal(parsed.__provided.port, true);
703
703
  }
704
704
 
705
+ function testParseArgsShouldSupportLatestPageScope() {
706
+ const parsed = parseArgs([
707
+ "--criteria", "test criteria",
708
+ "--baseurl", "https://example.com/v1",
709
+ "--apikey", "key",
710
+ "--model", "test-model",
711
+ "--page-scope", "latest",
712
+ "--port", "9222",
713
+ "--post-action", "none",
714
+ "--post-action-confirmed", "true"
715
+ ]);
716
+ assert.equal(parsed.pageScope, "latest");
717
+ assert.equal(parsed.port, 9222);
718
+ }
719
+
705
720
  async function main() {
706
721
  testShouldAbortResumeProbeEarly();
707
722
  await testSingleResumeCaptureFailureIsSkipped();
@@ -725,6 +740,7 @@ async function main() {
725
740
  testStitchWithAvailablePythonShouldFallbackToPython();
726
741
  testStitchWithAvailablePythonShouldFailWhenScriptMissing();
727
742
  testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
743
+ testParseArgsShouldSupportLatestPageScope();
728
744
  console.log("recoverable resume failure tests passed");
729
745
  }
730
746
 
@@ -41,6 +41,7 @@ function normalizePageScope(value) {
41
41
  const normalized = normalizeText(value).toLowerCase();
42
42
  if (!normalized) return null;
43
43
  if (["recommend", "推荐", "推荐页", "推荐页面"].includes(normalized)) return "recommend";
44
+ if (["latest", "最新", "最新页", "最新页面"].includes(normalized)) return "latest";
44
45
  if (["featured", "精选", "精选页", "精选页面", "精选牛人"].includes(normalized)) return "featured";
45
46
  return null;
46
47
  }
@@ -1449,6 +1450,8 @@ class RecommendSearchCli {
1449
1450
  const recommendCandidates = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
1450
1451
  const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
1451
1452
  const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
1453
+ const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
1454
+ const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
1452
1455
  const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
1453
1456
  const activeTab = tabs.find((node) => {
1454
1457
  const className = String(node.className || '');
@@ -1457,22 +1460,27 @@ class RecommendSearchCli {
1457
1460
  }) || null;
1458
1461
  const activeTabStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
1459
1462
  const inferredStatus = activeTabStatus
1460
- || (featuredCandidates.length > 0 && recommendCandidates.length === 0
1463
+ || (featuredCandidates.length > 0 && recommendCandidates.length === 0 && latestCandidates.length === 0
1461
1464
  ? '3'
1462
- : recommendCandidates.length > 0 && featuredCandidates.length === 0
1463
- ? '0'
1464
- : '');
1465
+ : latestCandidates.length > 0 && recommendCandidates.length === 0 && featuredCandidates.length === 0
1466
+ ? '1'
1467
+ : recommendCandidates.length > 0 && featuredCandidates.length === 0 && latestCandidates.length === 0
1468
+ ? '0'
1469
+ : '');
1465
1470
  const effectiveCount = inferredStatus === '3'
1466
1471
  ? featuredCandidates.length
1472
+ : inferredStatus === '1'
1473
+ ? latestCandidates.length
1467
1474
  : inferredStatus === '0'
1468
1475
  ? recommendCandidates.length
1469
- : Math.max(recommendCandidates.length, featuredCandidates.length);
1476
+ : Math.max(recommendCandidates.length, featuredCandidates.length, latestCandidates.length);
1470
1477
  const body = doc.body;
1471
1478
  return {
1472
1479
  ok: true,
1473
1480
  candidateCount: effectiveCount,
1474
1481
  recommendCandidateCount: recommendCandidates.length,
1475
1482
  featuredCandidateCount: featuredCandidates.length,
1483
+ latestCandidateCount: latestCandidates.length,
1476
1484
  activeTabStatus: inferredStatus || null,
1477
1485
  totalCardCount: cards.length,
1478
1486
  scrollTop: body ? body.scrollTop : 0,
@@ -1508,7 +1516,7 @@ class RecommendSearchCli {
1508
1516
  console.log(JSON.stringify({
1509
1517
  status: "COMPLETED",
1510
1518
  result: {
1511
- usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --job \"算法工程师(视频/图像模型方向) _ 杭州\" --page-scope recommend|featured --port 9222",
1519
+ usage: "node src/cli.js --school-tag 985/211 --degree 本科及以上 --gender 男 --recent-not-view 近14天没有 --job \"算法工程师(视频/图像模型方向) _ 杭州\" --page-scope recommend|latest|featured --port 9222",
1512
1520
  list_jobs_usage: "node src/cli.js --list-jobs --port 9222"
1513
1521
  }
1514
1522
  }));
@@ -43,6 +43,8 @@ function createArgs(overrides = {}) {
43
43
  function testParseArgsPageScope() {
44
44
  const parsed = parseArgs(["--page-scope", "featured"]);
45
45
  assert.equal(parsed.pageScope, "featured");
46
+ const latestParsed = parseArgs(["--page-scope", "latest"]);
47
+ assert.equal(latestParsed.pageScope, "latest");
46
48
  }
47
49
 
48
50
  class SelectJobCliMock extends RecommendSearchCli {