@reconcrap/boss-recommend-mcp 1.3.17 → 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;
|
|
@@ -652,6 +747,101 @@ function formatEducationSchoolTags(edu) {
|
|
|
652
747
|
return tags.join("、");
|
|
653
748
|
}
|
|
654
749
|
|
|
750
|
+
function inferDegreeRank(degreeText) {
|
|
751
|
+
const normalized = normalizeText(degreeText).toLowerCase();
|
|
752
|
+
if (!normalized) return 0;
|
|
753
|
+
if (/博士|phd|doctor/.test(normalized)) return 7;
|
|
754
|
+
if (/硕士|master/.test(normalized)) return 6;
|
|
755
|
+
if (/本科|学士|bachelor/.test(normalized)) return 5;
|
|
756
|
+
if (/大专|专科|junior/.test(normalized)) return 4;
|
|
757
|
+
if (/高中/.test(normalized)) return 3;
|
|
758
|
+
if (/中专|中技/.test(normalized)) return 2;
|
|
759
|
+
if (/初中|小学|及以下/.test(normalized)) return 1;
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function normalizeResumeDateToken(value) {
|
|
764
|
+
const raw = normalizeText(value);
|
|
765
|
+
if (!raw) return "";
|
|
766
|
+
const digits = raw.replace(/[^\d]/g, "");
|
|
767
|
+
if (/^\d{8}$/.test(digits)) {
|
|
768
|
+
return `${digits.slice(0, 4)}.${digits.slice(4, 6)}`;
|
|
769
|
+
}
|
|
770
|
+
if (/^\d{6}$/.test(digits)) {
|
|
771
|
+
return `${digits.slice(0, 4)}.${digits.slice(4, 6)}`;
|
|
772
|
+
}
|
|
773
|
+
if (/^\d{4}$/.test(digits)) {
|
|
774
|
+
return digits;
|
|
775
|
+
}
|
|
776
|
+
return raw;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function formatResumeTimeRange(startRaw, endRaw, fallbackEnd = "") {
|
|
780
|
+
const start = normalizeResumeDateToken(startRaw);
|
|
781
|
+
const end = normalizeResumeDateToken(endRaw) || normalizeText(fallbackEnd);
|
|
782
|
+
if (start && end) return `${start} ~ ${end}`;
|
|
783
|
+
if (start) return `${start} ~`;
|
|
784
|
+
if (end) return `~ ${end}`;
|
|
785
|
+
return "";
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function formatResumeTimeRangeFromFields(source, startFields = [], endFields = [], fallbackEnd = "") {
|
|
789
|
+
const startRaw = startFields
|
|
790
|
+
.map((field) => source?.[field])
|
|
791
|
+
.find((value) => normalizeText(value));
|
|
792
|
+
const endRaw = endFields
|
|
793
|
+
.map((field) => source?.[field])
|
|
794
|
+
.find((value) => normalizeText(value));
|
|
795
|
+
return formatResumeTimeRange(startRaw, endRaw, fallbackEnd);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function formatNamedListText(items = []) {
|
|
799
|
+
if (!Array.isArray(items) || items.length <= 0) return "";
|
|
800
|
+
return items
|
|
801
|
+
.map((item) => normalizeText(item?.name || item?.subjectName || item?.title || item))
|
|
802
|
+
.filter(Boolean)
|
|
803
|
+
.join("、");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function extractYearFromResumeDate(value) {
|
|
807
|
+
const token = normalizeResumeDateToken(value);
|
|
808
|
+
if (!token) return "";
|
|
809
|
+
const match = token.match(/(19|20)\d{2}/);
|
|
810
|
+
return match ? match[0] : "";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function deriveHighestEducation(eduExpList = []) {
|
|
814
|
+
const list = Array.isArray(eduExpList) ? eduExpList : [];
|
|
815
|
+
let selected = null;
|
|
816
|
+
for (const edu of list) {
|
|
817
|
+
const degree = formatEducationDegree(edu);
|
|
818
|
+
const rank = inferDegreeRank(degree);
|
|
819
|
+
const endYear = extractYearFromResumeDate(
|
|
820
|
+
edu?.endYearMonStr || edu?.endYearStr || edu?.endDateDesc || edu?.endDate || ""
|
|
821
|
+
);
|
|
822
|
+
const candidate = {
|
|
823
|
+
school: normalizeText(edu?.school || edu?.schoolName || ""),
|
|
824
|
+
degree,
|
|
825
|
+
rank,
|
|
826
|
+
endYear
|
|
827
|
+
};
|
|
828
|
+
if (!selected) {
|
|
829
|
+
selected = candidate;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const selectedYear = Number(selected.endYear || 0);
|
|
833
|
+
const candidateYear = Number(candidate.endYear || 0);
|
|
834
|
+
if (candidate.rank > selected.rank) {
|
|
835
|
+
selected = candidate;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (candidate.rank === selected.rank && candidateYear > selectedYear) {
|
|
839
|
+
selected = candidate;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return selected || { school: "", degree: "", rank: 0, endYear: "" };
|
|
843
|
+
}
|
|
844
|
+
|
|
655
845
|
function splitTextByChunks(text, chunkSize, overlap, maxChunks) {
|
|
656
846
|
const source = String(text || "");
|
|
657
847
|
if (!source) return [];
|
|
@@ -714,6 +904,101 @@ function parseBoolean(value) {
|
|
|
714
904
|
return null;
|
|
715
905
|
}
|
|
716
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
|
+
|
|
717
1002
|
function normalizeCliOptionToken(rawToken) {
|
|
718
1003
|
const token = String(rawToken || "").trim();
|
|
719
1004
|
if (!token) {
|
|
@@ -1536,12 +1821,21 @@ function formatResumeApiData(data) {
|
|
|
1536
1821
|
const parts = [];
|
|
1537
1822
|
const geekDetail = data?.geekDetail || data?.geekDetailInfo || data || {};
|
|
1538
1823
|
const baseInfo = geekDetail.geekBaseInfo || {};
|
|
1539
|
-
const expectList = geekDetail.geekExpectList
|
|
1824
|
+
const expectList = Array.isArray(geekDetail.geekExpectList) && geekDetail.geekExpectList.length > 0
|
|
1825
|
+
? geekDetail.geekExpectList
|
|
1826
|
+
: Array.isArray(geekDetail.geekExpPosList) && geekDetail.geekExpPosList.length > 0
|
|
1827
|
+
? geekDetail.geekExpPosList
|
|
1828
|
+
: geekDetail.showExpectPosition
|
|
1829
|
+
? [geekDetail.showExpectPosition]
|
|
1830
|
+
: [];
|
|
1540
1831
|
const workExpList = geekDetail.geekWorkExpList || [];
|
|
1541
1832
|
const projExpList = geekDetail.geekProjExpList || [];
|
|
1542
1833
|
const eduExpList = geekDetail.geekEduExpList || geekDetail.geekEducationList || [];
|
|
1543
1834
|
const advantage = geekDetail.geekAdvantage || baseInfo.userDesc || baseInfo.userDescription || "";
|
|
1544
1835
|
const skillList = geekDetail.geekSkillList || geekDetail.skillList || [];
|
|
1836
|
+
const certificationList = geekDetail.geekCertificationList || [];
|
|
1837
|
+
const workExpCheckRes = Array.isArray(geekDetail.workExpCheckRes) ? geekDetail.workExpCheckRes : [];
|
|
1838
|
+
const highestEdu = deriveHighestEducation(eduExpList);
|
|
1545
1839
|
|
|
1546
1840
|
parts.push("=== 基本信息 ===");
|
|
1547
1841
|
if (baseInfo.name) parts.push(`姓名: ${baseInfo.name}`);
|
|
@@ -1549,6 +1843,9 @@ function formatResumeApiData(data) {
|
|
|
1549
1843
|
if (baseInfo.gender !== undefined) parts.push(`性别: ${baseInfo.gender === 1 ? "男" : "女"}`);
|
|
1550
1844
|
if (baseInfo.degreeCategory) parts.push(`学历: ${baseInfo.degreeCategory}`);
|
|
1551
1845
|
if (baseInfo.workYearDesc) parts.push(`工作经验: ${baseInfo.workYearDesc}`);
|
|
1846
|
+
if (typeof baseInfo.freshGraduate === "number") parts.push(`应届状态: ${baseInfo.freshGraduate === 1 ? "应届生" : "非应届生"}`);
|
|
1847
|
+
const workDate = normalizeResumeDateToken(baseInfo.workDate8);
|
|
1848
|
+
if (workDate) parts.push(`参加工作时间: ${workDate}`);
|
|
1552
1849
|
if (baseInfo.activeTimeDesc) parts.push(`活跃状态: ${baseInfo.activeTimeDesc}`);
|
|
1553
1850
|
if (baseInfo.applyStatusContent) parts.push(`求职状态: ${baseInfo.applyStatusContent}`);
|
|
1554
1851
|
|
|
@@ -1573,13 +1870,27 @@ function formatResumeApiData(data) {
|
|
|
1573
1870
|
const company = exp.company || "";
|
|
1574
1871
|
const position = stripHtml(exp.positionName || "");
|
|
1575
1872
|
parts.push(`${index + 1}. ${company} - ${position}`);
|
|
1576
|
-
|
|
1577
|
-
|
|
1873
|
+
const workTime = formatResumeTimeRangeFromFields(
|
|
1874
|
+
exp,
|
|
1875
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1876
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"],
|
|
1877
|
+
"至今"
|
|
1878
|
+
);
|
|
1879
|
+
if (workTime) {
|
|
1880
|
+
parts.push(` 时间: ${workTime}`);
|
|
1578
1881
|
}
|
|
1579
1882
|
const workContent = exp.responsibility || exp.workContent || "";
|
|
1580
1883
|
if (workContent) {
|
|
1581
1884
|
parts.push(` 职责: ${stripHtml(workContent)}`);
|
|
1582
1885
|
}
|
|
1886
|
+
const workPerformance = exp.workPerformance || exp.performance || "";
|
|
1887
|
+
if (workPerformance) {
|
|
1888
|
+
parts.push(` 成果: ${stripHtml(workPerformance)}`);
|
|
1889
|
+
}
|
|
1890
|
+
const workEmphasis = exp.workEmphasis || "";
|
|
1891
|
+
if (workEmphasis) {
|
|
1892
|
+
parts.push(` 补充: ${stripHtml(workEmphasis)}`);
|
|
1893
|
+
}
|
|
1583
1894
|
});
|
|
1584
1895
|
}
|
|
1585
1896
|
|
|
@@ -1588,8 +1899,14 @@ function formatResumeApiData(data) {
|
|
|
1588
1899
|
projExpList.forEach((proj, index) => {
|
|
1589
1900
|
parts.push(`${index + 1}. ${proj.name || proj.projectName || "未知项目"}`);
|
|
1590
1901
|
if (proj.roleName) parts.push(` 角色: ${proj.roleName}`);
|
|
1591
|
-
|
|
1592
|
-
|
|
1902
|
+
const projTime = formatResumeTimeRangeFromFields(
|
|
1903
|
+
proj,
|
|
1904
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1905
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"],
|
|
1906
|
+
"至今"
|
|
1907
|
+
);
|
|
1908
|
+
if (projTime) {
|
|
1909
|
+
parts.push(` 时间: ${projTime}`);
|
|
1593
1910
|
}
|
|
1594
1911
|
const projectDescription = proj.description || proj.projectDescription || "";
|
|
1595
1912
|
if (projectDescription) parts.push(` 描述: ${stripHtml(projectDescription)}`);
|
|
@@ -1605,15 +1922,35 @@ function formatResumeApiData(data) {
|
|
|
1605
1922
|
if (edu.major || edu.majorName) parts.push(` 专业: ${edu.major || edu.majorName}`);
|
|
1606
1923
|
const eduDegree = formatEducationDegree(edu);
|
|
1607
1924
|
if (eduDegree) parts.push(` 学历: ${eduDegree}`);
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1925
|
+
const eduTime = formatResumeTimeRangeFromFields(
|
|
1926
|
+
edu,
|
|
1927
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1928
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"]
|
|
1929
|
+
);
|
|
1930
|
+
if (eduTime) {
|
|
1931
|
+
parts.push(` 时间: ${eduTime}`);
|
|
1612
1932
|
}
|
|
1613
1933
|
const schoolTags = formatEducationSchoolTags(edu);
|
|
1614
1934
|
if (schoolTags) {
|
|
1615
1935
|
parts.push(` 学校标签: ${schoolTags}`);
|
|
1616
1936
|
}
|
|
1937
|
+
const eduDescription = stripHtml(edu.eduDescription || edu.description || "");
|
|
1938
|
+
if (eduDescription) {
|
|
1939
|
+
parts.push(` 描述: ${eduDescription}`);
|
|
1940
|
+
}
|
|
1941
|
+
const courseDesc = stripHtml(edu.courseDesc || "");
|
|
1942
|
+
if (courseDesc) {
|
|
1943
|
+
parts.push(` 课程/研究: ${courseDesc}`);
|
|
1944
|
+
}
|
|
1945
|
+
const keySubjects = formatNamedListText(edu.keySubjectList || []);
|
|
1946
|
+
if (keySubjects) {
|
|
1947
|
+
parts.push(` 核心课程: ${keySubjects}`);
|
|
1948
|
+
}
|
|
1949
|
+
const thesisTitle = normalizeText(edu.thesisTitle || "");
|
|
1950
|
+
const thesisDesc = stripHtml(edu.thesisDesc || "");
|
|
1951
|
+
if (thesisTitle || thesisDesc) {
|
|
1952
|
+
parts.push(` 论文: ${thesisTitle}${thesisDesc ? ` - ${thesisDesc}` : ""}`);
|
|
1953
|
+
}
|
|
1617
1954
|
});
|
|
1618
1955
|
}
|
|
1619
1956
|
|
|
@@ -1626,6 +1963,33 @@ function formatResumeApiData(data) {
|
|
|
1626
1963
|
});
|
|
1627
1964
|
}
|
|
1628
1965
|
|
|
1966
|
+
if (certificationList.length > 0) {
|
|
1967
|
+
parts.push("\n=== 资格证书 ===");
|
|
1968
|
+
certificationList.forEach((cert) => {
|
|
1969
|
+
const certName = normalizeText(cert?.certName || cert?.name || "");
|
|
1970
|
+
if (certName) parts.push(`- ${certName}`);
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
parts.push("\n=== 结构化判定线索 ===");
|
|
1975
|
+
if (highestEdu.degree) parts.push(`最高学历: ${highestEdu.degree}`);
|
|
1976
|
+
if (highestEdu.school) parts.push(`最高学历学校: ${highestEdu.school}`);
|
|
1977
|
+
if (highestEdu.endYear) parts.push(`最高学历毕业年份: ${highestEdu.endYear}`);
|
|
1978
|
+
parts.push(`是否有工作经历: ${workExpList.length > 0 ? "是" : "否"}`);
|
|
1979
|
+
parts.push(`是否有项目经历: ${projExpList.length > 0 ? "是" : "否"}`);
|
|
1980
|
+
parts.push("相关经验硬判口径: 仅工作经历/项目经历可作为“相关经验”硬性证据;教育/课程/技能仅作补充。");
|
|
1981
|
+
if (workExpCheckRes.length > 0) {
|
|
1982
|
+
const riskText = workExpCheckRes
|
|
1983
|
+
.map((item) => normalizeText(item?.desc || item?.firstTip || item?.chatDesc || ""))
|
|
1984
|
+
.filter(Boolean)
|
|
1985
|
+
.slice(0, 3)
|
|
1986
|
+
.join(";");
|
|
1987
|
+
if (riskText) {
|
|
1988
|
+
parts.push(`软风险提示(需追问,不直接淘汰): ${riskText}`);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
parts.push("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。");
|
|
1992
|
+
|
|
1629
1993
|
return parts.join("\n");
|
|
1630
1994
|
}
|
|
1631
1995
|
|
|
@@ -1634,11 +1998,19 @@ function extractJsonObject(text) {
|
|
|
1634
1998
|
const start = raw.indexOf("{");
|
|
1635
1999
|
const end = raw.lastIndexOf("}");
|
|
1636
2000
|
if (start === -1 || end === -1 || end <= start) {
|
|
1637
|
-
throw new Error("
|
|
2001
|
+
throw new Error("Model response did not contain JSON");
|
|
1638
2002
|
}
|
|
1639
2003
|
return JSON.parse(raw.slice(start, end + 1));
|
|
1640
2004
|
}
|
|
1641
2005
|
|
|
2006
|
+
function tryExtractJsonObject(text) {
|
|
2007
|
+
try {
|
|
2008
|
+
return extractJsonObject(text);
|
|
2009
|
+
} catch {
|
|
2010
|
+
return {};
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1642
2014
|
async function promptPostAction() {
|
|
1643
2015
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1644
2016
|
throw new Error("POST_ACTION_CONFIRMATION_REQUIRED");
|
|
@@ -4301,6 +4673,18 @@ class RecommendScreenCli {
|
|
|
4301
4673
|
}
|
|
4302
4674
|
return "";
|
|
4303
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
|
+
};
|
|
4304
4688
|
const recommendInner = Array.from(doc.querySelectorAll(".card-inner[data-geekid]"))
|
|
4305
4689
|
.find((item) => (item.getAttribute("data-geekid") || "") === String(candidateKey)) || null;
|
|
4306
4690
|
const latestInner = recommendInner
|
|
@@ -4326,13 +4710,84 @@ class RecommendScreenCli {
|
|
|
4326
4710
|
const workSpans = latestWork
|
|
4327
4711
|
? Array.from(latestWork.querySelectorAll(".join-text-wrap.content span")).map((item) => textOf(item)).filter(Boolean)
|
|
4328
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
|
+
}
|
|
4329
4777
|
return {
|
|
4330
4778
|
ok: true,
|
|
4331
4779
|
name: pick(card, [".geek-name-wrap .name", ".name-wrap .name", "span.name", ".name"]),
|
|
4332
4780
|
school: eduSpans[0] || pick(card, [".edu-wrap .school-name", ".base-info .school-name", ".school-name"]),
|
|
4333
4781
|
major: eduSpans[1] || pick(card, [".edu-wrap .major", ".major"]),
|
|
4334
4782
|
company: workSpans[0] || pick(card, [".company-name-wrap .name", ".company-name"]),
|
|
4335
|
-
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
|
|
4336
4791
|
};
|
|
4337
4792
|
})(${JSON.stringify(candidateKey)})`);
|
|
4338
4793
|
} catch {
|
|
@@ -4346,7 +4801,25 @@ class RecommendScreenCli {
|
|
|
4346
4801
|
school: normalizeText(profile?.school || ""),
|
|
4347
4802
|
major: normalizeText(profile?.major || ""),
|
|
4348
4803
|
company: normalizeText(profile?.company || ""),
|
|
4349
|
-
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
|
+
: []
|
|
4350
4823
|
};
|
|
4351
4824
|
}
|
|
4352
4825
|
|
|
@@ -4479,22 +4952,32 @@ class RecommendScreenCli {
|
|
|
4479
4952
|
applyVisionEvidenceGate(result) {
|
|
4480
4953
|
const parsed = result && typeof result === "object" ? result : {};
|
|
4481
4954
|
const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
|
|
4482
|
-
const
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
const
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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);
|
|
4492
4974
|
const finalReason = evidenceGateDemoted
|
|
4493
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${
|
|
4494
|
-
: (
|
|
4975
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${cot ? ` 原始判断依据(CoT): ${cot}` : ""}`
|
|
4976
|
+
: (cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。"));
|
|
4495
4977
|
return {
|
|
4496
4978
|
passed: evidenceGateDemoted ? false : rawPassed,
|
|
4497
4979
|
rawPassed,
|
|
4980
|
+
cot: finalReason,
|
|
4498
4981
|
reason: finalReason,
|
|
4499
4982
|
summary: summary || finalReason,
|
|
4500
4983
|
evidence: parsedEvidence,
|
|
@@ -4698,13 +5181,17 @@ class RecommendScreenCli {
|
|
|
4698
5181
|
`筛选标准:\n${this.args.criteria}\n\n` +
|
|
4699
5182
|
"你将收到候选人完整简历的一个或多个顺序分段图片。必须完整阅读全部分段后再判断," +
|
|
4700
5183
|
"不能只根据前几段下结论;后续分段中的教育、项目、经历或否定信息必须纳入最终判断。" +
|
|
4701
|
-
"严禁编造任何不存在的经历、项目、学校、公司或时间线;证据不足时必须判定为不通过。\n
|
|
5184
|
+
"严禁编造任何不存在的经历、项目、学校、公司或时间线;证据不足时必须判定为不通过。\n" +
|
|
5185
|
+
"当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中出现教育起止时间、毕业时间或可推断年份信息,必须先推断再判断," +
|
|
5186
|
+
"只有完全不存在可推断时间信息时才可以写“无法判断”。\n" +
|
|
5187
|
+
"当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
5188
|
+
"workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
5189
|
+
"活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n\n" +
|
|
4702
5190
|
"要求:\n" +
|
|
4703
|
-
"1)
|
|
4704
|
-
"2)
|
|
4705
|
-
"3) evidence 至少给出 2 条可在简历中定位的原文短句。\n\n" +
|
|
5191
|
+
"1) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5192
|
+
"2) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
4706
5193
|
"请返回严格 JSON: " +
|
|
4707
|
-
"{\"passed\": true/false
|
|
5194
|
+
"{\"passed\": true/false}"
|
|
4708
5195
|
}
|
|
4709
5196
|
];
|
|
4710
5197
|
for (let index = 0; index < imagePaths.length; index += 1) {
|
|
@@ -4764,28 +5251,47 @@ class RecommendScreenCli {
|
|
|
4764
5251
|
throw this.buildError("VISION_MODEL_FAILED", `Vision model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
4765
5252
|
}
|
|
4766
5253
|
const json = await response.json();
|
|
4767
|
-
const
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
const
|
|
4771
|
-
const
|
|
4772
|
-
const
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
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;
|
|
4776
5280
|
const finalReason = evidenceGateDemoted
|
|
4777
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? `
|
|
4778
|
-
:
|
|
5281
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? ` 原始判断依据(CoT): ${reason}` : ""}`
|
|
5282
|
+
: reason;
|
|
4779
5283
|
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
4780
|
-
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason,
|
|
5284
|
+
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, parsedEvidence, passed);
|
|
4781
5285
|
return {
|
|
4782
5286
|
passed,
|
|
4783
5287
|
rawPassed,
|
|
5288
|
+
cot: reason,
|
|
4784
5289
|
reason: enrichedReason,
|
|
4785
5290
|
summary: summary || enrichedReason,
|
|
4786
|
-
evidence,
|
|
4787
|
-
evidenceRawCount
|
|
4788
|
-
evidenceMatchedCount
|
|
5291
|
+
evidence: parsedEvidence,
|
|
5292
|
+
evidenceRawCount,
|
|
5293
|
+
evidenceMatchedCount,
|
|
5294
|
+
evidenceGateEligible,
|
|
4789
5295
|
evidenceGateDemoted
|
|
4790
5296
|
};
|
|
4791
5297
|
}
|
|
@@ -4885,12 +5391,17 @@ class RecommendScreenCli {
|
|
|
4885
5391
|
"要求:\n" +
|
|
4886
5392
|
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
4887
5393
|
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
4888
|
-
"3)
|
|
4889
|
-
"4)
|
|
4890
|
-
"5)
|
|
4891
|
-
"
|
|
5394
|
+
"3) 若文本中包含“人选卡片兜底信息(仅在简历缺失时使用)”段落,只能在主简历缺失对应字段时引用该段,不可覆盖主简历已明确字段。\n" +
|
|
5395
|
+
"4) 若证据不足,必须返回 passed=false。\n\n" +
|
|
5396
|
+
"5) 当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中存在教育时间、毕业时间或可推断年份信息,必须先推断再判断;" +
|
|
5397
|
+
"只有完全不存在时间线信息时才可写“无法判断”。\n" +
|
|
5398
|
+
"6) 当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
5399
|
+
"7) workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
5400
|
+
"8) 活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n" +
|
|
5401
|
+
"9) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5402
|
+
"10) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
4892
5403
|
"请返回严格 JSON: " +
|
|
4893
|
-
"{\"passed\": true/false
|
|
5404
|
+
"{\"passed\": true/false}"
|
|
4894
5405
|
}
|
|
4895
5406
|
]
|
|
4896
5407
|
};
|
|
@@ -4916,32 +5427,43 @@ class RecommendScreenCli {
|
|
|
4916
5427
|
throw this.buildError("TEXT_MODEL_FAILED", `Text model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
4917
5428
|
}
|
|
4918
5429
|
const json = await response.json();
|
|
4919
|
-
const
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
const
|
|
4923
|
-
const reason = normalizeText(parsed.reason);
|
|
4924
|
-
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));
|
|
4925
5434
|
const normalizedResume = normalizeText(safeResumeText);
|
|
4926
5435
|
const normalizedResumeLower = toLowerSafe(normalizedResume);
|
|
4927
|
-
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) : [];
|
|
4928
5440
|
const evidence = [];
|
|
4929
5441
|
const unmatchedEvidence = [];
|
|
4930
|
-
|
|
4931
|
-
const
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
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
|
+
}
|
|
4936
5450
|
}
|
|
4937
5451
|
}
|
|
4938
|
-
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
|
+
}
|
|
4939
5461
|
let passed = rawPassed;
|
|
4940
|
-
let finalReason =
|
|
4941
|
-
const evidenceGateDemoted = rawPassed && evidence.length <= 0;
|
|
5462
|
+
let finalReason = cot || (passed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5463
|
+
const evidenceGateDemoted = evidenceGateEligible && rawPassed && evidence.length <= 0;
|
|
4942
5464
|
if (evidenceGateDemoted) {
|
|
4943
5465
|
passed = false;
|
|
4944
|
-
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${
|
|
5466
|
+
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${finalReason ? ` 原始判断依据(CoT): ${finalReason}` : ""}`;
|
|
4945
5467
|
if (unmatchedEvidence.length > 0) {
|
|
4946
5468
|
log(
|
|
4947
5469
|
`[EVIDENCE_GATE] passed=true 但证据未命中简历原文,已降级为不通过;` +
|
|
@@ -4949,15 +5471,17 @@ class RecommendScreenCli {
|
|
|
4949
5471
|
);
|
|
4950
5472
|
}
|
|
4951
5473
|
}
|
|
5474
|
+
const summary = finalReason;
|
|
4952
5475
|
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
4953
5476
|
return {
|
|
4954
5477
|
passed,
|
|
4955
5478
|
rawPassed,
|
|
5479
|
+
cot: finalReason,
|
|
4956
5480
|
reason: enrichedReason,
|
|
4957
5481
|
summary: summary || enrichedReason,
|
|
4958
5482
|
evidence,
|
|
4959
|
-
evidenceRawCount: parsedEvidence.length,
|
|
4960
|
-
evidenceMatchedCount: evidence.length,
|
|
5483
|
+
evidenceRawCount: evidenceGateEligible ? parsedEvidence.length : null,
|
|
5484
|
+
evidenceMatchedCount: evidenceGateEligible ? evidence.length : null,
|
|
4961
5485
|
evidenceGateDemoted,
|
|
4962
5486
|
chunkIndex,
|
|
4963
5487
|
chunkTotal
|
|
@@ -5538,6 +6062,7 @@ class RecommendScreenCli {
|
|
|
5538
6062
|
let domCandidateInfo = null;
|
|
5539
6063
|
|
|
5540
6064
|
if (networkCandidateInfo?.resumeText) {
|
|
6065
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(networkCandidateInfo, cardProfile || null);
|
|
5541
6066
|
screening = await timeCandidateStage(
|
|
5542
6067
|
"text_model_ms",
|
|
5543
6068
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5577,7 +6102,10 @@ class RecommendScreenCli {
|
|
|
5577
6102
|
})
|
|
5578
6103
|
);
|
|
5579
6104
|
if (lateNetworkCandidateInfo?.resumeText) {
|
|
5580
|
-
networkCandidateInfo =
|
|
6105
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6106
|
+
lateNetworkCandidateInfo,
|
|
6107
|
+
cardProfile || null
|
|
6108
|
+
);
|
|
5581
6109
|
screening = await timeCandidateStage(
|
|
5582
6110
|
"text_model_ms",
|
|
5583
6111
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5602,7 +6130,10 @@ class RecommendScreenCli {
|
|
|
5602
6130
|
() => this.resolveDomResumeFallback(nextCandidate, cardProfile || null)
|
|
5603
6131
|
);
|
|
5604
6132
|
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
5605
|
-
networkCandidateInfo =
|
|
6133
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6134
|
+
domFallback.networkCandidateInfo,
|
|
6135
|
+
cardProfile || null
|
|
6136
|
+
);
|
|
5606
6137
|
screening = await timeCandidateStage(
|
|
5607
6138
|
"text_model_ms",
|
|
5608
6139
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5622,7 +6153,10 @@ class RecommendScreenCli {
|
|
|
5622
6153
|
}
|
|
5623
6154
|
);
|
|
5624
6155
|
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5625
|
-
domCandidateInfo =
|
|
6156
|
+
domCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6157
|
+
domFallback.domCandidateInfo,
|
|
6158
|
+
cardProfile || null
|
|
6159
|
+
);
|
|
5626
6160
|
screening = await timeCandidateStage(
|
|
5627
6161
|
"text_model_ms",
|
|
5628
6162
|
() => this.callTextModel(domCandidateInfo.resumeText)
|
|
@@ -5985,6 +6519,8 @@ if (require.main === module) {
|
|
|
5985
6519
|
isRecoverablePostActionError,
|
|
5986
6520
|
classifyFinishedWrapState,
|
|
5987
6521
|
formatResumeApiData,
|
|
6522
|
+
buildCardProfileFallbackText,
|
|
6523
|
+
enrichCandidateInfoWithCardProfile,
|
|
5988
6524
|
extractEvidenceTokens,
|
|
5989
6525
|
matchEvidenceAgainstResume
|
|
5990
6526
|
}
|
|
@@ -959,6 +959,100 @@ function testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription
|
|
|
959
959
|
assert.equal(formatted.includes("描述: 采用stable diffusion进行编辑实验"), true);
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
function testFormatResumeApiDataShouldIncludeStructuredJudgementHints() {
|
|
963
|
+
const source = {
|
|
964
|
+
geekDetailInfo: {
|
|
965
|
+
geekBaseInfo: {
|
|
966
|
+
name: "测试候选人",
|
|
967
|
+
degreeCategory: "硕士"
|
|
968
|
+
},
|
|
969
|
+
geekWorkExpList: [
|
|
970
|
+
{
|
|
971
|
+
company: "中科院",
|
|
972
|
+
positionName: "科研助理",
|
|
973
|
+
startDate: "20241001",
|
|
974
|
+
endDate: "",
|
|
975
|
+
responsibility: "科研以及项目"
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
geekProjExpList: [],
|
|
979
|
+
geekEduExpList: [
|
|
980
|
+
{
|
|
981
|
+
school: "科克大学",
|
|
982
|
+
major: "理学",
|
|
983
|
+
degreeName: "硕士",
|
|
984
|
+
startDateDesc: "2020",
|
|
985
|
+
endDateDesc: "2023"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
school: "东北大学",
|
|
989
|
+
major: "数学与应用数学",
|
|
990
|
+
degreeName: "本科",
|
|
991
|
+
startDate: "20140101",
|
|
992
|
+
endDate: "20180101"
|
|
993
|
+
}
|
|
994
|
+
],
|
|
995
|
+
workExpCheckRes: [
|
|
996
|
+
{
|
|
997
|
+
desc: "毕业同年未填写工作经历"
|
|
998
|
+
}
|
|
999
|
+
],
|
|
1000
|
+
jobCompetitive: {
|
|
1001
|
+
tips: [{ content: "受欢迎程度高" }]
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
const formatted = __testables.formatResumeApiData(source);
|
|
1006
|
+
assert.equal(formatted.includes("=== 结构化判定线索 ==="), true);
|
|
1007
|
+
assert.equal(formatted.includes("最高学历: 硕士"), true);
|
|
1008
|
+
assert.equal(formatted.includes("最高学历毕业年份: 2023"), true);
|
|
1009
|
+
assert.equal(formatted.includes("是否有工作经历: 是"), true);
|
|
1010
|
+
assert.equal(formatted.includes("是否有项目经历: 否"), true);
|
|
1011
|
+
assert.equal(formatted.includes("相关经验硬判口径"), true);
|
|
1012
|
+
assert.equal(formatted.includes("软风险提示(需追问,不直接淘汰): 毕业同年未填写工作经历"), true);
|
|
1013
|
+
assert.equal(formatted.includes("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。"), true);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
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
|
+
|
|
962
1056
|
function testEvidenceTokenMatcherShouldSupportParaphrasedEvidence() {
|
|
963
1057
|
const resume = [
|
|
964
1058
|
"南京大学 专业: 数学",
|
|
@@ -1702,6 +1796,8 @@ async function main() {
|
|
|
1702
1796
|
testFavoriteActionParserShouldOnlyTrustKnownRequestShapes();
|
|
1703
1797
|
testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
|
|
1704
1798
|
testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription();
|
|
1799
|
+
testFormatResumeApiDataShouldIncludeStructuredJudgementHints();
|
|
1800
|
+
testEnrichCandidateInfoWithCardProfileShouldAppendCardFallbackWhenDomInfoMissing();
|
|
1705
1801
|
testEvidenceTokenMatcherShouldSupportParaphrasedEvidence();
|
|
1706
1802
|
testCheckpointPayloadShouldIncludeCandidateAudits();
|
|
1707
1803
|
testCheckpointShouldPersistAndRestoreInputSummary();
|