@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.
- package/README.md +2 -1
- package/config/screening-config.example.json +1 -0
- package/package.json +1 -1
- package/src/adapters.js +28 -9
- package/src/boss-chat.js +22 -0
- package/src/cli.js +6 -1
- package/src/test-boss-chat.js +89 -0
- package/vendor/boss-chat-cli/src/cli.js +10 -0
- package/vendor/boss-chat-cli/src/mcp/server.js +1 -1
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +1 -1
- package/vendor/boss-chat-cli/src/services/llm.js +119 -24
- package/vendor/boss-chat-cli/src/services/profile-store.js +5 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +482 -110
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +109 -41
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +237 -11
|
@@ -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
|
|
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
|
|
640
|
-
|
|
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 = "
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
658
|
-
assert.equal(result.result.skipped_count,
|
|
659
|
-
assert.equal(result.result.resume_source, "
|
|
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
|
|
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
|
|
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();
|