@reconcrap/boss-recommend-mcp 1.3.18 → 1.3.19
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
CHANGED
|
@@ -16,7 +16,7 @@ const CSV_HEADER = [
|
|
|
16
16
|
"最近工作职位",
|
|
17
17
|
"评估通过详细原因",
|
|
18
18
|
"处理结果",
|
|
19
|
-
"
|
|
19
|
+
"判断依据(CoT)",
|
|
20
20
|
"动作执行结果",
|
|
21
21
|
"简历来源",
|
|
22
22
|
"原始判定通过",
|
|
@@ -592,6 +592,101 @@ function mergeCandidateProfiles(...profiles) {
|
|
|
592
592
|
};
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
function buildCardProfileFallbackText(cardProfile = {}) {
|
|
596
|
+
const profile = cardProfile && typeof cardProfile === "object" ? cardProfile : {};
|
|
597
|
+
const educationList = Array.isArray(profile.educationList)
|
|
598
|
+
? profile.educationList
|
|
599
|
+
.map((item) => ({
|
|
600
|
+
school: normalizeText(item?.school || ""),
|
|
601
|
+
major: normalizeText(item?.major || ""),
|
|
602
|
+
degree: normalizeText(item?.degree || ""),
|
|
603
|
+
start: normalizeText(item?.start || ""),
|
|
604
|
+
end: normalizeText(item?.end || "")
|
|
605
|
+
}))
|
|
606
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
607
|
+
.slice(0, 2)
|
|
608
|
+
: [];
|
|
609
|
+
const hasCore = Boolean(
|
|
610
|
+
normalizeText(profile.name || "")
|
|
611
|
+
|| normalizeText(profile.age || "")
|
|
612
|
+
|| normalizeText(profile.gender || "")
|
|
613
|
+
|| normalizeText(profile.highestDegree || "")
|
|
614
|
+
|| normalizeText(profile.workYears || "")
|
|
615
|
+
|| normalizeText(profile.company || "")
|
|
616
|
+
|| normalizeText(profile.position || "")
|
|
617
|
+
|| normalizeText(profile.latestWorkStart || "")
|
|
618
|
+
|| normalizeText(profile.latestWorkEnd || "")
|
|
619
|
+
|| educationList.length > 0
|
|
620
|
+
);
|
|
621
|
+
if (!hasCore) return "";
|
|
622
|
+
|
|
623
|
+
const lines = ["=== 人选卡片兜底信息(仅在简历缺失时使用) ==="];
|
|
624
|
+
const pushField = (label, value) => {
|
|
625
|
+
const text = normalizeText(value);
|
|
626
|
+
if (!text) return;
|
|
627
|
+
lines.push(`${label}: ${text}`);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
pushField("姓名", profile.name);
|
|
631
|
+
pushField("年龄", profile.age);
|
|
632
|
+
pushField("性别", profile.gender);
|
|
633
|
+
pushField("最高学历", profile.highestDegree);
|
|
634
|
+
pushField("工作年限", profile.workYears);
|
|
635
|
+
pushField("最近一份工作公司", profile.company);
|
|
636
|
+
pushField("最近一份职位", profile.position);
|
|
637
|
+
const workPeriod = formatResumeTimeRange(profile.latestWorkStart, profile.latestWorkEnd, "至今");
|
|
638
|
+
if (workPeriod) {
|
|
639
|
+
lines.push(`最近一份工作在职日期: ${workPeriod}`);
|
|
640
|
+
}
|
|
641
|
+
if (educationList.length > 0) {
|
|
642
|
+
lines.push("最近两段学校经历:");
|
|
643
|
+
educationList.forEach((item, index) => {
|
|
644
|
+
const eduPeriod = formatResumeTimeRange(item.start, item.end);
|
|
645
|
+
const detailParts = [
|
|
646
|
+
item.school ? `学校=${item.school}` : "",
|
|
647
|
+
item.major ? `专业=${item.major}` : "",
|
|
648
|
+
item.degree ? `学历=${item.degree}` : "",
|
|
649
|
+
eduPeriod ? `时间=${eduPeriod}` : ""
|
|
650
|
+
].filter(Boolean);
|
|
651
|
+
if (detailParts.length > 0) {
|
|
652
|
+
lines.push(`${index + 1}. ${detailParts.join(";")}`);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function enrichCandidateInfoWithCardProfile(candidateInfo = {}, cardProfile = null) {
|
|
660
|
+
const info = candidateInfo && typeof candidateInfo === "object" ? candidateInfo : {};
|
|
661
|
+
const profile = cardProfile && typeof cardProfile === "object" ? cardProfile : null;
|
|
662
|
+
if (!profile) return { ...info };
|
|
663
|
+
|
|
664
|
+
const educationList = Array.isArray(profile.educationList) ? profile.educationList : [];
|
|
665
|
+
const firstEducation = educationList[0] || {};
|
|
666
|
+
const merged = {
|
|
667
|
+
...info,
|
|
668
|
+
name: preferReadableName(info?.name || "", profile?.name || ""),
|
|
669
|
+
school: normalizeText(info?.school || "") || normalizeText(profile?.school || "") || normalizeText(firstEducation?.school || ""),
|
|
670
|
+
major: normalizeText(info?.major || "") || normalizeText(profile?.major || "") || normalizeText(firstEducation?.major || ""),
|
|
671
|
+
company: normalizeText(info?.company || "") || normalizeText(profile?.company || ""),
|
|
672
|
+
position: normalizeText(info?.position || "") || normalizeText(profile?.position || ""),
|
|
673
|
+
alreadyInterested: info?.alreadyInterested === true
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const baseResumeText = normalizeText(info?.resumeText || "");
|
|
677
|
+
const cardFallbackText = buildCardProfileFallbackText(profile);
|
|
678
|
+
if (cardFallbackText) {
|
|
679
|
+
merged.resumeText = baseResumeText.includes("=== 人选卡片兜底信息(仅在简历缺失时使用) ===")
|
|
680
|
+
? baseResumeText
|
|
681
|
+
: baseResumeText
|
|
682
|
+
? `${baseResumeText}\n\n${cardFallbackText}`
|
|
683
|
+
: cardFallbackText;
|
|
684
|
+
} else {
|
|
685
|
+
merged.resumeText = baseResumeText;
|
|
686
|
+
}
|
|
687
|
+
return merged;
|
|
688
|
+
}
|
|
689
|
+
|
|
595
690
|
function isDomProfileConsistentWithCard(cardProfile, domProfile) {
|
|
596
691
|
if (!cardProfile || !domProfile) return true;
|
|
597
692
|
let compared = 0;
|
|
@@ -809,6 +904,101 @@ function parseBoolean(value) {
|
|
|
809
904
|
return null;
|
|
810
905
|
}
|
|
811
906
|
|
|
907
|
+
function parsePassedDecision(value) {
|
|
908
|
+
if (typeof value === "boolean") return value;
|
|
909
|
+
if (typeof value === "number") {
|
|
910
|
+
if (value === 1) return true;
|
|
911
|
+
if (value === 0) return false;
|
|
912
|
+
}
|
|
913
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
914
|
+
if (!normalized) return null;
|
|
915
|
+
if (/不符合|不通过|未通过|未命中|不匹配|不满足/.test(normalized)) return false;
|
|
916
|
+
if (/符合|通过|命中|匹配|满足/.test(normalized)) return true;
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function parsePassedDecisionFromContent(content) {
|
|
921
|
+
const raw = String(content || "");
|
|
922
|
+
const explicit = raw.match(/"passed"\s*:\s*(true|false)/i);
|
|
923
|
+
if (explicit) {
|
|
924
|
+
return explicit[1].toLowerCase() === "true";
|
|
925
|
+
}
|
|
926
|
+
return parsePassedDecision(raw);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function flattenChatMessageContent(content) {
|
|
930
|
+
if (Array.isArray(content)) {
|
|
931
|
+
return content
|
|
932
|
+
.map((item) => {
|
|
933
|
+
if (typeof item === "string") return item;
|
|
934
|
+
if (item && typeof item === "object") {
|
|
935
|
+
return item.text || item.content || item.reasoning_content || "";
|
|
936
|
+
}
|
|
937
|
+
return "";
|
|
938
|
+
})
|
|
939
|
+
.filter(Boolean)
|
|
940
|
+
.join("\n");
|
|
941
|
+
}
|
|
942
|
+
return String(content || "");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function collectNestedText(value, out = [], depth = 0) {
|
|
946
|
+
if (depth > 6 || value === null || value === undefined) return out;
|
|
947
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
948
|
+
const normalized = normalizeText(String(value));
|
|
949
|
+
if (normalized) out.push(normalized);
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
if (Array.isArray(value)) {
|
|
953
|
+
for (const item of value) {
|
|
954
|
+
collectNestedText(item, out, depth + 1);
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
if (typeof value === "object") {
|
|
959
|
+
const priorityKeys = ["text", "reasoning_content", "summary_text", "summary", "content", "cot", "reason"];
|
|
960
|
+
const seen = new Set();
|
|
961
|
+
for (const key of priorityKeys) {
|
|
962
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
963
|
+
seen.add(key);
|
|
964
|
+
collectNestedText(value[key], out, depth + 1);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
968
|
+
if (seen.has(key)) continue;
|
|
969
|
+
collectNestedText(nested, out, depth + 1);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function extractCotFromChoice(choice, parsed = {}) {
|
|
976
|
+
const fragments = [];
|
|
977
|
+
const candidates = [
|
|
978
|
+
choice?.message?.reasoning_content,
|
|
979
|
+
choice?.message?.reasoning,
|
|
980
|
+
choice?.reasoning_content,
|
|
981
|
+
choice?.reasoning,
|
|
982
|
+
parsed?.cot,
|
|
983
|
+
parsed?.reasoning_content,
|
|
984
|
+
parsed?.reasoning,
|
|
985
|
+
parsed?.summary_text,
|
|
986
|
+
parsed?.summary,
|
|
987
|
+
parsed?.reason
|
|
988
|
+
];
|
|
989
|
+
for (const candidate of candidates) {
|
|
990
|
+
collectNestedText(candidate, fragments);
|
|
991
|
+
}
|
|
992
|
+
const deduped = [];
|
|
993
|
+
const seen = new Set();
|
|
994
|
+
for (const item of fragments) {
|
|
995
|
+
if (seen.has(item)) continue;
|
|
996
|
+
seen.add(item);
|
|
997
|
+
deduped.push(item);
|
|
998
|
+
}
|
|
999
|
+
return deduped.join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
812
1002
|
function normalizeCliOptionToken(rawToken) {
|
|
813
1003
|
const token = String(rawToken || "").trim();
|
|
814
1004
|
if (!token) {
|
|
@@ -1808,11 +1998,19 @@ function extractJsonObject(text) {
|
|
|
1808
1998
|
const start = raw.indexOf("{");
|
|
1809
1999
|
const end = raw.lastIndexOf("}");
|
|
1810
2000
|
if (start === -1 || end === -1 || end <= start) {
|
|
1811
|
-
throw new Error("
|
|
2001
|
+
throw new Error("Model response did not contain JSON");
|
|
1812
2002
|
}
|
|
1813
2003
|
return JSON.parse(raw.slice(start, end + 1));
|
|
1814
2004
|
}
|
|
1815
2005
|
|
|
2006
|
+
function tryExtractJsonObject(text) {
|
|
2007
|
+
try {
|
|
2008
|
+
return extractJsonObject(text);
|
|
2009
|
+
} catch {
|
|
2010
|
+
return {};
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1816
2014
|
async function promptPostAction() {
|
|
1817
2015
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1818
2016
|
throw new Error("POST_ACTION_CONFIRMATION_REQUIRED");
|
|
@@ -4475,6 +4673,18 @@ class RecommendScreenCli {
|
|
|
4475
4673
|
}
|
|
4476
4674
|
return "";
|
|
4477
4675
|
};
|
|
4676
|
+
const rankDegree = (value) => {
|
|
4677
|
+
const text = normalize(value).toLowerCase();
|
|
4678
|
+
if (!text) return 0;
|
|
4679
|
+
if (/博士|phd|doctor/.test(text)) return 7;
|
|
4680
|
+
if (/硕士|master/.test(text)) return 6;
|
|
4681
|
+
if (/本科|学士|bachelor/.test(text)) return 5;
|
|
4682
|
+
if (/大专|专科/.test(text)) return 4;
|
|
4683
|
+
if (/高中/.test(text)) return 3;
|
|
4684
|
+
if (/中专|中技/.test(text)) return 2;
|
|
4685
|
+
if (/初中|小学/.test(text)) return 1;
|
|
4686
|
+
return 0;
|
|
4687
|
+
};
|
|
4478
4688
|
const recommendInner = Array.from(doc.querySelectorAll(".card-inner[data-geekid]"))
|
|
4479
4689
|
.find((item) => (item.getAttribute("data-geekid") || "") === String(candidateKey)) || null;
|
|
4480
4690
|
const latestInner = recommendInner
|
|
@@ -4500,13 +4710,84 @@ class RecommendScreenCli {
|
|
|
4500
4710
|
const workSpans = latestWork
|
|
4501
4711
|
? Array.from(latestWork.querySelectorAll(".join-text-wrap.content span")).map((item) => textOf(item)).filter(Boolean)
|
|
4502
4712
|
: [];
|
|
4713
|
+
const workTimeSpans = latestWork
|
|
4714
|
+
? Array.from(latestWork.querySelectorAll(".join-text-wrap.time span")).map((item) => textOf(item)).filter(Boolean)
|
|
4715
|
+
: [];
|
|
4716
|
+
const eduItems = Array.from(card.querySelectorAll(".timeline-wrap.edu-exps .timeline-item"))
|
|
4717
|
+
.map((item) => {
|
|
4718
|
+
const timeSpans = Array.from(item.querySelectorAll(".join-text-wrap.time span")).map((node) => textOf(node)).filter(Boolean);
|
|
4719
|
+
const contentSpans = Array.from(item.querySelectorAll(".join-text-wrap.content span")).map((node) => textOf(node)).filter(Boolean);
|
|
4720
|
+
return {
|
|
4721
|
+
school: contentSpans[0] || "",
|
|
4722
|
+
major: contentSpans[1] || "",
|
|
4723
|
+
degree: contentSpans[2] || "",
|
|
4724
|
+
start: timeSpans[0] || "",
|
|
4725
|
+
end: timeSpans[1] || ""
|
|
4726
|
+
};
|
|
4727
|
+
})
|
|
4728
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
4729
|
+
.slice(0, 2);
|
|
4730
|
+
if (eduItems.length === 0 && (eduSpans[0] || eduSpans[1] || eduSpans[2])) {
|
|
4731
|
+
eduItems.push({
|
|
4732
|
+
school: eduSpans[0] || "",
|
|
4733
|
+
major: eduSpans[1] || "",
|
|
4734
|
+
degree: eduSpans[2] || "",
|
|
4735
|
+
start: "",
|
|
4736
|
+
end: ""
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
const baseInfoTokens = Array.from(card.querySelectorAll(".join-text-wrap.base-info span, .base-info span"))
|
|
4740
|
+
.map((item) => textOf(item))
|
|
4741
|
+
.filter(Boolean);
|
|
4742
|
+
let age = "";
|
|
4743
|
+
let workYears = "";
|
|
4744
|
+
let highestDegree = "";
|
|
4745
|
+
for (const token of baseInfoTokens) {
|
|
4746
|
+
if (!age && /\d+\s*岁/u.test(token)) {
|
|
4747
|
+
age = token;
|
|
4748
|
+
continue;
|
|
4749
|
+
}
|
|
4750
|
+
if (!workYears && /(\d+\s*年|应届|在校)/u.test(token) && !/\d+\s*岁/u.test(token)) {
|
|
4751
|
+
workYears = token;
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
if (!highestDegree && /(博士|硕士|本科|大专|专科|高中|中专|中技|初中)/u.test(token)) {
|
|
4755
|
+
highestDegree = token;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
const genderUse = card.querySelector("svg.gender use, .gender use, svg[class*='gender'] use");
|
|
4759
|
+
const genderHref = String(
|
|
4760
|
+
(genderUse && (genderUse.getAttribute("xlink:href") || genderUse.getAttribute("href") || ""))
|
|
4761
|
+
|| ""
|
|
4762
|
+
).toLowerCase();
|
|
4763
|
+
let gender = "";
|
|
4764
|
+
if (/(man|male|boy|icon-man|男)/.test(genderHref)) {
|
|
4765
|
+
gender = "男";
|
|
4766
|
+
} else if (/(woman|female|girl|icon-woman|女)/.test(genderHref)) {
|
|
4767
|
+
gender = "女";
|
|
4768
|
+
}
|
|
4769
|
+
if (!highestDegree) {
|
|
4770
|
+
const degreeFromEdu = eduItems
|
|
4771
|
+
.slice()
|
|
4772
|
+
.sort((a, b) => rankDegree(b.degree) - rankDegree(a.degree))[0];
|
|
4773
|
+
if (degreeFromEdu?.degree) {
|
|
4774
|
+
highestDegree = degreeFromEdu.degree;
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4503
4777
|
return {
|
|
4504
4778
|
ok: true,
|
|
4505
4779
|
name: pick(card, [".geek-name-wrap .name", ".name-wrap .name", "span.name", ".name"]),
|
|
4506
4780
|
school: eduSpans[0] || pick(card, [".edu-wrap .school-name", ".base-info .school-name", ".school-name"]),
|
|
4507
4781
|
major: eduSpans[1] || pick(card, [".edu-wrap .major", ".major"]),
|
|
4508
4782
|
company: workSpans[0] || pick(card, [".company-name-wrap .name", ".company-name"]),
|
|
4509
|
-
position: workSpans[1] || pick(card, [".position span", ".position"])
|
|
4783
|
+
position: workSpans[1] || pick(card, [".position span", ".position"]),
|
|
4784
|
+
age,
|
|
4785
|
+
gender,
|
|
4786
|
+
highest_degree: highestDegree,
|
|
4787
|
+
work_years: workYears,
|
|
4788
|
+
latest_work_start: workTimeSpans[0] || "",
|
|
4789
|
+
latest_work_end: workTimeSpans[1] || "",
|
|
4790
|
+
education_list: eduItems
|
|
4510
4791
|
};
|
|
4511
4792
|
})(${JSON.stringify(candidateKey)})`);
|
|
4512
4793
|
} catch {
|
|
@@ -4520,7 +4801,25 @@ class RecommendScreenCli {
|
|
|
4520
4801
|
school: normalizeText(profile?.school || ""),
|
|
4521
4802
|
major: normalizeText(profile?.major || ""),
|
|
4522
4803
|
company: normalizeText(profile?.company || ""),
|
|
4523
|
-
position: normalizeText(profile?.position || "")
|
|
4804
|
+
position: normalizeText(profile?.position || ""),
|
|
4805
|
+
age: normalizeText(profile?.age || ""),
|
|
4806
|
+
gender: normalizeText(profile?.gender || ""),
|
|
4807
|
+
highestDegree: normalizeText(profile?.highest_degree || ""),
|
|
4808
|
+
workYears: normalizeText(profile?.work_years || ""),
|
|
4809
|
+
latestWorkStart: normalizeText(profile?.latest_work_start || ""),
|
|
4810
|
+
latestWorkEnd: normalizeText(profile?.latest_work_end || ""),
|
|
4811
|
+
educationList: Array.isArray(profile?.education_list)
|
|
4812
|
+
? profile.education_list
|
|
4813
|
+
.map((item) => ({
|
|
4814
|
+
school: normalizeText(item?.school || ""),
|
|
4815
|
+
major: normalizeText(item?.major || ""),
|
|
4816
|
+
degree: normalizeText(item?.degree || ""),
|
|
4817
|
+
start: normalizeText(item?.start || ""),
|
|
4818
|
+
end: normalizeText(item?.end || "")
|
|
4819
|
+
}))
|
|
4820
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
4821
|
+
.slice(0, 2)
|
|
4822
|
+
: []
|
|
4524
4823
|
};
|
|
4525
4824
|
}
|
|
4526
4825
|
|
|
@@ -4653,22 +4952,32 @@ class RecommendScreenCli {
|
|
|
4653
4952
|
applyVisionEvidenceGate(result) {
|
|
4654
4953
|
const parsed = result && typeof result === "object" ? result : {};
|
|
4655
4954
|
const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4955
|
+
const evidenceGateEligible = parsed?.evidenceGateEligible === true
|
|
4956
|
+
|| Array.isArray(parsed?.evidence)
|
|
4957
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
4958
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
4959
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
|
|
4960
|
+
const evidenceRawCount = evidenceGateEligible
|
|
4961
|
+
? (Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
4962
|
+
? Number(parsed.evidenceRawCount)
|
|
4963
|
+
: parsedEvidence.length)
|
|
4964
|
+
: null;
|
|
4965
|
+
const evidenceMatchedCount = evidenceGateEligible
|
|
4966
|
+
? (Number.isFinite(Number(parsed?.evidenceMatchedCount))
|
|
4967
|
+
? Number(parsed.evidenceMatchedCount)
|
|
4968
|
+
: parsedEvidence.length)
|
|
4969
|
+
: null;
|
|
4970
|
+
const evidenceGateDemoted = parsed?.evidenceGateDemoted === true
|
|
4971
|
+
|| (evidenceGateEligible && rawPassed && evidenceMatchedCount <= 0);
|
|
4972
|
+
const cot = normalizeText(parsed?.cot || parsed?.reason || "");
|
|
4973
|
+
const summary = normalizeText(parsed?.summary || cot);
|
|
4666
4974
|
const finalReason = evidenceGateDemoted
|
|
4667
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${
|
|
4668
|
-
: (
|
|
4975
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${cot ? ` 原始判断依据(CoT): ${cot}` : ""}`
|
|
4976
|
+
: (cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。"));
|
|
4669
4977
|
return {
|
|
4670
4978
|
passed: evidenceGateDemoted ? false : rawPassed,
|
|
4671
4979
|
rawPassed,
|
|
4980
|
+
cot: finalReason,
|
|
4672
4981
|
reason: finalReason,
|
|
4673
4982
|
summary: summary || finalReason,
|
|
4674
4983
|
evidence: parsedEvidence,
|
|
@@ -4879,11 +5188,10 @@ class RecommendScreenCli {
|
|
|
4879
5188
|
"workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
4880
5189
|
"活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n\n" +
|
|
4881
5190
|
"要求:\n" +
|
|
4882
|
-
"1)
|
|
4883
|
-
"2)
|
|
4884
|
-
"3) evidence 至少给出 2 条可在简历中定位的原文短句。\n\n" +
|
|
5191
|
+
"1) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5192
|
+
"2) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
4885
5193
|
"请返回严格 JSON: " +
|
|
4886
|
-
"{\"passed\": true/false
|
|
5194
|
+
"{\"passed\": true/false}"
|
|
4887
5195
|
}
|
|
4888
5196
|
];
|
|
4889
5197
|
for (let index = 0; index < imagePaths.length; index += 1) {
|
|
@@ -4943,28 +5251,47 @@ class RecommendScreenCli {
|
|
|
4943
5251
|
throw this.buildError("VISION_MODEL_FAILED", `Vision model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
4944
5252
|
}
|
|
4945
5253
|
const json = await response.json();
|
|
4946
|
-
const
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
const
|
|
4950
|
-
const
|
|
4951
|
-
const
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
5254
|
+
const choice = json?.choices?.[0] || {};
|
|
5255
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
5256
|
+
const parsed = tryExtractJsonObject(content);
|
|
5257
|
+
const parsedPassed = parsePassedDecision(parsed?.passed);
|
|
5258
|
+
const fallbackPassed = parsePassedDecisionFromContent(content);
|
|
5259
|
+
const rawPassed = parsedPassed !== null ? parsedPassed : fallbackPassed;
|
|
5260
|
+
if (rawPassed === null) {
|
|
5261
|
+
throw this.buildError(
|
|
5262
|
+
"VISION_MODEL_FAILED",
|
|
5263
|
+
`Vision model response missing boolean passed decision. content=${truncateText(content, 180)}`
|
|
5264
|
+
);
|
|
5265
|
+
}
|
|
5266
|
+
const cot = normalizeText(extractCotFromChoice(choice, parsed));
|
|
5267
|
+
const reason = cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5268
|
+
const summary = reason;
|
|
5269
|
+
const evidenceGateEligible = Array.isArray(parsed?.evidence)
|
|
5270
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
5271
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
5272
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
|
|
5273
|
+
const evidenceRawCount = evidenceGateEligible
|
|
5274
|
+
? (Number.isFinite(Number(parsed?.evidenceRawCount)) ? Number(parsed.evidenceRawCount) : parsedEvidence.length)
|
|
5275
|
+
: null;
|
|
5276
|
+
const evidenceMatchedCount = evidenceGateEligible
|
|
5277
|
+
? (Number.isFinite(Number(parsed?.evidenceMatchedCount)) ? Number(parsed.evidenceMatchedCount) : parsedEvidence.length)
|
|
5278
|
+
: null;
|
|
5279
|
+
const evidenceGateDemoted = evidenceGateEligible && rawPassed && (evidenceMatchedCount ?? 0) <= 0;
|
|
4955
5280
|
const finalReason = evidenceGateDemoted
|
|
4956
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? `
|
|
4957
|
-
:
|
|
5281
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? ` 原始判断依据(CoT): ${reason}` : ""}`
|
|
5282
|
+
: reason;
|
|
4958
5283
|
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
4959
|
-
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason,
|
|
5284
|
+
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, parsedEvidence, passed);
|
|
4960
5285
|
return {
|
|
4961
5286
|
passed,
|
|
4962
5287
|
rawPassed,
|
|
5288
|
+
cot: reason,
|
|
4963
5289
|
reason: enrichedReason,
|
|
4964
5290
|
summary: summary || enrichedReason,
|
|
4965
|
-
evidence,
|
|
4966
|
-
evidenceRawCount
|
|
4967
|
-
evidenceMatchedCount
|
|
5291
|
+
evidence: parsedEvidence,
|
|
5292
|
+
evidenceRawCount,
|
|
5293
|
+
evidenceMatchedCount,
|
|
5294
|
+
evidenceGateEligible,
|
|
4968
5295
|
evidenceGateDemoted
|
|
4969
5296
|
};
|
|
4970
5297
|
}
|
|
@@ -5064,17 +5391,17 @@ class RecommendScreenCli {
|
|
|
5064
5391
|
"要求:\n" +
|
|
5065
5392
|
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
5066
5393
|
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
5067
|
-
"3)
|
|
5068
|
-
"4)
|
|
5394
|
+
"3) 若文本中包含“人选卡片兜底信息(仅在简历缺失时使用)”段落,只能在主简历缺失对应字段时引用该段,不可覆盖主简历已明确字段。\n" +
|
|
5395
|
+
"4) 若证据不足,必须返回 passed=false。\n\n" +
|
|
5396
|
+
"5) 当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中存在教育时间、毕业时间或可推断年份信息,必须先推断再判断;" +
|
|
5069
5397
|
"只有完全不存在时间线信息时才可写“无法判断”。\n" +
|
|
5070
|
-
"
|
|
5071
|
-
"
|
|
5072
|
-
"
|
|
5073
|
-
"
|
|
5074
|
-
"
|
|
5075
|
-
"10) evidence 至少给出 2 条可在简历原文定位的证据短句。\n\n" +
|
|
5398
|
+
"6) 当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
5399
|
+
"7) workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
5400
|
+
"8) 活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n" +
|
|
5401
|
+
"9) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5402
|
+
"10) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
5076
5403
|
"请返回严格 JSON: " +
|
|
5077
|
-
"{\"passed\": true/false
|
|
5404
|
+
"{\"passed\": true/false}"
|
|
5078
5405
|
}
|
|
5079
5406
|
]
|
|
5080
5407
|
};
|
|
@@ -5100,32 +5427,43 @@ class RecommendScreenCli {
|
|
|
5100
5427
|
throw this.buildError("TEXT_MODEL_FAILED", `Text model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
5101
5428
|
}
|
|
5102
5429
|
const json = await response.json();
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
const
|
|
5107
|
-
const reason = normalizeText(parsed.reason);
|
|
5108
|
-
const summary = normalizeText(parsed.summary || reason);
|
|
5430
|
+
const choice = json?.choices?.[0] || {};
|
|
5431
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
5432
|
+
const parsed = tryExtractJsonObject(content);
|
|
5433
|
+
const cot = normalizeText(extractCotFromChoice(choice, parsed));
|
|
5109
5434
|
const normalizedResume = normalizeText(safeResumeText);
|
|
5110
5435
|
const normalizedResumeLower = toLowerSafe(normalizedResume);
|
|
5111
|
-
const
|
|
5436
|
+
const evidenceGateEligible = Array.isArray(parsed?.evidence)
|
|
5437
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
5438
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
5439
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed.evidence) : [];
|
|
5112
5440
|
const evidence = [];
|
|
5113
5441
|
const unmatchedEvidence = [];
|
|
5114
|
-
|
|
5115
|
-
const
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5442
|
+
if (evidenceGateEligible) {
|
|
5443
|
+
for (const item of parsedEvidence) {
|
|
5444
|
+
const matched = matchEvidenceAgainstResume(item, safeResumeText, normalizedResume, normalizedResumeLower);
|
|
5445
|
+
if (matched.matched) {
|
|
5446
|
+
evidence.push(item);
|
|
5447
|
+
} else {
|
|
5448
|
+
unmatchedEvidence.push(item);
|
|
5449
|
+
}
|
|
5120
5450
|
}
|
|
5121
5451
|
}
|
|
5122
|
-
const
|
|
5452
|
+
const parsedPassed = parsePassedDecision(parsed?.passed);
|
|
5453
|
+
const fallbackPassed = parsePassedDecisionFromContent(content);
|
|
5454
|
+
const rawPassed = parsedPassed !== null ? parsedPassed : fallbackPassed;
|
|
5455
|
+
if (rawPassed === null) {
|
|
5456
|
+
throw this.buildError(
|
|
5457
|
+
"TEXT_MODEL_FAILED",
|
|
5458
|
+
`Text model response missing boolean passed decision. content=${truncateText(content, 180)}`
|
|
5459
|
+
);
|
|
5460
|
+
}
|
|
5123
5461
|
let passed = rawPassed;
|
|
5124
|
-
let finalReason =
|
|
5125
|
-
const evidenceGateDemoted = rawPassed && evidence.length <= 0;
|
|
5462
|
+
let finalReason = cot || (passed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5463
|
+
const evidenceGateDemoted = evidenceGateEligible && rawPassed && evidence.length <= 0;
|
|
5126
5464
|
if (evidenceGateDemoted) {
|
|
5127
5465
|
passed = false;
|
|
5128
|
-
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${
|
|
5466
|
+
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${finalReason ? ` 原始判断依据(CoT): ${finalReason}` : ""}`;
|
|
5129
5467
|
if (unmatchedEvidence.length > 0) {
|
|
5130
5468
|
log(
|
|
5131
5469
|
`[EVIDENCE_GATE] passed=true 但证据未命中简历原文,已降级为不通过;` +
|
|
@@ -5133,15 +5471,17 @@ class RecommendScreenCli {
|
|
|
5133
5471
|
);
|
|
5134
5472
|
}
|
|
5135
5473
|
}
|
|
5474
|
+
const summary = finalReason;
|
|
5136
5475
|
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
5137
5476
|
return {
|
|
5138
5477
|
passed,
|
|
5139
5478
|
rawPassed,
|
|
5479
|
+
cot: finalReason,
|
|
5140
5480
|
reason: enrichedReason,
|
|
5141
5481
|
summary: summary || enrichedReason,
|
|
5142
5482
|
evidence,
|
|
5143
|
-
evidenceRawCount: parsedEvidence.length,
|
|
5144
|
-
evidenceMatchedCount: evidence.length,
|
|
5483
|
+
evidenceRawCount: evidenceGateEligible ? parsedEvidence.length : null,
|
|
5484
|
+
evidenceMatchedCount: evidenceGateEligible ? evidence.length : null,
|
|
5145
5485
|
evidenceGateDemoted,
|
|
5146
5486
|
chunkIndex,
|
|
5147
5487
|
chunkTotal
|
|
@@ -5722,6 +6062,7 @@ class RecommendScreenCli {
|
|
|
5722
6062
|
let domCandidateInfo = null;
|
|
5723
6063
|
|
|
5724
6064
|
if (networkCandidateInfo?.resumeText) {
|
|
6065
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(networkCandidateInfo, cardProfile || null);
|
|
5725
6066
|
screening = await timeCandidateStage(
|
|
5726
6067
|
"text_model_ms",
|
|
5727
6068
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5761,7 +6102,10 @@ class RecommendScreenCli {
|
|
|
5761
6102
|
})
|
|
5762
6103
|
);
|
|
5763
6104
|
if (lateNetworkCandidateInfo?.resumeText) {
|
|
5764
|
-
networkCandidateInfo =
|
|
6105
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6106
|
+
lateNetworkCandidateInfo,
|
|
6107
|
+
cardProfile || null
|
|
6108
|
+
);
|
|
5765
6109
|
screening = await timeCandidateStage(
|
|
5766
6110
|
"text_model_ms",
|
|
5767
6111
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5786,7 +6130,10 @@ class RecommendScreenCli {
|
|
|
5786
6130
|
() => this.resolveDomResumeFallback(nextCandidate, cardProfile || null)
|
|
5787
6131
|
);
|
|
5788
6132
|
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
5789
|
-
networkCandidateInfo =
|
|
6133
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6134
|
+
domFallback.networkCandidateInfo,
|
|
6135
|
+
cardProfile || null
|
|
6136
|
+
);
|
|
5790
6137
|
screening = await timeCandidateStage(
|
|
5791
6138
|
"text_model_ms",
|
|
5792
6139
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5806,7 +6153,10 @@ class RecommendScreenCli {
|
|
|
5806
6153
|
}
|
|
5807
6154
|
);
|
|
5808
6155
|
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5809
|
-
domCandidateInfo =
|
|
6156
|
+
domCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6157
|
+
domFallback.domCandidateInfo,
|
|
6158
|
+
cardProfile || null
|
|
6159
|
+
);
|
|
5810
6160
|
screening = await timeCandidateStage(
|
|
5811
6161
|
"text_model_ms",
|
|
5812
6162
|
() => this.callTextModel(domCandidateInfo.resumeText)
|
|
@@ -6169,6 +6519,8 @@ if (require.main === module) {
|
|
|
6169
6519
|
isRecoverablePostActionError,
|
|
6170
6520
|
classifyFinishedWrapState,
|
|
6171
6521
|
formatResumeApiData,
|
|
6522
|
+
buildCardProfileFallbackText,
|
|
6523
|
+
enrichCandidateInfoWithCardProfile,
|
|
6172
6524
|
extractEvidenceTokens,
|
|
6173
6525
|
matchEvidenceAgainstResume
|
|
6174
6526
|
}
|
|
@@ -1013,6 +1013,46 @@ function testFormatResumeApiDataShouldIncludeStructuredJudgementHints() {
|
|
|
1013
1013
|
assert.equal(formatted.includes("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。"), true);
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
+
function testEnrichCandidateInfoWithCardProfileShouldAppendCardFallbackWhenDomInfoMissing() {
|
|
1017
|
+
const candidateInfo = {
|
|
1018
|
+
name: "",
|
|
1019
|
+
school: "",
|
|
1020
|
+
major: "",
|
|
1021
|
+
company: "",
|
|
1022
|
+
position: "",
|
|
1023
|
+
resumeText: "=== 基本信息 ===\n姓名: 赵梓轩\n"
|
|
1024
|
+
};
|
|
1025
|
+
const cardProfile = {
|
|
1026
|
+
name: "赵梓轩",
|
|
1027
|
+
age: "29岁",
|
|
1028
|
+
gender: "男",
|
|
1029
|
+
highestDegree: "硕士",
|
|
1030
|
+
workYears: "2年",
|
|
1031
|
+
company: "中科院",
|
|
1032
|
+
position: "科研助理",
|
|
1033
|
+
latestWorkStart: "2024.10",
|
|
1034
|
+
latestWorkEnd: "至今",
|
|
1035
|
+
educationList: [
|
|
1036
|
+
{ school: "科克大学", major: "理学", degree: "硕士", start: "2020", end: "2023" },
|
|
1037
|
+
{ school: "东北大学", major: "数学与应用数学", degree: "本科", start: "2014", end: "2018" }
|
|
1038
|
+
]
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const enriched = __testables.enrichCandidateInfoWithCardProfile(candidateInfo, cardProfile);
|
|
1042
|
+
assert.equal(enriched.name, "赵梓轩");
|
|
1043
|
+
assert.equal(enriched.company, "中科院");
|
|
1044
|
+
assert.equal(enriched.position, "科研助理");
|
|
1045
|
+
assert.equal(enriched.resumeText.includes("=== 人选卡片兜底信息(仅在简历缺失时使用) ==="), true);
|
|
1046
|
+
assert.equal(enriched.resumeText.includes("年龄: 29岁"), true);
|
|
1047
|
+
assert.equal(enriched.resumeText.includes("性别: 男"), true);
|
|
1048
|
+
assert.equal(enriched.resumeText.includes("最近一份工作在职日期: 2024.10 ~ 至今"), true);
|
|
1049
|
+
assert.equal(enriched.resumeText.includes("1. 学校=科克大学;专业=理学;学历=硕士;时间=2020 ~ 2023"), true);
|
|
1050
|
+
assert.equal(enriched.resumeText.includes("2. 学校=东北大学;专业=数学与应用数学;学历=本科;时间=2014 ~ 2018"), true);
|
|
1051
|
+
|
|
1052
|
+
const enrichedAgain = __testables.enrichCandidateInfoWithCardProfile(enriched, cardProfile);
|
|
1053
|
+
assert.equal((enrichedAgain.resumeText.match(/人选卡片兜底信息(仅在简历缺失时使用)/g) || []).length, 1);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1016
1056
|
function testEvidenceTokenMatcherShouldSupportParaphrasedEvidence() {
|
|
1017
1057
|
const resume = [
|
|
1018
1058
|
"南京大学 专业: 数学",
|
|
@@ -1757,6 +1797,7 @@ async function main() {
|
|
|
1757
1797
|
testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
|
|
1758
1798
|
testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription();
|
|
1759
1799
|
testFormatResumeApiDataShouldIncludeStructuredJudgementHints();
|
|
1800
|
+
testEnrichCandidateInfoWithCardProfileShouldAppendCardFallbackWhenDomInfoMissing();
|
|
1760
1801
|
testEvidenceTokenMatcherShouldSupportParaphrasedEvidence();
|
|
1761
1802
|
testCheckpointPayloadShouldIncludeCandidateAudits();
|
|
1762
1803
|
testCheckpointShouldPersistAndRestoreInputSummary();
|