@reconcrap/boss-recommend-mcp 1.2.6 → 1.2.8

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.
@@ -124,6 +124,30 @@ class FakeRecommendScreenCli extends RecommendScreenCli {
124
124
  saveCheckpoint() {}
125
125
  }
126
126
 
127
+ class FakeDetailCloseProbeCli extends RecommendScreenCli {
128
+ constructor(args, options = {}) {
129
+ super(args);
130
+ this.listReady = options.listReady === true;
131
+ this.evaluateCallCount = 0;
132
+ }
133
+
134
+ async getDetailClosedState() {
135
+ return { closed: false, reason: "popup visible: .boss-popup__wrapper" };
136
+ }
137
+
138
+ async evaluate() {
139
+ this.evaluateCallCount += 1;
140
+ if (this.evaluateCallCount >= 2) {
141
+ return this.listReady
142
+ ? { ok: true, candidate_count: 1 }
143
+ : { ok: false, error: "LIST_NOT_READY" };
144
+ }
145
+ return { ok: false, error: "CLOSE_ACTION_NOOP" };
146
+ }
147
+
148
+ async pressEsc() {}
149
+ }
150
+
127
151
  function createResumeCaptureError(message = "Resume canvas not found") {
128
152
  const error = new Error(message);
129
153
  error.code = "RESUME_CAPTURE_FAILED";
@@ -306,6 +330,55 @@ async function testPageExhaustedWithoutTargetShouldStillComplete() {
306
330
  assert.equal(result.result.completion_reason, "page_exhausted");
307
331
  }
308
332
 
333
+ async function testTargetCountShouldStopWhenPassedCountReached() {
334
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-stop-"));
335
+ const args = createArgs(tempDir);
336
+ args.targetCount = 1;
337
+ const first = { key: "pass-1", geek_id: "pass-1", name: "pass-1" };
338
+ const second = { key: "skip-2", geek_id: "skip-2", name: "skip-2" };
339
+ const cli = new FakeRecommendScreenCli(args, {
340
+ candidates: [first, second],
341
+ screeningByKey: new Map([
342
+ ["pass-1", { passed: true, reason: "matched", summary: "matched" }],
343
+ ["skip-2", { passed: false, reason: "not matched", summary: "not matched" }]
344
+ ])
345
+ });
346
+
347
+ const result = await cli.run();
348
+ assert.equal(result.status, "COMPLETED");
349
+ assert.equal(result.result.processed_count, 1);
350
+ assert.equal(result.result.passed_count, 1);
351
+ assert.equal(result.result.skipped_count, 0);
352
+ assert.equal(result.result.completion_reason, "target_count_reached");
353
+ }
354
+
355
+ async function testTargetCountShouldNotTreatProcessedCountAsReached() {
356
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-only-"));
357
+ const args = createArgs(tempDir);
358
+ args.targetCount = 1;
359
+ const first = { key: "skip-a", geek_id: "skip-a", name: "skip-a" };
360
+ const second = { key: "skip-b", geek_id: "skip-b", name: "skip-b" };
361
+ const cli = new FakeRecommendScreenCli(args, {
362
+ candidates: [first, second],
363
+ screeningByKey: new Map([
364
+ ["skip-a", { passed: false, reason: "not matched", summary: "not matched" }],
365
+ ["skip-b", { passed: false, reason: "not matched", summary: "not matched" }]
366
+ ])
367
+ });
368
+
369
+ await assert.rejects(
370
+ () => cli.run(),
371
+ (error) => {
372
+ assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
373
+ assert.equal(error.retryable, true);
374
+ assert.equal(error.partial_result?.processed_count, 2);
375
+ assert.equal(error.partial_result?.passed_count, 0);
376
+ assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
377
+ return true;
378
+ }
379
+ );
380
+ }
381
+
309
382
  async function testFeaturedShouldUseNetworkResumeOnly() {
310
383
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
311
384
  const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
@@ -842,12 +915,152 @@ function testParseArgsShouldSupportLatestPageScope() {
842
915
  assert.equal(parsed.port, 9222);
843
916
  }
844
917
 
918
+ async function testCallTextModelShouldNotTruncateLongResume() {
919
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-full-"));
920
+ const cli = new RecommendScreenCli(createArgs(tempDir));
921
+ const marker = "__END_OF_RESUME_MARKER__";
922
+ const resumeText = `${"A".repeat(32000)}${marker}`;
923
+ const originalFetch = global.fetch;
924
+ let capturedUserContent = "";
925
+ global.fetch = async (_url, options = {}) => {
926
+ const payload = JSON.parse(String(options.body || "{}"));
927
+ capturedUserContent = String(payload?.messages?.[1]?.content || "");
928
+ return {
929
+ ok: true,
930
+ status: 200,
931
+ async json() {
932
+ return {
933
+ choices: [
934
+ {
935
+ message: {
936
+ content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"A\"]}"
937
+ }
938
+ }
939
+ ]
940
+ };
941
+ }
942
+ };
943
+ };
944
+ try {
945
+ const result = await cli.callTextModel(resumeText);
946
+ assert.equal(result.passed, false);
947
+ assert.equal(capturedUserContent.includes(marker), true);
948
+ } finally {
949
+ global.fetch = originalFetch;
950
+ }
951
+ }
952
+
953
+ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
954
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-chunk-fallback-"));
955
+ const cli = new RecommendScreenCli(createArgs(tempDir));
956
+ const originalFetch = global.fetch;
957
+ const prevChunkSize = process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
958
+ const prevChunkOverlap = process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
959
+ const prevMaxChunks = process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
960
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = "80";
961
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = "0";
962
+ process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = "6";
963
+
964
+ const passMarker = "PASS_MARKER_ABC";
965
+ const resumeText = `${"x".repeat(120)}${passMarker}${"y".repeat(120)}`;
966
+ let callCount = 0;
967
+ global.fetch = async (_url, options = {}) => {
968
+ callCount += 1;
969
+ if (callCount === 1) {
970
+ return {
971
+ ok: false,
972
+ status: 400,
973
+ async text() {
974
+ return "maximum context length exceeded";
975
+ }
976
+ };
977
+ }
978
+
979
+ const payload = JSON.parse(String(options.body || "{}"));
980
+ const userContent = String(payload?.messages?.[1]?.content || "");
981
+ const passed = userContent.includes(passMarker);
982
+ const response = passed
983
+ ? "{\"passed\": true, \"reason\": \"命中证据\", \"summary\": \"命中\", \"evidence\": [\"PASS_MARKER_ABC\"]}"
984
+ : "{\"passed\": false, \"reason\": \"本段证据不足\", \"summary\": \"不足\", \"evidence\": []}";
985
+ return {
986
+ ok: true,
987
+ status: 200,
988
+ async json() {
989
+ return {
990
+ choices: [
991
+ {
992
+ message: {
993
+ content: response
994
+ }
995
+ }
996
+ ]
997
+ };
998
+ }
999
+ };
1000
+ };
1001
+ try {
1002
+ const result = await cli.callTextModel(resumeText);
1003
+ assert.equal(result.passed, true);
1004
+ assert.equal(callCount >= 2, true);
1005
+ assert.equal(Array.isArray(result.evidence), true);
1006
+ } finally {
1007
+ global.fetch = originalFetch;
1008
+ if (prevChunkSize === undefined) {
1009
+ delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
1010
+ } else {
1011
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = prevChunkSize;
1012
+ }
1013
+ if (prevChunkOverlap === undefined) {
1014
+ delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
1015
+ } else {
1016
+ process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = prevChunkOverlap;
1017
+ }
1018
+ if (prevMaxChunks === undefined) {
1019
+ delete process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
1020
+ } else {
1021
+ process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = prevMaxChunks;
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
1027
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
1028
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1029
+ const imagePath = path.join(tempDir, "long.png");
1030
+ await sharp({
1031
+ create: { width: 400, height: 1200, channels: 3, background: { r: 240, g: 240, b: 240 } }
1032
+ }).png().toFile(imagePath);
1033
+
1034
+ const prepared = await cli.prepareVisionImageSegmentsForModel(imagePath, 120000, "test");
1035
+ assert.equal(Array.isArray(prepared.imagePaths), true);
1036
+ assert.equal(prepared.imagePaths.length > 1, true);
1037
+ for (const segmentPath of prepared.imagePaths) {
1038
+ assert.equal(fs.existsSync(segmentPath), true);
1039
+ }
1040
+ }
1041
+
1042
+ async function testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady() {
1043
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-fail-"));
1044
+ const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: false });
1045
+ const closed = await cli.closeDetailPage(1);
1046
+ assert.equal(closed, false);
1047
+ }
1048
+
1049
+ async function testCloseDetailPageShouldContinueWhenListReady() {
1050
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-list-ready-"));
1051
+ const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: true });
1052
+ const closed = await cli.closeDetailPage(1);
1053
+ assert.equal(closed, true);
1054
+ }
1055
+
845
1056
  async function main() {
846
1057
  testShouldAbortResumeProbeEarly();
847
1058
  await testSingleResumeCaptureFailureIsSkipped();
848
1059
  await testConsecutiveResumeCaptureFailuresStillAbort();
849
1060
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
850
1061
  await testPageExhaustedWithoutTargetShouldStillComplete();
1062
+ await testTargetCountShouldStopWhenPassedCountReached();
1063
+ await testTargetCountShouldNotTreatProcessedCountAsReached();
851
1064
  await testFeaturedShouldUseNetworkResumeOnly();
852
1065
  await testRecommendShouldPreferNetworkResumeWhenAvailable();
853
1066
  await testNetworkMissShouldFallbackToImageCapture();
@@ -871,6 +1084,11 @@ async function main() {
871
1084
  testStitchWithAvailablePythonShouldFailWhenScriptMissing();
872
1085
  testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
873
1086
  testParseArgsShouldSupportLatestPageScope();
1087
+ await testCallTextModelShouldNotTruncateLongResume();
1088
+ await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
1089
+ await testPrepareVisionImageSegmentsShouldSplitLongImage();
1090
+ await testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady();
1091
+ await testCloseDetailPageShouldContinueWhenListReady();
874
1092
  console.log("recoverable resume failure tests passed");
875
1093
  }
876
1094
 
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import process from "node:process";
3
3
  import readline from "node:readline";
4
+ import { createRequire } from "node:module";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import CDP from "chrome-remote-interface";
7
+ import {
8
+ buildFirstSelectorLookupExpression,
9
+ getRecommendSelectorRule
10
+ } from "../../../src/recommend-healing-config.js";
6
11
 
7
12
  const DEFAULT_PORT = 9222;
8
13
  const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
@@ -14,6 +19,56 @@ const DEGREE_OPTIONS = ["不限", "初中及以下", "中专/中技", "高中",
14
19
  const DEGREE_ORDER = ["初中及以下", "中专/中技", "高中", "大专", "本科", "硕士", "博士"];
15
20
  const GENDER_OPTIONS = ["不限", "男", "女"];
16
21
  const RECENT_NOT_VIEW_OPTIONS = ["不限", "近14天没有"];
22
+ const require = createRequire(import.meta.url);
23
+ require("../../../src/recommend-healing-rules.json");
24
+ const RECOMMEND_IFRAME_SELECTORS = getRecommendSelectorRule(
25
+ ["top", "recommend_iframe"],
26
+ ['iframe[name="recommendFrame"]', 'iframe[src*="/web/frame/recommend/"]', "iframe"]
27
+ );
28
+ const FILTER_TRIGGER_SELECTORS = getRecommendSelectorRule(
29
+ ["frame", "filter_trigger"],
30
+ [".filter-label-wrap", ".recommend-filter.op-filter"]
31
+ );
32
+ const JOB_DROPDOWN_TRIGGER_SELECTORS = getRecommendSelectorRule(
33
+ ["frame", "job_dropdown_trigger"],
34
+ [
35
+ ".chat-job-select",
36
+ ".chat-job-selector",
37
+ ".job-selecter",
38
+ ".job-selector",
39
+ ".job-select-wrap",
40
+ ".job-select",
41
+ ".job-select-box",
42
+ ".job-wrap",
43
+ ".chat-job-name",
44
+ ".top-chat-search"
45
+ ]
46
+ );
47
+ const JOB_LIST_ITEM_SELECTORS = getRecommendSelectorRule(
48
+ ["frame", "job_list_items"],
49
+ [
50
+ ".ui-dropmenu-list .job-list .job-item",
51
+ ".job-selecter-options .job-list .job-item",
52
+ ".job-selector-options .job-list .job-item",
53
+ ".dropmenu-list .job-list .job-item",
54
+ ".job-list .job-item"
55
+ ]
56
+ );
57
+ const JOB_SELECTED_LABEL_SELECTORS = getRecommendSelectorRule(
58
+ ["frame", "job_selected_label"],
59
+ [".chat-job-name", ".job-selecter .label", ".job-selecter .job-name", ".job-select .label"]
60
+ );
61
+ const RECOMMEND_CARD_SELECTORS = getRecommendSelectorRule(["frame", "recommend_cards"], ["ul.card-list > li.card-item"]);
62
+ const FEATURED_CARD_SELECTORS = getRecommendSelectorRule(["frame", "featured_cards"], ["li.geek-info-card"]);
63
+ const LATEST_CARD_SELECTORS = getRecommendSelectorRule(["frame", "latest_cards"], [".candidate-card-wrap"]);
64
+ const RECOMMEND_TAB_SELECTORS = getRecommendSelectorRule(
65
+ ["frame", "tab_items"],
66
+ ["li.tab-item[data-status]", 'li[data-status][class*="tab"]']
67
+ );
68
+
69
+ function buildRecommendFrameExpression() {
70
+ return buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS);
71
+ }
17
72
 
18
73
  function normalizeText(value) {
19
74
  return String(value || "").replace(/\s+/g, " ").trim();
@@ -419,9 +474,7 @@ class RecommendSearchCli {
419
474
  title
420
475
  };
421
476
  }
422
- const frame = document.querySelector('iframe[name="recommendFrame"]')
423
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
424
- || document.querySelector('iframe');
477
+ const frame = ${buildRecommendFrameExpression()};
425
478
  if (!frame || !frame.contentDocument) {
426
479
  return { ok: false, error: 'NO_RECOMMEND_IFRAME', currentUrl, title };
427
480
  }
@@ -438,14 +491,16 @@ class RecommendSearchCli {
438
491
 
439
492
  async getFilterEntryPoint() {
440
493
  return this.evaluate(`(() => {
441
- const frame = document.querySelector('iframe[name="recommendFrame"]')
442
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
443
- || document.querySelector('iframe');
494
+ const frame = ${buildRecommendFrameExpression()};
444
495
  if (!frame || !frame.contentDocument) {
445
496
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
446
497
  }
447
498
  const doc = frame.contentDocument;
448
- const el = doc.querySelector('.filter-label-wrap') || doc.querySelector('.recommend-filter.op-filter');
499
+ const el = ${JSON.stringify(FILTER_TRIGGER_SELECTORS)}
500
+ .map((selector) => {
501
+ try { return doc.querySelector(selector); } catch { return null; }
502
+ })
503
+ .find((node) => node) || null;
449
504
  if (!el) {
450
505
  return { ok: false, error: 'FILTER_TRIGGER_NOT_FOUND' };
451
506
  }
@@ -461,9 +516,7 @@ class RecommendSearchCli {
461
516
 
462
517
  async getJobListState() {
463
518
  return this.evaluate(`(() => {
464
- const frame = document.querySelector('iframe[name="recommendFrame"]')
465
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
466
- || document.querySelector('iframe');
519
+ const frame = ${buildRecommendFrameExpression()};
467
520
  if (!frame || !frame.contentDocument) {
468
521
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
469
522
  }
@@ -491,13 +544,10 @@ class RecommendSearchCli {
491
544
  return rect.width > 2 && rect.height > 2;
492
545
  };
493
546
 
494
- const items = Array.from(doc.querySelectorAll([
495
- '.ui-dropmenu-list .job-list .job-item',
496
- '.job-selecter-options .job-list .job-item',
497
- '.job-selector-options .job-list .job-item',
498
- '.dropmenu-list .job-list .job-item',
499
- '.job-list .job-item'
500
- ].join(',')));
547
+ const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
548
+ .flatMap((selector) => {
549
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
550
+ });
501
551
  const jobs = [];
502
552
  const seen = new Set();
503
553
  for (const item of items) {
@@ -516,7 +566,11 @@ class RecommendSearchCli {
516
566
  });
517
567
  }
518
568
 
519
- const selectedLabelNode = doc.querySelector('.chat-job-name, .job-selecter .label, .job-selecter .job-name, .job-select .label');
569
+ const selectedLabelNode = ${JSON.stringify(JOB_SELECTED_LABEL_SELECTORS)}
570
+ .map((selector) => {
571
+ try { return doc.querySelector(selector); } catch { return null; }
572
+ })
573
+ .find((node) => node) || null;
520
574
  return {
521
575
  ok: true,
522
576
  jobs,
@@ -530,25 +584,12 @@ class RecommendSearchCli {
530
584
 
531
585
  async clickJobDropdownTriggerBySelector() {
532
586
  return this.evaluate(`(() => {
533
- const frame = document.querySelector('iframe[name="recommendFrame"]')
534
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
535
- || document.querySelector('iframe');
587
+ const frame = ${buildRecommendFrameExpression()};
536
588
  if (!frame || !frame.contentDocument) {
537
589
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
538
590
  }
539
591
  const doc = frame.contentDocument;
540
- const selectors = [
541
- '.chat-job-select',
542
- '.chat-job-selector',
543
- '.job-selecter',
544
- '.job-selector',
545
- '.job-select-wrap',
546
- '.job-select',
547
- '.job-select-box',
548
- '.job-wrap',
549
- '.chat-job-name',
550
- '.top-chat-search'
551
- ];
592
+ const selectors = ${JSON.stringify(JOB_DROPDOWN_TRIGGER_SELECTORS)};
552
593
  const isVisible = (el) => {
553
594
  if (!el) return false;
554
595
  const style = getComputedStyle(el);
@@ -615,9 +656,7 @@ class RecommendSearchCli {
615
656
 
616
657
  async clickJobBySelector(job) {
617
658
  return this.evaluate(`((job) => {
618
- const frame = document.querySelector('iframe[name="recommendFrame"]')
619
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
620
- || document.querySelector('iframe');
659
+ const frame = ${buildRecommendFrameExpression()};
621
660
  if (!frame || !frame.contentDocument) {
622
661
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
623
662
  }
@@ -635,13 +674,10 @@ class RecommendSearchCli {
635
674
  .trim();
636
675
  return strippedSingle || byGap;
637
676
  };
638
- const items = Array.from(doc.querySelectorAll([
639
- '.ui-dropmenu-list .job-list .job-item',
640
- '.job-selecter-options .job-list .job-item',
641
- '.job-selector-options .job-list .job-item',
642
- '.dropmenu-list .job-list .job-item',
643
- '.job-list .job-item'
644
- ].join(',')));
677
+ const items = ${JSON.stringify(JOB_LIST_ITEM_SELECTORS)}
678
+ .flatMap((selector) => {
679
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
680
+ });
645
681
  const target = items.find((item) => {
646
682
  const value = normalize(item.getAttribute('value') || item.dataset?.value || '');
647
683
  const label = normalize(item.querySelector('.label')?.textContent || item.textContent || '');
@@ -1439,20 +1475,30 @@ class RecommendSearchCli {
1439
1475
 
1440
1476
  async countCandidates() {
1441
1477
  return this.evaluate(`(() => {
1442
- const frame = document.querySelector('iframe[name="recommendFrame"]')
1443
- || document.querySelector('iframe[src*="/web/frame/recommend/"]')
1444
- || document.querySelector('iframe');
1478
+ const frame = ${buildRecommendFrameExpression()};
1445
1479
  if (!frame || !frame.contentDocument) {
1446
1480
  return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
1447
1481
  }
1448
1482
  const doc = frame.contentDocument;
1449
- const cards = Array.from(doc.querySelectorAll('ul.card-list > li.card-item'));
1483
+ const cards = ${JSON.stringify(RECOMMEND_CARD_SELECTORS)}
1484
+ .flatMap((selector) => {
1485
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
1486
+ });
1450
1487
  const recommendCandidates = cards.filter((card) => card.querySelector('.card-inner[data-geekid]'));
1451
- const featuredCards = Array.from(doc.querySelectorAll('li.geek-info-card'));
1488
+ const featuredCards = ${JSON.stringify(FEATURED_CARD_SELECTORS)}
1489
+ .flatMap((selector) => {
1490
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
1491
+ });
1452
1492
  const featuredCandidates = featuredCards.filter((card) => card.querySelector('a[data-geekid]'));
1453
- const latestCards = Array.from(doc.querySelectorAll('.candidate-card-wrap'));
1493
+ const latestCards = ${JSON.stringify(LATEST_CARD_SELECTORS)}
1494
+ .flatMap((selector) => {
1495
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
1496
+ });
1454
1497
  const latestCandidates = latestCards.filter((card) => card.querySelector('.card-inner[data-geek], [data-geek]'));
1455
- const tabs = Array.from(doc.querySelectorAll('li.tab-item[data-status], li[data-status][class*="tab"]'));
1498
+ const tabs = ${JSON.stringify(RECOMMEND_TAB_SELECTORS)}
1499
+ .flatMap((selector) => {
1500
+ try { return Array.from(doc.querySelectorAll(selector)); } catch { return []; }
1501
+ });
1456
1502
  const activeTab = tabs.find((node) => {
1457
1503
  const className = String(node.className || '');
1458
1504
  const selected = String(node.getAttribute('aria-selected') || '').toLowerCase() === 'true';
@@ -1648,3 +1694,5 @@ export {
1648
1694
  normalizeJobTitle,
1649
1695
  parseArgs
1650
1696
  };
1697
+
1698
+