@reconcrap/boss-recommend-mcp 1.3.16 → 1.3.18
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/README.md +2 -1
- package/config/screening-config.example.json +1 -0
- package/package.json +1 -1
- package/src/adapters.js +28 -9
- package/src/boss-chat.js +22 -0
- package/src/cli.js +6 -1
- package/src/test-boss-chat.js +89 -0
- package/vendor/boss-chat-cli/src/cli.js +10 -0
- package/vendor/boss-chat-cli/src/mcp/server.js +1 -1
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +1 -1
- package/vendor/boss-chat-cli/src/services/llm.js +119 -24
- package/vendor/boss-chat-cli/src/services/profile-store.js +5 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +597 -82
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +109 -41
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +259 -0
|
@@ -26,7 +26,21 @@ const CSV_HEADER = [
|
|
|
26
26
|
"证据门控降级",
|
|
27
27
|
"错误码",
|
|
28
28
|
"错误信息",
|
|
29
|
-
"候选人ID"
|
|
29
|
+
"候选人ID",
|
|
30
|
+
"总耗时ms",
|
|
31
|
+
"候选卡片读取ms",
|
|
32
|
+
"点击候选人ms",
|
|
33
|
+
"详情打开ms",
|
|
34
|
+
"network简历等待ms",
|
|
35
|
+
"文本模型ms",
|
|
36
|
+
"截图获取ms",
|
|
37
|
+
"视觉模型ms",
|
|
38
|
+
"late network retry ms",
|
|
39
|
+
"DOM fallback ms",
|
|
40
|
+
"通过后动作ms",
|
|
41
|
+
"关闭详情ms",
|
|
42
|
+
"休息ms",
|
|
43
|
+
"checkpoint保存ms"
|
|
30
44
|
].join(",");
|
|
31
45
|
const INPUT_SUMMARY_HEADER = ["运行输入字段", "运行输入值"].join(",");
|
|
32
46
|
const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
@@ -34,6 +48,8 @@ const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
|
34
48
|
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
35
49
|
const NETWORK_RESUME_WAIT_MS = 4200;
|
|
36
50
|
const NETWORK_RESUME_RETRY_WAIT_MS = 2000;
|
|
51
|
+
const NETWORK_RESUME_IMAGE_MODE_GRACE_MS = 1000;
|
|
52
|
+
const NETWORK_RESUME_LATE_RETRY_MS = 3000;
|
|
37
53
|
const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
|
|
38
54
|
const DEFAULT_VISION_MAX_IMAGE_PIXELS = 36000000;
|
|
39
55
|
const DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS = 30000000;
|
|
@@ -636,6 +652,101 @@ function formatEducationSchoolTags(edu) {
|
|
|
636
652
|
return tags.join("、");
|
|
637
653
|
}
|
|
638
654
|
|
|
655
|
+
function inferDegreeRank(degreeText) {
|
|
656
|
+
const normalized = normalizeText(degreeText).toLowerCase();
|
|
657
|
+
if (!normalized) return 0;
|
|
658
|
+
if (/博士|phd|doctor/.test(normalized)) return 7;
|
|
659
|
+
if (/硕士|master/.test(normalized)) return 6;
|
|
660
|
+
if (/本科|学士|bachelor/.test(normalized)) return 5;
|
|
661
|
+
if (/大专|专科|junior/.test(normalized)) return 4;
|
|
662
|
+
if (/高中/.test(normalized)) return 3;
|
|
663
|
+
if (/中专|中技/.test(normalized)) return 2;
|
|
664
|
+
if (/初中|小学|及以下/.test(normalized)) return 1;
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function normalizeResumeDateToken(value) {
|
|
669
|
+
const raw = normalizeText(value);
|
|
670
|
+
if (!raw) return "";
|
|
671
|
+
const digits = raw.replace(/[^\d]/g, "");
|
|
672
|
+
if (/^\d{8}$/.test(digits)) {
|
|
673
|
+
return `${digits.slice(0, 4)}.${digits.slice(4, 6)}`;
|
|
674
|
+
}
|
|
675
|
+
if (/^\d{6}$/.test(digits)) {
|
|
676
|
+
return `${digits.slice(0, 4)}.${digits.slice(4, 6)}`;
|
|
677
|
+
}
|
|
678
|
+
if (/^\d{4}$/.test(digits)) {
|
|
679
|
+
return digits;
|
|
680
|
+
}
|
|
681
|
+
return raw;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function formatResumeTimeRange(startRaw, endRaw, fallbackEnd = "") {
|
|
685
|
+
const start = normalizeResumeDateToken(startRaw);
|
|
686
|
+
const end = normalizeResumeDateToken(endRaw) || normalizeText(fallbackEnd);
|
|
687
|
+
if (start && end) return `${start} ~ ${end}`;
|
|
688
|
+
if (start) return `${start} ~`;
|
|
689
|
+
if (end) return `~ ${end}`;
|
|
690
|
+
return "";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function formatResumeTimeRangeFromFields(source, startFields = [], endFields = [], fallbackEnd = "") {
|
|
694
|
+
const startRaw = startFields
|
|
695
|
+
.map((field) => source?.[field])
|
|
696
|
+
.find((value) => normalizeText(value));
|
|
697
|
+
const endRaw = endFields
|
|
698
|
+
.map((field) => source?.[field])
|
|
699
|
+
.find((value) => normalizeText(value));
|
|
700
|
+
return formatResumeTimeRange(startRaw, endRaw, fallbackEnd);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function formatNamedListText(items = []) {
|
|
704
|
+
if (!Array.isArray(items) || items.length <= 0) return "";
|
|
705
|
+
return items
|
|
706
|
+
.map((item) => normalizeText(item?.name || item?.subjectName || item?.title || item))
|
|
707
|
+
.filter(Boolean)
|
|
708
|
+
.join("、");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function extractYearFromResumeDate(value) {
|
|
712
|
+
const token = normalizeResumeDateToken(value);
|
|
713
|
+
if (!token) return "";
|
|
714
|
+
const match = token.match(/(19|20)\d{2}/);
|
|
715
|
+
return match ? match[0] : "";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function deriveHighestEducation(eduExpList = []) {
|
|
719
|
+
const list = Array.isArray(eduExpList) ? eduExpList : [];
|
|
720
|
+
let selected = null;
|
|
721
|
+
for (const edu of list) {
|
|
722
|
+
const degree = formatEducationDegree(edu);
|
|
723
|
+
const rank = inferDegreeRank(degree);
|
|
724
|
+
const endYear = extractYearFromResumeDate(
|
|
725
|
+
edu?.endYearMonStr || edu?.endYearStr || edu?.endDateDesc || edu?.endDate || ""
|
|
726
|
+
);
|
|
727
|
+
const candidate = {
|
|
728
|
+
school: normalizeText(edu?.school || edu?.schoolName || ""),
|
|
729
|
+
degree,
|
|
730
|
+
rank,
|
|
731
|
+
endYear
|
|
732
|
+
};
|
|
733
|
+
if (!selected) {
|
|
734
|
+
selected = candidate;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const selectedYear = Number(selected.endYear || 0);
|
|
738
|
+
const candidateYear = Number(candidate.endYear || 0);
|
|
739
|
+
if (candidate.rank > selected.rank) {
|
|
740
|
+
selected = candidate;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (candidate.rank === selected.rank && candidateYear > selectedYear) {
|
|
744
|
+
selected = candidate;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return selected || { school: "", degree: "", rank: 0, endYear: "" };
|
|
748
|
+
}
|
|
749
|
+
|
|
639
750
|
function splitTextByChunks(text, chunkSize, overlap, maxChunks) {
|
|
640
751
|
const source = String(text || "");
|
|
641
752
|
if (!source) return [];
|
|
@@ -962,12 +1073,69 @@ function shouldBringChromeToFront() {
|
|
|
962
1073
|
}
|
|
963
1074
|
|
|
964
1075
|
const SHOULD_BRING_TO_FRONT = shouldBringChromeToFront();
|
|
1076
|
+
const LLM_THINKING_ENV_KEYS = [
|
|
1077
|
+
"BOSS_RECOMMEND_LLM_THINKING_LEVEL",
|
|
1078
|
+
"BOSS_LLM_THINKING_LEVEL",
|
|
1079
|
+
"LLM_THINKING_LEVEL"
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
function normalizeLlmThinkingLevel(value) {
|
|
1083
|
+
const normalized = normalizeText(value).toLowerCase().replace(/[_\s]+/g, "-");
|
|
1084
|
+
if (!normalized) return "";
|
|
1085
|
+
if (["off", "disabled", "disable", "minimal", "none", "false", "0"].includes(normalized)) return "off";
|
|
1086
|
+
if (["low", "medium", "high", "auto", "current", "default", "provider-default", "unchanged", "inherit"].includes(normalized)) {
|
|
1087
|
+
return normalized;
|
|
1088
|
+
}
|
|
1089
|
+
return "";
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function getEnvLlmThinkingLevel() {
|
|
1093
|
+
for (const key of LLM_THINKING_ENV_KEYS) {
|
|
1094
|
+
const normalized = normalizeLlmThinkingLevel(process.env[key]);
|
|
1095
|
+
if (normalized) return normalized;
|
|
1096
|
+
}
|
|
1097
|
+
return "";
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function resolveLlmThinkingLevel(value) {
|
|
1101
|
+
return normalizeLlmThinkingLevel(value) || getEnvLlmThinkingLevel() || "off";
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function isVolcengineModel(baseUrl, model) {
|
|
1105
|
+
const combined = `${baseUrl || ""} ${model || ""}`;
|
|
1106
|
+
return /volces\.com|volcengine|ark\.cn-|doubao|seed/i.test(combined);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
|
|
1110
|
+
const level = resolveLlmThinkingLevel(thinkingLevel);
|
|
1111
|
+
if (["current", "default", "provider-default", "unchanged", "inherit"].includes(level)) return payload;
|
|
1112
|
+
const isVolc = isVolcengineModel(baseUrl, model);
|
|
1113
|
+
if (isVolc) {
|
|
1114
|
+
if (level === "auto") {
|
|
1115
|
+
payload.thinking = { type: "auto" };
|
|
1116
|
+
return payload;
|
|
1117
|
+
}
|
|
1118
|
+
if (level === "off") {
|
|
1119
|
+
payload.thinking = { type: "disabled" };
|
|
1120
|
+
payload.reasoning_effort = "minimal";
|
|
1121
|
+
return payload;
|
|
1122
|
+
}
|
|
1123
|
+
payload.thinking = { type: "enabled" };
|
|
1124
|
+
payload.reasoning_effort = level;
|
|
1125
|
+
return payload;
|
|
1126
|
+
}
|
|
1127
|
+
if (level !== "auto") {
|
|
1128
|
+
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
1129
|
+
}
|
|
1130
|
+
return payload;
|
|
1131
|
+
}
|
|
965
1132
|
|
|
966
1133
|
function parseArgs(argv) {
|
|
967
1134
|
const parsed = {
|
|
968
1135
|
baseUrl: null,
|
|
969
1136
|
apiKey: null,
|
|
970
1137
|
model: null,
|
|
1138
|
+
thinkingLevel: null,
|
|
971
1139
|
openaiOrganization: null,
|
|
972
1140
|
openaiProject: null,
|
|
973
1141
|
criteria: null,
|
|
@@ -988,6 +1156,7 @@ function parseArgs(argv) {
|
|
|
988
1156
|
baseUrl: false,
|
|
989
1157
|
apiKey: false,
|
|
990
1158
|
model: false,
|
|
1159
|
+
thinkingLevel: false,
|
|
991
1160
|
criteria: false,
|
|
992
1161
|
targetCount: false,
|
|
993
1162
|
maxGreetCount: false,
|
|
@@ -1016,6 +1185,10 @@ function parseArgs(argv) {
|
|
|
1016
1185
|
parsed.model = inlineValue || next;
|
|
1017
1186
|
parsed.__provided.model = true;
|
|
1018
1187
|
if (!inlineValue) index += 1;
|
|
1188
|
+
} else if ((token === "--thinking-level" || token === "--thinkingLevel" || token === "--llm-thinking-level" || token === "--reasoning-effort") && (inlineValue || next)) {
|
|
1189
|
+
parsed.thinkingLevel = inlineValue || next;
|
|
1190
|
+
parsed.__provided.thinkingLevel = true;
|
|
1191
|
+
if (!inlineValue) index += 1;
|
|
1019
1192
|
} else if (token === "--openai-organization" && (inlineValue || next)) {
|
|
1020
1193
|
parsed.openaiOrganization = inlineValue || next;
|
|
1021
1194
|
if (!inlineValue) index += 1;
|
|
@@ -1208,6 +1381,30 @@ function csvEscape(value) {
|
|
|
1208
1381
|
return `"${String(value || "").replace(/"/g, '""')}"`;
|
|
1209
1382
|
}
|
|
1210
1383
|
|
|
1384
|
+
function normalizeTimingMs(value) {
|
|
1385
|
+
const parsed = Number(value);
|
|
1386
|
+
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
1387
|
+
return Math.round(parsed);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function sanitizeTimingBreakdown(value) {
|
|
1391
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1392
|
+
const result = {};
|
|
1393
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
1394
|
+
const normalizedKey = normalizeText(key);
|
|
1395
|
+
if (!normalizedKey) continue;
|
|
1396
|
+
const normalizedValue = normalizeTimingMs(raw);
|
|
1397
|
+
if (normalizedValue === null) continue;
|
|
1398
|
+
result[normalizedKey] = normalizedValue;
|
|
1399
|
+
}
|
|
1400
|
+
return result;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function getTimingMs(timing, key) {
|
|
1404
|
+
const normalized = normalizeTimingMs(timing?.[key]);
|
|
1405
|
+
return normalized === null ? "" : normalized;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1211
1408
|
function stringifyInputSummaryValue(value) {
|
|
1212
1409
|
if (value === null) return "null";
|
|
1213
1410
|
if (value === undefined) return "";
|
|
@@ -1434,12 +1631,21 @@ function formatResumeApiData(data) {
|
|
|
1434
1631
|
const parts = [];
|
|
1435
1632
|
const geekDetail = data?.geekDetail || data?.geekDetailInfo || data || {};
|
|
1436
1633
|
const baseInfo = geekDetail.geekBaseInfo || {};
|
|
1437
|
-
const expectList = geekDetail.geekExpectList
|
|
1634
|
+
const expectList = Array.isArray(geekDetail.geekExpectList) && geekDetail.geekExpectList.length > 0
|
|
1635
|
+
? geekDetail.geekExpectList
|
|
1636
|
+
: Array.isArray(geekDetail.geekExpPosList) && geekDetail.geekExpPosList.length > 0
|
|
1637
|
+
? geekDetail.geekExpPosList
|
|
1638
|
+
: geekDetail.showExpectPosition
|
|
1639
|
+
? [geekDetail.showExpectPosition]
|
|
1640
|
+
: [];
|
|
1438
1641
|
const workExpList = geekDetail.geekWorkExpList || [];
|
|
1439
1642
|
const projExpList = geekDetail.geekProjExpList || [];
|
|
1440
1643
|
const eduExpList = geekDetail.geekEduExpList || geekDetail.geekEducationList || [];
|
|
1441
1644
|
const advantage = geekDetail.geekAdvantage || baseInfo.userDesc || baseInfo.userDescription || "";
|
|
1442
1645
|
const skillList = geekDetail.geekSkillList || geekDetail.skillList || [];
|
|
1646
|
+
const certificationList = geekDetail.geekCertificationList || [];
|
|
1647
|
+
const workExpCheckRes = Array.isArray(geekDetail.workExpCheckRes) ? geekDetail.workExpCheckRes : [];
|
|
1648
|
+
const highestEdu = deriveHighestEducation(eduExpList);
|
|
1443
1649
|
|
|
1444
1650
|
parts.push("=== 基本信息 ===");
|
|
1445
1651
|
if (baseInfo.name) parts.push(`姓名: ${baseInfo.name}`);
|
|
@@ -1447,6 +1653,9 @@ function formatResumeApiData(data) {
|
|
|
1447
1653
|
if (baseInfo.gender !== undefined) parts.push(`性别: ${baseInfo.gender === 1 ? "男" : "女"}`);
|
|
1448
1654
|
if (baseInfo.degreeCategory) parts.push(`学历: ${baseInfo.degreeCategory}`);
|
|
1449
1655
|
if (baseInfo.workYearDesc) parts.push(`工作经验: ${baseInfo.workYearDesc}`);
|
|
1656
|
+
if (typeof baseInfo.freshGraduate === "number") parts.push(`应届状态: ${baseInfo.freshGraduate === 1 ? "应届生" : "非应届生"}`);
|
|
1657
|
+
const workDate = normalizeResumeDateToken(baseInfo.workDate8);
|
|
1658
|
+
if (workDate) parts.push(`参加工作时间: ${workDate}`);
|
|
1450
1659
|
if (baseInfo.activeTimeDesc) parts.push(`活跃状态: ${baseInfo.activeTimeDesc}`);
|
|
1451
1660
|
if (baseInfo.applyStatusContent) parts.push(`求职状态: ${baseInfo.applyStatusContent}`);
|
|
1452
1661
|
|
|
@@ -1471,13 +1680,27 @@ function formatResumeApiData(data) {
|
|
|
1471
1680
|
const company = exp.company || "";
|
|
1472
1681
|
const position = stripHtml(exp.positionName || "");
|
|
1473
1682
|
parts.push(`${index + 1}. ${company} - ${position}`);
|
|
1474
|
-
|
|
1475
|
-
|
|
1683
|
+
const workTime = formatResumeTimeRangeFromFields(
|
|
1684
|
+
exp,
|
|
1685
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1686
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"],
|
|
1687
|
+
"至今"
|
|
1688
|
+
);
|
|
1689
|
+
if (workTime) {
|
|
1690
|
+
parts.push(` 时间: ${workTime}`);
|
|
1476
1691
|
}
|
|
1477
1692
|
const workContent = exp.responsibility || exp.workContent || "";
|
|
1478
1693
|
if (workContent) {
|
|
1479
1694
|
parts.push(` 职责: ${stripHtml(workContent)}`);
|
|
1480
1695
|
}
|
|
1696
|
+
const workPerformance = exp.workPerformance || exp.performance || "";
|
|
1697
|
+
if (workPerformance) {
|
|
1698
|
+
parts.push(` 成果: ${stripHtml(workPerformance)}`);
|
|
1699
|
+
}
|
|
1700
|
+
const workEmphasis = exp.workEmphasis || "";
|
|
1701
|
+
if (workEmphasis) {
|
|
1702
|
+
parts.push(` 补充: ${stripHtml(workEmphasis)}`);
|
|
1703
|
+
}
|
|
1481
1704
|
});
|
|
1482
1705
|
}
|
|
1483
1706
|
|
|
@@ -1486,8 +1709,14 @@ function formatResumeApiData(data) {
|
|
|
1486
1709
|
projExpList.forEach((proj, index) => {
|
|
1487
1710
|
parts.push(`${index + 1}. ${proj.name || proj.projectName || "未知项目"}`);
|
|
1488
1711
|
if (proj.roleName) parts.push(` 角色: ${proj.roleName}`);
|
|
1489
|
-
|
|
1490
|
-
|
|
1712
|
+
const projTime = formatResumeTimeRangeFromFields(
|
|
1713
|
+
proj,
|
|
1714
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1715
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"],
|
|
1716
|
+
"至今"
|
|
1717
|
+
);
|
|
1718
|
+
if (projTime) {
|
|
1719
|
+
parts.push(` 时间: ${projTime}`);
|
|
1491
1720
|
}
|
|
1492
1721
|
const projectDescription = proj.description || proj.projectDescription || "";
|
|
1493
1722
|
if (projectDescription) parts.push(` 描述: ${stripHtml(projectDescription)}`);
|
|
@@ -1503,15 +1732,35 @@ function formatResumeApiData(data) {
|
|
|
1503
1732
|
if (edu.major || edu.majorName) parts.push(` 专业: ${edu.major || edu.majorName}`);
|
|
1504
1733
|
const eduDegree = formatEducationDegree(edu);
|
|
1505
1734
|
if (eduDegree) parts.push(` 学历: ${eduDegree}`);
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1735
|
+
const eduTime = formatResumeTimeRangeFromFields(
|
|
1736
|
+
edu,
|
|
1737
|
+
["startYearMonStr", "startYearStr", "startDateDesc", "startDate"],
|
|
1738
|
+
["endYearMonStr", "endYearStr", "endDateDesc", "endDate"]
|
|
1739
|
+
);
|
|
1740
|
+
if (eduTime) {
|
|
1741
|
+
parts.push(` 时间: ${eduTime}`);
|
|
1510
1742
|
}
|
|
1511
1743
|
const schoolTags = formatEducationSchoolTags(edu);
|
|
1512
1744
|
if (schoolTags) {
|
|
1513
1745
|
parts.push(` 学校标签: ${schoolTags}`);
|
|
1514
1746
|
}
|
|
1747
|
+
const eduDescription = stripHtml(edu.eduDescription || edu.description || "");
|
|
1748
|
+
if (eduDescription) {
|
|
1749
|
+
parts.push(` 描述: ${eduDescription}`);
|
|
1750
|
+
}
|
|
1751
|
+
const courseDesc = stripHtml(edu.courseDesc || "");
|
|
1752
|
+
if (courseDesc) {
|
|
1753
|
+
parts.push(` 课程/研究: ${courseDesc}`);
|
|
1754
|
+
}
|
|
1755
|
+
const keySubjects = formatNamedListText(edu.keySubjectList || []);
|
|
1756
|
+
if (keySubjects) {
|
|
1757
|
+
parts.push(` 核心课程: ${keySubjects}`);
|
|
1758
|
+
}
|
|
1759
|
+
const thesisTitle = normalizeText(edu.thesisTitle || "");
|
|
1760
|
+
const thesisDesc = stripHtml(edu.thesisDesc || "");
|
|
1761
|
+
if (thesisTitle || thesisDesc) {
|
|
1762
|
+
parts.push(` 论文: ${thesisTitle}${thesisDesc ? ` - ${thesisDesc}` : ""}`);
|
|
1763
|
+
}
|
|
1515
1764
|
});
|
|
1516
1765
|
}
|
|
1517
1766
|
|
|
@@ -1524,6 +1773,33 @@ function formatResumeApiData(data) {
|
|
|
1524
1773
|
});
|
|
1525
1774
|
}
|
|
1526
1775
|
|
|
1776
|
+
if (certificationList.length > 0) {
|
|
1777
|
+
parts.push("\n=== 资格证书 ===");
|
|
1778
|
+
certificationList.forEach((cert) => {
|
|
1779
|
+
const certName = normalizeText(cert?.certName || cert?.name || "");
|
|
1780
|
+
if (certName) parts.push(`- ${certName}`);
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
parts.push("\n=== 结构化判定线索 ===");
|
|
1785
|
+
if (highestEdu.degree) parts.push(`最高学历: ${highestEdu.degree}`);
|
|
1786
|
+
if (highestEdu.school) parts.push(`最高学历学校: ${highestEdu.school}`);
|
|
1787
|
+
if (highestEdu.endYear) parts.push(`最高学历毕业年份: ${highestEdu.endYear}`);
|
|
1788
|
+
parts.push(`是否有工作经历: ${workExpList.length > 0 ? "是" : "否"}`);
|
|
1789
|
+
parts.push(`是否有项目经历: ${projExpList.length > 0 ? "是" : "否"}`);
|
|
1790
|
+
parts.push("相关经验硬判口径: 仅工作经历/项目经历可作为“相关经验”硬性证据;教育/课程/技能仅作补充。");
|
|
1791
|
+
if (workExpCheckRes.length > 0) {
|
|
1792
|
+
const riskText = workExpCheckRes
|
|
1793
|
+
.map((item) => normalizeText(item?.desc || item?.firstTip || item?.chatDesc || ""))
|
|
1794
|
+
.filter(Boolean)
|
|
1795
|
+
.slice(0, 3)
|
|
1796
|
+
.join(";");
|
|
1797
|
+
if (riskText) {
|
|
1798
|
+
parts.push(`软风险提示(需追问,不直接淘汰): ${riskText}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
parts.push("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。");
|
|
1802
|
+
|
|
1527
1803
|
return parts.join("\n");
|
|
1528
1804
|
}
|
|
1529
1805
|
|
|
@@ -2844,6 +3120,8 @@ class RecommendScreenCli {
|
|
|
2844
3120
|
dom_fallback: 0,
|
|
2845
3121
|
image_fallback: 0
|
|
2846
3122
|
};
|
|
3123
|
+
this.resumeAcquisitionMode = "unknown";
|
|
3124
|
+
this.resumeAcquisitionModeReason = "";
|
|
2847
3125
|
this.lastActiveTabStatus = PAGE_SCOPE_TAB_STATUS[this.args.pageScope] || null;
|
|
2848
3126
|
this.featuredCalibration = this.args.pageScope === "featured"
|
|
2849
3127
|
? loadCalibrationPosition(this.args.calibrationPath)
|
|
@@ -2890,6 +3168,8 @@ class RecommendScreenCli {
|
|
|
2890
3168
|
skipped_count: this.skippedCount,
|
|
2891
3169
|
greet_count: this.greetCount,
|
|
2892
3170
|
greet_limit_fallback_count: this.greetLimitFallbackCount,
|
|
3171
|
+
resume_acquisition_mode: this.resumeAcquisitionMode,
|
|
3172
|
+
resume_acquisition_mode_reason: this.resumeAcquisitionModeReason,
|
|
2893
3173
|
processed_keys: Array.from(this.processedKeys),
|
|
2894
3174
|
passed_candidates: this.passedCandidates.map((item) => ({
|
|
2895
3175
|
name: item?.name || "",
|
|
@@ -2926,7 +3206,8 @@ class RecommendScreenCli {
|
|
|
2926
3206
|
error_code: item?.error_code || "",
|
|
2927
3207
|
error_message: item?.error_message || "",
|
|
2928
3208
|
chunk_index: Number.isFinite(Number(item?.chunk_index)) ? Number(item.chunk_index) : null,
|
|
2929
|
-
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null
|
|
3209
|
+
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null,
|
|
3210
|
+
timing_ms: sanitizeTimingBreakdown(item?.timing_ms)
|
|
2930
3211
|
})),
|
|
2931
3212
|
input_summary: sanitizeInputSummary(this.inputSummary)
|
|
2932
3213
|
};
|
|
@@ -2942,6 +3223,7 @@ class RecommendScreenCli {
|
|
|
2942
3223
|
checkpoint_path: this.checkpointPath,
|
|
2943
3224
|
selected_page: this.args.pageScope || "recommend",
|
|
2944
3225
|
active_tab_status: this.lastActiveTabStatus || PAGE_SCOPE_TAB_STATUS[this.args.pageScope] || null,
|
|
3226
|
+
resume_acquisition_mode: this.resumeAcquisitionMode,
|
|
2945
3227
|
resume_source: this.resumeSourceStats.image_fallback > 0
|
|
2946
3228
|
? "image_fallback"
|
|
2947
3229
|
: this.resumeSourceStats.dom_fallback > 0
|
|
@@ -3092,7 +3374,8 @@ class RecommendScreenCli {
|
|
|
3092
3374
|
error_code: normalizeText(entry?.error_code || "") || "",
|
|
3093
3375
|
error_message: normalizeText(entry?.error_message || "") || "",
|
|
3094
3376
|
chunk_index: Number.isFinite(Number(entry?.chunk_index)) ? Number(entry.chunk_index) : null,
|
|
3095
|
-
chunk_total: Number.isFinite(Number(entry?.chunk_total)) ? Number(entry.chunk_total) : null
|
|
3377
|
+
chunk_total: Number.isFinite(Number(entry?.chunk_total)) ? Number(entry.chunk_total) : null,
|
|
3378
|
+
timing_ms: sanitizeTimingBreakdown(entry?.timing_ms)
|
|
3096
3379
|
};
|
|
3097
3380
|
this.candidateAudits.push(normalized);
|
|
3098
3381
|
const maxItems = parsePositiveInteger(process.env.BOSS_RECOMMEND_MAX_CANDIDATE_AUDITS);
|
|
@@ -3101,6 +3384,22 @@ class RecommendScreenCli {
|
|
|
3101
3384
|
}
|
|
3102
3385
|
}
|
|
3103
3386
|
|
|
3387
|
+
updateCandidateAuditTiming(candidateKey, timing = {}) {
|
|
3388
|
+
const normalizedKey = normalizeText(candidateKey || "");
|
|
3389
|
+
if (!normalizedKey) return;
|
|
3390
|
+
const timingMs = sanitizeTimingBreakdown(timing);
|
|
3391
|
+
for (let index = this.candidateAudits.length - 1; index >= 0; index -= 1) {
|
|
3392
|
+
const audit = this.candidateAudits[index];
|
|
3393
|
+
if (
|
|
3394
|
+
normalizeText(audit?.candidate_key || "") === normalizedKey
|
|
3395
|
+
|| normalizeText(audit?.geek_id || "") === normalizedKey
|
|
3396
|
+
) {
|
|
3397
|
+
audit.timing_ms = timingMs;
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3104
3403
|
logResumeNetworkMissDiagnostics(candidate, options = {}) {
|
|
3105
3404
|
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
3106
3405
|
const candidateName = normalizeText(candidate?.name || "");
|
|
@@ -3240,6 +3539,60 @@ class RecommendScreenCli {
|
|
|
3240
3539
|
return null;
|
|
3241
3540
|
}
|
|
3242
3541
|
|
|
3542
|
+
setResumeAcquisitionMode(mode, reason = "") {
|
|
3543
|
+
if (!["unknown", "network", "image"].includes(mode)) return;
|
|
3544
|
+
if (this.resumeAcquisitionMode === mode) return;
|
|
3545
|
+
this.resumeAcquisitionMode = mode;
|
|
3546
|
+
this.resumeAcquisitionModeReason = normalizeText(reason || "");
|
|
3547
|
+
log(`[简历获取模式] mode=${mode}${this.resumeAcquisitionModeReason ? ` reason=${this.resumeAcquisitionModeReason}` : ""}`);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
async waitForResumeNetworkByMode(candidate, options = {}) {
|
|
3551
|
+
const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
|
|
3552
|
+
const mode = this.resumeAcquisitionMode || "unknown";
|
|
3553
|
+
const firstWaitMs = mode === "image" ? NETWORK_RESUME_IMAGE_MODE_GRACE_MS : NETWORK_RESUME_WAIT_MS;
|
|
3554
|
+
const waitStartedAt = Date.now();
|
|
3555
|
+
let networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(candidate, firstWaitMs, { minTs });
|
|
3556
|
+
if (normalizeText(networkCandidateInfo?.resumeText)) {
|
|
3557
|
+
this.setResumeAcquisitionMode("network", "network_resume_hit");
|
|
3558
|
+
return networkCandidateInfo;
|
|
3559
|
+
}
|
|
3560
|
+
if (typeof this.logResumeNetworkMissDiagnostics === "function") {
|
|
3561
|
+
this.logResumeNetworkMissDiagnostics(candidate, {
|
|
3562
|
+
timeoutMs: firstWaitMs,
|
|
3563
|
+
waitStartedAt
|
|
3564
|
+
});
|
|
3565
|
+
}
|
|
3566
|
+
if (mode === "image") {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3569
|
+
await sleep(NETWORK_RESUME_RETRY_WAIT_MS);
|
|
3570
|
+
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
3571
|
+
candidate,
|
|
3572
|
+
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
3573
|
+
{ minTs }
|
|
3574
|
+
);
|
|
3575
|
+
if (normalizeText(networkCandidateInfo?.resumeText)) {
|
|
3576
|
+
this.setResumeAcquisitionMode("network", "network_resume_retry_hit");
|
|
3577
|
+
return networkCandidateInfo;
|
|
3578
|
+
}
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
async waitForLateNetworkResumeCandidateInfo(candidate, options = {}) {
|
|
3583
|
+
const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
|
|
3584
|
+
const networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
3585
|
+
candidate,
|
|
3586
|
+
NETWORK_RESUME_LATE_RETRY_MS,
|
|
3587
|
+
{ minTs }
|
|
3588
|
+
);
|
|
3589
|
+
if (normalizeText(networkCandidateInfo?.resumeText)) {
|
|
3590
|
+
this.setResumeAcquisitionMode("network", "late_network_resume_hit");
|
|
3591
|
+
return networkCandidateInfo;
|
|
3592
|
+
}
|
|
3593
|
+
return null;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3243
3596
|
async extractResumeTextFromDom(candidate) {
|
|
3244
3597
|
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
3245
3598
|
const candidateLabel = normalizeText(candidate?.name || candidateKey || "unknown");
|
|
@@ -3637,7 +3990,8 @@ class RecommendScreenCli {
|
|
|
3637
3990
|
error_code: normalizeText(item?.error_code || "") || "",
|
|
3638
3991
|
error_message: normalizeText(item?.error_message || "") || "",
|
|
3639
3992
|
chunk_index: Number.isFinite(Number(item?.chunk_index)) ? Number(item.chunk_index) : null,
|
|
3640
|
-
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null
|
|
3993
|
+
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null,
|
|
3994
|
+
timing_ms: sanitizeTimingBreakdown(item?.timing_ms)
|
|
3641
3995
|
}))
|
|
3642
3996
|
: [];
|
|
3643
3997
|
if (!this.inputSummary) {
|
|
@@ -3665,6 +4019,17 @@ class RecommendScreenCli {
|
|
|
3665
4019
|
this.resumeSourceStats.image_fallback = 1;
|
|
3666
4020
|
}
|
|
3667
4021
|
}
|
|
4022
|
+
const checkpointMode = normalizeText(parsed.resume_acquisition_mode || "").toLowerCase();
|
|
4023
|
+
if (["network", "image"].includes(checkpointMode)) {
|
|
4024
|
+
this.resumeAcquisitionMode = checkpointMode;
|
|
4025
|
+
this.resumeAcquisitionModeReason = normalizeText(parsed.resume_acquisition_mode_reason || "checkpoint");
|
|
4026
|
+
} else if (this.resumeSourceStats.network > 0) {
|
|
4027
|
+
this.resumeAcquisitionMode = "network";
|
|
4028
|
+
this.resumeAcquisitionModeReason = "checkpoint_source_stats";
|
|
4029
|
+
} else if (this.resumeSourceStats.image_fallback > 0) {
|
|
4030
|
+
this.resumeAcquisitionMode = "image";
|
|
4031
|
+
this.resumeAcquisitionModeReason = "checkpoint_source_stats";
|
|
4032
|
+
}
|
|
3668
4033
|
|
|
3669
4034
|
return true;
|
|
3670
4035
|
}
|
|
@@ -4191,7 +4556,8 @@ class RecommendScreenCli {
|
|
|
4191
4556
|
outPrefix,
|
|
4192
4557
|
targetPattern: RECOMMEND_URL_FRAGMENT,
|
|
4193
4558
|
waitResumeMs: RESUME_CAPTURE_WAIT_MS,
|
|
4194
|
-
scrollSettleMs: 500
|
|
4559
|
+
scrollSettleMs: 500,
|
|
4560
|
+
stitchFullImage: false
|
|
4195
4561
|
});
|
|
4196
4562
|
} catch (error) {
|
|
4197
4563
|
lastError = error;
|
|
@@ -4218,7 +4584,7 @@ class RecommendScreenCli {
|
|
|
4218
4584
|
DEFAULT_VISION_MAX_IMAGE_PIXELS
|
|
4219
4585
|
);
|
|
4220
4586
|
const retryLimit = resolveVisionRetryPixelLimit(primaryLimit);
|
|
4221
|
-
const preparedPrimary = await this.
|
|
4587
|
+
const preparedPrimary = await this.prepareVisionInputsForModel(imagePath, primaryLimit, "primary");
|
|
4222
4588
|
try {
|
|
4223
4589
|
const primaryResult = await this.requestVisionModel(preparedPrimary.imagePaths);
|
|
4224
4590
|
return this.applyVisionEvidenceGate(primaryResult);
|
|
@@ -4233,7 +4599,7 @@ class RecommendScreenCli {
|
|
|
4233
4599
|
`segments=${preparedPrimary.imagePaths?.length || 1}`
|
|
4234
4600
|
);
|
|
4235
4601
|
}
|
|
4236
|
-
const preparedRetry = await this.
|
|
4602
|
+
const preparedRetry = await this.prepareVisionInputsForModel(imagePath, retryLimit, "retry");
|
|
4237
4603
|
try {
|
|
4238
4604
|
const retryResult = await this.requestVisionModel(preparedRetry.imagePaths);
|
|
4239
4605
|
return this.applyVisionEvidenceGate(retryResult);
|
|
@@ -4253,6 +4619,37 @@ class RecommendScreenCli {
|
|
|
4253
4619
|
}
|
|
4254
4620
|
}
|
|
4255
4621
|
|
|
4622
|
+
async prepareVisionInputsForModel(imageInput, maxPixels, attemptTag = "primary") {
|
|
4623
|
+
const sourcePaths = Array.isArray(imageInput) ? imageInput.filter(Boolean) : [imageInput].filter(Boolean);
|
|
4624
|
+
if (sourcePaths.length <= 0) {
|
|
4625
|
+
return {
|
|
4626
|
+
imagePaths: [],
|
|
4627
|
+
source: "empty",
|
|
4628
|
+
sourcePixels: null,
|
|
4629
|
+
currentPixels: null
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
const preparedItems = [];
|
|
4633
|
+
for (let index = 0; index < sourcePaths.length; index += 1) {
|
|
4634
|
+
const prepared = await this.prepareVisionImageSegmentsForModel(
|
|
4635
|
+
sourcePaths[index],
|
|
4636
|
+
maxPixels,
|
|
4637
|
+
`${attemptTag}.input${String(index + 1).padStart(3, "0")}`
|
|
4638
|
+
);
|
|
4639
|
+
preparedItems.push(prepared);
|
|
4640
|
+
}
|
|
4641
|
+
return {
|
|
4642
|
+
imagePaths: preparedItems.flatMap((item) => item.imagePaths || []),
|
|
4643
|
+
source: sourcePaths.length > 1 ? "ordered_chunks" : (preparedItems[0]?.source || "single"),
|
|
4644
|
+
sourcePixels: preparedItems.reduce((acc, item) => (
|
|
4645
|
+
Number.isFinite(Number(item?.sourcePixels)) ? acc + Number(item.sourcePixels) : acc
|
|
4646
|
+
), 0) || null,
|
|
4647
|
+
currentPixels: preparedItems.reduce((acc, item) => (
|
|
4648
|
+
Number.isFinite(Number(item?.currentPixels)) ? acc + Number(item.currentPixels) : acc
|
|
4649
|
+
), 0) || null
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4256
4653
|
applyVisionEvidenceGate(result) {
|
|
4257
4654
|
const parsed = result && typeof result === "object" ? result : {};
|
|
4258
4655
|
const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
|
|
@@ -4474,7 +4871,13 @@ class RecommendScreenCli {
|
|
|
4474
4871
|
"请根据以下标准判断候选人是否通过筛选。\n\n" +
|
|
4475
4872
|
`筛选标准:\n${this.args.criteria}\n\n` +
|
|
4476
4873
|
"你将收到候选人完整简历的一个或多个顺序分段图片。必须完整阅读全部分段后再判断," +
|
|
4477
|
-
"
|
|
4874
|
+
"不能只根据前几段下结论;后续分段中的教育、项目、经历或否定信息必须纳入最终判断。" +
|
|
4875
|
+
"严禁编造任何不存在的经历、项目、学校、公司或时间线;证据不足时必须判定为不通过。\n" +
|
|
4876
|
+
"当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中出现教育起止时间、毕业时间或可推断年份信息,必须先推断再判断," +
|
|
4877
|
+
"只有完全不存在可推断时间信息时才可以写“无法判断”。\n" +
|
|
4878
|
+
"当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
4879
|
+
"workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
4880
|
+
"活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n\n" +
|
|
4478
4881
|
"要求:\n" +
|
|
4479
4882
|
"1) reason 必须写出可审计的判定依据,至少包含 2 条与筛选标准直接相关的事实。\n" +
|
|
4480
4883
|
"2) 禁止只写“候选人同时满足全部筛选条件/不满足筛选条件”等泛化句。\n" +
|
|
@@ -4518,6 +4921,11 @@ class RecommendScreenCli {
|
|
|
4518
4921
|
}
|
|
4519
4922
|
]
|
|
4520
4923
|
};
|
|
4924
|
+
applyChatCompletionThinking(payload, {
|
|
4925
|
+
baseUrl,
|
|
4926
|
+
model: this.args.model,
|
|
4927
|
+
thinkingLevel: this.args.thinkingLevel
|
|
4928
|
+
});
|
|
4521
4929
|
const headers = {
|
|
4522
4930
|
"Content-Type": "application/json",
|
|
4523
4931
|
Authorization: `Bearer ${this.args.apiKey}`
|
|
@@ -4657,14 +5065,24 @@ class RecommendScreenCli {
|
|
|
4657
5065
|
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
4658
5066
|
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
4659
5067
|
"3) 若证据不足,必须返回 passed=false。\n\n" +
|
|
4660
|
-
"4)
|
|
4661
|
-
"
|
|
4662
|
-
"
|
|
5068
|
+
"4) 当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中存在教育时间、毕业时间或可推断年份信息,必须先推断再判断;" +
|
|
5069
|
+
"只有完全不存在时间线信息时才可写“无法判断”。\n" +
|
|
5070
|
+
"5) 当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
5071
|
+
"6) workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
5072
|
+
"7) 活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n" +
|
|
5073
|
+
"8) reason 必须写出可审计依据,至少包含 2 条与筛选标准直接相关的事实。\n" +
|
|
5074
|
+
"9) 禁止只写“候选人同时满足全部筛选条件/不满足筛选条件”等泛化句。\n" +
|
|
5075
|
+
"10) evidence 至少给出 2 条可在简历原文定位的证据短句。\n\n" +
|
|
4663
5076
|
"请返回严格 JSON: " +
|
|
4664
5077
|
"{\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\", \"evidence\": [\"证据原文1\", \"证据原文2\"]}"
|
|
4665
5078
|
}
|
|
4666
5079
|
]
|
|
4667
5080
|
};
|
|
5081
|
+
applyChatCompletionThinking(payload, {
|
|
5082
|
+
baseUrl,
|
|
5083
|
+
model: this.args.model,
|
|
5084
|
+
thinkingLevel: this.args.thinkingLevel
|
|
5085
|
+
});
|
|
4668
5086
|
const headers = {
|
|
4669
5087
|
"Content-Type": "application/json",
|
|
4670
5088
|
Authorization: `Bearer ${this.args.apiKey}`
|
|
@@ -5044,6 +5462,7 @@ class RecommendScreenCli {
|
|
|
5044
5462
|
const finalPassed = audit?.final_passed === true || normalizeText(audit?.outcome || "") === "passed";
|
|
5045
5463
|
const screeningReason = normalizeText(audit?.screening_reason || passedItem?.reason || "");
|
|
5046
5464
|
const passReason = finalPassed ? screeningReason : "";
|
|
5465
|
+
const timing = sanitizeTimingBreakdown(audit?.timing_ms);
|
|
5047
5466
|
lines.push([
|
|
5048
5467
|
csvEscape(audit?.candidate_name || passedItem?.name || ""),
|
|
5049
5468
|
csvEscape(audit?.school || passedItem?.school || ""),
|
|
@@ -5062,7 +5481,21 @@ class RecommendScreenCli {
|
|
|
5062
5481
|
csvEscape(audit?.evidence_gate_demoted === true ? "true" : "false"),
|
|
5063
5482
|
csvEscape(audit?.error_code || ""),
|
|
5064
5483
|
csvEscape(audit?.error_message || ""),
|
|
5065
|
-
csvEscape(auditGeekId || passedItem?.geekId || "")
|
|
5484
|
+
csvEscape(auditGeekId || passedItem?.geekId || ""),
|
|
5485
|
+
csvEscape(getTimingMs(timing, "total_ms")),
|
|
5486
|
+
csvEscape(getTimingMs(timing, "card_profile_ms")),
|
|
5487
|
+
csvEscape(getTimingMs(timing, "click_candidate_ms")),
|
|
5488
|
+
csvEscape(getTimingMs(timing, "detail_open_ms")),
|
|
5489
|
+
csvEscape(getTimingMs(timing, "network_resume_wait_ms")),
|
|
5490
|
+
csvEscape(getTimingMs(timing, "text_model_ms")),
|
|
5491
|
+
csvEscape(getTimingMs(timing, "image_capture_ms")),
|
|
5492
|
+
csvEscape(getTimingMs(timing, "vision_model_ms")),
|
|
5493
|
+
csvEscape(getTimingMs(timing, "late_network_retry_ms")),
|
|
5494
|
+
csvEscape(getTimingMs(timing, "dom_fallback_ms")),
|
|
5495
|
+
csvEscape(getTimingMs(timing, "post_action_ms")),
|
|
5496
|
+
csvEscape(getTimingMs(timing, "close_detail_ms")),
|
|
5497
|
+
csvEscape(getTimingMs(timing, "rest_ms")),
|
|
5498
|
+
csvEscape(getTimingMs(timing, "checkpoint_save_ms"))
|
|
5066
5499
|
].join(","));
|
|
5067
5500
|
}
|
|
5068
5501
|
fs.mkdirSync(path.dirname(this.args.output), { recursive: true });
|
|
@@ -5218,6 +5651,29 @@ class RecommendScreenCli {
|
|
|
5218
5651
|
this.scrollRetryCount = 0;
|
|
5219
5652
|
this.processedCount += 1;
|
|
5220
5653
|
log(`处理第 ${this.processedCount} 位候选人: ${nextCandidate.name || nextCandidate.geek_id}`);
|
|
5654
|
+
const candidateStartedAt = Date.now();
|
|
5655
|
+
const candidateTiming = {};
|
|
5656
|
+
const candidateKeyForTiming = nextCandidate.key || nextCandidate.geek_id || "";
|
|
5657
|
+
const addCandidateTiming = (key, startedAt) => {
|
|
5658
|
+
const elapsed = Math.max(0, Date.now() - startedAt);
|
|
5659
|
+
candidateTiming[key] = Math.round((Number(candidateTiming[key]) || 0) + elapsed);
|
|
5660
|
+
};
|
|
5661
|
+
const timeCandidateStage = async (key, fn) => {
|
|
5662
|
+
const startedAt = Date.now();
|
|
5663
|
+
try {
|
|
5664
|
+
return await fn();
|
|
5665
|
+
} finally {
|
|
5666
|
+
addCandidateTiming(key, startedAt);
|
|
5667
|
+
}
|
|
5668
|
+
};
|
|
5669
|
+
const timeCandidateStageSync = (key, fn) => {
|
|
5670
|
+
const startedAt = Date.now();
|
|
5671
|
+
try {
|
|
5672
|
+
return fn();
|
|
5673
|
+
} finally {
|
|
5674
|
+
addCandidateTiming(key, startedAt);
|
|
5675
|
+
}
|
|
5676
|
+
};
|
|
5221
5677
|
let shouldMarkProcessed = true;
|
|
5222
5678
|
let resumeSource = "";
|
|
5223
5679
|
let resumeTextLength = null;
|
|
@@ -5235,7 +5691,10 @@ class RecommendScreenCli {
|
|
|
5235
5691
|
|
|
5236
5692
|
try {
|
|
5237
5693
|
this.currentCandidateKey = nextCandidate.key || nextCandidate.geek_id || null;
|
|
5238
|
-
const cardProfile = await
|
|
5694
|
+
const cardProfile = await timeCandidateStage(
|
|
5695
|
+
"card_profile_ms",
|
|
5696
|
+
() => this.extractCandidateProfileFromCard(nextCandidate)
|
|
5697
|
+
);
|
|
5239
5698
|
candidateProfile = mergeCandidateProfiles(
|
|
5240
5699
|
cardProfile || null,
|
|
5241
5700
|
{
|
|
@@ -5247,38 +5706,26 @@ class RecommendScreenCli {
|
|
|
5247
5706
|
}
|
|
5248
5707
|
);
|
|
5249
5708
|
const candidateCaptureStartedAt = Date.now();
|
|
5250
|
-
await this.clickCandidate(nextCandidate);
|
|
5251
|
-
const detailOpen = await this.ensureDetailOpen();
|
|
5709
|
+
await timeCandidateStage("click_candidate_ms", () => this.clickCandidate(nextCandidate));
|
|
5710
|
+
const detailOpen = await timeCandidateStage("detail_open_ms", () => this.ensureDetailOpen());
|
|
5252
5711
|
if (!detailOpen) {
|
|
5253
5712
|
throw this.buildError("DETAIL_OPEN_FAILED", "详情页打开超时");
|
|
5254
5713
|
}
|
|
5255
5714
|
|
|
5256
5715
|
let capture = null;
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5716
|
+
let networkCandidateInfo = await timeCandidateStage(
|
|
5717
|
+
"network_resume_wait_ms",
|
|
5718
|
+
() => this.waitForResumeNetworkByMode(nextCandidate, {
|
|
5719
|
+
minTs: candidateCaptureStartedAt
|
|
5720
|
+
})
|
|
5721
|
+
);
|
|
5262
5722
|
let domCandidateInfo = null;
|
|
5263
|
-
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
5264
|
-
if (typeof this.logResumeNetworkMissDiagnostics === "function") {
|
|
5265
|
-
this.logResumeNetworkMissDiagnostics(nextCandidate, {
|
|
5266
|
-
timeoutMs: networkWaitMs,
|
|
5267
|
-
waitStartedAt: networkWaitStartedAt
|
|
5268
|
-
});
|
|
5269
|
-
}
|
|
5270
|
-
await sleep(NETWORK_RESUME_RETRY_WAIT_MS);
|
|
5271
|
-
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
5272
|
-
nextCandidate,
|
|
5273
|
-
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
5274
|
-
{
|
|
5275
|
-
minTs: candidateCaptureStartedAt
|
|
5276
|
-
}
|
|
5277
|
-
);
|
|
5278
|
-
}
|
|
5279
5723
|
|
|
5280
5724
|
if (networkCandidateInfo?.resumeText) {
|
|
5281
|
-
screening = await
|
|
5725
|
+
screening = await timeCandidateStage(
|
|
5726
|
+
"text_model_ms",
|
|
5727
|
+
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
5728
|
+
);
|
|
5282
5729
|
resumeSource = "network";
|
|
5283
5730
|
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
5284
5731
|
this.resumeSourceStats.network += 1;
|
|
@@ -5296,14 +5743,29 @@ class RecommendScreenCli {
|
|
|
5296
5743
|
} else {
|
|
5297
5744
|
try {
|
|
5298
5745
|
resumeSource = "image_fallback";
|
|
5299
|
-
capture = await
|
|
5300
|
-
|
|
5746
|
+
capture = await timeCandidateStage(
|
|
5747
|
+
"image_capture_ms",
|
|
5748
|
+
() => this.captureResumeImage(nextCandidate)
|
|
5749
|
+
);
|
|
5750
|
+
this.setResumeAcquisitionMode("image", "image_capture_success");
|
|
5751
|
+
screening = await timeCandidateStage(
|
|
5752
|
+
"vision_model_ms",
|
|
5753
|
+
() => this.callVisionModel(capture.modelImagePaths || capture.stitchedImage)
|
|
5754
|
+
);
|
|
5301
5755
|
this.resumeSourceStats.image_fallback += 1;
|
|
5302
5756
|
} catch (imageFallbackError) {
|
|
5303
|
-
const
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5757
|
+
const lateNetworkCandidateInfo = await timeCandidateStage(
|
|
5758
|
+
"late_network_retry_ms",
|
|
5759
|
+
() => this.waitForLateNetworkResumeCandidateInfo(nextCandidate, {
|
|
5760
|
+
minTs: candidateCaptureStartedAt
|
|
5761
|
+
})
|
|
5762
|
+
);
|
|
5763
|
+
if (lateNetworkCandidateInfo?.resumeText) {
|
|
5764
|
+
networkCandidateInfo = lateNetworkCandidateInfo;
|
|
5765
|
+
screening = await timeCandidateStage(
|
|
5766
|
+
"text_model_ms",
|
|
5767
|
+
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
5768
|
+
);
|
|
5307
5769
|
resumeSource = "network";
|
|
5308
5770
|
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
5309
5771
|
this.resumeSourceStats.network += 1;
|
|
@@ -5318,25 +5780,54 @@ class RecommendScreenCli {
|
|
|
5318
5780
|
position: nextCandidate.last_position || ""
|
|
5319
5781
|
}
|
|
5320
5782
|
);
|
|
5321
|
-
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5322
|
-
domCandidateInfo = domFallback.domCandidateInfo;
|
|
5323
|
-
screening = await this.callTextModel(domCandidateInfo.resumeText);
|
|
5324
|
-
resumeSource = "dom_fallback";
|
|
5325
|
-
resumeTextLength = normalizeText(domCandidateInfo.resumeText).length;
|
|
5326
|
-
this.resumeSourceStats.dom_fallback += 1;
|
|
5327
|
-
candidateProfile = mergeCandidateProfiles(
|
|
5328
|
-
domCandidateInfo || null,
|
|
5329
|
-
cardProfile || null,
|
|
5330
|
-
{
|
|
5331
|
-
name: nextCandidate.name || "",
|
|
5332
|
-
school: nextCandidate.school || "",
|
|
5333
|
-
major: nextCandidate.major || "",
|
|
5334
|
-
company: nextCandidate.last_company || "",
|
|
5335
|
-
position: nextCandidate.last_position || ""
|
|
5336
|
-
}
|
|
5337
|
-
);
|
|
5338
5783
|
} else {
|
|
5339
|
-
|
|
5784
|
+
const domFallback = await timeCandidateStage(
|
|
5785
|
+
"dom_fallback_ms",
|
|
5786
|
+
() => this.resolveDomResumeFallback(nextCandidate, cardProfile || null)
|
|
5787
|
+
);
|
|
5788
|
+
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
5789
|
+
networkCandidateInfo = domFallback.networkCandidateInfo;
|
|
5790
|
+
screening = await timeCandidateStage(
|
|
5791
|
+
"text_model_ms",
|
|
5792
|
+
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
5793
|
+
);
|
|
5794
|
+
resumeSource = "network";
|
|
5795
|
+
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
5796
|
+
this.resumeSourceStats.network += 1;
|
|
5797
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5798
|
+
networkCandidateInfo || null,
|
|
5799
|
+
cardProfile || null,
|
|
5800
|
+
{
|
|
5801
|
+
name: nextCandidate.name || "",
|
|
5802
|
+
school: nextCandidate.school || "",
|
|
5803
|
+
major: nextCandidate.major || "",
|
|
5804
|
+
company: nextCandidate.last_company || "",
|
|
5805
|
+
position: nextCandidate.last_position || ""
|
|
5806
|
+
}
|
|
5807
|
+
);
|
|
5808
|
+
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5809
|
+
domCandidateInfo = domFallback.domCandidateInfo;
|
|
5810
|
+
screening = await timeCandidateStage(
|
|
5811
|
+
"text_model_ms",
|
|
5812
|
+
() => this.callTextModel(domCandidateInfo.resumeText)
|
|
5813
|
+
);
|
|
5814
|
+
resumeSource = "dom_fallback";
|
|
5815
|
+
resumeTextLength = normalizeText(domCandidateInfo.resumeText).length;
|
|
5816
|
+
this.resumeSourceStats.dom_fallback += 1;
|
|
5817
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5818
|
+
domCandidateInfo || null,
|
|
5819
|
+
cardProfile || null,
|
|
5820
|
+
{
|
|
5821
|
+
name: nextCandidate.name || "",
|
|
5822
|
+
school: nextCandidate.school || "",
|
|
5823
|
+
major: nextCandidate.major || "",
|
|
5824
|
+
company: nextCandidate.last_company || "",
|
|
5825
|
+
position: nextCandidate.last_position || ""
|
|
5826
|
+
}
|
|
5827
|
+
);
|
|
5828
|
+
} else {
|
|
5829
|
+
throw imageFallbackError;
|
|
5830
|
+
}
|
|
5340
5831
|
}
|
|
5341
5832
|
}
|
|
5342
5833
|
}
|
|
@@ -5356,13 +5847,16 @@ class RecommendScreenCli {
|
|
|
5356
5847
|
}
|
|
5357
5848
|
let actionResult = { actionTaken: "none" };
|
|
5358
5849
|
try {
|
|
5359
|
-
actionResult =
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
:
|
|
5850
|
+
actionResult = await timeCandidateStage(
|
|
5851
|
+
"post_action_ms",
|
|
5852
|
+
() => effectiveAction === "favorite"
|
|
5853
|
+
? this.favoriteCandidate({
|
|
5854
|
+
alreadyInterested: networkCandidateInfo?.alreadyInterested === true
|
|
5855
|
+
})
|
|
5856
|
+
: effectiveAction === "greet"
|
|
5857
|
+
? this.greetCandidate()
|
|
5858
|
+
: Promise.resolve({ actionTaken: "none" })
|
|
5859
|
+
);
|
|
5366
5860
|
} catch (postActionError) {
|
|
5367
5861
|
if (!isRecoverablePostActionError(postActionError, effectiveAction)) {
|
|
5368
5862
|
throw postActionError;
|
|
@@ -5395,7 +5889,7 @@ class RecommendScreenCli {
|
|
|
5395
5889
|
action: actionResult.actionTaken,
|
|
5396
5890
|
geekId: nextCandidate.geek_id,
|
|
5397
5891
|
summary: screening.summary,
|
|
5398
|
-
imagePath: capture?.stitchedImage || "",
|
|
5892
|
+
imagePath: capture?.stitchedImage || capture?.modelImagePaths?.[0] || capture?.chunkFiles?.[0] || "",
|
|
5399
5893
|
resumeSource
|
|
5400
5894
|
});
|
|
5401
5895
|
this.recordCandidateAudit({
|
|
@@ -5527,7 +6021,7 @@ class RecommendScreenCli {
|
|
|
5527
6021
|
);
|
|
5528
6022
|
}
|
|
5529
6023
|
} finally {
|
|
5530
|
-
const closed = await this.closeDetailPage();
|
|
6024
|
+
const closed = await timeCandidateStage("close_detail_ms", () => this.closeDetailPage());
|
|
5531
6025
|
if (!closed) {
|
|
5532
6026
|
if (allowDetailCloseFailure) {
|
|
5533
6027
|
log("[详情关闭兜底] 本候选人 post_action 失败后详情页关闭未确认,已记录错误并继续下一位候选人。");
|
|
@@ -5540,12 +6034,31 @@ class RecommendScreenCli {
|
|
|
5540
6034
|
}
|
|
5541
6035
|
}
|
|
5542
6036
|
|
|
5543
|
-
await this.takeBreakIfNeeded();
|
|
6037
|
+
await timeCandidateStage("rest_ms", () => this.takeBreakIfNeeded());
|
|
6038
|
+
candidateTiming.total_ms = Math.max(0, Date.now() - candidateStartedAt);
|
|
6039
|
+
this.updateCandidateAuditTiming(candidateKeyForTiming, candidateTiming);
|
|
5544
6040
|
try {
|
|
5545
|
-
this.saveCheckpoint();
|
|
6041
|
+
timeCandidateStageSync("checkpoint_save_ms", () => this.saveCheckpoint());
|
|
6042
|
+
candidateTiming.total_ms = Math.max(0, Date.now() - candidateStartedAt);
|
|
6043
|
+
this.updateCandidateAuditTiming(candidateKeyForTiming, candidateTiming);
|
|
5546
6044
|
} catch (checkpointError) {
|
|
5547
6045
|
log(`[保存checkpoint失败] ${checkpointError.message || checkpointError}`);
|
|
5548
6046
|
}
|
|
6047
|
+
try {
|
|
6048
|
+
this.saveCsv();
|
|
6049
|
+
} catch (csvError) {
|
|
6050
|
+
log(`[增量保存CSV失败] ${csvError.message || csvError}`);
|
|
6051
|
+
}
|
|
6052
|
+
log(
|
|
6053
|
+
`[TIMING] candidate=${candidateKeyForTiming || nextCandidate.name || "unknown"} ` +
|
|
6054
|
+
`total_ms=${candidateTiming.total_ms ?? ""} ` +
|
|
6055
|
+
`network_ms=${candidateTiming.network_resume_wait_ms ?? 0} ` +
|
|
6056
|
+
`text_model_ms=${candidateTiming.text_model_ms ?? 0} ` +
|
|
6057
|
+
`image_capture_ms=${candidateTiming.image_capture_ms ?? 0} ` +
|
|
6058
|
+
`vision_model_ms=${candidateTiming.vision_model_ms ?? 0} ` +
|
|
6059
|
+
`post_action_ms=${candidateTiming.post_action_ms ?? 0} ` +
|
|
6060
|
+
`close_ms=${candidateTiming.close_detail_ms ?? 0}`
|
|
6061
|
+
);
|
|
5549
6062
|
}
|
|
5550
6063
|
|
|
5551
6064
|
if (this.args.targetCount && this.passedCandidates.length < this.args.targetCount) {
|
|
@@ -5606,7 +6119,7 @@ async function main() {
|
|
|
5606
6119
|
console.log(JSON.stringify({
|
|
5607
6120
|
status: "COMPLETED",
|
|
5608
6121
|
result: {
|
|
5609
|
-
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> [--input-summary-json <json>] --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
6122
|
+
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --thinking-level off|low|medium|high|current --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> [--input-summary-json <json>] --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
5610
6123
|
}
|
|
5611
6124
|
}));
|
|
5612
6125
|
return;
|
|
@@ -5646,6 +6159,8 @@ if (require.main === module) {
|
|
|
5646
6159
|
MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES,
|
|
5647
6160
|
RESUME_CAPTURE_MAX_ATTEMPTS,
|
|
5648
6161
|
RESUME_CAPTURE_WAIT_MS,
|
|
6162
|
+
NETWORK_RESUME_IMAGE_MODE_GRACE_MS,
|
|
6163
|
+
NETWORK_RESUME_LATE_RETRY_MS,
|
|
5649
6164
|
parseFavoriteActionFromPostData,
|
|
5650
6165
|
parseFavoriteActionFromRequest,
|
|
5651
6166
|
parseFavoriteActionFromKnownRequest,
|