@reconcrap/boss-recommend-mcp 1.3.16 → 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" };
@@ -525,6 +550,64 @@ async function testNetworkMissShouldFallbackToImageCapture() {
525
550
  assert.equal(result.result.resume_source, "image_fallback");
526
551
  }
527
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
+
528
611
  async function testLatestShouldPreferNetworkResumeWhenAvailable() {
529
612
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-latest-network-main-"));
530
613
  const args = createArgs(tempDir);
@@ -1348,6 +1431,75 @@ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
1348
1431
  }
1349
1432
  }
1350
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
+
1351
1503
  async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
1352
1504
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
1353
1505
  const cli = new RecommendScreenCli(createArgs(tempDir));
@@ -1473,8 +1625,55 @@ async function testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence()
1473
1625
  assert.equal(result.evidenceMatchedCount, 0);
1474
1626
  }
1475
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
+
1476
1674
  async function main() {
1477
1675
  testShouldAbortResumeProbeEarly();
1676
+ testResumeViewportStabilityRequiresSettledScrollAndClip();
1478
1677
  await testSingleResumeCaptureFailureIsSkipped();
1479
1678
  await testConsecutiveResumeCaptureFailuresStillAbort();
1480
1679
  await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
@@ -1485,6 +1684,8 @@ async function main() {
1485
1684
  await testRecommendShouldPreferNetworkResumeWhenAvailable();
1486
1685
  await testNetworkMissShouldFallbackToImageThenDom();
1487
1686
  await testNetworkMissShouldFallbackToImageCapture();
1687
+ await testImageModeShouldUseShortNetworkGraceWindow();
1688
+ await testImageFailureShouldLateRetryNetworkBeforeDomFallback();
1488
1689
  await testLatestShouldPreferNetworkResumeWhenAvailable();
1489
1690
  await testLatestNetworkMissShouldFallbackToImageCapture();
1490
1691
  testLatestPayloadShouldNotLeakAcrossCandidates();
@@ -1515,8 +1716,11 @@ async function main() {
1515
1716
  testParseArgsShouldSupportInputSummaryJson();
1516
1717
  await testCallTextModelShouldNotTruncateLongResume();
1517
1718
  await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
1719
+ await testTextModelShouldDefaultThinkingOffForVolcengine();
1720
+ await testTextModelShouldSupportLowThinkingForVolcengine();
1518
1721
  await testPrepareVisionImageSegmentsShouldSplitLongImage();
1519
1722
  await testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence();
1723
+ await testVisionModelShouldSendAllOrderedChunks();
1520
1724
  testRecoverablePostActionErrorShouldTreatGreetContinueAndNoButtonAsRecoverable();
1521
1725
  await testRecoverableGreetContinueButtonShouldNotAbortWhenDetailCloseFails();
1522
1726
  await testRecoverableGreetButtonNotFoundShouldNotAbortWhenDetailCloseFails();