@reconcrap/boss-recommend-mcp 1.3.23 → 1.3.25
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/src/test-boss-chat.js +87 -0
- package/vendor/boss-chat-cli/src/browser/chat-page.js +23 -8
- package/vendor/boss-chat-cli/src/cli.js +44 -10
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +28 -68
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +43 -6
package/package.json
CHANGED
package/src/test-boss-chat.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { __testables as cliTestables } from "./cli.js";
|
|
17
17
|
import { __testables as indexTestables } from "./index.js";
|
|
18
18
|
import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
|
|
19
|
+
import { BossChatPage } from "../vendor/boss-chat-cli/src/browser/chat-page.js";
|
|
19
20
|
import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
|
|
20
21
|
|
|
21
22
|
const { handleRequest } = indexTestables;
|
|
@@ -398,6 +399,90 @@ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
|
|
|
398
399
|
});
|
|
399
400
|
}
|
|
400
401
|
|
|
402
|
+
async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
|
|
403
|
+
const fakeChromeClient = {
|
|
404
|
+
async callFunction() {
|
|
405
|
+
return {
|
|
406
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
407
|
+
readyState: "complete",
|
|
408
|
+
hasListContainer: false,
|
|
409
|
+
listItemCount: 0
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const page = new BossChatPage(fakeChromeClient);
|
|
415
|
+
const pageState = await page.ensureOnChatPage();
|
|
416
|
+
assert.equal(pageState.href, "https://www.zhipin.com/web/chat/index");
|
|
417
|
+
|
|
418
|
+
await assert.rejects(
|
|
419
|
+
() => page.ensureReady(),
|
|
420
|
+
/CHAT_LIST_CONTAINER_NOT_FOUND/
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad() {
|
|
425
|
+
const calls = [];
|
|
426
|
+
let stateIndex = 0;
|
|
427
|
+
const states = [
|
|
428
|
+
{
|
|
429
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
430
|
+
readyState: "loading",
|
|
431
|
+
hasListContainer: false,
|
|
432
|
+
listItemCount: 0
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
436
|
+
readyState: "interactive",
|
|
437
|
+
hasListContainer: false,
|
|
438
|
+
listItemCount: 0
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
href: "https://www.zhipin.com/web/chat/index",
|
|
442
|
+
readyState: "complete",
|
|
443
|
+
hasListContainer: false,
|
|
444
|
+
listItemCount: 0
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const fakeChromeClient = {
|
|
449
|
+
async callFunction(fn, arg) {
|
|
450
|
+
calls.push({ name: fn.name, arg });
|
|
451
|
+
if (fn.name === "browserGetCurrentHref") {
|
|
452
|
+
return { href: "https://www.zhipin.com/web/chat/index" };
|
|
453
|
+
}
|
|
454
|
+
if (fn.name === "browserNavigateToChatIndex") {
|
|
455
|
+
return { ok: true, changed: true, href: "https://www.zhipin.com/web/chat/index" };
|
|
456
|
+
}
|
|
457
|
+
if (fn.name === "browserGetPageState") {
|
|
458
|
+
const value = states[Math.min(stateIndex, states.length - 1)];
|
|
459
|
+
stateIndex += 1;
|
|
460
|
+
return value;
|
|
461
|
+
}
|
|
462
|
+
throw new Error(`unexpected function: ${fn.name}`);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const page = new BossChatPage(fakeChromeClient);
|
|
467
|
+
const result = await page.recoverToChatIndex({
|
|
468
|
+
forceNavigate: true,
|
|
469
|
+
waitForReadyState: "complete",
|
|
470
|
+
maxAttempts: 5,
|
|
471
|
+
delayMs: 0
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
assert.equal(result.changed, true);
|
|
475
|
+
assert.equal(result.href, "https://www.zhipin.com/web/chat/index");
|
|
476
|
+
assert.equal(
|
|
477
|
+
calls.some((entry) => entry.name === "browserNavigateToChatIndex" && entry.arg?.force === true),
|
|
478
|
+
true
|
|
479
|
+
);
|
|
480
|
+
assert.equal(
|
|
481
|
+
calls.filter((entry) => entry.name === "browserGetPageState").length >= 3,
|
|
482
|
+
true
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
401
486
|
async function testBossChatMcpToolsShouldValidateAndRoute() {
|
|
402
487
|
await withBossChatWorkspace(async (workspaceRoot) => {
|
|
403
488
|
const toolsResponse = await handleRequest({
|
|
@@ -941,6 +1026,8 @@ async function testBossChatAppShouldPersistEvidenceArtifacts() {
|
|
|
941
1026
|
async function main() {
|
|
942
1027
|
await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
|
|
943
1028
|
await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
|
|
1029
|
+
await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
|
|
1030
|
+
await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
|
|
944
1031
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
945
1032
|
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
946
1033
|
testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
|
|
@@ -24,6 +24,7 @@ function browserGetPageState() {
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
href: window.location.href,
|
|
27
|
+
readyState: document.readyState,
|
|
27
28
|
hasListContainer: Boolean(listContainer),
|
|
28
29
|
listItemCount: listItems.length,
|
|
29
30
|
};
|
|
@@ -33,9 +34,10 @@ function browserGetCurrentHref() {
|
|
|
33
34
|
return { href: window.location.href };
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function browserNavigateToChatIndex() {
|
|
37
|
+
function browserNavigateToChatIndex(options = {}) {
|
|
37
38
|
const chatUrl = 'https://www.zhipin.com/web/chat/index';
|
|
38
|
-
|
|
39
|
+
const force = options?.force === true;
|
|
40
|
+
if (force || !String(window.location.href || '').includes('/web/chat/index')) {
|
|
39
41
|
window.location.assign(chatUrl);
|
|
40
42
|
return { ok: true, changed: true, href: chatUrl };
|
|
41
43
|
}
|
|
@@ -2236,11 +2238,20 @@ export class BossChatPage {
|
|
|
2236
2238
|
return target?.type === 'page' && String(target.url || '').includes(CHAT_URL_TOKEN);
|
|
2237
2239
|
}
|
|
2238
2240
|
|
|
2239
|
-
async
|
|
2240
|
-
|
|
2241
|
+
async getPageState() {
|
|
2242
|
+
return this.chromeClient.callFunction(browserGetPageState);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async ensureOnChatPage() {
|
|
2246
|
+
const pageState = await this.getPageState();
|
|
2241
2247
|
if (!pageState?.href?.includes(CHAT_URL_TOKEN)) {
|
|
2242
2248
|
throw new Error('ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE');
|
|
2243
2249
|
}
|
|
2250
|
+
return pageState;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
async ensureReady() {
|
|
2254
|
+
const pageState = await this.ensureOnChatPage();
|
|
2244
2255
|
if (!pageState.hasListContainer && Number(pageState.listItemCount || 0) <= 0) {
|
|
2245
2256
|
throw new Error('CHAT_LIST_CONTAINER_NOT_FOUND');
|
|
2246
2257
|
}
|
|
@@ -2250,16 +2261,20 @@ export class BossChatPage {
|
|
|
2250
2261
|
async recoverToChatIndex(options = {}) {
|
|
2251
2262
|
const maxAttempts = options.maxAttempts || 20;
|
|
2252
2263
|
const delayMs = options.delayMs || 500;
|
|
2264
|
+
const forceNavigate = options.forceNavigate === true;
|
|
2265
|
+
const waitForReadyState = options.waitForReadyState || 'complete';
|
|
2253
2266
|
const hrefResult = await this.chromeClient.callFunction(browserGetCurrentHref);
|
|
2254
|
-
if (String(hrefResult?.href || '').includes(CHAT_URL_TOKEN)) {
|
|
2267
|
+
if (!forceNavigate && String(hrefResult?.href || '').includes(CHAT_URL_TOKEN)) {
|
|
2255
2268
|
return { changed: false, href: hrefResult?.href || '' };
|
|
2256
2269
|
}
|
|
2257
2270
|
|
|
2258
|
-
await this.chromeClient.callFunction(browserNavigateToChatIndex);
|
|
2271
|
+
await this.chromeClient.callFunction(browserNavigateToChatIndex, { force: forceNavigate });
|
|
2259
2272
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
2260
2273
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2261
|
-
const state = await this.
|
|
2262
|
-
|
|
2274
|
+
const state = await this.getPageState();
|
|
2275
|
+
const onChatPage = String(state?.href || '').includes(CHAT_URL_TOKEN);
|
|
2276
|
+
const ready = !waitForReadyState || String(state?.readyState || '').toLowerCase() === String(waitForReadyState).toLowerCase();
|
|
2277
|
+
if (onChatPage && ready) {
|
|
2263
2278
|
return { changed: true, href: state.href };
|
|
2264
2279
|
}
|
|
2265
2280
|
}
|
|
@@ -44,6 +44,7 @@ const CLI_FILE_PATH = fileURLToPath(import.meta.url);
|
|
|
44
44
|
const MINIMAL_TERMINAL_PATTERNS = [/^进度: /, /^候选人结果: /];
|
|
45
45
|
const CHAT_INDEX_URL = 'https://www.zhipin.com/web/chat/index';
|
|
46
46
|
const CHAT_START_REQUIRED_FIELDS = ['job', 'start_from', 'target_count', 'criteria'];
|
|
47
|
+
const CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS = 3;
|
|
47
48
|
|
|
48
49
|
function sanitizePathToken(value, fallback = 'run') {
|
|
49
50
|
const token = String(value || '')
|
|
@@ -651,6 +652,8 @@ async function connectBossChatPage(chromeClient) {
|
|
|
651
652
|
target?.type === 'page' && /zhipin\.com/i.test(String(target?.url || ''));
|
|
652
653
|
let target = null;
|
|
653
654
|
let recoveredToChatIndex = false;
|
|
655
|
+
let blankChatPage = false;
|
|
656
|
+
let renavigateAttempts = 0;
|
|
654
657
|
|
|
655
658
|
try {
|
|
656
659
|
target = await chromeClient.connect(BossChatPage.targetMatcher);
|
|
@@ -659,15 +662,41 @@ async function connectBossChatPage(chromeClient) {
|
|
|
659
662
|
}
|
|
660
663
|
|
|
661
664
|
const page = new BossChatPage(chromeClient);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
for (let attempt = 1; attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS + 1; attempt += 1) {
|
|
666
|
+
try {
|
|
667
|
+
await page.ensureReady();
|
|
668
|
+
return {
|
|
669
|
+
target,
|
|
670
|
+
page,
|
|
671
|
+
recoveredToChatIndex,
|
|
672
|
+
blankChatPage,
|
|
673
|
+
renavigateAttempts,
|
|
674
|
+
};
|
|
675
|
+
} catch (error) {
|
|
676
|
+
const message = String(error?.message || error || '');
|
|
677
|
+
const canRetry =
|
|
678
|
+
/ACTIVE_TAB_IS_NOT_BOSS_CHAT_PAGE|CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)
|
|
679
|
+
&& attempt <= CHAT_PAGE_RENAVIGATE_MAX_ATTEMPTS;
|
|
680
|
+
|
|
681
|
+
if (!canRetry) {
|
|
682
|
+
if (/CHAT_LIST_CONTAINER_NOT_FOUND/.test(message)) {
|
|
683
|
+
blankChatPage = true;
|
|
684
|
+
await page.ensureOnChatPage();
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
await page.recoverToChatIndex({
|
|
691
|
+
forceNavigate: true,
|
|
692
|
+
waitForReadyState: 'complete',
|
|
693
|
+
});
|
|
694
|
+
recoveredToChatIndex = true;
|
|
695
|
+
renavigateAttempts += 1;
|
|
696
|
+
}
|
|
668
697
|
}
|
|
669
698
|
|
|
670
|
-
return { target, page, recoveredToChatIndex };
|
|
699
|
+
return { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts };
|
|
671
700
|
}
|
|
672
701
|
|
|
673
702
|
async function handlePrepareRunCommand(args, dataDir) {
|
|
@@ -704,7 +733,7 @@ async function handlePrepareRunCommand(args, dataDir) {
|
|
|
704
733
|
let chromeClient = null;
|
|
705
734
|
try {
|
|
706
735
|
chromeClient = new ChromeClient(mergedProfile.chrome.port);
|
|
707
|
-
const { target, page, recoveredToChatIndex } = await connectBossChatPage(chromeClient);
|
|
736
|
+
const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
|
|
708
737
|
const jobs = await page.listJobs();
|
|
709
738
|
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
710
739
|
return {
|
|
@@ -723,6 +752,8 @@ async function handlePrepareRunCommand(args, dataDir) {
|
|
|
723
752
|
page_url: CHAT_INDEX_URL,
|
|
724
753
|
connected_target: target?.url || '',
|
|
725
754
|
recovered_to_chat_index: recoveredToChatIndex,
|
|
755
|
+
blank_chat_page: blankChatPage,
|
|
756
|
+
renavigate_attempts: renavigateAttempts,
|
|
726
757
|
required_fields: CHAT_START_REQUIRED_FIELDS.slice(),
|
|
727
758
|
defaults: {
|
|
728
759
|
profile: String(args.profile || 'default').trim() || 'default',
|
|
@@ -1159,10 +1190,13 @@ async function executeRunCommand(args, dataDir) {
|
|
|
1159
1190
|
|
|
1160
1191
|
chromeClient = new ChromeClient(persistentProfile.chrome.port);
|
|
1161
1192
|
|
|
1162
|
-
const { target, page, recoveredToChatIndex } = await connectBossChatPage(chromeClient);
|
|
1193
|
+
const { target, page, recoveredToChatIndex, blankChatPage, renavigateAttempts } = await connectBossChatPage(chromeClient);
|
|
1163
1194
|
logger.log(`已连接 Chrome tab: ${target.title || target.url}`);
|
|
1164
1195
|
if (recoveredToChatIndex) {
|
|
1165
|
-
logger.log(
|
|
1196
|
+
logger.log(`检测到页面不符合预期,已重新跳转到 ${CHAT_INDEX_URL} 并等待加载完成。attempts=${renavigateAttempts}`);
|
|
1197
|
+
}
|
|
1198
|
+
if (blankChatPage) {
|
|
1199
|
+
logger.log('检测到聊天页处于空白未初始化状态,将继续通过岗位选择和首位候选人预热来恢复列表。');
|
|
1166
1200
|
}
|
|
1167
1201
|
|
|
1168
1202
|
const runProfile = await promptRunProfile({
|
|
@@ -475,15 +475,6 @@ function toStringArray(value, maxItems = 8) {
|
|
|
475
475
|
return normalized;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
function hasEvidenceGateSignal(parsed, parsedEvidence = null) {
|
|
479
|
-
if (!parsed || typeof parsed !== "object") return false;
|
|
480
|
-
if (parsed?.evidenceGateEligible === true) return true;
|
|
481
|
-
if (Number.isFinite(Number(parsed?.evidenceRawCount))) return true;
|
|
482
|
-
if (Number.isFinite(Number(parsed?.evidenceMatchedCount))) return true;
|
|
483
|
-
const normalizedEvidence = Array.isArray(parsedEvidence) ? parsedEvidence : toStringArray(parsed?.evidence);
|
|
484
|
-
return normalizedEvidence.length > 0;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
478
|
function toLowerSafe(text) {
|
|
488
479
|
return String(text || "").toLowerCase();
|
|
489
480
|
}
|
|
@@ -4969,26 +4960,17 @@ class RecommendScreenCli {
|
|
|
4969
4960
|
const parsed = result && typeof result === "object" ? result : {};
|
|
4970
4961
|
const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
|
|
4971
4962
|
const parsedEvidence = toStringArray(parsed?.evidence);
|
|
4972
|
-
const
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
:
|
|
4978
|
-
const evidenceMatchedCount = evidenceGateEligible
|
|
4979
|
-
? (Number.isFinite(Number(parsed?.evidenceMatchedCount))
|
|
4980
|
-
? Number(parsed.evidenceMatchedCount)
|
|
4981
|
-
: parsedEvidence.length)
|
|
4982
|
-
: null;
|
|
4983
|
-
const evidenceGateDemoted = parsed?.evidenceGateDemoted === true
|
|
4984
|
-
|| (evidenceGateEligible && rawPassed && evidenceMatchedCount <= 0);
|
|
4963
|
+
const evidenceRawCount = Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
4964
|
+
? Number(parsed.evidenceRawCount)
|
|
4965
|
+
: parsedEvidence.length;
|
|
4966
|
+
const evidenceMatchedCount = Number.isFinite(Number(parsed?.evidenceMatchedCount))
|
|
4967
|
+
? Number(parsed.evidenceMatchedCount)
|
|
4968
|
+
: parsedEvidence.length;
|
|
4985
4969
|
const cot = normalizeText(parsed?.cot || parsed?.reason || "");
|
|
4986
4970
|
const summary = normalizeText(parsed?.summary || cot);
|
|
4987
|
-
const finalReason =
|
|
4988
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${cot ? ` 原始判断依据(CoT): ${cot}` : ""}`
|
|
4989
|
-
: (cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。"));
|
|
4971
|
+
const finalReason = cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
4990
4972
|
return {
|
|
4991
|
-
passed:
|
|
4973
|
+
passed: rawPassed,
|
|
4992
4974
|
rawPassed,
|
|
4993
4975
|
cot: finalReason,
|
|
4994
4976
|
reason: finalReason,
|
|
@@ -4996,7 +4978,7 @@ class RecommendScreenCli {
|
|
|
4996
4978
|
evidence: parsedEvidence,
|
|
4997
4979
|
evidenceRawCount,
|
|
4998
4980
|
evidenceMatchedCount,
|
|
4999
|
-
evidenceGateDemoted
|
|
4981
|
+
evidenceGateDemoted: false
|
|
5000
4982
|
};
|
|
5001
4983
|
}
|
|
5002
4984
|
|
|
@@ -5280,19 +5262,14 @@ class RecommendScreenCli {
|
|
|
5280
5262
|
const reason = cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5281
5263
|
const summary = reason;
|
|
5282
5264
|
const parsedEvidence = toStringArray(parsed?.evidence);
|
|
5283
|
-
const
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
const
|
|
5291
|
-
const finalReason = evidenceGateDemoted
|
|
5292
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? ` 原始判断依据(CoT): ${reason}` : ""}`
|
|
5293
|
-
: reason;
|
|
5294
|
-
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
5295
|
-
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, parsedEvidence, passed);
|
|
5265
|
+
const evidenceRawCount = Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
5266
|
+
? Number(parsed.evidenceRawCount)
|
|
5267
|
+
: parsedEvidence.length;
|
|
5268
|
+
const evidenceMatchedCount = Number.isFinite(Number(parsed?.evidenceMatchedCount))
|
|
5269
|
+
? Number(parsed.evidenceMatchedCount)
|
|
5270
|
+
: parsedEvidence.length;
|
|
5271
|
+
const passed = rawPassed;
|
|
5272
|
+
const enrichedReason = enrichReasonWithEvidence(reason, summary || reason, parsedEvidence, passed);
|
|
5296
5273
|
return {
|
|
5297
5274
|
passed,
|
|
5298
5275
|
rawPassed,
|
|
@@ -5302,8 +5279,8 @@ class RecommendScreenCli {
|
|
|
5302
5279
|
evidence: parsedEvidence,
|
|
5303
5280
|
evidenceRawCount,
|
|
5304
5281
|
evidenceMatchedCount,
|
|
5305
|
-
evidenceGateEligible,
|
|
5306
|
-
evidenceGateDemoted
|
|
5282
|
+
evidenceGateEligible: false,
|
|
5283
|
+
evidenceGateDemoted: false
|
|
5307
5284
|
};
|
|
5308
5285
|
}
|
|
5309
5286
|
|
|
@@ -5445,17 +5422,11 @@ class RecommendScreenCli {
|
|
|
5445
5422
|
const normalizedResume = normalizeText(safeResumeText);
|
|
5446
5423
|
const normalizedResumeLower = toLowerSafe(normalizedResume);
|
|
5447
5424
|
const parsedEvidence = toStringArray(parsed?.evidence);
|
|
5448
|
-
const evidenceGateEligible = hasEvidenceGateSignal(parsed, parsedEvidence);
|
|
5449
5425
|
const evidence = [];
|
|
5450
|
-
const
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
if (matched.matched) {
|
|
5455
|
-
evidence.push(item);
|
|
5456
|
-
} else {
|
|
5457
|
-
unmatchedEvidence.push(item);
|
|
5458
|
-
}
|
|
5426
|
+
for (const item of parsedEvidence) {
|
|
5427
|
+
const matched = matchEvidenceAgainstResume(item, safeResumeText, normalizedResume, normalizedResumeLower);
|
|
5428
|
+
if (matched.matched) {
|
|
5429
|
+
evidence.push(item);
|
|
5459
5430
|
}
|
|
5460
5431
|
}
|
|
5461
5432
|
const parsedPassed = parsePassedDecision(parsed?.passed);
|
|
@@ -5467,19 +5438,8 @@ class RecommendScreenCli {
|
|
|
5467
5438
|
`Text model response missing boolean passed decision. content=${truncateText(content, 180)}`
|
|
5468
5439
|
);
|
|
5469
5440
|
}
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
const evidenceGateDemoted = evidenceGateEligible && rawPassed && evidence.length <= 0;
|
|
5473
|
-
if (evidenceGateDemoted) {
|
|
5474
|
-
passed = false;
|
|
5475
|
-
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${finalReason ? ` 原始判断依据(CoT): ${finalReason}` : ""}`;
|
|
5476
|
-
if (unmatchedEvidence.length > 0) {
|
|
5477
|
-
log(
|
|
5478
|
-
`[EVIDENCE_GATE] passed=true 但证据未命中简历原文,已降级为不通过;` +
|
|
5479
|
-
`chunk=${chunkIndex}/${chunkTotal}; unmatched=${unmatchedEvidence.slice(0, 3).join(" | ")}`
|
|
5480
|
-
);
|
|
5481
|
-
}
|
|
5482
|
-
}
|
|
5441
|
+
const passed = rawPassed;
|
|
5442
|
+
const finalReason = cot || (passed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5483
5443
|
const summary = finalReason;
|
|
5484
5444
|
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
5485
5445
|
return {
|
|
@@ -5489,9 +5449,9 @@ class RecommendScreenCli {
|
|
|
5489
5449
|
reason: enrichedReason,
|
|
5490
5450
|
summary: summary || enrichedReason,
|
|
5491
5451
|
evidence,
|
|
5492
|
-
evidenceRawCount:
|
|
5493
|
-
evidenceMatchedCount:
|
|
5494
|
-
evidenceGateDemoted,
|
|
5452
|
+
evidenceRawCount: parsedEvidence.length,
|
|
5453
|
+
evidenceMatchedCount: evidence.length,
|
|
5454
|
+
evidenceGateDemoted: false,
|
|
5495
5455
|
chunkIndex,
|
|
5496
5456
|
chunkTotal
|
|
5497
5457
|
};
|
|
@@ -1715,11 +1715,11 @@ async function testVisionEvidenceGateShouldKeepRawPassWithoutExplicitEvidencePro
|
|
|
1715
1715
|
assert.equal(result.rawPassed, true);
|
|
1716
1716
|
assert.equal(result.passed, true);
|
|
1717
1717
|
assert.equal(result.evidenceGateDemoted, false);
|
|
1718
|
-
assert.equal(result.evidenceRawCount,
|
|
1719
|
-
assert.equal(result.evidenceMatchedCount,
|
|
1718
|
+
assert.equal(result.evidenceRawCount, 0);
|
|
1719
|
+
assert.equal(result.evidenceMatchedCount, 0);
|
|
1720
1720
|
}
|
|
1721
1721
|
|
|
1722
|
-
async function
|
|
1722
|
+
async function testVisionEvidenceGateShouldNotDemoteWhenExplicitlyArmedWithoutEvidence() {
|
|
1723
1723
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-evidence-gate-explicit-"));
|
|
1724
1724
|
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1725
1725
|
cli.prepareVisionImageSegmentsForModel = async () => ({
|
|
@@ -1740,12 +1740,48 @@ async function testVisionEvidenceGateShouldDemoteWhenExplicitlyArmedWithoutEvide
|
|
|
1740
1740
|
});
|
|
1741
1741
|
const result = await cli.callVisionModel(path.join(tempDir, "fake.png"));
|
|
1742
1742
|
assert.equal(result.rawPassed, true);
|
|
1743
|
-
assert.equal(result.passed,
|
|
1744
|
-
assert.equal(result.evidenceGateDemoted,
|
|
1743
|
+
assert.equal(result.passed, true);
|
|
1744
|
+
assert.equal(result.evidenceGateDemoted, false);
|
|
1745
1745
|
assert.equal(result.evidenceRawCount, 0);
|
|
1746
1746
|
assert.equal(result.evidenceMatchedCount, 0);
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
|
+
async function testTextModelShouldNotDemoteRawPassWhenEvidenceDoesNotMatchResume() {
|
|
1750
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-text-no-demote-"));
|
|
1751
|
+
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1752
|
+
const originalFetch = global.fetch;
|
|
1753
|
+
global.fetch = async () => ({
|
|
1754
|
+
ok: true,
|
|
1755
|
+
status: 200,
|
|
1756
|
+
async json() {
|
|
1757
|
+
return {
|
|
1758
|
+
choices: [
|
|
1759
|
+
{
|
|
1760
|
+
message: {
|
|
1761
|
+
content: JSON.stringify({
|
|
1762
|
+
passed: true,
|
|
1763
|
+
reason: "matched",
|
|
1764
|
+
summary: "matched",
|
|
1765
|
+
evidence: ["完全不在简历里的证据"]
|
|
1766
|
+
})
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
]
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
try {
|
|
1774
|
+
const result = await cli.callTextModel("这是简历原文,没有那条证据");
|
|
1775
|
+
assert.equal(result.rawPassed, true);
|
|
1776
|
+
assert.equal(result.passed, true);
|
|
1777
|
+
assert.equal(result.evidenceGateDemoted, false);
|
|
1778
|
+
assert.equal(result.evidenceRawCount, 1);
|
|
1779
|
+
assert.equal(result.evidenceMatchedCount, 0);
|
|
1780
|
+
} finally {
|
|
1781
|
+
global.fetch = originalFetch;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1749
1785
|
async function testVisionModelShouldSendAllOrderedChunks() {
|
|
1750
1786
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-vision-all-chunks-"));
|
|
1751
1787
|
const chunkPaths = [];
|
|
@@ -1843,7 +1879,8 @@ async function main() {
|
|
|
1843
1879
|
await testTextModelShouldSupportLowThinkingForVolcengine();
|
|
1844
1880
|
await testPrepareVisionImageSegmentsShouldSplitLongImage();
|
|
1845
1881
|
await testVisionEvidenceGateShouldKeepRawPassWithoutExplicitEvidenceProtocol();
|
|
1846
|
-
await
|
|
1882
|
+
await testVisionEvidenceGateShouldNotDemoteWhenExplicitlyArmedWithoutEvidence();
|
|
1883
|
+
await testTextModelShouldNotDemoteRawPassWhenEvidenceDoesNotMatchResume();
|
|
1847
1884
|
await testVisionModelShouldSendAllOrderedChunks();
|
|
1848
1885
|
testRecoverablePostActionErrorShouldTreatGreetContinueAndNoButtonAsRecoverable();
|
|
1849
1886
|
await testRecoverableGreetContinueButtonShouldNotAbortWhenDetailCloseFails();
|