@reconcrap/boss-recommend-mcp 1.2.7 → 1.2.9
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/package.json +1 -1
- package/skills/boss-recommend-pipeline/SKILL.md +101 -219
- package/src/parser.js +48 -15
- package/src/pipeline.js +56 -2
- package/src/test-parser.js +53 -2
- package/src/test-pipeline.js +70 -10
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +311 -36
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +218 -0
|
@@ -124,6 +124,30 @@ class FakeRecommendScreenCli extends RecommendScreenCli {
|
|
|
124
124
|
saveCheckpoint() {}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
class FakeDetailCloseProbeCli extends RecommendScreenCli {
|
|
128
|
+
constructor(args, options = {}) {
|
|
129
|
+
super(args);
|
|
130
|
+
this.listReady = options.listReady === true;
|
|
131
|
+
this.evaluateCallCount = 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getDetailClosedState() {
|
|
135
|
+
return { closed: false, reason: "popup visible: .boss-popup__wrapper" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async evaluate() {
|
|
139
|
+
this.evaluateCallCount += 1;
|
|
140
|
+
if (this.evaluateCallCount >= 2) {
|
|
141
|
+
return this.listReady
|
|
142
|
+
? { ok: true, candidate_count: 1 }
|
|
143
|
+
: { ok: false, error: "LIST_NOT_READY" };
|
|
144
|
+
}
|
|
145
|
+
return { ok: false, error: "CLOSE_ACTION_NOOP" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async pressEsc() {}
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
function createResumeCaptureError(message = "Resume canvas not found") {
|
|
128
152
|
const error = new Error(message);
|
|
129
153
|
error.code = "RESUME_CAPTURE_FAILED";
|
|
@@ -306,6 +330,55 @@ async function testPageExhaustedWithoutTargetShouldStillComplete() {
|
|
|
306
330
|
assert.equal(result.result.completion_reason, "page_exhausted");
|
|
307
331
|
}
|
|
308
332
|
|
|
333
|
+
async function testTargetCountShouldStopWhenPassedCountReached() {
|
|
334
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-stop-"));
|
|
335
|
+
const args = createArgs(tempDir);
|
|
336
|
+
args.targetCount = 1;
|
|
337
|
+
const first = { key: "pass-1", geek_id: "pass-1", name: "pass-1" };
|
|
338
|
+
const second = { key: "skip-2", geek_id: "skip-2", name: "skip-2" };
|
|
339
|
+
const cli = new FakeRecommendScreenCli(args, {
|
|
340
|
+
candidates: [first, second],
|
|
341
|
+
screeningByKey: new Map([
|
|
342
|
+
["pass-1", { passed: true, reason: "matched", summary: "matched" }],
|
|
343
|
+
["skip-2", { passed: false, reason: "not matched", summary: "not matched" }]
|
|
344
|
+
])
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const result = await cli.run();
|
|
348
|
+
assert.equal(result.status, "COMPLETED");
|
|
349
|
+
assert.equal(result.result.processed_count, 1);
|
|
350
|
+
assert.equal(result.result.passed_count, 1);
|
|
351
|
+
assert.equal(result.result.skipped_count, 0);
|
|
352
|
+
assert.equal(result.result.completion_reason, "target_count_reached");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function testTargetCountShouldNotTreatProcessedCountAsReached() {
|
|
356
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-target-pass-only-"));
|
|
357
|
+
const args = createArgs(tempDir);
|
|
358
|
+
args.targetCount = 1;
|
|
359
|
+
const first = { key: "skip-a", geek_id: "skip-a", name: "skip-a" };
|
|
360
|
+
const second = { key: "skip-b", geek_id: "skip-b", name: "skip-b" };
|
|
361
|
+
const cli = new FakeRecommendScreenCli(args, {
|
|
362
|
+
candidates: [first, second],
|
|
363
|
+
screeningByKey: new Map([
|
|
364
|
+
["skip-a", { passed: false, reason: "not matched", summary: "not matched" }],
|
|
365
|
+
["skip-b", { passed: false, reason: "not matched", summary: "not matched" }]
|
|
366
|
+
])
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await assert.rejects(
|
|
370
|
+
() => cli.run(),
|
|
371
|
+
(error) => {
|
|
372
|
+
assert.equal(error.code, "TARGET_COUNT_NOT_REACHED_PAGE_EXHAUSTED");
|
|
373
|
+
assert.equal(error.retryable, true);
|
|
374
|
+
assert.equal(error.partial_result?.processed_count, 2);
|
|
375
|
+
assert.equal(error.partial_result?.passed_count, 0);
|
|
376
|
+
assert.equal(error.partial_result?.completion_reason, "page_exhausted_before_target_count");
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
309
382
|
async function testFeaturedShouldUseNetworkResumeOnly() {
|
|
310
383
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-screen-network-first-"));
|
|
311
384
|
const candidate = { key: "net-1", geek_id: "net-1", name: "network candidate" };
|
|
@@ -842,12 +915,152 @@ function testParseArgsShouldSupportLatestPageScope() {
|
|
|
842
915
|
assert.equal(parsed.port, 9222);
|
|
843
916
|
}
|
|
844
917
|
|
|
918
|
+
async function testCallTextModelShouldNotTruncateLongResume() {
|
|
919
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-full-"));
|
|
920
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
921
|
+
const marker = "__END_OF_RESUME_MARKER__";
|
|
922
|
+
const resumeText = `${"A".repeat(32000)}${marker}`;
|
|
923
|
+
const originalFetch = global.fetch;
|
|
924
|
+
let capturedUserContent = "";
|
|
925
|
+
global.fetch = async (_url, options = {}) => {
|
|
926
|
+
const payload = JSON.parse(String(options.body || "{}"));
|
|
927
|
+
capturedUserContent = String(payload?.messages?.[1]?.content || "");
|
|
928
|
+
return {
|
|
929
|
+
ok: true,
|
|
930
|
+
status: 200,
|
|
931
|
+
async json() {
|
|
932
|
+
return {
|
|
933
|
+
choices: [
|
|
934
|
+
{
|
|
935
|
+
message: {
|
|
936
|
+
content: "{\"passed\": false, \"reason\": \"not matched\", \"summary\": \"not matched\", \"evidence\": [\"A\"]}"
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
try {
|
|
945
|
+
const result = await cli.callTextModel(resumeText);
|
|
946
|
+
assert.equal(result.passed, false);
|
|
947
|
+
assert.equal(capturedUserContent.includes(marker), true);
|
|
948
|
+
} finally {
|
|
949
|
+
global.fetch = originalFetch;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
|
|
954
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-chunk-fallback-"));
|
|
955
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
956
|
+
const originalFetch = global.fetch;
|
|
957
|
+
const prevChunkSize = process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
|
|
958
|
+
const prevChunkOverlap = process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
|
|
959
|
+
const prevMaxChunks = process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
|
|
960
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = "80";
|
|
961
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = "0";
|
|
962
|
+
process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = "6";
|
|
963
|
+
|
|
964
|
+
const passMarker = "PASS_MARKER_ABC";
|
|
965
|
+
const resumeText = `${"x".repeat(120)}${passMarker}${"y".repeat(120)}`;
|
|
966
|
+
let callCount = 0;
|
|
967
|
+
global.fetch = async (_url, options = {}) => {
|
|
968
|
+
callCount += 1;
|
|
969
|
+
if (callCount === 1) {
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
status: 400,
|
|
973
|
+
async text() {
|
|
974
|
+
return "maximum context length exceeded";
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const payload = JSON.parse(String(options.body || "{}"));
|
|
980
|
+
const userContent = String(payload?.messages?.[1]?.content || "");
|
|
981
|
+
const passed = userContent.includes(passMarker);
|
|
982
|
+
const response = passed
|
|
983
|
+
? "{\"passed\": true, \"reason\": \"命中证据\", \"summary\": \"命中\", \"evidence\": [\"PASS_MARKER_ABC\"]}"
|
|
984
|
+
: "{\"passed\": false, \"reason\": \"本段证据不足\", \"summary\": \"不足\", \"evidence\": []}";
|
|
985
|
+
return {
|
|
986
|
+
ok: true,
|
|
987
|
+
status: 200,
|
|
988
|
+
async json() {
|
|
989
|
+
return {
|
|
990
|
+
choices: [
|
|
991
|
+
{
|
|
992
|
+
message: {
|
|
993
|
+
content: response
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await cli.callTextModel(resumeText);
|
|
1003
|
+
assert.equal(result.passed, true);
|
|
1004
|
+
assert.equal(callCount >= 2, true);
|
|
1005
|
+
assert.equal(Array.isArray(result.evidence), true);
|
|
1006
|
+
} finally {
|
|
1007
|
+
global.fetch = originalFetch;
|
|
1008
|
+
if (prevChunkSize === undefined) {
|
|
1009
|
+
delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS;
|
|
1010
|
+
} else {
|
|
1011
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_SIZE_CHARS = prevChunkSize;
|
|
1012
|
+
}
|
|
1013
|
+
if (prevChunkOverlap === undefined) {
|
|
1014
|
+
delete process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS;
|
|
1015
|
+
} else {
|
|
1016
|
+
process.env.BOSS_RECOMMEND_TEXT_CHUNK_OVERLAP_CHARS = prevChunkOverlap;
|
|
1017
|
+
}
|
|
1018
|
+
if (prevMaxChunks === undefined) {
|
|
1019
|
+
delete process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS;
|
|
1020
|
+
} else {
|
|
1021
|
+
process.env.BOSS_RECOMMEND_TEXT_MAX_CHUNKS = prevMaxChunks;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function testPrepareVisionImageSegmentsShouldSplitLongImage() {
|
|
1027
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-segments-"));
|
|
1028
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1029
|
+
const imagePath = path.join(tempDir, "long.png");
|
|
1030
|
+
await sharp({
|
|
1031
|
+
create: { width: 400, height: 1200, channels: 3, background: { r: 240, g: 240, b: 240 } }
|
|
1032
|
+
}).png().toFile(imagePath);
|
|
1033
|
+
|
|
1034
|
+
const prepared = await cli.prepareVisionImageSegmentsForModel(imagePath, 120000, "test");
|
|
1035
|
+
assert.equal(Array.isArray(prepared.imagePaths), true);
|
|
1036
|
+
assert.equal(prepared.imagePaths.length > 1, true);
|
|
1037
|
+
for (const segmentPath of prepared.imagePaths) {
|
|
1038
|
+
assert.equal(fs.existsSync(segmentPath), true);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady() {
|
|
1043
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-fail-"));
|
|
1044
|
+
const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: false });
|
|
1045
|
+
const closed = await cli.closeDetailPage(1);
|
|
1046
|
+
assert.equal(closed, false);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function testCloseDetailPageShouldContinueWhenListReady() {
|
|
1050
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-close-detail-list-ready-"));
|
|
1051
|
+
const cli = new FakeDetailCloseProbeCli(createArgs(tempDir), { listReady: true });
|
|
1052
|
+
const closed = await cli.closeDetailPage(1);
|
|
1053
|
+
assert.equal(closed, true);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
845
1056
|
async function main() {
|
|
846
1057
|
testShouldAbortResumeProbeEarly();
|
|
847
1058
|
await testSingleResumeCaptureFailureIsSkipped();
|
|
848
1059
|
await testConsecutiveResumeCaptureFailuresStillAbort();
|
|
849
1060
|
await testPageExhaustedBeforeTargetShouldRaiseRecoverableError();
|
|
850
1061
|
await testPageExhaustedWithoutTargetShouldStillComplete();
|
|
1062
|
+
await testTargetCountShouldStopWhenPassedCountReached();
|
|
1063
|
+
await testTargetCountShouldNotTreatProcessedCountAsReached();
|
|
851
1064
|
await testFeaturedShouldUseNetworkResumeOnly();
|
|
852
1065
|
await testRecommendShouldPreferNetworkResumeWhenAvailable();
|
|
853
1066
|
await testNetworkMissShouldFallbackToImageCapture();
|
|
@@ -871,6 +1084,11 @@ async function main() {
|
|
|
871
1084
|
testStitchWithAvailablePythonShouldFailWhenScriptMissing();
|
|
872
1085
|
testParseArgsShouldSupportFeaturedAliasesAndInlinePort();
|
|
873
1086
|
testParseArgsShouldSupportLatestPageScope();
|
|
1087
|
+
await testCallTextModelShouldNotTruncateLongResume();
|
|
1088
|
+
await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
|
|
1089
|
+
await testPrepareVisionImageSegmentsShouldSplitLongImage();
|
|
1090
|
+
await testCloseDetailPageShouldFailWhenDetailStillOpenAndListNotReady();
|
|
1091
|
+
await testCloseDetailPageShouldContinueWhenListReady();
|
|
874
1092
|
console.log("recoverable resume failure tests passed");
|
|
875
1093
|
}
|
|
876
1094
|
|