@reconcrap/boss-recommend-mcp 1.3.16 → 1.3.18
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 +597 -82
- 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 +259 -0
|
@@ -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" };
|
|
@@ -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);
|
|
@@ -876,6 +959,60 @@ function testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription
|
|
|
876
959
|
assert.equal(formatted.includes("描述: 采用stable diffusion进行编辑实验"), true);
|
|
877
960
|
}
|
|
878
961
|
|
|
962
|
+
function testFormatResumeApiDataShouldIncludeStructuredJudgementHints() {
|
|
963
|
+
const source = {
|
|
964
|
+
geekDetailInfo: {
|
|
965
|
+
geekBaseInfo: {
|
|
966
|
+
name: "测试候选人",
|
|
967
|
+
degreeCategory: "硕士"
|
|
968
|
+
},
|
|
969
|
+
geekWorkExpList: [
|
|
970
|
+
{
|
|
971
|
+
company: "中科院",
|
|
972
|
+
positionName: "科研助理",
|
|
973
|
+
startDate: "20241001",
|
|
974
|
+
endDate: "",
|
|
975
|
+
responsibility: "科研以及项目"
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
geekProjExpList: [],
|
|
979
|
+
geekEduExpList: [
|
|
980
|
+
{
|
|
981
|
+
school: "科克大学",
|
|
982
|
+
major: "理学",
|
|
983
|
+
degreeName: "硕士",
|
|
984
|
+
startDateDesc: "2020",
|
|
985
|
+
endDateDesc: "2023"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
school: "东北大学",
|
|
989
|
+
major: "数学与应用数学",
|
|
990
|
+
degreeName: "本科",
|
|
991
|
+
startDate: "20140101",
|
|
992
|
+
endDate: "20180101"
|
|
993
|
+
}
|
|
994
|
+
],
|
|
995
|
+
workExpCheckRes: [
|
|
996
|
+
{
|
|
997
|
+
desc: "毕业同年未填写工作经历"
|
|
998
|
+
}
|
|
999
|
+
],
|
|
1000
|
+
jobCompetitive: {
|
|
1001
|
+
tips: [{ content: "受欢迎程度高" }]
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
const formatted = __testables.formatResumeApiData(source);
|
|
1006
|
+
assert.equal(formatted.includes("=== 结构化判定线索 ==="), true);
|
|
1007
|
+
assert.equal(formatted.includes("最高学历: 硕士"), true);
|
|
1008
|
+
assert.equal(formatted.includes("最高学历毕业年份: 2023"), true);
|
|
1009
|
+
assert.equal(formatted.includes("是否有工作经历: 是"), true);
|
|
1010
|
+
assert.equal(formatted.includes("是否有项目经历: 否"), true);
|
|
1011
|
+
assert.equal(formatted.includes("相关经验硬判口径"), true);
|
|
1012
|
+
assert.equal(formatted.includes("软风险提示(需追问,不直接淘汰): 毕业同年未填写工作经历"), true);
|
|
1013
|
+
assert.equal(formatted.includes("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。"), true);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
879
1016
|
function testEvidenceTokenMatcherShouldSupportParaphrasedEvidence() {
|
|
880
1017
|
const resume = [
|
|
881
1018
|
"南京大学 专业: 数学",
|
|
@@ -1348,6 +1485,75 @@ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
|
|
|
1348
1485
|
}
|
|
1349
1486
|
}
|
|
1350
1487
|
|
|
1488
|
+
async function testTextModelShouldDefaultThinkingOffForVolcengine() {
|
|
1489
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-off-"));
|
|
1490
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1491
|
+
cli.args.baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
|
|
1492
|
+
cli.args.model = "doubao-seed-2-0-mini-260215";
|
|
1493
|
+
const originalFetch = global.fetch;
|
|
1494
|
+
let capturedPayload = null;
|
|
1495
|
+
global.fetch = async (_url, options = {}) => {
|
|
1496
|
+
capturedPayload = JSON.parse(String(options.body || "{}"));
|
|
1497
|
+
return {
|
|
1498
|
+
ok: true,
|
|
1499
|
+
status: 200,
|
|
1500
|
+
async json() {
|
|
1501
|
+
return {
|
|
1502
|
+
choices: [
|
|
1503
|
+
{
|
|
1504
|
+
message: {
|
|
1505
|
+
content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
]
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
};
|
|
1513
|
+
try {
|
|
1514
|
+
await cli.callTextModel("resume");
|
|
1515
|
+
assert.deepEqual(capturedPayload?.thinking, { type: "disabled" });
|
|
1516
|
+
assert.equal(capturedPayload?.reasoning_effort, "minimal");
|
|
1517
|
+
} finally {
|
|
1518
|
+
global.fetch = originalFetch;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
async function testTextModelShouldSupportLowThinkingForVolcengine() {
|
|
1523
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-low-"));
|
|
1524
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1525
|
+
cli.args.baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
|
|
1526
|
+
cli.args.model = "doubao-seed-2-0-mini-260215";
|
|
1527
|
+
cli.args.thinkingLevel = "low";
|
|
1528
|
+
const originalFetch = global.fetch;
|
|
1529
|
+
let capturedPayload = null;
|
|
1530
|
+
global.fetch = async (_url, options = {}) => {
|
|
1531
|
+
capturedPayload = JSON.parse(String(options.body || "{}"));
|
|
1532
|
+
return {
|
|
1533
|
+
ok: true,
|
|
1534
|
+
status: 200,
|
|
1535
|
+
async json() {
|
|
1536
|
+
return {
|
|
1537
|
+
choices: [
|
|
1538
|
+
{
|
|
1539
|
+
message: {
|
|
1540
|
+
content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"resume\"]}"
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
]
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
};
|
|
1548
|
+
try {
|
|
1549
|
+
await cli.callTextModel("resume");
|
|
1550
|
+
assert.deepEqual(capturedPayload?.thinking, { type: "enabled" });
|
|
1551
|
+
assert.equal(capturedPayload?.reasoning_effort, "low");
|
|
1552
|
+
} finally {
|
|
1553
|
+
global.fetch = originalFetch;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1351
1557
|
async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
|
|
1352
1558
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
|
|
1353
1559
|
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
@@ -1473,8 +1679,55 @@ async function testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence()
|
|
|
1473
1679
|
assert.equal(result.evidenceMatchedCount, 0);
|
|
1474
1680
|
}
|
|
1475
1681
|
|
|
1682
|
+
async function testVisionModelShouldSendAllOrderedChunks() {
|
|
1683
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-all-chunks-"));
|
|
1684
|
+
const chunkPaths = [];
|
|
1685
|
+
for (let index = 0; index < 3; index += 1) {
|
|
1686
|
+
const chunkPath = path.join(tempDir, `chunk-${index + 1}.png`);
|
|
1687
|
+
await sharp({
|
|
1688
|
+
create: { width: 16, height: 16, channels: 3, background: { r: 255 - index, g: 250, b: 245 } }
|
|
1689
|
+
}).png().toFile(chunkPath);
|
|
1690
|
+
chunkPaths.push(chunkPath);
|
|
1691
|
+
}
|
|
1692
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1693
|
+
const originalFetch = global.fetch;
|
|
1694
|
+
let capturedPayload = null;
|
|
1695
|
+
global.fetch = async (_url, options = {}) => {
|
|
1696
|
+
capturedPayload = JSON.parse(String(options.body || "{}"));
|
|
1697
|
+
return {
|
|
1698
|
+
ok: true,
|
|
1699
|
+
status: 200,
|
|
1700
|
+
async json() {
|
|
1701
|
+
return {
|
|
1702
|
+
choices: [
|
|
1703
|
+
{
|
|
1704
|
+
message: {
|
|
1705
|
+
content: "{\"passed\": false, \"reason\": \"checked all chunks\", \"summary\": \"checked\", \"evidence\": [\"chunk evidence\", \"more evidence\"]}"
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
]
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
};
|
|
1713
|
+
try {
|
|
1714
|
+
const result = await cli.requestVisionModel(chunkPaths);
|
|
1715
|
+
assert.equal(result.passed, false);
|
|
1716
|
+
const userContent = capturedPayload?.messages?.[1]?.content || [];
|
|
1717
|
+
assert.equal(userContent.filter((item) => item?.type === "image_url").length, 3);
|
|
1718
|
+
const text = userContent.map((item) => item?.text || "").join("\n");
|
|
1719
|
+
assert.equal(text.includes("简历分段 1/3"), true);
|
|
1720
|
+
assert.equal(text.includes("简历分段 2/3"), true);
|
|
1721
|
+
assert.equal(text.includes("简历分段 3/3"), true);
|
|
1722
|
+
assert.equal(text.includes("不能只根据前几段下结论"), true);
|
|
1723
|
+
} finally {
|
|
1724
|
+
global.fetch = originalFetch;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1476
1728
|
async function main() {
|
|
1477
1729
|
testShouldAbortResumeProbeEarly();
|
|
1730
|
+
testResumeViewportStabilityRequiresSettledScrollAndClip();
|
|
1478
1731
|
await testSingleResumeCaptureFailureIsSkipped();
|
|
1479
1732
|
await testConsecutiveResumeCaptureFailuresStillAbort();
|
|
1480
1733
|
await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
|
|
@@ -1485,6 +1738,8 @@ async function main() {
|
|
|
1485
1738
|
await testRecommendShouldPreferNetworkResumeWhenAvailable();
|
|
1486
1739
|
await testNetworkMissShouldFallbackToImageThenDom();
|
|
1487
1740
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
1741
|
+
await testImageModeShouldUseShortNetworkGraceWindow();
|
|
1742
|
+
await testImageFailureShouldLateRetryNetworkBeforeDomFallback();
|
|
1488
1743
|
await testLatestShouldPreferNetworkResumeWhenAvailable();
|
|
1489
1744
|
await testLatestNetworkMissShouldFallbackToImageCapture();
|
|
1490
1745
|
testLatestPayloadShouldNotLeakAcrossCandidates();
|
|
@@ -1501,6 +1756,7 @@ async function main() {
|
|
|
1501
1756
|
testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
|
|
1502
1757
|
testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
|
|
1503
1758
|
testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription();
|
|
1759
|
+
testFormatResumeApiDataShouldIncludeStructuredJudgementHints();
|
|
1504
1760
|
testEvidenceTokenMatcherShouldSupportParaphrasedEvidence();
|
|
1505
1761
|
testCheckpointPayloadShouldIncludeCandidateAudits();
|
|
1506
1762
|
testCheckpointShouldPersistAndRestoreInputSummary();
|
|
@@ -1515,8 +1771,11 @@ async function main() {
|
|
|
1515
1771
|
testParseArgsShouldSupportInputSummaryJson();
|
|
1516
1772
|
await testCallTextModelShouldNotTruncateLongResume();
|
|
1517
1773
|
await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
|
|
1774
|
+
await testTextModelShouldDefaultThinkingOffForVolcengine();
|
|
1775
|
+
await testTextModelShouldSupportLowThinkingForVolcengine();
|
|
1518
1776
|
await testPrepareVisionImageSegmentsShouldSplitLongImage();
|
|
1519
1777
|
await testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence();
|
|
1778
|
+
await testVisionModelShouldSendAllOrderedChunks();
|
|
1520
1779
|
testRecoverablePostActionErrorShouldTreatGreetContinueAndNoButtonAsRecoverable();
|
|
1521
1780
|
await testRecoverableGreetContinueButtonShouldNotAbortWhenDetailCloseFails();
|
|
1522
1781
|
await testRecoverableGreetButtonNotFoundShouldNotAbortWhenDetailCloseFails();
|