@reconcrap/boss-recommend-mcp 1.3.10 → 1.3.11
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 +258 -0
- package/vendor/boss-chat-cli/src/app.js +61 -4
- package/vendor/boss-chat-cli/src/browser/chat-page.js +75 -0
- package/vendor/boss-chat-cli/src/cli.js +1 -1
- package/vendor/boss-chat-cli/src/services/llm.js +393 -52
- package/vendor/boss-chat-cli/src/services/state-store.js +4 -131
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +45 -6
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +25 -0
package/package.json
CHANGED
package/src/test-boss-chat.js
CHANGED
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
cancelBossChatRun,
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
} from "./boss-chat.js";
|
|
15
16
|
import { __testables as cliTestables } from "./cli.js";
|
|
16
17
|
import { __testables as indexTestables } from "./index.js";
|
|
18
|
+
import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
|
|
19
|
+
import { LlmClient, parseLlmJson } from "../vendor/boss-chat-cli/src/services/llm.js";
|
|
17
20
|
|
|
18
21
|
const { handleRequest } = indexTestables;
|
|
19
22
|
|
|
@@ -550,10 +553,265 @@ async function testBossChatCliShouldSupportRunAndFollowUpParsing() {
|
|
|
550
553
|
});
|
|
551
554
|
}
|
|
552
555
|
|
|
556
|
+
function testBossChatLlmEvidenceGateShouldDemoteMissingEvidence() {
|
|
557
|
+
const parsed = parseLlmJson(
|
|
558
|
+
JSON.stringify({
|
|
559
|
+
passed: true,
|
|
560
|
+
reason: "命中标准",
|
|
561
|
+
summary: "命中",
|
|
562
|
+
evidence: [],
|
|
563
|
+
}),
|
|
564
|
+
{
|
|
565
|
+
evidenceCorpus: "南京大学 机器学习 项目经历",
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
assert.equal(parsed.rawPassed, true);
|
|
569
|
+
assert.equal(parsed.passed, false);
|
|
570
|
+
assert.equal(parsed.evidenceGateDemoted, true);
|
|
571
|
+
assert.equal(parsed.evidenceMatchedCount, 0);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence() {
|
|
575
|
+
const parsed = parseLlmJson(
|
|
576
|
+
JSON.stringify({
|
|
577
|
+
passed: true,
|
|
578
|
+
reason: "命中标准",
|
|
579
|
+
summary: "命中",
|
|
580
|
+
evidence: ["十年金融风控投研经验"],
|
|
581
|
+
}),
|
|
582
|
+
{
|
|
583
|
+
evidenceCorpus: "南京大学 机器学习 项目经历",
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
assert.equal(parsed.rawPassed, true);
|
|
587
|
+
assert.equal(parsed.passed, false);
|
|
588
|
+
assert.equal(parsed.evidenceGateDemoted, true);
|
|
589
|
+
assert.equal(parsed.evidenceRawCount, 1);
|
|
590
|
+
assert.equal(parsed.evidenceMatchedCount, 0);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function testBossChatLlmTextChunkFallbackShouldWork() {
|
|
594
|
+
const originalChunkSize = process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
|
|
595
|
+
const originalChunkOverlap = process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
|
|
596
|
+
const originalMaxChunks = process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
|
|
597
|
+
process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = "1000";
|
|
598
|
+
process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = "120";
|
|
599
|
+
process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = "6";
|
|
600
|
+
try {
|
|
601
|
+
class FakeChunkFallbackClient extends LlmClient {
|
|
602
|
+
constructor() {
|
|
603
|
+
super({
|
|
604
|
+
baseUrl: "https://api.example.com/v1",
|
|
605
|
+
apiKey: "sk-test",
|
|
606
|
+
model: "gpt-test",
|
|
607
|
+
});
|
|
608
|
+
this.calls = [];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async requestByPreference(payload) {
|
|
612
|
+
this.calls.push(payload);
|
|
613
|
+
if (payload.chunkTotal === 1 && !payload.imageDataUrl) {
|
|
614
|
+
const error = new Error("maximum context length exceeded");
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
if (payload.chunkTotal > 1) {
|
|
618
|
+
if (payload.chunkIndex === 2) {
|
|
619
|
+
return {
|
|
620
|
+
passed: true,
|
|
621
|
+
rawPassed: true,
|
|
622
|
+
reason: "命中分段证据",
|
|
623
|
+
summary: "命中",
|
|
624
|
+
evidence: ["PASS_MARKER_ABC"],
|
|
625
|
+
evidenceRawCount: 1,
|
|
626
|
+
evidenceMatchedCount: 1,
|
|
627
|
+
evidenceGateDemoted: false,
|
|
628
|
+
chunkIndex: payload.chunkIndex,
|
|
629
|
+
chunkTotal: payload.chunkTotal,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
passed: false,
|
|
634
|
+
rawPassed: false,
|
|
635
|
+
reason: "本段证据不足",
|
|
636
|
+
summary: "不足",
|
|
637
|
+
evidence: [],
|
|
638
|
+
evidenceRawCount: 0,
|
|
639
|
+
evidenceMatchedCount: 0,
|
|
640
|
+
evidenceGateDemoted: false,
|
|
641
|
+
chunkIndex: payload.chunkIndex,
|
|
642
|
+
chunkTotal: payload.chunkTotal,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
passed: false,
|
|
647
|
+
rawPassed: false,
|
|
648
|
+
reason: "unexpected",
|
|
649
|
+
summary: "unexpected",
|
|
650
|
+
evidence: [],
|
|
651
|
+
evidenceRawCount: 0,
|
|
652
|
+
evidenceMatchedCount: 0,
|
|
653
|
+
evidenceGateDemoted: false,
|
|
654
|
+
chunkIndex: 1,
|
|
655
|
+
chunkTotal: 1,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const client = new FakeChunkFallbackClient();
|
|
661
|
+
const longResume = `${"A".repeat(1200)} PASS_MARKER_ABC ${"B".repeat(1200)} PASS_MARKER_DEF`;
|
|
662
|
+
const result = await client.evaluateResume({
|
|
663
|
+
screeningCriteria: "有 AI 项目经验",
|
|
664
|
+
candidate: {
|
|
665
|
+
name: "候选人A",
|
|
666
|
+
sourceJob: "算法工程师",
|
|
667
|
+
resumeText: longResume,
|
|
668
|
+
evidenceCorpus: longResume,
|
|
669
|
+
},
|
|
670
|
+
imagePath: null,
|
|
671
|
+
});
|
|
672
|
+
assert.equal(result.passed, true);
|
|
673
|
+
assert.equal(result.evaluationMode, "text");
|
|
674
|
+
assert.equal(result.chunkIndex, 2);
|
|
675
|
+
assert.equal(Number(result.chunkTotal) > 1, true);
|
|
676
|
+
} finally {
|
|
677
|
+
if (originalChunkSize === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS;
|
|
678
|
+
else process.env.BOSS_CHAT_TEXT_CHUNK_SIZE_CHARS = originalChunkSize;
|
|
679
|
+
if (originalChunkOverlap === undefined) delete process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS;
|
|
680
|
+
else process.env.BOSS_CHAT_TEXT_CHUNK_OVERLAP_CHARS = originalChunkOverlap;
|
|
681
|
+
if (originalMaxChunks === undefined) delete process.env.BOSS_CHAT_TEXT_MAX_CHUNKS;
|
|
682
|
+
else process.env.BOSS_CHAT_TEXT_MAX_CHUNKS = originalMaxChunks;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function testBossChatAppShouldPersistEvidenceArtifacts() {
|
|
687
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-artifacts-"));
|
|
688
|
+
await mkdir(tempDir, { recursive: true });
|
|
689
|
+
const records = [];
|
|
690
|
+
const page = {
|
|
691
|
+
async closeResumeModalDomOnce() {
|
|
692
|
+
return {
|
|
693
|
+
closed: true,
|
|
694
|
+
method: "dom",
|
|
695
|
+
finalState: { scopeCount: 0, iframeCount: 0, closeCount: 0, topScopeClass: "" },
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
async waitForConversationReady() {
|
|
699
|
+
return {
|
|
700
|
+
hasOnlineResume: true,
|
|
701
|
+
hasAskResume: true,
|
|
702
|
+
hasAttachmentResume: false,
|
|
703
|
+
attachmentResumeEnabled: false,
|
|
704
|
+
};
|
|
705
|
+
},
|
|
706
|
+
async openOnlineResume() {
|
|
707
|
+
return { clicked: true, detectedOpen: true, by: "dom" };
|
|
708
|
+
},
|
|
709
|
+
async getResumeRateLimitWarning() {
|
|
710
|
+
return { hit: false, text: "" };
|
|
711
|
+
},
|
|
712
|
+
async getResumeProfileFromDom() {
|
|
713
|
+
return {
|
|
714
|
+
ok: true,
|
|
715
|
+
primarySchool: "南京大学",
|
|
716
|
+
schools: ["南京大学"],
|
|
717
|
+
major: "计算机",
|
|
718
|
+
majors: ["计算机"],
|
|
719
|
+
company: "OpenAI",
|
|
720
|
+
position: "工程师",
|
|
721
|
+
resumeText: "南京大学 计算机 PASS_MARKER_ABC",
|
|
722
|
+
evidenceCorpus: "南京大学 计算机 PASS_MARKER_ABC",
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
async getResumeModalState() {
|
|
726
|
+
return { open: true, iframeCount: 1, scopeCount: 1, closeCount: 1 };
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
const llmClient = {
|
|
730
|
+
async evaluateResume() {
|
|
731
|
+
return {
|
|
732
|
+
passed: false,
|
|
733
|
+
rawPassed: true,
|
|
734
|
+
reason: "模型未给出可在简历原文中校验的证据,按安全策略判为不通过。",
|
|
735
|
+
summary: "降级",
|
|
736
|
+
evidence: [],
|
|
737
|
+
evidenceRawCount: 1,
|
|
738
|
+
evidenceMatchedCount: 0,
|
|
739
|
+
evidenceGateDemoted: true,
|
|
740
|
+
evaluationMode: "text",
|
|
741
|
+
chunkIndex: 1,
|
|
742
|
+
chunkTotal: 1,
|
|
743
|
+
};
|
|
744
|
+
},
|
|
745
|
+
};
|
|
746
|
+
const interaction = {
|
|
747
|
+
async sleepRange() {},
|
|
748
|
+
async clickRect() {},
|
|
749
|
+
};
|
|
750
|
+
const resumeCaptureService = {
|
|
751
|
+
async captureResume({ artifactDir }) {
|
|
752
|
+
return {
|
|
753
|
+
stitchedImage: path.join(artifactDir, "resume.png"),
|
|
754
|
+
metadataFile: path.join(artifactDir, "chunks.json"),
|
|
755
|
+
chunkDir: path.join(artifactDir, "chunks"),
|
|
756
|
+
chunkCount: 1,
|
|
757
|
+
quality: { likelyBlank: false },
|
|
758
|
+
};
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
const stateStore = {
|
|
762
|
+
async record(_key, result) {
|
|
763
|
+
records.push(result);
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
const app = new BossChatApp({
|
|
767
|
+
page,
|
|
768
|
+
llmClient,
|
|
769
|
+
interaction,
|
|
770
|
+
resumeCaptureService,
|
|
771
|
+
stateStore,
|
|
772
|
+
reportStore: { async write() { return ""; } },
|
|
773
|
+
dryRun: true,
|
|
774
|
+
artifactRootDir: tempDir,
|
|
775
|
+
resumeOpenCooldownMs: 0,
|
|
776
|
+
logger: { log() {} },
|
|
777
|
+
});
|
|
778
|
+
app.waitResumeOpenCooldown = async () => {};
|
|
779
|
+
|
|
780
|
+
const result = await app.processCustomer(
|
|
781
|
+
{
|
|
782
|
+
customerKey: "candidate-key",
|
|
783
|
+
name: "候选人A",
|
|
784
|
+
sourceJob: "算法工程师",
|
|
785
|
+
domIndex: 0,
|
|
786
|
+
customerId: "1001",
|
|
787
|
+
textSnippet: "",
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
screeningCriteria: "有 AI 项目经验",
|
|
791
|
+
},
|
|
792
|
+
"run-test",
|
|
793
|
+
{ skipCardClick: true },
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
assert.equal(result.passed, false);
|
|
797
|
+
assert.equal(result.artifacts.rawPassed, true);
|
|
798
|
+
assert.equal(result.artifacts.finalPassed, false);
|
|
799
|
+
assert.equal(result.artifacts.evidenceRawCount, 1);
|
|
800
|
+
assert.equal(result.artifacts.evidenceMatchedCount, 0);
|
|
801
|
+
assert.equal(result.artifacts.evidenceGateDemoted, true);
|
|
802
|
+
assert.equal(result.artifacts.evaluationMode, "text");
|
|
803
|
+
assert.equal(Array.isArray(records), true);
|
|
804
|
+
assert.equal(records.length, 1);
|
|
805
|
+
}
|
|
806
|
+
|
|
553
807
|
async function main() {
|
|
554
808
|
await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
|
|
555
809
|
await testBossChatMcpToolsShouldValidateAndRoute();
|
|
556
810
|
await testBossChatCliShouldSupportRunAndFollowUpParsing();
|
|
811
|
+
testBossChatLlmEvidenceGateShouldDemoteMissingEvidence();
|
|
812
|
+
testBossChatLlmEvidenceGateShouldDemoteUnmatchedEvidence();
|
|
813
|
+
await testBossChatLlmTextChunkFallbackShouldWork();
|
|
814
|
+
await testBossChatAppShouldPersistEvidenceArtifacts();
|
|
557
815
|
console.log("boss-chat tests passed");
|
|
558
816
|
}
|
|
559
817
|
|
|
@@ -233,7 +233,11 @@ export class BossChatApp {
|
|
|
233
233
|
|
|
234
234
|
let consecutiveErrors = 0;
|
|
235
235
|
let exhaustedScrolls = 0;
|
|
236
|
-
|
|
236
|
+
let noMoreMarkerHits = 0;
|
|
237
|
+
let fallbackBottomHits = 0;
|
|
238
|
+
const noMoreMarkerConfirmations = 2;
|
|
239
|
+
const exhaustedScrollLimit = targetCount ? 10 : 60;
|
|
240
|
+
const fallbackBottomLimit = targetCount ? 4 : 12;
|
|
237
241
|
|
|
238
242
|
try {
|
|
239
243
|
while (shouldContinue(summary, targetCount)) {
|
|
@@ -324,6 +328,9 @@ export class BossChatApp {
|
|
|
324
328
|
stage: 'running',
|
|
325
329
|
message: `已处理候选人:${result.name || '未知'}`,
|
|
326
330
|
});
|
|
331
|
+
exhaustedScrolls = 0;
|
|
332
|
+
noMoreMarkerHits = 0;
|
|
333
|
+
fallbackBottomHits = 0;
|
|
327
334
|
if (consecutiveErrors >= 3) {
|
|
328
335
|
this.logger.log('连续 3 位候选人处理失败,提前停止本轮运行。');
|
|
329
336
|
break;
|
|
@@ -340,13 +347,33 @@ export class BossChatApp {
|
|
|
340
347
|
if (!nextCustomer) {
|
|
341
348
|
const ratio = 0.52 + Math.random() * 0.34;
|
|
342
349
|
const scrollResult = await this.page.scrollCustomerList(ratio);
|
|
350
|
+
const noMoreDetected =
|
|
351
|
+
Boolean(scrollResult.noMoreDetectedAfter) || Boolean(scrollResult.noMoreDetectedBefore);
|
|
343
352
|
this.logger.log(
|
|
344
|
-
`列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | scrollRetry=${exhaustedScrolls + 1}`,
|
|
353
|
+
`列表滚动:ratio=${ratio.toFixed(2)} | didScroll=${Boolean(scrollResult.didScroll)} | top=${scrollResult.after?.top ?? 'n/a'} | atBottom=${Boolean(scrollResult.atBottom)} | noMore=${noMoreDetected}${scrollResult.noMoreTextAfter ? `(${scrollResult.noMoreTextAfter})` : ''} | scrollRetry=${exhaustedScrolls + 1}`,
|
|
345
354
|
);
|
|
355
|
+
if (noMoreDetected) {
|
|
356
|
+
noMoreMarkerHits += 1;
|
|
357
|
+
if (noMoreMarkerHits >= noMoreMarkerConfirmations) {
|
|
358
|
+
summary.exhausted = true;
|
|
359
|
+
this.logger.log('列表滚动终止:检测到“没有更多了”标识,判定为 exhausted。');
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
await this.interaction.sleepRange(920, 260);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
noMoreMarkerHits = 0;
|
|
346
367
|
exhaustedScrolls = scrollResult.didScroll ? exhaustedScrolls + 1 : exhaustedScrolls + 2;
|
|
368
|
+
fallbackBottomHits = scrollResult.atBottom ? fallbackBottomHits + 1 : 0;
|
|
369
|
+
if (fallbackBottomHits >= fallbackBottomLimit && exhaustedScrolls >= Math.ceil(exhaustedScrollLimit / 2)) {
|
|
370
|
+
summary.exhausted = true;
|
|
371
|
+
this.logger.log('列表滚动终止:未发现“没有更多了”标识,但已多次触底且无可处理候选人,判定为 exhausted。');
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
347
374
|
if (exhaustedScrolls >= exhaustedScrollLimit) {
|
|
348
375
|
summary.exhausted = true;
|
|
349
|
-
this.logger.log('
|
|
376
|
+
this.logger.log('列表滚动终止:连续无可处理候选人达到保护上限,判定为 exhausted。');
|
|
350
377
|
break;
|
|
351
378
|
}
|
|
352
379
|
await this.interaction.sleepRange(920, 260);
|
|
@@ -354,6 +381,8 @@ export class BossChatApp {
|
|
|
354
381
|
}
|
|
355
382
|
|
|
356
383
|
exhaustedScrolls = 0;
|
|
384
|
+
noMoreMarkerHits = 0;
|
|
385
|
+
fallbackBottomHits = 0;
|
|
357
386
|
this.logger.log(
|
|
358
387
|
`准备处理候选人:name=${nextCustomer.name || '未知'} | key=${nextCustomer.customerKey} | job=${nextCustomer.sourceJob || '未知'} | domIndex=${nextCustomer.domIndex}`,
|
|
359
388
|
);
|
|
@@ -578,6 +607,8 @@ export class BossChatApp {
|
|
|
578
607
|
majors: Array.isArray(resumeProfile.majors) ? resumeProfile.majors : [],
|
|
579
608
|
company: resumeProfile.company || '',
|
|
580
609
|
position: resumeProfile.position || '',
|
|
610
|
+
resumeTextLength: String(resumeProfile.resumeText || '').length,
|
|
611
|
+
evidenceCorpusLength: String(resumeProfile.evidenceCorpus || '').length,
|
|
581
612
|
};
|
|
582
613
|
} else {
|
|
583
614
|
this.logger.log(`简历结构化提取未命中:${resumeProfile?.error || 'unknown'}`);
|
|
@@ -638,6 +669,8 @@ export class BossChatApp {
|
|
|
638
669
|
company: resumeProfile.company || '',
|
|
639
670
|
position: resumeProfile.position || '',
|
|
640
671
|
} : null,
|
|
672
|
+
resumeText: resumeProfile?.ok ? String(resumeProfile.resumeText || '') : '',
|
|
673
|
+
evidenceCorpus: resumeProfile?.ok ? String(resumeProfile.evidenceCorpus || '') : '',
|
|
641
674
|
},
|
|
642
675
|
imagePath: capture.stitchedImage,
|
|
643
676
|
});
|
|
@@ -647,13 +680,37 @@ export class BossChatApp {
|
|
|
647
680
|
`评估理由学校字段已按主简历纠偏:rawReason=${evaluation.reason} | finalReason=${finalReason}`,
|
|
648
681
|
);
|
|
649
682
|
}
|
|
683
|
+
if (evaluation.evidenceGateDemoted === true) {
|
|
684
|
+
this.logger.log(
|
|
685
|
+
`证据闸门降级:rawPassed=${Boolean(evaluation.rawPassed)} | evidenceRawCount=${Number(evaluation.evidenceRawCount || 0)} | evidenceMatchedCount=${Number(evaluation.evidenceMatchedCount || 0)} | mode=${evaluation.evaluationMode || 'unknown'}`,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
650
688
|
this.logger.log(
|
|
651
|
-
`LLM评估完成:passed=${evaluation.passed} | reason=${finalReason}`,
|
|
689
|
+
`LLM评估完成:passed=${evaluation.passed} | rawPassed=${Boolean(evaluation.rawPassed)} | mode=${evaluation.evaluationMode || 'unknown'} | reason=${finalReason}`,
|
|
652
690
|
);
|
|
653
691
|
|
|
654
692
|
baseResult.reason = finalReason;
|
|
655
693
|
baseResult.passed = evaluation.passed;
|
|
656
694
|
baseResult.decision = evaluation.passed ? 'passed' : 'skipped';
|
|
695
|
+
baseResult.artifacts.rawPassed = Boolean(evaluation.rawPassed);
|
|
696
|
+
baseResult.artifacts.finalPassed = Boolean(evaluation.passed);
|
|
697
|
+
baseResult.artifacts.evidenceRawCount = Number.isFinite(Number(evaluation.evidenceRawCount))
|
|
698
|
+
? Number(evaluation.evidenceRawCount)
|
|
699
|
+
: 0;
|
|
700
|
+
baseResult.artifacts.evidenceMatchedCount = Number.isFinite(Number(evaluation.evidenceMatchedCount))
|
|
701
|
+
? Number(evaluation.evidenceMatchedCount)
|
|
702
|
+
: 0;
|
|
703
|
+
baseResult.artifacts.evidenceGateDemoted = evaluation.evidenceGateDemoted === true;
|
|
704
|
+
baseResult.artifacts.evaluationMode = String(evaluation.evaluationMode || '');
|
|
705
|
+
baseResult.artifacts.evaluationChunkIndex = Number.isFinite(Number(evaluation.chunkIndex))
|
|
706
|
+
? Number(evaluation.chunkIndex)
|
|
707
|
+
: null;
|
|
708
|
+
baseResult.artifacts.evaluationChunkTotal = Number.isFinite(Number(evaluation.chunkTotal))
|
|
709
|
+
? Number(evaluation.chunkTotal)
|
|
710
|
+
: null;
|
|
711
|
+
baseResult.artifacts.evaluationEvidence = Array.isArray(evaluation.evidence)
|
|
712
|
+
? evaluation.evidence.slice(0, 5).map((item) => String(item || '').trim()).filter(Boolean)
|
|
713
|
+
: [];
|
|
657
714
|
|
|
658
715
|
await this.checkpoint();
|
|
659
716
|
const closeResult =
|
|
@@ -630,6 +630,16 @@ function browserActivateCandidate(options = {}) {
|
|
|
630
630
|
function browserScrollCustomerList(options = {}) {
|
|
631
631
|
const ratio = Number(options.ratio || 0.72);
|
|
632
632
|
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
|
633
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
634
|
+
const isVisible = (el) => {
|
|
635
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
636
|
+
const style = getComputedStyle(el);
|
|
637
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.01) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
const rect = el.getBoundingClientRect();
|
|
641
|
+
return rect.width > 2 && rect.height > 2;
|
|
642
|
+
};
|
|
633
643
|
const isOverflowScrollable = (el) => {
|
|
634
644
|
if (!(el instanceof HTMLElement)) return false;
|
|
635
645
|
const style = getComputedStyle(el);
|
|
@@ -706,6 +716,20 @@ function browserScrollCustomerList(options = {}) {
|
|
|
706
716
|
return { ok: false, error: 'CHAT_LIST_CONTAINER_NOT_FOUND' };
|
|
707
717
|
}
|
|
708
718
|
|
|
719
|
+
const findNoMoreTips = () => {
|
|
720
|
+
const host =
|
|
721
|
+
listContainer.closest('.chat-user, .user-container, .chat-container, .chat-main') || document;
|
|
722
|
+
const tips = Array.from(host.querySelectorAll('div[role="tfoot"] .load-tips, p.load-tips')).find((node) => {
|
|
723
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
724
|
+
const text = normalize(node.textContent || '');
|
|
725
|
+
return text.includes('没有更多了') && isVisible(node);
|
|
726
|
+
});
|
|
727
|
+
return {
|
|
728
|
+
detected: Boolean(tips),
|
|
729
|
+
text: normalize(tips?.textContent || ''),
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
|
|
709
733
|
const firstCard = listContainer.querySelector('div[role="listitem"]') || document.querySelector('div[role="listitem"]');
|
|
710
734
|
if (firstCard instanceof HTMLElement) {
|
|
711
735
|
const best = findBestScrollableContainer(firstCard);
|
|
@@ -714,6 +738,7 @@ function browserScrollCustomerList(options = {}) {
|
|
|
714
738
|
}
|
|
715
739
|
}
|
|
716
740
|
|
|
741
|
+
const noMoreBefore = findNoMoreTips();
|
|
717
742
|
const before = {
|
|
718
743
|
top: Number(listContainer.scrollTop || 0),
|
|
719
744
|
height: Number(listContainer.scrollHeight || 0),
|
|
@@ -750,11 +775,18 @@ function browserScrollCustomerList(options = {}) {
|
|
|
750
775
|
clientHeight: Number(listContainer.clientHeight || 0),
|
|
751
776
|
cardCount: Number(listContainer.querySelectorAll('div[role="listitem"]').length || 0),
|
|
752
777
|
};
|
|
778
|
+
const noMoreAfter = findNoMoreTips();
|
|
779
|
+
const atBottom = after.height <= after.clientHeight + 2 || after.top >= Math.max(0, after.height - after.clientHeight - 2);
|
|
753
780
|
|
|
754
781
|
return {
|
|
755
782
|
ok: true,
|
|
756
783
|
before,
|
|
757
784
|
after,
|
|
785
|
+
atBottom,
|
|
786
|
+
noMoreDetectedBefore: noMoreBefore.detected,
|
|
787
|
+
noMoreDetectedAfter: noMoreAfter.detected,
|
|
788
|
+
noMoreTextBefore: noMoreBefore.text,
|
|
789
|
+
noMoreTextAfter: noMoreAfter.text,
|
|
758
790
|
didScroll:
|
|
759
791
|
before.top !== after.top ||
|
|
760
792
|
before.height !== after.height ||
|
|
@@ -1992,6 +2024,18 @@ function browserExtractResumeProfileFromModal() {
|
|
|
1992
2024
|
normalized.includes('匿名牛人')
|
|
1993
2025
|
);
|
|
1994
2026
|
};
|
|
2027
|
+
const stripNoiseText = (text) => {
|
|
2028
|
+
let cleaned = normalize(text);
|
|
2029
|
+
const noisePhrases = ['其他名企大厂经历牛人', '相似牛人', '推荐牛人', '匿名牛人'];
|
|
2030
|
+
for (const phrase of noisePhrases) {
|
|
2031
|
+
cleaned = cleaned.split(phrase).join(' ');
|
|
2032
|
+
}
|
|
2033
|
+
return normalize(cleaned);
|
|
2034
|
+
};
|
|
2035
|
+
const pickSectionText = (section) => {
|
|
2036
|
+
if (!section) return '';
|
|
2037
|
+
return stripNoiseText(section.innerText || section.textContent || '');
|
|
2038
|
+
};
|
|
1995
2039
|
const pickFirstText = (scope, selectors) => {
|
|
1996
2040
|
for (const selector of selectors) {
|
|
1997
2041
|
let nodes = [];
|
|
@@ -2057,6 +2101,8 @@ function browserExtractResumeProfileFromModal() {
|
|
|
2057
2101
|
position: '',
|
|
2058
2102
|
schools: [],
|
|
2059
2103
|
majors: [],
|
|
2104
|
+
resumeText: '',
|
|
2105
|
+
evidenceCorpus: '',
|
|
2060
2106
|
};
|
|
2061
2107
|
}
|
|
2062
2108
|
|
|
@@ -2070,6 +2116,16 @@ function browserExtractResumeProfileFromModal() {
|
|
|
2070
2116
|
root.querySelector('.geek-work-experience-wrap') ||
|
|
2071
2117
|
root.querySelector('.resume-section[class*="work"]') ||
|
|
2072
2118
|
root;
|
|
2119
|
+
const projectSection =
|
|
2120
|
+
root.querySelector('.resume-section.geek-project-experience-wrap') ||
|
|
2121
|
+
root.querySelector('.geek-project-experience-wrap') ||
|
|
2122
|
+
root.querySelector('.resume-section[class*="project"]') ||
|
|
2123
|
+
null;
|
|
2124
|
+
const skillSection =
|
|
2125
|
+
root.querySelector('.resume-section.geek-skill-wrap') ||
|
|
2126
|
+
root.querySelector('.geek-skill-wrap') ||
|
|
2127
|
+
root.querySelector('.resume-section[class*="skill"]') ||
|
|
2128
|
+
null;
|
|
2073
2129
|
const baseSection =
|
|
2074
2130
|
root.querySelector('.resume-section.geek-base-info-wrap') ||
|
|
2075
2131
|
root.querySelector('.geek-base-info-wrap') ||
|
|
@@ -2102,6 +2158,21 @@ function browserExtractResumeProfileFromModal() {
|
|
|
2102
2158
|
'.position span',
|
|
2103
2159
|
'.position',
|
|
2104
2160
|
]);
|
|
2161
|
+
const baseText = pickSectionText(baseSection);
|
|
2162
|
+
const educationText = pickSectionText(educationSection);
|
|
2163
|
+
const workText = pickSectionText(workSection);
|
|
2164
|
+
const projectText = pickSectionText(projectSection);
|
|
2165
|
+
const skillText = pickSectionText(skillSection);
|
|
2166
|
+
const evidenceCorpus = stripNoiseText(root.innerText || root.textContent || '');
|
|
2167
|
+
const resumeText = [
|
|
2168
|
+
baseText ? `基础信息: ${baseText}` : '',
|
|
2169
|
+
educationText ? `教育经历: ${educationText}` : '',
|
|
2170
|
+
workText ? `工作经历: ${workText}` : '',
|
|
2171
|
+
projectText ? `项目经历: ${projectText}` : '',
|
|
2172
|
+
skillText ? `技能信息: ${skillText}` : '',
|
|
2173
|
+
]
|
|
2174
|
+
.filter(Boolean)
|
|
2175
|
+
.join('\n');
|
|
2105
2176
|
|
|
2106
2177
|
return {
|
|
2107
2178
|
ok: true,
|
|
@@ -2112,11 +2183,15 @@ function browserExtractResumeProfileFromModal() {
|
|
|
2112
2183
|
position,
|
|
2113
2184
|
schools,
|
|
2114
2185
|
majors,
|
|
2186
|
+
resumeText: resumeText || evidenceCorpus || '',
|
|
2187
|
+
evidenceCorpus: evidenceCorpus || resumeText || '',
|
|
2115
2188
|
debug: {
|
|
2116
2189
|
rootClass: String(root.className || ''),
|
|
2117
2190
|
educationClass: String(educationSection?.className || ''),
|
|
2118
2191
|
workClass: String(workSection?.className || ''),
|
|
2119
2192
|
wrapperClass: String(scope?.className || ''),
|
|
2193
|
+
resumeTextLength: Number((resumeText || '').length),
|
|
2194
|
+
evidenceCorpusLength: Number((evidenceCorpus || '').length),
|
|
2120
2195
|
},
|
|
2121
2196
|
};
|
|
2122
2197
|
}
|
|
@@ -291,7 +291,7 @@ function printUsage() {
|
|
|
291
291
|
console.log('');
|
|
292
292
|
console.log('Run options:');
|
|
293
293
|
console.log(' --dry-run Evaluate and click, but do not request resume');
|
|
294
|
-
console.log(' --no-state
|
|
294
|
+
console.log(' --no-state Disable in-run candidate deduplication');
|
|
295
295
|
console.log(' --job <text|value|index> Select job by label/value/index');
|
|
296
296
|
console.log(' --criteria <text> Screening criteria for resume evaluation');
|
|
297
297
|
console.log(' --start-from <unread|all> Start from unread or all list');
|