@reconcrap/boss-recommend-mcp 1.3.15 → 1.3.17

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.
@@ -23,6 +23,8 @@ const SHOULD_BRING_TO_FRONT = shouldBringChromeToFront();
23
23
 
24
24
  const EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS = 5000;
25
25
  const EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS = 4;
26
+ const RESUME_VIEWPORT_STABILITY_POLL_MS = 80;
27
+ const RESUME_VIEWPORT_STABLE_POLLS = 2;
26
28
 
27
29
  function clampInteger(value, low, high) {
28
30
  return Math.max(low, Math.min(high, value));
@@ -105,14 +107,71 @@ function isStableNoResumeIframeProbe(probe) {
105
107
  return activeScopeCount === 0 && totalResumeIframes === 0 && visibleResumeIframes === 0;
106
108
  }
107
109
 
108
- function shouldAbortResumeProbeEarly({ probe, stableNoResumeIframePolls, elapsedMs, waitResumeMs }) {
110
+ function shouldAbortResumeProbeEarly({ probe, stableNoResumeIframePolls, elapsedMs, waitResumeMs }) {
109
111
  if (!isStableNoResumeIframeProbe(probe)) {
110
112
  return false;
111
113
  }
112
114
  const minWaitMs = Math.min(waitResumeMs, EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS);
113
115
  return stableNoResumeIframePolls >= EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS
114
- && elapsedMs >= minWaitMs;
115
- }
116
+ && elapsedMs >= minWaitMs;
117
+ }
118
+
119
+ function parseBooleanOption(value, fallback = false) {
120
+ if (value === undefined || value === null || value === "") return fallback;
121
+ if (typeof value === "boolean") return value;
122
+ const normalized = String(value).trim().toLowerCase();
123
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
124
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
125
+ return fallback;
126
+ }
127
+
128
+ function numbersClose(left, right, tolerance = 1) {
129
+ return Math.abs(Number(left || 0) - Number(right || 0)) <= tolerance;
130
+ }
131
+
132
+ function isStableResumeViewport(previous, current, targetScroll) {
133
+ if (!previous?.ok || !current?.ok) return false;
134
+ const target = Number(targetScroll || 0);
135
+ const scrollTop = Number(current.scrollTop || 0);
136
+ const maxScroll = Number(current.maxScroll || 0);
137
+ const targetReached = numbersClose(scrollTop, target, 2)
138
+ || (target >= maxScroll && numbersClose(scrollTop, maxScroll, 2));
139
+ if (!targetReached) return false;
140
+ const prevClip = previous.clip || {};
141
+ const currentClip = current.clip || {};
142
+ return numbersClose(previous.scrollTop, current.scrollTop, 1)
143
+ && numbersClose(previous.scrollHeight, current.scrollHeight, 1)
144
+ && numbersClose(previous.clientHeight, current.clientHeight, 1)
145
+ && numbersClose(prevClip.x, currentClip.x, 1)
146
+ && numbersClose(prevClip.y, currentClip.y, 1)
147
+ && numbersClose(prevClip.width, currentClip.width, 1)
148
+ && numbersClose(prevClip.height, currentClip.height, 1);
149
+ }
150
+
151
+ async function waitForStableResumeViewport(evaluate, targetScroll, maxWaitMs) {
152
+ const maxWait = Math.max(160, Number(maxWaitMs || 0));
153
+ const start = Date.now();
154
+ let previous = null;
155
+ let latest = null;
156
+ let stablePolls = 0;
157
+ while (Date.now() - start < maxWait) {
158
+ await sleep(RESUME_VIEWPORT_STABILITY_POLL_MS);
159
+ const current = await evaluate(buildResumeProbeExpr({ init: false, targetScroll: null }));
160
+ if (current?.ok) {
161
+ latest = current;
162
+ if (isStableResumeViewport(previous, current, targetScroll)) {
163
+ stablePolls += 1;
164
+ if (stablePolls >= RESUME_VIEWPORT_STABLE_POLLS) {
165
+ return current;
166
+ }
167
+ } else {
168
+ stablePolls = 0;
169
+ }
170
+ previous = current;
171
+ }
172
+ }
173
+ return latest;
174
+ }
116
175
 
117
176
  async function stitchWithSharp(metadataFile, stitchedImage) {
118
177
  const sharp = loadSharp();
@@ -490,10 +549,14 @@ function buildResumeProbeExpr({ init, targetScroll }) {
490
549
 
491
550
  async function captureFullResumeCanvas(options = {}) {
492
551
  const host = options.host || process.env.CDP_HOST || "127.0.0.1";
493
- const port = Number(options.port || process.env.CDP_PORT || 9222);
494
- const waitResumeMs = Number(options.waitResumeMs || process.env.WAIT_RESUME_MS || 30000);
495
- const scrollSettleMs = Number(options.scrollSettleMs || process.env.SCROLL_SETTLE_MS || 500);
496
- const outPrefix = options.outPrefix || process.env.OUT_PREFIX || path.resolve(process.cwd(), "recommend_resume_full");
552
+ const port = Number(options.port || process.env.CDP_PORT || 9222);
553
+ const waitResumeMs = Number(options.waitResumeMs || process.env.WAIT_RESUME_MS || 30000);
554
+ const scrollSettleMs = Number(options.scrollSettleMs || process.env.SCROLL_SETTLE_MS || 500);
555
+ const stitchFullImage = parseBooleanOption(
556
+ options.stitchFullImage,
557
+ parseBooleanOption(process.env.BOSS_RECOMMEND_STITCH_FULL_IMAGE, true)
558
+ );
559
+ const outPrefix = options.outPrefix || process.env.OUT_PREFIX || path.resolve(process.cwd(), "recommend_resume_full");
497
560
  const targetPattern = options.targetPattern || process.env.TARGET_PATTERN || "/web/chat/recommend";
498
561
  const stitchScript = path.resolve(__dirname, "stitch_resume_chunks.py");
499
562
  const chunkDir = `${outPrefix}_chunks`;
@@ -633,12 +696,11 @@ async function captureFullResumeCanvas(options = {}) {
633
696
  const chunks = [];
634
697
  const seenScroll = [];
635
698
 
636
- for (let index = 0; index < uniquePositions.length; index += 1) {
637
- const targetScroll = uniquePositions[index];
638
- await evaluate(buildResumeProbeExpr({ init: false, targetScroll }));
639
- await sleep(scrollSettleMs);
640
- const current = await evaluate(buildResumeProbeExpr({ init: false, targetScroll: null }));
641
- if (!current?.ok) continue;
699
+ for (let index = 0; index < uniquePositions.length; index += 1) {
700
+ const targetScroll = uniquePositions[index];
701
+ await evaluate(buildResumeProbeExpr({ init: false, targetScroll }));
702
+ const current = await waitForStableResumeViewport(evaluate, targetScroll, scrollSettleMs);
703
+ if (!current?.ok) continue;
642
704
 
643
705
  const actualScroll = Number(current.scrollTop || 0);
644
706
  if (seenScroll.some((value) => Math.abs(value - actualScroll) < 1)) {
@@ -688,31 +750,36 @@ async function captureFullResumeCanvas(options = {}) {
688
750
  };
689
751
  fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 2), "utf8");
690
752
 
691
- let stitchEngine = "sharp";
692
- try {
693
- await stitchWithSharp(metadataFile, stitchedImage);
694
- } catch (sharpError) {
695
- const fallback = stitchWithAvailablePython(stitchScript, metadataFile, stitchedImage);
696
- if (!fallback.ok) {
697
- const fallbackSummary = fallback.attempts
698
- .map((item) => {
699
- const message = item.stderr || item.stdout || item.error || "unknown error";
700
- return `${item.command}(status=${item.status ?? "null"}): ${message}`;
701
- })
702
- .join(" | ");
703
- throw new Error(
704
- `Stitch failed (sharp + python fallback). sharp=${sharpError?.message || sharpError}; fallback=${fallbackSummary}`
705
- );
706
- }
707
- stitchEngine = fallback.command || "python";
708
- }
709
-
710
- return {
711
- stitchedImage,
712
- metadataFile,
713
- chunkDir,
714
- chunkCount: chunks.length,
715
- stitch_engine: stitchEngine,
753
+ let stitchEngine = "skipped";
754
+ if (stitchFullImage) {
755
+ stitchEngine = "sharp";
756
+ try {
757
+ await stitchWithSharp(metadataFile, stitchedImage);
758
+ } catch (sharpError) {
759
+ const fallback = stitchWithAvailablePython(stitchScript, metadataFile, stitchedImage);
760
+ if (!fallback.ok) {
761
+ const fallbackSummary = fallback.attempts
762
+ .map((item) => {
763
+ const message = item.stderr || item.stdout || item.error || "unknown error";
764
+ return `${item.command}(status=${item.status ?? "null"}): ${message}`;
765
+ })
766
+ .join(" | ");
767
+ throw new Error(
768
+ `Stitch failed (sharp + python fallback). sharp=${sharpError?.message || sharpError}; fallback=${fallbackSummary}`
769
+ );
770
+ }
771
+ stitchEngine = fallback.command || "python";
772
+ }
773
+ }
774
+
775
+ return {
776
+ stitchedImage: stitchFullImage ? stitchedImage : "",
777
+ metadataFile,
778
+ chunkDir,
779
+ chunkCount: chunks.length,
780
+ chunkFiles: chunks.map((chunk) => chunk.file),
781
+ modelImagePaths: chunks.map((chunk) => chunk.file),
782
+ stitch_engine: stitchEngine,
716
783
  target: {
717
784
  title: target.title,
718
785
  url: target.url
@@ -729,9 +796,10 @@ module.exports = {
729
796
  captureFullResumeCanvas,
730
797
  __testables: {
731
798
  EARLY_FAIL_NO_RESUME_IFRAME_MIN_WAIT_MS,
732
- EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
733
- isStableNoResumeIframeProbe,
734
- shouldAbortResumeProbeEarly,
799
+ EARLY_FAIL_NO_RESUME_IFRAME_STABLE_POLLS,
800
+ isStableNoResumeIframeProbe,
801
+ isStableResumeViewport,
802
+ shouldAbortResumeProbeEarly,
735
803
  stitchWithAvailablePython,
736
804
  stitchWithSharp
737
805
  }
@@ -238,6 +238,31 @@ function testShouldAbortResumeProbeEarly() {
238
238
  assert.equal(shouldAbort, true);
239
239
  }
240
240
 
241
+ function testResumeViewportStabilityRequiresSettledScrollAndClip() {
242
+ const previous = {
243
+ ok: true,
244
+ scrollTop: 200,
245
+ scrollHeight: 1000,
246
+ clientHeight: 400,
247
+ maxScroll: 600,
248
+ clip: { x: 10, y: 20, width: 300, height: 400 }
249
+ };
250
+ const current = {
251
+ ok: true,
252
+ scrollTop: 200.5,
253
+ scrollHeight: 1000,
254
+ clientHeight: 400,
255
+ maxScroll: 600,
256
+ clip: { x: 10, y: 20, width: 300, height: 400 }
257
+ };
258
+ assert.equal(captureTestables.isStableResumeViewport(previous, current, 200), true);
259
+ assert.equal(captureTestables.isStableResumeViewport(previous, { ...current, scrollTop: 180 }, 200), false);
260
+ assert.equal(
261
+ captureTestables.isStableResumeViewport(previous, { ...current, clip: { ...current.clip, height: 360 } }, 200),
262
+ false
263
+ );
264
+ }
265
+
241
266
  async function testSingleResumeCaptureFailureIsSkipped() {
242
267
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-skip-"));
243
268
  const badCandidate = { key: "bad", geek_id: "bad", name: "bad candidate" };
@@ -467,7 +492,7 @@ async function testRecommendShouldPreferNetworkResumeWhenAvailable() {
467
492
  assert.equal(result.result.resume_source, "network");
468
493
  }
469
494
 
470
- async function testNetworkMissShouldFallbackToDomBeforeImageCapture() {
495
+ async function testNetworkMissShouldFallbackToImageThenDom() {
471
496
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-dom-fallback-"));
472
497
  const candidate = { key: "dom-1", geek_id: "dom-1", name: "dom candidate" };
473
498
  const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
@@ -485,13 +510,15 @@ async function testNetworkMissShouldFallbackToDomBeforeImageCapture() {
485
510
  });
486
511
 
487
512
  cli.waitForNetworkResumeCandidateInfo = async () => null;
513
+ let captureAttempted = false;
488
514
  cli.callTextModel = async (resumeText) => ({
489
515
  passed: true,
490
516
  reason: resumeText.includes("华中科技大学") ? "dom fallback used" : "unexpected",
491
517
  summary: "dom fallback used"
492
518
  });
493
519
  cli.captureResumeImage = async () => {
494
- throw new Error("capture should not be called when dom fallback resume exists");
520
+ captureAttempted = true;
521
+ throw new Error("capture failed, should fallback to dom");
495
522
  };
496
523
 
497
524
  const result = await cli.run();
@@ -501,6 +528,7 @@ async function testNetworkMissShouldFallbackToDomBeforeImageCapture() {
501
528
  assert.equal(cli.passedCandidates.length, 1);
502
529
  assert.equal(cli.passedCandidates[0].school, "华中科技大学");
503
530
  assert.equal(cli.passedCandidates[0].resumeSource, "dom_fallback");
531
+ assert.equal(captureAttempted, true);
504
532
  }
505
533
 
506
534
  async function testNetworkMissShouldFallbackToImageCapture() {
@@ -522,6 +550,64 @@ async function testNetworkMissShouldFallbackToImageCapture() {
522
550
  assert.equal(result.result.resume_source, "image_fallback");
523
551
  }
524
552
 
553
+ async function testImageModeShouldUseShortNetworkGraceWindow() {
554
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-image-mode-grace-"));
555
+ const first = { key: "img-mode-1", geek_id: "img-mode-1", name: "image mode one" };
556
+ const second = { key: "img-mode-2", geek_id: "img-mode-2", name: "image mode two" };
557
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
558
+ candidates: [first, second],
559
+ captureOutcomes: new Map([
560
+ [first.key, { stitchedImage: path.join(tempDir, "img-mode-1.png") }],
561
+ [second.key, { stitchedImage: path.join(tempDir, "img-mode-2.png") }]
562
+ ]),
563
+ screeningByKey: new Map([
564
+ [first.key, { passed: false, reason: "image one", summary: "image one" }],
565
+ [second.key, { passed: false, reason: "image two", summary: "image two" }]
566
+ ])
567
+ });
568
+ const waits = [];
569
+ cli.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => {
570
+ waits.push(timeoutMs);
571
+ return null;
572
+ };
573
+
574
+ const result = await cli.run();
575
+ assert.equal(result.status, "COMPLETED");
576
+ assert.equal(cli.resumeAcquisitionMode, "image");
577
+ assert.deepEqual(waits.slice(-1), [__testables.NETWORK_RESUME_IMAGE_MODE_GRACE_MS]);
578
+ }
579
+
580
+ async function testImageFailureShouldLateRetryNetworkBeforeDomFallback() {
581
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-image-fail-late-network-"));
582
+ const candidate = { key: "late-network-1", geek_id: "late-network-1", name: "late network candidate" };
583
+ const cli = new FakeRecommendScreenCli(createArgs(tempDir), {
584
+ candidates: [candidate]
585
+ });
586
+ let domUsed = false;
587
+ cli.waitForNetworkResumeCandidateInfo = async (_candidate, timeoutMs) => (
588
+ timeoutMs === __testables.NETWORK_RESUME_LATE_RETRY_MS
589
+ ? { resumeText: "late network resume text" }
590
+ : null
591
+ );
592
+ cli.captureResumeImage = async () => {
593
+ throw createResumeCaptureError("image capture failed before late network");
594
+ };
595
+ cli.extractResumeTextFromDom = async () => {
596
+ domUsed = true;
597
+ return null;
598
+ };
599
+ cli.callTextModel = async () => ({
600
+ passed: true,
601
+ reason: "late network used",
602
+ summary: "late network used"
603
+ });
604
+
605
+ const result = await cli.run();
606
+ assert.equal(result.status, "COMPLETED");
607
+ assert.equal(result.result.resume_source, "network");
608
+ assert.equal(domUsed, false);
609
+ }
610
+
525
611
  async function testLatestShouldPreferNetworkResumeWhenAvailable() {
526
612
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-main-"));
527
613
  const args = createArgs(tempDir);
@@ -600,7 +686,7 @@ function testLatestPayloadShouldRemainAvailableWhenCandidateKeyMissing() {
600
686
  key: "",
601
687
  geek_id: ""
602
688
  });
603
- assert.equal(extracted?.resumeText, "recent resume payload");
689
+ assert.equal(extracted?.candidateInfo?.resumeText, "recent resume payload");
604
690
  }
605
691
 
606
692
  async function testVisionModelFailureShouldSkipCandidateAndContinue() {
@@ -638,25 +724,44 @@ async function testVisionModelFailureShouldSkipCandidateAndContinue() {
638
724
  assert.equal(result.result.skipped_count, 1);
639
725
  }
640
726
 
641
- async function testFeaturedNetworkMissShouldSkipWithoutImageCapture() {
727
+ async function testFeaturedNetworkMissShouldFallbackToDomAfterImageFailure() {
642
728
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-featured-network-only-"));
643
729
  const args = createArgs(tempDir);
644
730
  args.pageScope = "featured";
645
731
  const candidate = { key: "featured-no-network", geek_id: "featured-no-network", name: "featured no network" };
646
732
  const cli = new FakeRecommendScreenCli(args, {
647
- candidates: [candidate]
733
+ candidates: [candidate],
734
+ domResumeByKey: new Map([
735
+ ["featured-no-network", {
736
+ name: "featured no network",
737
+ school: "华中科技大学",
738
+ major: "软件工程",
739
+ company: "测试公司",
740
+ position: "后端工程师",
741
+ resumeText: "featured network miss 后应在截图失败后走 DOM 兜底。"
742
+ }]
743
+ ])
648
744
  });
649
745
  cli.waitForNetworkResumeCandidateInfo = async () => null;
746
+ let captureAttempted = false;
747
+ cli.callTextModel = async () => ({
748
+ passed: true,
749
+ reason: "dom fallback used",
750
+ summary: "dom fallback used"
751
+ });
650
752
  cli.captureResumeImage = async () => {
651
- throw new Error("capture should not be called for featured scope");
753
+ captureAttempted = true;
754
+ throw new Error("capture failed for featured scope");
652
755
  };
653
756
 
654
757
  const result = await cli.run();
655
758
  assert.equal(result.status, "COMPLETED");
656
759
  assert.equal(result.result.processed_count, 1);
657
- assert.equal(result.result.passed_count, 0);
658
- assert.equal(result.result.skipped_count, 1);
659
- assert.equal(result.result.resume_source, "network");
760
+ assert.equal(result.result.passed_count, 1);
761
+ assert.equal(result.result.skipped_count, 0);
762
+ assert.equal(result.result.resume_source, "dom_fallback");
763
+ assert.equal(captureAttempted, true);
764
+ assert.equal(cli.passedCandidates[0].resumeSource, "dom_fallback");
660
765
  }
661
766
 
662
767
  async function testFeaturedFavoriteShouldNotUseDomFallback() {
@@ -1326,6 +1431,75 @@ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
1326
1431
  }
1327
1432
  }
1328
1433
 
1434
+ async function testTextModelShouldDefaultThinkingOffForVolcengine() {
1435
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-off-"));
1436
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1437
+ cli.args.baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
1438
+ cli.args.model = "doubao-seed-2-0-mini-260215";
1439
+ const originalFetch = global.fetch;
1440
+ let capturedPayload = null;
1441
+ global.fetch = async (_url, options = {}) => {
1442
+ capturedPayload = JSON.parse(String(options.body || "{}"));
1443
+ return {
1444
+ ok: true,
1445
+ status: 200,
1446
+ async json() {
1447
+ return {
1448
+ choices: [
1449
+ {
1450
+ message: {
1451
+ content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1452
+ }
1453
+ }
1454
+ ]
1455
+ };
1456
+ }
1457
+ };
1458
+ };
1459
+ try {
1460
+ await cli.callTextModel("resume");
1461
+ assert.deepEqual(capturedPayload?.thinking, { type: "disabled" });
1462
+ assert.equal(capturedPayload?.reasoning_effort, "minimal");
1463
+ } finally {
1464
+ global.fetch = originalFetch;
1465
+ }
1466
+ }
1467
+
1468
+ async function testTextModelShouldSupportLowThinkingForVolcengine() {
1469
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-low-"));
1470
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1471
+ cli.args.baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
1472
+ cli.args.model = "doubao-seed-2-0-mini-260215";
1473
+ cli.args.thinkingLevel = "low";
1474
+ const originalFetch = global.fetch;
1475
+ let capturedPayload = null;
1476
+ global.fetch = async (_url, options = {}) => {
1477
+ capturedPayload = JSON.parse(String(options.body || "{}"));
1478
+ return {
1479
+ ok: true,
1480
+ status: 200,
1481
+ async json() {
1482
+ return {
1483
+ choices: [
1484
+ {
1485
+ message: {
1486
+ content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
1487
+ }
1488
+ }
1489
+ ]
1490
+ };
1491
+ }
1492
+ };
1493
+ };
1494
+ try {
1495
+ await cli.callTextModel("resume");
1496
+ assert.deepEqual(capturedPayload?.thinking, { type: "enabled" });
1497
+ assert.equal(capturedPayload?.reasoning_effort, "low");
1498
+ } finally {
1499
+ global.fetch = originalFetch;
1500
+ }
1501
+ }
1502
+
1329
1503
  async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
1330
1504
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
1331
1505
  const cli = new RecommendScreenCli(createArgs(tempDir));
@@ -1451,8 +1625,55 @@ async function testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence()
1451
1625
  assert.equal(result.evidenceMatchedCount, 0);
1452
1626
  }
1453
1627
 
1628
+ async function testVisionModelShouldSendAllOrderedChunks() {
1629
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-all-chunks-"));
1630
+ const chunkPaths = [];
1631
+ for (let index = 0; index < 3; index += 1) {
1632
+ const chunkPath = path.join(tempDir, `chunk-${index + 1}.png`);
1633
+ await sharp({
1634
+ create: { width: 16, height: 16, channels: 3, background: { r: 255 - index, g: 250, b: 245 } }
1635
+ }).png().toFile(chunkPath);
1636
+ chunkPaths.push(chunkPath);
1637
+ }
1638
+ const cli = new RecommendScreenCli(createArgs(tempDir));
1639
+ const originalFetch = global.fetch;
1640
+ let capturedPayload = null;
1641
+ global.fetch = async (_url, options = {}) => {
1642
+ capturedPayload = JSON.parse(String(options.body || "{}"));
1643
+ return {
1644
+ ok: true,
1645
+ status: 200,
1646
+ async json() {
1647
+ return {
1648
+ choices: [
1649
+ {
1650
+ message: {
1651
+ content: "{\"passed\": false, \"reason\": \"checked all chunks\", \"summary\": \"checked\", \"evidence\": [\"chunk evidence\", \"more evidence\"]}"
1652
+ }
1653
+ }
1654
+ ]
1655
+ };
1656
+ }
1657
+ };
1658
+ };
1659
+ try {
1660
+ const result = await cli.requestVisionModel(chunkPaths);
1661
+ assert.equal(result.passed, false);
1662
+ const userContent = capturedPayload?.messages?.[1]?.content || [];
1663
+ assert.equal(userContent.filter((item) => item?.type === "image_url").length, 3);
1664
+ const text = userContent.map((item) => item?.text || "").join("\n");
1665
+ assert.equal(text.includes("简历分段 1/3"), true);
1666
+ assert.equal(text.includes("简历分段 2/3"), true);
1667
+ assert.equal(text.includes("简历分段 3/3"), true);
1668
+ assert.equal(text.includes("不能只根据前几段下结论"), true);
1669
+ } finally {
1670
+ global.fetch = originalFetch;
1671
+ }
1672
+ }
1673
+
1454
1674
  async function main() {
1455
1675
  testShouldAbortResumeProbeEarly();
1676
+ testResumeViewportStabilityRequiresSettledScrollAndClip();
1456
1677
  await testSingleResumeCaptureFailureIsSkipped();
1457
1678
  await testConsecutiveResumeCaptureFailuresStillAbort();
1458
1679
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
@@ -1461,14 +1682,16 @@ async function main() {
1461
1682
  await testTargetCountShouldNotTreatProcessedCountAsReached();
1462
1683
  await testFeaturedShouldUseNetworkResumeOnly();
1463
1684
  await testRecommendShouldPreferNetworkResumeWhenAvailable();
1464
- await testNetworkMissShouldFallbackToDomBeforeImageCapture();
1685
+ await testNetworkMissShouldFallbackToImageThenDom();
1465
1686
  await testNetworkMissShouldFallbackToImageCapture();
1687
+ await testImageModeShouldUseShortNetworkGraceWindow();
1688
+ await testImageFailureShouldLateRetryNetworkBeforeDomFallback();
1466
1689
  await testLatestShouldPreferNetworkResumeWhenAvailable();
1467
1690
  await testLatestNetworkMissShouldFallbackToImageCapture();
1468
1691
  testLatestPayloadShouldNotLeakAcrossCandidates();
1469
1692
  testLatestPayloadShouldRemainAvailableWhenCandidateKeyMissing();
1470
1693
  await testVisionModelFailureShouldSkipCandidateAndContinue();
1471
- await testFeaturedNetworkMissShouldSkipWithoutImageCapture();
1694
+ await testFeaturedNetworkMissShouldFallbackToDomAfterImageFailure();
1472
1695
  await testFeaturedFavoriteShouldNotUseDomFallback();
1473
1696
  await testFeaturedFavoriteShouldSkipClickWhenAlreadyInterested();
1474
1697
  await testFeaturedFavoriteShouldRecognizeAlreadyFavoritedByDelThenAdd();
@@ -1493,8 +1716,11 @@ async function main() {
1493
1716
  testParseArgsShouldSupportInputSummaryJson();
1494
1717
  await testCallTextModelShouldNotTruncateLongResume();
1495
1718
  await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
1719
+ await testTextModelShouldDefaultThinkingOffForVolcengine();
1720
+ await testTextModelShouldSupportLowThinkingForVolcengine();
1496
1721
  await testPrepareVisionImageSegmentsShouldSplitLongImage();
1497
1722
  await testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence();
1723
+ await testVisionModelShouldSendAllOrderedChunks();
1498
1724
  testRecoverablePostActionErrorShouldTreatGreetContinueAndNoButtonAsRecoverable();
1499
1725
  await testRecoverableGreetContinueButtonShouldNotAbortWhenDetailCloseFails();
1500
1726
  await testRecoverableGreetButtonNotFoundShouldNotAbortWhenDetailCloseFails();