@reconcrap/boss-recommend-mcp 1.2.9 → 1.3.0
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 +82 -1
- package/package.json +2 -1
- package/skills/boss-chat/README.md +5 -0
- package/skills/boss-chat/SKILL.md +69 -0
- package/skills/boss-recommend-pipeline/SKILL.md +40 -4
- package/src/adapters.js +19 -5
- package/src/boss-chat.js +436 -0
- package/src/cli.js +294 -129
- package/src/index.js +459 -108
- package/src/parser.js +4 -5
- package/src/pipeline.js +605 -8
- package/src/run-state.js +5 -0
- package/src/test-adapters-runtime.js +69 -0
- package/src/test-boss-chat.js +399 -0
- package/src/test-index-async.js +238 -4
- package/src/test-parser.js +33 -6
- package/src/test-pipeline.js +408 -1
- package/vendor/boss-chat-cli/README.md +134 -0
- package/vendor/boss-chat-cli/package.json +53 -0
- package/vendor/boss-chat-cli/src/app.js +769 -0
- package/vendor/boss-chat-cli/src/browser/chat-page.js +2681 -0
- package/vendor/boss-chat-cli/src/cli.js +1350 -0
- package/vendor/boss-chat-cli/src/mcp/server.js +149 -0
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +193 -0
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +260 -0
- package/vendor/boss-chat-cli/src/runtime/interaction.js +102 -0
- package/vendor/boss-chat-cli/src/runtime/run-control.js +102 -0
- package/vendor/boss-chat-cli/src/services/chrome-client.js +97 -0
- package/vendor/boss-chat-cli/src/services/llm.js +352 -0
- package/vendor/boss-chat-cli/src/services/profile-store.js +157 -0
- package/vendor/boss-chat-cli/src/services/report-store.js +19 -0
- package/vendor/boss-chat-cli/src/services/resume-capture.js +554 -0
- package/vendor/boss-chat-cli/src/services/state-store.js +217 -0
- package/vendor/boss-chat-cli/src/utils/customer-key.js +82 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +902 -56
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +387 -1
|
@@ -8,7 +8,27 @@ const { captureFullResumeCanvas } = require("./scripts/capture-full-resume-canva
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_PORT = 9222;
|
|
10
10
|
const RECOMMEND_URL_FRAGMENT = "/web/chat/recommend";
|
|
11
|
-
const CSV_HEADER = [
|
|
11
|
+
const CSV_HEADER = [
|
|
12
|
+
"姓名",
|
|
13
|
+
"最高学历学校",
|
|
14
|
+
"最高学历专业",
|
|
15
|
+
"最近工作公司",
|
|
16
|
+
"最近工作职位",
|
|
17
|
+
"评估通过详细原因",
|
|
18
|
+
"处理结果",
|
|
19
|
+
"筛选原因",
|
|
20
|
+
"动作执行结果",
|
|
21
|
+
"简历来源",
|
|
22
|
+
"原始判定通过",
|
|
23
|
+
"最终判定通过",
|
|
24
|
+
"证据总数",
|
|
25
|
+
"证据命中数",
|
|
26
|
+
"证据门控降级",
|
|
27
|
+
"错误码",
|
|
28
|
+
"错误信息",
|
|
29
|
+
"候选人ID"
|
|
30
|
+
].join(",");
|
|
31
|
+
const INPUT_SUMMARY_HEADER = ["运行输入字段", "运行输入值"].join(",");
|
|
12
32
|
const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
13
33
|
const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
14
34
|
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
@@ -18,6 +38,7 @@ const DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS = 30000000;
|
|
|
18
38
|
const DEFAULT_TEXT_MODEL_CHUNK_SIZE_CHARS = 24000;
|
|
19
39
|
const DEFAULT_TEXT_MODEL_CHUNK_OVERLAP_CHARS = 1200;
|
|
20
40
|
const DEFAULT_TEXT_MODEL_MAX_CHUNKS = 12;
|
|
41
|
+
const MAX_EVIDENCE_TOKENS = 12;
|
|
21
42
|
let visionSharpFactory = null;
|
|
22
43
|
const PAGE_SCOPE_TAB_STATUS = {
|
|
23
44
|
recommend: "0",
|
|
@@ -139,6 +160,51 @@ const DETAIL_RESUME_IFRAME_SELECTORS = getHealingValue(
|
|
|
139
160
|
["selectors", "detail", "resume_iframe"],
|
|
140
161
|
['iframe[src*="/web/frame/c-resume/"]', 'iframe[name*="resume"]']
|
|
141
162
|
);
|
|
163
|
+
const RESUME_DOM_ROOT_SELECTORS = getHealingValue(
|
|
164
|
+
HEALING_RULES,
|
|
165
|
+
["selectors", "detail", "resume_dom_root"],
|
|
166
|
+
[
|
|
167
|
+
".resume-center-side",
|
|
168
|
+
".resume-detail-wrap",
|
|
169
|
+
".resume-item-detail",
|
|
170
|
+
".resume-section"
|
|
171
|
+
]
|
|
172
|
+
);
|
|
173
|
+
const RESUME_DOM_BLOCK_SELECTORS = getHealingValue(
|
|
174
|
+
HEALING_RULES,
|
|
175
|
+
["selectors", "detail", "resume_dom_blocks"],
|
|
176
|
+
[
|
|
177
|
+
".resume-section .section-title",
|
|
178
|
+
".resume-section .section-content",
|
|
179
|
+
".resume-section .item-content",
|
|
180
|
+
".resume-section .geek-desc",
|
|
181
|
+
".resume-section .text-item",
|
|
182
|
+
".resume-warning"
|
|
183
|
+
]
|
|
184
|
+
);
|
|
185
|
+
const RESUME_DOM_PROFILE_SELECTORS = {
|
|
186
|
+
name: [
|
|
187
|
+
".resume-section.geek-base-info-wrap .name",
|
|
188
|
+
".geek-name .name",
|
|
189
|
+
".name-wrap .name"
|
|
190
|
+
],
|
|
191
|
+
school: [
|
|
192
|
+
".geek-education-experience-wrap .school-name",
|
|
193
|
+
".edu-wrap .school-name"
|
|
194
|
+
],
|
|
195
|
+
major: [
|
|
196
|
+
".geek-education-experience-wrap .major",
|
|
197
|
+
".edu-wrap .major"
|
|
198
|
+
],
|
|
199
|
+
company: [
|
|
200
|
+
".geek-work-experience-wrap .company-name-wrap .name",
|
|
201
|
+
".geek-work-experience-wrap .company-name"
|
|
202
|
+
],
|
|
203
|
+
position: [
|
|
204
|
+
".geek-work-experience-wrap .position span",
|
|
205
|
+
".geek-work-experience-wrap .position"
|
|
206
|
+
]
|
|
207
|
+
};
|
|
142
208
|
const DETAIL_CLOSE_SELECTORS = getHealingValue(
|
|
143
209
|
HEALING_RULES,
|
|
144
210
|
["selectors", "detail", "close_button"],
|
|
@@ -307,6 +373,41 @@ function parsePositiveInteger(raw) {
|
|
|
307
373
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
308
374
|
}
|
|
309
375
|
|
|
376
|
+
function parseInputSummary(raw) {
|
|
377
|
+
const text = String(raw || "").trim();
|
|
378
|
+
if (!text) return null;
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(text);
|
|
381
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isSensitiveInputSummaryKey(key) {
|
|
388
|
+
const normalized = String(key || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
389
|
+
return normalized === "baseurl" || normalized === "apikey" || normalized === "model";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function sanitizeInputSummary(value) {
|
|
393
|
+
if (value === null || value === undefined) return null;
|
|
394
|
+
if (Array.isArray(value)) {
|
|
395
|
+
return value.map((item) => sanitizeInputSummary(item));
|
|
396
|
+
}
|
|
397
|
+
if (typeof value === "object") {
|
|
398
|
+
const sanitized = {};
|
|
399
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
400
|
+
if (isSensitiveInputSummaryKey(key)) continue;
|
|
401
|
+
const next = sanitizeInputSummary(raw);
|
|
402
|
+
if (next !== undefined) {
|
|
403
|
+
sanitized[key] = next;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return sanitized;
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
|
|
310
411
|
function resolveVisionPixelLimitFromEnv(envName, fallback) {
|
|
311
412
|
const parsed = parsePositiveInteger(process.env[envName]);
|
|
312
413
|
return parsed || fallback;
|
|
@@ -356,6 +457,85 @@ function toStringArray(value, maxItems = 8) {
|
|
|
356
457
|
return normalized;
|
|
357
458
|
}
|
|
358
459
|
|
|
460
|
+
function toLowerSafe(text) {
|
|
461
|
+
return String(text || "").toLowerCase();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function extractEvidenceTokens(text, maxItems = MAX_EVIDENCE_TOKENS) {
|
|
465
|
+
const normalized = normalizeText(text);
|
|
466
|
+
if (!normalized) return [];
|
|
467
|
+
const matched = normalized.match(/[\u4e00-\u9fff]{2,}|[A-Za-z][A-Za-z0-9.+#_-]{2,}|\d{3,}/g) || [];
|
|
468
|
+
const seen = new Set();
|
|
469
|
+
const picked = [];
|
|
470
|
+
const sorted = matched
|
|
471
|
+
.map((item) => normalizeText(item))
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.sort((a, b) => b.length - a.length);
|
|
474
|
+
for (const token of sorted) {
|
|
475
|
+
const key = toLowerSafe(token);
|
|
476
|
+
if (seen.has(key)) continue;
|
|
477
|
+
seen.add(key);
|
|
478
|
+
picked.push(token);
|
|
479
|
+
if (picked.length >= maxItems) break;
|
|
480
|
+
}
|
|
481
|
+
return picked;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function matchEvidenceAgainstResume(evidenceText, rawResumeText, normalizedResumeText, normalizedResumeLowerText) {
|
|
485
|
+
const normalizedEvidence = normalizeText(evidenceText);
|
|
486
|
+
if (!normalizedEvidence) {
|
|
487
|
+
return {
|
|
488
|
+
matched: false,
|
|
489
|
+
mode: "empty",
|
|
490
|
+
matchedTokens: []
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (rawResumeText.includes(evidenceText) || normalizedResumeText.includes(normalizedEvidence)) {
|
|
494
|
+
return {
|
|
495
|
+
matched: true,
|
|
496
|
+
mode: "exact",
|
|
497
|
+
matchedTokens: [normalizedEvidence]
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const evidenceTokens = extractEvidenceTokens(normalizedEvidence, MAX_EVIDENCE_TOKENS);
|
|
501
|
+
if (evidenceTokens.length <= 0) {
|
|
502
|
+
return {
|
|
503
|
+
matched: false,
|
|
504
|
+
mode: "token_empty",
|
|
505
|
+
matchedTokens: []
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const matchedTokens = [];
|
|
509
|
+
for (const token of evidenceTokens) {
|
|
510
|
+
if (normalizedResumeLowerText.includes(toLowerSafe(token))) {
|
|
511
|
+
matchedTokens.push(token);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const requiredHits = evidenceTokens.length >= 4 ? 2 : 1;
|
|
515
|
+
return {
|
|
516
|
+
matched: matchedTokens.length >= requiredHits,
|
|
517
|
+
mode: "token_fuzzy",
|
|
518
|
+
matchedTokens
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function formatEducationDegree(edu) {
|
|
523
|
+
const degreeName = normalizeText(edu?.degreeName || edu?.degreeCategory || "");
|
|
524
|
+
if (degreeName) return degreeName;
|
|
525
|
+
if (typeof edu?.degree === "string") {
|
|
526
|
+
return normalizeText(edu.degree);
|
|
527
|
+
}
|
|
528
|
+
return "";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function formatEducationSchoolTags(edu) {
|
|
532
|
+
if (!Array.isArray(edu?.schoolTags) || edu.schoolTags.length <= 0) return "";
|
|
533
|
+
const tags = edu.schoolTags
|
|
534
|
+
.map((item) => normalizeText(item?.name || item?.tagName || item))
|
|
535
|
+
.filter(Boolean);
|
|
536
|
+
return tags.join("、");
|
|
537
|
+
}
|
|
538
|
+
|
|
359
539
|
function splitTextByChunks(text, chunkSize, overlap, maxChunks) {
|
|
360
540
|
const source = String(text || "");
|
|
361
541
|
if (!source) return [];
|
|
@@ -642,7 +822,9 @@ function isRecoverablePostActionError(error, action) {
|
|
|
642
822
|
const normalizedCode = normalizeText(error?.code).toUpperCase();
|
|
643
823
|
if (!normalizedAction || !normalizedCode) return false;
|
|
644
824
|
if (normalizedAction === "favorite" && normalizedCode === "FAVORITE_BUTTON_FAILED") return true;
|
|
645
|
-
if (normalizedAction === "greet" &&
|
|
825
|
+
if (normalizedAction === "greet" && ["GREET_BUTTON_FAILED", "GREET_BUTTON_NOT_FOUND", "GREET_CONTINUE_BUTTON_FOUND"].includes(normalizedCode)) {
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
646
828
|
return false;
|
|
647
829
|
}
|
|
648
830
|
|
|
@@ -695,6 +877,7 @@ function parseArgs(argv) {
|
|
|
695
877
|
calibrationPath: getDefaultCalibrationPath(),
|
|
696
878
|
port: DEFAULT_PORT,
|
|
697
879
|
output: path.resolve(process.cwd(), `筛选结果_${Date.now()}.csv`),
|
|
880
|
+
inputSummary: null,
|
|
698
881
|
checkpointPath: null,
|
|
699
882
|
pauseControlPath: null,
|
|
700
883
|
resume: false,
|
|
@@ -766,6 +949,9 @@ function parseArgs(argv) {
|
|
|
766
949
|
} else if (token === "--output" && (inlineValue || next)) {
|
|
767
950
|
parsed.output = path.resolve(inlineValue || next);
|
|
768
951
|
if (!inlineValue) index += 1;
|
|
952
|
+
} else if ((token === "--input-summary-json" || token === "--inputSummaryJson") && (inlineValue || next)) {
|
|
953
|
+
parsed.inputSummary = parseInputSummary(inlineValue || next);
|
|
954
|
+
if (!inlineValue) index += 1;
|
|
769
955
|
} else if (token === "--checkpoint-path" && (inlineValue || next)) {
|
|
770
956
|
parsed.checkpointPath = path.resolve(inlineValue || next);
|
|
771
957
|
if (!inlineValue) index += 1;
|
|
@@ -922,6 +1108,56 @@ function csvEscape(value) {
|
|
|
922
1108
|
return `"${String(value || "").replace(/"/g, '""')}"`;
|
|
923
1109
|
}
|
|
924
1110
|
|
|
1111
|
+
function stringifyInputSummaryValue(value) {
|
|
1112
|
+
if (value === null) return "null";
|
|
1113
|
+
if (value === undefined) return "";
|
|
1114
|
+
if (typeof value === "string") return value;
|
|
1115
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1116
|
+
try {
|
|
1117
|
+
return JSON.stringify(value);
|
|
1118
|
+
} catch {
|
|
1119
|
+
return String(value);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function appendInputSummaryRows(rows, value, prefix = "") {
|
|
1124
|
+
if (value === null || value === undefined) {
|
|
1125
|
+
if (prefix) rows.push([prefix, stringifyInputSummaryValue(value)]);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (Array.isArray(value)) {
|
|
1129
|
+
rows.push([prefix, stringifyInputSummaryValue(value)]);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (typeof value !== "object") {
|
|
1133
|
+
rows.push([prefix, stringifyInputSummaryValue(value)]);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const entries = Object.entries(value);
|
|
1137
|
+
if (entries.length === 0) {
|
|
1138
|
+
if (prefix) rows.push([prefix, "{}"]);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
for (const [key, item] of entries) {
|
|
1142
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1143
|
+
if (!nextPrefix) continue;
|
|
1144
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
1145
|
+
appendInputSummaryRows(rows, item, nextPrefix);
|
|
1146
|
+
} else {
|
|
1147
|
+
rows.push([nextPrefix, stringifyInputSummaryValue(item)]);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function buildInputSummaryRows(inputSummary) {
|
|
1153
|
+
if (!inputSummary || typeof inputSummary !== "object" || Array.isArray(inputSummary)) {
|
|
1154
|
+
return [];
|
|
1155
|
+
}
|
|
1156
|
+
const rows = [];
|
|
1157
|
+
appendInputSummaryRows(rows, inputSummary);
|
|
1158
|
+
return rows;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
925
1161
|
function stripHtml(value) {
|
|
926
1162
|
return String(value || "")
|
|
927
1163
|
.replace(/<[^>]+>/g, "")
|
|
@@ -1096,7 +1332,7 @@ function isResumeRelatedWapiUrl(url) {
|
|
|
1096
1332
|
|
|
1097
1333
|
function formatResumeApiData(data) {
|
|
1098
1334
|
const parts = [];
|
|
1099
|
-
const geekDetail = data?.geekDetail || data || {};
|
|
1335
|
+
const geekDetail = data?.geekDetail || data?.geekDetailInfo || data || {};
|
|
1100
1336
|
const baseInfo = geekDetail.geekBaseInfo || {};
|
|
1101
1337
|
const expectList = geekDetail.geekExpectList || [];
|
|
1102
1338
|
const workExpList = geekDetail.geekWorkExpList || [];
|
|
@@ -1138,8 +1374,9 @@ function formatResumeApiData(data) {
|
|
|
1138
1374
|
if (exp.startYearMonStr) {
|
|
1139
1375
|
parts.push(` 时间: ${exp.startYearMonStr} ~ ${exp.endYearMonStr || "至今"}`);
|
|
1140
1376
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1377
|
+
const workContent = exp.responsibility || exp.workContent || "";
|
|
1378
|
+
if (workContent) {
|
|
1379
|
+
parts.push(` 职责: ${stripHtml(workContent)}`);
|
|
1143
1380
|
}
|
|
1144
1381
|
});
|
|
1145
1382
|
}
|
|
@@ -1147,13 +1384,15 @@ function formatResumeApiData(data) {
|
|
|
1147
1384
|
if (projExpList.length > 0) {
|
|
1148
1385
|
parts.push("\n=== 项目经历 ===");
|
|
1149
1386
|
projExpList.forEach((proj, index) => {
|
|
1150
|
-
parts.push(`${index + 1}. ${proj.name || "未知项目"}`);
|
|
1387
|
+
parts.push(`${index + 1}. ${proj.name || proj.projectName || "未知项目"}`);
|
|
1151
1388
|
if (proj.roleName) parts.push(` 角色: ${proj.roleName}`);
|
|
1152
1389
|
if (proj.startYearMonStr) {
|
|
1153
1390
|
parts.push(` 时间: ${proj.startYearMonStr} ~ ${proj.endYearMonStr || "至今"}`);
|
|
1154
1391
|
}
|
|
1155
|
-
|
|
1156
|
-
if (
|
|
1392
|
+
const projectDescription = proj.description || proj.projectDescription || "";
|
|
1393
|
+
if (projectDescription) parts.push(` 描述: ${stripHtml(projectDescription)}`);
|
|
1394
|
+
const projectPerformance = proj.performance || proj.projectPerformance || "";
|
|
1395
|
+
if (projectPerformance) parts.push(` 成果: ${stripHtml(projectPerformance)}`);
|
|
1157
1396
|
});
|
|
1158
1397
|
}
|
|
1159
1398
|
|
|
@@ -1162,13 +1401,17 @@ function formatResumeApiData(data) {
|
|
|
1162
1401
|
eduExpList.forEach((edu, index) => {
|
|
1163
1402
|
parts.push(`${index + 1}. ${edu.school || edu.schoolName || "未知学校"}`);
|
|
1164
1403
|
if (edu.major || edu.majorName) parts.push(` 专业: ${edu.major || edu.majorName}`);
|
|
1165
|
-
const eduDegree = edu
|
|
1404
|
+
const eduDegree = formatEducationDegree(edu);
|
|
1166
1405
|
if (eduDegree) parts.push(` 学历: ${eduDegree}`);
|
|
1167
1406
|
const eduStart = edu.startYearMonStr || edu.startYearStr;
|
|
1168
1407
|
if (eduStart) {
|
|
1169
1408
|
const eduEnd = edu.endYearMonStr || edu.endYearStr || "";
|
|
1170
1409
|
parts.push(` 时间: ${eduStart} ~ ${eduEnd}`);
|
|
1171
1410
|
}
|
|
1411
|
+
const schoolTags = formatEducationSchoolTags(edu);
|
|
1412
|
+
if (schoolTags) {
|
|
1413
|
+
parts.push(` 学校标签: ${schoolTags}`);
|
|
1414
|
+
}
|
|
1172
1415
|
});
|
|
1173
1416
|
}
|
|
1174
1417
|
|
|
@@ -1599,6 +1842,182 @@ const jsWaitForDetail = `(() => {
|
|
|
1599
1842
|
return { open, scope: 'frame' };
|
|
1600
1843
|
})()`;
|
|
1601
1844
|
|
|
1845
|
+
const jsExtractResumeTextFromDom = `(() => {
|
|
1846
|
+
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1847
|
+
const rootSelectors = ${JSON.stringify(RESUME_DOM_ROOT_SELECTORS)};
|
|
1848
|
+
const blockSelectors = ${JSON.stringify(RESUME_DOM_BLOCK_SELECTORS)};
|
|
1849
|
+
const profileSelectors = ${JSON.stringify(RESUME_DOM_PROFILE_SELECTORS)};
|
|
1850
|
+
|
|
1851
|
+
const isVisible = (doc, el) => {
|
|
1852
|
+
if (!el) return false;
|
|
1853
|
+
const win = (doc && doc.defaultView) || window;
|
|
1854
|
+
const style = win.getComputedStyle(el);
|
|
1855
|
+
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') < 0.02) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
const rect = el.getBoundingClientRect();
|
|
1859
|
+
return rect.width > 2 && rect.height > 2;
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
const pickFirstText = (doc, scopeRoot, selectors) => {
|
|
1863
|
+
const scopeNode = scopeRoot && typeof scopeRoot.querySelectorAll === 'function' ? scopeRoot : doc;
|
|
1864
|
+
for (const selector of selectors || []) {
|
|
1865
|
+
let nodes = [];
|
|
1866
|
+
try {
|
|
1867
|
+
nodes = Array.from(scopeNode.querySelectorAll(selector)).slice(0, 12);
|
|
1868
|
+
} catch {
|
|
1869
|
+
nodes = [];
|
|
1870
|
+
}
|
|
1871
|
+
for (const node of nodes) {
|
|
1872
|
+
if (!isVisible(doc, node)) continue;
|
|
1873
|
+
const text = normalize(node.textContent || '');
|
|
1874
|
+
if (text) return text;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return '';
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const extractProfileFromRoot = (doc, root) => ({
|
|
1881
|
+
name: pickFirstText(doc, root, profileSelectors.name),
|
|
1882
|
+
school: pickFirstText(doc, root, profileSelectors.school),
|
|
1883
|
+
major: pickFirstText(doc, root, profileSelectors.major),
|
|
1884
|
+
company: pickFirstText(doc, root, profileSelectors.company),
|
|
1885
|
+
position: pickFirstText(doc, root, profileSelectors.position)
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
const extractRootText = (doc, root) => {
|
|
1889
|
+
const sectionSelector = '.resume-section';
|
|
1890
|
+
const titleSelector = '.section-title';
|
|
1891
|
+
const contentSelector = '.section-content';
|
|
1892
|
+
const dedup = new Set();
|
|
1893
|
+
const lines = [];
|
|
1894
|
+
const pushLine = (raw) => {
|
|
1895
|
+
const text = normalize(raw);
|
|
1896
|
+
if (!text) return;
|
|
1897
|
+
const key = text.toLowerCase();
|
|
1898
|
+
if (dedup.has(key)) return;
|
|
1899
|
+
dedup.add(key);
|
|
1900
|
+
lines.push(text);
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
let sections = [];
|
|
1904
|
+
try {
|
|
1905
|
+
sections = Array.from(root.querySelectorAll(sectionSelector)).slice(0, 120);
|
|
1906
|
+
} catch {
|
|
1907
|
+
sections = [];
|
|
1908
|
+
}
|
|
1909
|
+
if (sections.length > 0) {
|
|
1910
|
+
for (const section of sections) {
|
|
1911
|
+
if (!isVisible(doc, section)) continue;
|
|
1912
|
+
const title = normalize((section.querySelector(titleSelector)?.textContent) || '');
|
|
1913
|
+
const contentNode = section.querySelector(contentSelector);
|
|
1914
|
+
const content = normalize((contentNode && contentNode.textContent) || section.textContent || '');
|
|
1915
|
+
if (title && content) {
|
|
1916
|
+
pushLine('[' + title + '] ' + content);
|
|
1917
|
+
} else if (content) {
|
|
1918
|
+
pushLine(content);
|
|
1919
|
+
} else if (title) {
|
|
1920
|
+
pushLine('[' + title + ']');
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
if (lines.length === 0) {
|
|
1926
|
+
let blocks = [];
|
|
1927
|
+
try {
|
|
1928
|
+
blocks = Array.from(root.querySelectorAll(blockSelectors.join(','))).slice(0, 260);
|
|
1929
|
+
} catch {
|
|
1930
|
+
blocks = [];
|
|
1931
|
+
}
|
|
1932
|
+
if (blocks.length > 0) {
|
|
1933
|
+
for (const node of blocks) {
|
|
1934
|
+
if (!isVisible(doc, node)) continue;
|
|
1935
|
+
pushLine(node.textContent || '');
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (lines.length === 0) {
|
|
1941
|
+
pushLine(root.textContent || '');
|
|
1942
|
+
}
|
|
1943
|
+
return normalize(lines.join('\\n'));
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
const collectFromDocument = (doc, scope) => {
|
|
1947
|
+
if (!doc) return [];
|
|
1948
|
+
const rows = [];
|
|
1949
|
+
const seen = new Set();
|
|
1950
|
+
|
|
1951
|
+
const pushCandidate = (root, selectorLabel) => {
|
|
1952
|
+
if (!root || seen.has(root)) return;
|
|
1953
|
+
seen.add(root);
|
|
1954
|
+
if (!isVisible(doc, root)) return;
|
|
1955
|
+
const text = extractRootText(doc, root);
|
|
1956
|
+
if (text.length < 120) return;
|
|
1957
|
+
const profile = extractProfileFromRoot(doc, root);
|
|
1958
|
+
rows.push({
|
|
1959
|
+
scope,
|
|
1960
|
+
selector: selectorLabel,
|
|
1961
|
+
text,
|
|
1962
|
+
text_length: text.length,
|
|
1963
|
+
name: profile.name || '',
|
|
1964
|
+
school: profile.school || '',
|
|
1965
|
+
major: profile.major || '',
|
|
1966
|
+
company: profile.company || '',
|
|
1967
|
+
position: profile.position || ''
|
|
1968
|
+
});
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
for (const selector of rootSelectors) {
|
|
1972
|
+
let nodes = [];
|
|
1973
|
+
try {
|
|
1974
|
+
nodes = Array.from(doc.querySelectorAll(selector)).slice(0, 20);
|
|
1975
|
+
} catch {
|
|
1976
|
+
nodes = [];
|
|
1977
|
+
}
|
|
1978
|
+
for (const node of nodes) {
|
|
1979
|
+
pushCandidate(node, selector);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (rows.length === 0) {
|
|
1984
|
+
const fallbackRoot = doc.querySelector('.resume-center-side')
|
|
1985
|
+
|| doc.querySelector('.resume-detail-wrap')
|
|
1986
|
+
|| doc.querySelector('.resume-section');
|
|
1987
|
+
if (fallbackRoot) {
|
|
1988
|
+
pushCandidate(fallbackRoot, 'fallback_any_resume_root');
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
return rows;
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
const topRows = collectFromDocument(document, 'top');
|
|
1995
|
+
let frameRows = [];
|
|
1996
|
+
try {
|
|
1997
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1998
|
+
if (frame && frame.contentDocument) {
|
|
1999
|
+
frameRows = collectFromDocument(frame.contentDocument, 'frame');
|
|
2000
|
+
}
|
|
2001
|
+
} catch {}
|
|
2002
|
+
|
|
2003
|
+
const candidates = [...topRows, ...frameRows]
|
|
2004
|
+
.filter((item) => normalize(item?.text || '').length > 0)
|
|
2005
|
+
.sort((a, b) => Number(b?.text_length || 0) - Number(a?.text_length || 0));
|
|
2006
|
+
const best = candidates[0] || null;
|
|
2007
|
+
if (!best) {
|
|
2008
|
+
return {
|
|
2009
|
+
ok: false,
|
|
2010
|
+
reason: 'resume_dom_not_found',
|
|
2011
|
+
candidate_count: 0
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
return {
|
|
2015
|
+
ok: true,
|
|
2016
|
+
...best,
|
|
2017
|
+
candidate_count: candidates.length
|
|
2018
|
+
};
|
|
2019
|
+
})()`;
|
|
2020
|
+
|
|
1602
2021
|
const jsCloseDetail = `(() => {
|
|
1603
2022
|
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1604
2023
|
const pickVisibleKnowButton = (rootDoc) => {
|
|
@@ -1882,7 +2301,18 @@ const jsGetGreetStateRecommend = `(() => {
|
|
|
1882
2301
|
const resolveGreet = (doc, offsetX, offsetY, scope) => {
|
|
1883
2302
|
if (!doc) return null;
|
|
1884
2303
|
const candidates = ${buildSelectorCollectionExpression(GREET_BUTTON_RECOMMEND_SELECTORS, "doc")};
|
|
1885
|
-
const
|
|
2304
|
+
const visibleButtons = candidates.filter((item) => isVisible(doc, item));
|
|
2305
|
+
const normalizeLabel = (item) => normalize(item?.textContent || '');
|
|
2306
|
+
const isContinue = (item) => /继续沟通/.test(normalizeLabel(item));
|
|
2307
|
+
const isGreetEntry = (item) => (
|
|
2308
|
+
/打招呼|聊一聊|立即沟通/.test(normalizeLabel(item))
|
|
2309
|
+
|| (/沟通/.test(normalizeLabel(item)) && !isContinue(item))
|
|
2310
|
+
);
|
|
2311
|
+
const button = visibleButtons.find((item) => isGreetEntry(item)) || null;
|
|
2312
|
+
const continueButton = visibleButtons.find((item) => isContinue(item)) || null;
|
|
2313
|
+
if (!button && continueButton) {
|
|
2314
|
+
return { ok: false, error: 'GREET_CONTINUE_BUTTON_FOUND', scope };
|
|
2315
|
+
}
|
|
1886
2316
|
if (!button) return null;
|
|
1887
2317
|
const rect = button.getBoundingClientRect();
|
|
1888
2318
|
return {
|
|
@@ -1908,7 +2338,12 @@ const jsGetGreetStateRecommend = `(() => {
|
|
|
1908
2338
|
|
|
1909
2339
|
const jsClickGreetFallbackRecommend = `(() => {
|
|
1910
2340
|
const topButton = Array.from(document.querySelectorAll('.resume-footer.item-operate button, .resume-footer-wrap button, button.btn-v2.btn-sure-v2'))
|
|
1911
|
-
.find((item) =>
|
|
2341
|
+
.find((item) => {
|
|
2342
|
+
if (!item || item.offsetParent === null) return false;
|
|
2343
|
+
const text = String(item.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
2344
|
+
if (/继续沟通/.test(text)) return false;
|
|
2345
|
+
return /打招呼|聊一聊|立即沟通/.test(text) || /沟通/.test(text);
|
|
2346
|
+
});
|
|
1912
2347
|
if (topButton) {
|
|
1913
2348
|
topButton.click();
|
|
1914
2349
|
return { ok: true, scope: 'top' };
|
|
@@ -1916,7 +2351,13 @@ const jsClickGreetFallbackRecommend = `(() => {
|
|
|
1916
2351
|
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1917
2352
|
if (!frame || !frame.contentDocument) return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1918
2353
|
const doc = frame.contentDocument;
|
|
1919
|
-
const button = ${
|
|
2354
|
+
const button = ${buildSelectorCollectionExpression(GREET_BUTTON_RECOMMEND_SELECTORS, "doc")}
|
|
2355
|
+
.find((item) => {
|
|
2356
|
+
if (!item || item.offsetParent === null) return false;
|
|
2357
|
+
const text = String(item.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
2358
|
+
if (/继续沟通/.test(text)) return false;
|
|
2359
|
+
return /打招呼|聊一聊|立即沟通/.test(text) || /沟通/.test(text);
|
|
2360
|
+
}) || null;
|
|
1920
2361
|
if (!button || button.offsetParent === null) return { ok: false, error: 'GREET_BUTTON_NOT_FOUND' };
|
|
1921
2362
|
button.click();
|
|
1922
2363
|
return { ok: true };
|
|
@@ -1937,7 +2378,18 @@ const jsGetGreetStateFeatured = `(() => {
|
|
|
1937
2378
|
const resolveGreet = (doc, offsetX, offsetY, scope) => {
|
|
1938
2379
|
if (!doc) return null;
|
|
1939
2380
|
const candidates = ${buildSelectorCollectionExpression(GREET_BUTTON_FEATURED_SELECTORS, "doc")};
|
|
1940
|
-
const
|
|
2381
|
+
const visibleButtons = candidates.filter((item) => isVisible(doc, item));
|
|
2382
|
+
const normalizeLabel = (item) => normalize(item?.textContent || '');
|
|
2383
|
+
const isContinue = (item) => /继续沟通/.test(normalizeLabel(item));
|
|
2384
|
+
const isGreetEntry = (item) => (
|
|
2385
|
+
/打招呼|聊一聊|立即沟通/.test(normalizeLabel(item))
|
|
2386
|
+
|| (/沟通/.test(normalizeLabel(item)) && !isContinue(item))
|
|
2387
|
+
);
|
|
2388
|
+
const button = visibleButtons.find((item) => isGreetEntry(item)) || null;
|
|
2389
|
+
const continueButton = visibleButtons.find((item) => isContinue(item)) || null;
|
|
2390
|
+
if (!button && continueButton) {
|
|
2391
|
+
return { ok: false, error: 'GREET_CONTINUE_BUTTON_FOUND', scope };
|
|
2392
|
+
}
|
|
1941
2393
|
if (!button) return null;
|
|
1942
2394
|
const rect = button.getBoundingClientRect();
|
|
1943
2395
|
return {
|
|
@@ -1963,7 +2415,12 @@ const jsGetGreetStateFeatured = `(() => {
|
|
|
1963
2415
|
|
|
1964
2416
|
const jsClickGreetFallbackFeatured = `(() => {
|
|
1965
2417
|
const topButton = Array.from(document.querySelectorAll('button.btn-v2.position-rights.btn-sure-v2, button.btn-v2.btn-sure-v2.position-rights, .resume-footer.item-operate button, .resume-footer-wrap button'))
|
|
1966
|
-
.find((item) =>
|
|
2418
|
+
.find((item) => {
|
|
2419
|
+
if (!item || item.offsetParent === null) return false;
|
|
2420
|
+
const text = String(item.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
2421
|
+
if (/继续沟通/.test(text)) return false;
|
|
2422
|
+
return /打招呼|聊一聊|立即沟通/.test(text) || /沟通/.test(text);
|
|
2423
|
+
});
|
|
1967
2424
|
if (topButton) {
|
|
1968
2425
|
topButton.click();
|
|
1969
2426
|
return { ok: true, scope: 'top' };
|
|
@@ -1971,7 +2428,13 @@ const jsClickGreetFallbackFeatured = `(() => {
|
|
|
1971
2428
|
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
1972
2429
|
if (!frame || !frame.contentDocument) return { ok: false, error: 'NO_RECOMMEND_IFRAME' };
|
|
1973
2430
|
const doc = frame.contentDocument;
|
|
1974
|
-
const button = ${
|
|
2431
|
+
const button = ${buildSelectorCollectionExpression(GREET_BUTTON_FEATURED_SELECTORS, "doc")}
|
|
2432
|
+
.find((item) => {
|
|
2433
|
+
if (!item || item.offsetParent === null) return false;
|
|
2434
|
+
const text = String(item.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
2435
|
+
if (/继续沟通/.test(text)) return false;
|
|
2436
|
+
return /打招呼|聊一聊|立即沟通/.test(text) || /沟通/.test(text);
|
|
2437
|
+
}) || null;
|
|
1975
2438
|
if (!button || button.offsetParent === null) return { ok: false, error: 'GREET_BUTTON_NOT_FOUND' };
|
|
1976
2439
|
button.click();
|
|
1977
2440
|
return { ok: true };
|
|
@@ -2261,8 +2724,10 @@ class RecommendScreenCli {
|
|
|
2261
2724
|
this.favoriteClickPendingSince = 0;
|
|
2262
2725
|
this.favoriteNetworkTraces = [];
|
|
2263
2726
|
this.webSocketByRequestId = new Map();
|
|
2727
|
+
this.candidateAudits = [];
|
|
2264
2728
|
this.resumeSourceStats = {
|
|
2265
2729
|
network: 0,
|
|
2730
|
+
dom_fallback: 0,
|
|
2266
2731
|
image_fallback: 0
|
|
2267
2732
|
};
|
|
2268
2733
|
this.lastActiveTabStatus = PAGE_SCOPE_TAB_STATUS[this.args.pageScope] || null;
|
|
@@ -2273,6 +2738,7 @@ class RecommendScreenCli {
|
|
|
2273
2738
|
this.restThreshold = 25 + Math.floor(Math.random() * 8);
|
|
2274
2739
|
this.checkpointPath = this.args.checkpointPath ? path.resolve(this.args.checkpointPath) : null;
|
|
2275
2740
|
this.pauseControlPath = this.args.pauseControlPath ? path.resolve(this.args.pauseControlPath) : null;
|
|
2741
|
+
this.inputSummary = sanitizeInputSummary(this.args.inputSummary);
|
|
2276
2742
|
this.debugDir = path.join(os.tmpdir(), "boss-recommend-screen", String(Date.now()));
|
|
2277
2743
|
fs.mkdirSync(this.debugDir, { recursive: true });
|
|
2278
2744
|
}
|
|
@@ -2323,7 +2789,32 @@ class RecommendScreenCli {
|
|
|
2323
2789
|
summary: item?.summary || "",
|
|
2324
2790
|
imagePath: item?.imagePath || "",
|
|
2325
2791
|
resumeSource: item?.resumeSource || ""
|
|
2326
|
-
}))
|
|
2792
|
+
})),
|
|
2793
|
+
candidate_audits: this.candidateAudits.map((item) => ({
|
|
2794
|
+
ts: item?.ts || null,
|
|
2795
|
+
candidate_key: item?.candidate_key || "",
|
|
2796
|
+
geek_id: item?.geek_id || "",
|
|
2797
|
+
candidate_name: item?.candidate_name || "",
|
|
2798
|
+
school: item?.school || "",
|
|
2799
|
+
major: item?.major || "",
|
|
2800
|
+
company: item?.company || "",
|
|
2801
|
+
position: item?.position || "",
|
|
2802
|
+
outcome: item?.outcome || "",
|
|
2803
|
+
resume_source: item?.resume_source || "",
|
|
2804
|
+
resume_text_len: Number.isFinite(Number(item?.resume_text_len)) ? Number(item.resume_text_len) : null,
|
|
2805
|
+
raw_passed: item?.raw_passed === true,
|
|
2806
|
+
final_passed: item?.final_passed === true,
|
|
2807
|
+
evidence_raw_count: Number.isFinite(Number(item?.evidence_raw_count)) ? Number(item.evidence_raw_count) : null,
|
|
2808
|
+
evidence_matched_count: Number.isFinite(Number(item?.evidence_matched_count)) ? Number(item.evidence_matched_count) : null,
|
|
2809
|
+
evidence_gate_demoted: item?.evidence_gate_demoted === true,
|
|
2810
|
+
screening_reason: item?.screening_reason || "",
|
|
2811
|
+
action_taken: item?.action_taken || "",
|
|
2812
|
+
error_code: item?.error_code || "",
|
|
2813
|
+
error_message: item?.error_message || "",
|
|
2814
|
+
chunk_index: Number.isFinite(Number(item?.chunk_index)) ? Number(item.chunk_index) : null,
|
|
2815
|
+
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null
|
|
2816
|
+
})),
|
|
2817
|
+
input_summary: sanitizeInputSummary(this.inputSummary)
|
|
2327
2818
|
};
|
|
2328
2819
|
}
|
|
2329
2820
|
|
|
@@ -2339,6 +2830,8 @@ class RecommendScreenCli {
|
|
|
2339
2830
|
active_tab_status: this.lastActiveTabStatus || PAGE_SCOPE_TAB_STATUS[this.args.pageScope] || null,
|
|
2340
2831
|
resume_source: this.resumeSourceStats.image_fallback > 0
|
|
2341
2832
|
? "image_fallback"
|
|
2833
|
+
: this.resumeSourceStats.dom_fallback > 0
|
|
2834
|
+
? "dom_fallback"
|
|
2342
2835
|
: this.resumeSourceStats.network > 0
|
|
2343
2836
|
? "network"
|
|
2344
2837
|
: defaultResumeSource,
|
|
@@ -2443,10 +2936,51 @@ class RecommendScreenCli {
|
|
|
2443
2936
|
if (item.kind === "wait_timeout") {
|
|
2444
2937
|
return `${prefix} candidate=${item.candidate_key || "-"} waited_ms=${item.waited_ms ?? "?"} reason=${item.reason || "timeout"}`;
|
|
2445
2938
|
}
|
|
2939
|
+
if (item.kind === "dom_fallback_hit") {
|
|
2940
|
+
return `${prefix} candidate=${item.candidate_key || "-"} scope=${item.source || "-"} selector=${item.reason || "-"} resume_len=${item.resume_text_len ?? "?"}`;
|
|
2941
|
+
}
|
|
2942
|
+
if (item.kind === "dom_fallback_miss") {
|
|
2943
|
+
return `${prefix} candidate=${item.candidate_key || "-"} reason=${item.reason || "dom_not_found"}`;
|
|
2944
|
+
}
|
|
2945
|
+
if (item.kind === "dom_fallback_error") {
|
|
2946
|
+
return `${prefix} candidate=${item.candidate_key || "-"} error=${item.error || "unknown"}`;
|
|
2947
|
+
}
|
|
2446
2948
|
return `${prefix} ${item.url || item.reason || "n/a"}`;
|
|
2447
2949
|
});
|
|
2448
2950
|
}
|
|
2449
2951
|
|
|
2952
|
+
recordCandidateAudit(entry = {}) {
|
|
2953
|
+
const normalized = {
|
|
2954
|
+
ts: new Date().toISOString(),
|
|
2955
|
+
candidate_key: normalizeText(entry?.candidate_key || entry?.geek_id || "") || "",
|
|
2956
|
+
geek_id: normalizeText(entry?.geek_id || entry?.candidate_key || "") || "",
|
|
2957
|
+
candidate_name: normalizeText(entry?.candidate_name || "") || "",
|
|
2958
|
+
school: normalizeText(entry?.school || "") || "",
|
|
2959
|
+
major: normalizeText(entry?.major || "") || "",
|
|
2960
|
+
company: normalizeText(entry?.company || "") || "",
|
|
2961
|
+
position: normalizeText(entry?.position || "") || "",
|
|
2962
|
+
outcome: normalizeText(entry?.outcome || "unknown") || "unknown",
|
|
2963
|
+
resume_source: normalizeText(entry?.resume_source || "") || "",
|
|
2964
|
+
resume_text_len: Number.isFinite(Number(entry?.resume_text_len)) ? Number(entry.resume_text_len) : null,
|
|
2965
|
+
raw_passed: entry?.raw_passed === true,
|
|
2966
|
+
final_passed: entry?.final_passed === true,
|
|
2967
|
+
evidence_raw_count: Number.isFinite(Number(entry?.evidence_raw_count)) ? Number(entry.evidence_raw_count) : null,
|
|
2968
|
+
evidence_matched_count: Number.isFinite(Number(entry?.evidence_matched_count)) ? Number(entry.evidence_matched_count) : null,
|
|
2969
|
+
evidence_gate_demoted: entry?.evidence_gate_demoted === true,
|
|
2970
|
+
screening_reason: normalizeText(entry?.screening_reason || "") || "",
|
|
2971
|
+
action_taken: normalizeText(entry?.action_taken || "") || "",
|
|
2972
|
+
error_code: normalizeText(entry?.error_code || "") || "",
|
|
2973
|
+
error_message: normalizeText(entry?.error_message || "") || "",
|
|
2974
|
+
chunk_index: Number.isFinite(Number(entry?.chunk_index)) ? Number(entry.chunk_index) : null,
|
|
2975
|
+
chunk_total: Number.isFinite(Number(entry?.chunk_total)) ? Number(entry.chunk_total) : null
|
|
2976
|
+
};
|
|
2977
|
+
this.candidateAudits.push(normalized);
|
|
2978
|
+
const maxItems = parsePositiveInteger(process.env.BOSS_RECOMMEND_MAX_CANDIDATE_AUDITS);
|
|
2979
|
+
if (maxItems && this.candidateAudits.length > maxItems) {
|
|
2980
|
+
this.candidateAudits = this.candidateAudits.slice(-maxItems);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2450
2984
|
logResumeNetworkMissDiagnostics(candidate, options = {}) {
|
|
2451
2985
|
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
2452
2986
|
const candidateName = normalizeText(candidate?.name || "");
|
|
@@ -2507,7 +3041,9 @@ class RecommendScreenCli {
|
|
|
2507
3041
|
};
|
|
2508
3042
|
this.latestResumeNetworkPayload = wrapped;
|
|
2509
3043
|
for (const id of geekIds) {
|
|
2510
|
-
|
|
3044
|
+
const normalizedId = normalizeText(id);
|
|
3045
|
+
if (!normalizedId) continue;
|
|
3046
|
+
this.resumeNetworkByGeekId.set(normalizedId, wrapped);
|
|
2511
3047
|
}
|
|
2512
3048
|
}
|
|
2513
3049
|
|
|
@@ -2518,7 +3054,13 @@ class RecommendScreenCli {
|
|
|
2518
3054
|
}
|
|
2519
3055
|
if (this.latestResumeNetworkPayload) {
|
|
2520
3056
|
const ageMs = Date.now() - Number(this.latestResumeNetworkPayload.ts || 0);
|
|
2521
|
-
|
|
3057
|
+
const latestGeekIds = Array.isArray(this.latestResumeNetworkPayload.geekIds)
|
|
3058
|
+
? this.latestResumeNetworkPayload.geekIds.map((id) => normalizeText(id)).filter(Boolean)
|
|
3059
|
+
: [];
|
|
3060
|
+
if (!candidateKey && ageMs <= 12000) {
|
|
3061
|
+
return this.latestResumeNetworkPayload.candidateInfo || null;
|
|
3062
|
+
}
|
|
3063
|
+
if (candidateKey && ageMs <= 12000 && latestGeekIds.includes(candidateKey)) {
|
|
2522
3064
|
return this.latestResumeNetworkPayload.candidateInfo || null;
|
|
2523
3065
|
}
|
|
2524
3066
|
}
|
|
@@ -2532,9 +3074,12 @@ class RecommendScreenCli {
|
|
|
2532
3074
|
while (Date.now() < deadline) {
|
|
2533
3075
|
const info = this.tryExtractNetworkResumeForCandidate(candidate);
|
|
2534
3076
|
if (info && normalizeText(info.resumeText)) {
|
|
3077
|
+
const latestGeekIds = Array.isArray(this.latestResumeNetworkPayload?.geekIds)
|
|
3078
|
+
? this.latestResumeNetworkPayload.geekIds.map((id) => normalizeText(id)).filter(Boolean)
|
|
3079
|
+
: [];
|
|
2535
3080
|
const source = candidateKey && this.resumeNetworkByGeekId.has(candidateKey)
|
|
2536
3081
|
? "geek_id_map"
|
|
2537
|
-
: "latest_payload";
|
|
3082
|
+
: (candidateKey && latestGeekIds.includes(candidateKey) ? "latest_payload_key_match" : "latest_payload");
|
|
2538
3083
|
this.recordResumeNetworkDiagnostic({
|
|
2539
3084
|
kind: "wait_hit",
|
|
2540
3085
|
candidate_key: candidateKey,
|
|
@@ -2555,6 +3100,71 @@ class RecommendScreenCli {
|
|
|
2555
3100
|
return null;
|
|
2556
3101
|
}
|
|
2557
3102
|
|
|
3103
|
+
async extractResumeTextFromDom(candidate) {
|
|
3104
|
+
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
3105
|
+
const candidateLabel = normalizeText(candidate?.name || candidateKey || "unknown");
|
|
3106
|
+
if (!this.Runtime || typeof this.Runtime.evaluate !== "function") {
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
let extracted = null;
|
|
3110
|
+
try {
|
|
3111
|
+
extracted = await this.evaluate(jsExtractResumeTextFromDom);
|
|
3112
|
+
} catch (error) {
|
|
3113
|
+
this.recordResumeNetworkDiagnostic({
|
|
3114
|
+
kind: "dom_fallback_error",
|
|
3115
|
+
candidate_key: candidateKey,
|
|
3116
|
+
error: normalizeText(error?.message || error)
|
|
3117
|
+
});
|
|
3118
|
+
log(`[DOM简历提取失败] candidate=${candidateLabel} error=${normalizeText(error?.message || error)}`);
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
if (!extracted || extracted.ok !== true) {
|
|
3122
|
+
this.recordResumeNetworkDiagnostic({
|
|
3123
|
+
kind: "dom_fallback_miss",
|
|
3124
|
+
candidate_key: candidateKey,
|
|
3125
|
+
reason: normalizeText(extracted?.reason || "resume_dom_not_found")
|
|
3126
|
+
});
|
|
3127
|
+
log(
|
|
3128
|
+
`[DOM简历未命中] candidate=${candidateLabel} reason=${normalizeText(extracted?.reason || "resume_dom_not_found")}`
|
|
3129
|
+
);
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
const resumeText = normalizeText(extracted.text || "");
|
|
3134
|
+
if (!resumeText) {
|
|
3135
|
+
this.recordResumeNetworkDiagnostic({
|
|
3136
|
+
kind: "dom_fallback_miss",
|
|
3137
|
+
candidate_key: candidateKey,
|
|
3138
|
+
reason: "resume_dom_text_empty"
|
|
3139
|
+
});
|
|
3140
|
+
log(`[DOM简历未命中] candidate=${candidateLabel} reason=resume_dom_text_empty`);
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const info = {
|
|
3145
|
+
name: normalizeText(extracted.name || candidate?.name || ""),
|
|
3146
|
+
school: normalizeText(extracted.school || candidate?.school || ""),
|
|
3147
|
+
major: normalizeText(extracted.major || candidate?.major || ""),
|
|
3148
|
+
company: normalizeText(extracted.company || candidate?.last_company || ""),
|
|
3149
|
+
position: normalizeText(extracted.position || candidate?.last_position || ""),
|
|
3150
|
+
resumeText,
|
|
3151
|
+
alreadyInterested: false
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
this.recordResumeNetworkDiagnostic({
|
|
3155
|
+
kind: "dom_fallback_hit",
|
|
3156
|
+
candidate_key: candidateKey,
|
|
3157
|
+
source: normalizeText(extracted.scope || "unknown"),
|
|
3158
|
+
reason: normalizeText(extracted.selector || "unknown"),
|
|
3159
|
+
resume_text_len: resumeText.length
|
|
3160
|
+
});
|
|
3161
|
+
log(
|
|
3162
|
+
`[DOM简历命中] candidate=${candidateLabel} scope=${normalizeText(extracted.scope || "unknown")} `
|
|
3163
|
+
+ `selector=${normalizeText(extracted.selector || "unknown")} resume_len=${resumeText.length}`
|
|
3164
|
+
);
|
|
3165
|
+
return info;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
2558
3168
|
handleNetworkRequestWillBeSent(params) {
|
|
2559
3169
|
const url = normalizeText(params?.request?.url || "");
|
|
2560
3170
|
const postData = params?.request?.postData || "";
|
|
@@ -2740,6 +3350,8 @@ class RecommendScreenCli {
|
|
|
2740
3350
|
this.processedKeys.delete(key);
|
|
2741
3351
|
this.discoveredKeys.delete(key);
|
|
2742
3352
|
}
|
|
3353
|
+
const rollbackSet = new Set(streakKeys);
|
|
3354
|
+
this.candidateAudits = this.candidateAudits.filter((item) => !rollbackSet.has(item?.candidate_key));
|
|
2743
3355
|
this.resetResumeCaptureFailureStreak();
|
|
2744
3356
|
return {
|
|
2745
3357
|
rollback_count: rollbackCount,
|
|
@@ -2808,16 +3420,53 @@ class RecommendScreenCli {
|
|
|
2808
3420
|
resumeSource: item?.resumeSource || ""
|
|
2809
3421
|
}))
|
|
2810
3422
|
: [];
|
|
3423
|
+
this.candidateAudits = Array.isArray(parsed.candidate_audits)
|
|
3424
|
+
? parsed.candidate_audits.map((item) => ({
|
|
3425
|
+
ts: normalizeText(item?.ts || "") || null,
|
|
3426
|
+
candidate_key: normalizeText(item?.candidate_key || "") || "",
|
|
3427
|
+
geek_id: normalizeText(item?.geek_id || "") || "",
|
|
3428
|
+
candidate_name: normalizeText(item?.candidate_name || "") || "",
|
|
3429
|
+
school: normalizeText(item?.school || "") || "",
|
|
3430
|
+
major: normalizeText(item?.major || "") || "",
|
|
3431
|
+
company: normalizeText(item?.company || "") || "",
|
|
3432
|
+
position: normalizeText(item?.position || "") || "",
|
|
3433
|
+
outcome: normalizeText(item?.outcome || "unknown") || "unknown",
|
|
3434
|
+
resume_source: normalizeText(item?.resume_source || "") || "",
|
|
3435
|
+
resume_text_len: Number.isFinite(Number(item?.resume_text_len)) ? Number(item.resume_text_len) : null,
|
|
3436
|
+
raw_passed: item?.raw_passed === true,
|
|
3437
|
+
final_passed: item?.final_passed === true,
|
|
3438
|
+
evidence_raw_count: Number.isFinite(Number(item?.evidence_raw_count)) ? Number(item.evidence_raw_count) : null,
|
|
3439
|
+
evidence_matched_count: Number.isFinite(Number(item?.evidence_matched_count)) ? Number(item.evidence_matched_count) : null,
|
|
3440
|
+
evidence_gate_demoted: item?.evidence_gate_demoted === true,
|
|
3441
|
+
screening_reason: normalizeText(item?.screening_reason || "") || "",
|
|
3442
|
+
action_taken: normalizeText(item?.action_taken || "") || "",
|
|
3443
|
+
error_code: normalizeText(item?.error_code || "") || "",
|
|
3444
|
+
error_message: normalizeText(item?.error_message || "") || "",
|
|
3445
|
+
chunk_index: Number.isFinite(Number(item?.chunk_index)) ? Number(item.chunk_index) : null,
|
|
3446
|
+
chunk_total: Number.isFinite(Number(item?.chunk_total)) ? Number(item.chunk_total) : null
|
|
3447
|
+
}))
|
|
3448
|
+
: [];
|
|
3449
|
+
if (!this.inputSummary) {
|
|
3450
|
+
this.inputSummary = sanitizeInputSummary(parsed.input_summary);
|
|
3451
|
+
}
|
|
2811
3452
|
const networkCount = this.passedCandidates.filter((item) => item?.resumeSource === "network").length;
|
|
3453
|
+
const domFallbackCount = this.passedCandidates.filter((item) => item?.resumeSource === "dom_fallback").length;
|
|
2812
3454
|
const imageFallbackCount = this.passedCandidates.filter((item) => item?.resumeSource === "image_fallback").length;
|
|
2813
3455
|
this.resumeSourceStats = {
|
|
2814
3456
|
network: networkCount,
|
|
3457
|
+
dom_fallback: domFallbackCount,
|
|
2815
3458
|
image_fallback: imageFallbackCount
|
|
2816
3459
|
};
|
|
2817
|
-
if (
|
|
3460
|
+
if (
|
|
3461
|
+
this.resumeSourceStats.network <= 0
|
|
3462
|
+
&& this.resumeSourceStats.dom_fallback <= 0
|
|
3463
|
+
&& this.resumeSourceStats.image_fallback <= 0
|
|
3464
|
+
) {
|
|
2818
3465
|
const snapshotSource = normalizeText(parsed.resume_source || "").toLowerCase();
|
|
2819
3466
|
if (snapshotSource === "network") {
|
|
2820
3467
|
this.resumeSourceStats.network = 1;
|
|
3468
|
+
} else if (snapshotSource === "dom_fallback") {
|
|
3469
|
+
this.resumeSourceStats.dom_fallback = 1;
|
|
2821
3470
|
} else if (snapshotSource === "image_fallback") {
|
|
2822
3471
|
this.resumeSourceStats.image_fallback = 1;
|
|
2823
3472
|
}
|
|
@@ -3636,18 +4285,30 @@ class RecommendScreenCli {
|
|
|
3636
4285
|
const best = passedChunks[0];
|
|
3637
4286
|
return {
|
|
3638
4287
|
passed: true,
|
|
4288
|
+
rawPassed: best?.rawPassed === true || best?.passed === true,
|
|
3639
4289
|
reason: best.reason || `分段筛选命中(${best.chunkIndex}/${chunks.length})。`,
|
|
3640
4290
|
summary: best.summary || best.reason || "分段筛选命中",
|
|
3641
|
-
evidence: Array.isArray(best.evidence) ? best.evidence : []
|
|
4291
|
+
evidence: Array.isArray(best.evidence) ? best.evidence : [],
|
|
4292
|
+
evidenceRawCount: Number.isFinite(Number(best?.evidenceRawCount)) ? Number(best.evidenceRawCount) : null,
|
|
4293
|
+
evidenceMatchedCount: Number.isFinite(Number(best?.evidenceMatchedCount)) ? Number(best.evidenceMatchedCount) : null,
|
|
4294
|
+
evidenceGateDemoted: best?.evidenceGateDemoted === true,
|
|
4295
|
+
chunkIndex: best?.chunkIndex || null,
|
|
4296
|
+
chunkTotal: best?.chunkTotal || chunks.length
|
|
3642
4297
|
};
|
|
3643
4298
|
}
|
|
3644
4299
|
|
|
3645
4300
|
const firstReason = chunkResults.map((item) => normalizeText(item?.reason)).find(Boolean);
|
|
3646
4301
|
return {
|
|
3647
4302
|
passed: false,
|
|
4303
|
+
rawPassed: chunkResults.some((item) => item?.rawPassed === true),
|
|
3648
4304
|
reason: firstReason || `分段筛选未找到满足标准的证据(共 ${chunks.length} 段)。`,
|
|
3649
4305
|
summary: firstReason || `分段筛选未找到满足标准的证据(共 ${chunks.length} 段)。`,
|
|
3650
|
-
evidence: []
|
|
4306
|
+
evidence: [],
|
|
4307
|
+
evidenceRawCount: chunkResults.reduce((acc, item) => acc + (Number.isFinite(Number(item?.evidenceRawCount)) ? Number(item.evidenceRawCount) : 0), 0),
|
|
4308
|
+
evidenceMatchedCount: chunkResults.reduce((acc, item) => acc + (Number.isFinite(Number(item?.evidenceMatchedCount)) ? Number(item.evidenceMatchedCount) : 0), 0),
|
|
4309
|
+
evidenceGateDemoted: chunkResults.some((item) => item?.evidenceGateDemoted === true),
|
|
4310
|
+
chunkIndex: null,
|
|
4311
|
+
chunkTotal: chunks.length
|
|
3651
4312
|
};
|
|
3652
4313
|
}
|
|
3653
4314
|
|
|
@@ -3709,23 +4370,41 @@ class RecommendScreenCli {
|
|
|
3709
4370
|
const reason = normalizeText(parsed.reason);
|
|
3710
4371
|
const summary = normalizeText(parsed.summary || reason);
|
|
3711
4372
|
const normalizedResume = normalizeText(safeResumeText);
|
|
4373
|
+
const normalizedResumeLower = toLowerSafe(normalizedResume);
|
|
3712
4374
|
const parsedEvidence = toStringArray(parsed.evidence);
|
|
3713
|
-
const evidence =
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
4375
|
+
const evidence = [];
|
|
4376
|
+
const unmatchedEvidence = [];
|
|
4377
|
+
for (const item of parsedEvidence) {
|
|
4378
|
+
const matched = matchEvidenceAgainstResume(item, safeResumeText, normalizedResume, normalizedResumeLower);
|
|
4379
|
+
if (matched.matched) {
|
|
4380
|
+
evidence.push(item);
|
|
4381
|
+
} else {
|
|
4382
|
+
unmatchedEvidence.push(item);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
const rawPassed = parsed.passed === true;
|
|
4386
|
+
let passed = rawPassed;
|
|
3719
4387
|
let finalReason = reason || (passed ? "满足筛选标准。" : "不满足筛选标准。");
|
|
3720
|
-
|
|
4388
|
+
const evidenceGateDemoted = rawPassed && evidence.length <= 0;
|
|
4389
|
+
if (evidenceGateDemoted) {
|
|
3721
4390
|
passed = false;
|
|
3722
4391
|
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${reason ? ` 原始原因: ${reason}` : ""}`;
|
|
4392
|
+
if (unmatchedEvidence.length > 0) {
|
|
4393
|
+
log(
|
|
4394
|
+
`[EVIDENCE_GATE] passed=true 但证据未命中简历原文,已降级为不通过;` +
|
|
4395
|
+
`chunk=${chunkIndex}/${chunkTotal}; unmatched=${unmatchedEvidence.slice(0, 3).join(" | ")}`
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
3723
4398
|
}
|
|
3724
4399
|
return {
|
|
3725
4400
|
passed,
|
|
4401
|
+
rawPassed,
|
|
3726
4402
|
reason: finalReason,
|
|
3727
4403
|
summary: summary || finalReason,
|
|
3728
4404
|
evidence,
|
|
4405
|
+
evidenceRawCount: parsedEvidence.length,
|
|
4406
|
+
evidenceMatchedCount: evidence.length,
|
|
4407
|
+
evidenceGateDemoted,
|
|
3729
4408
|
chunkIndex,
|
|
3730
4409
|
chunkTotal
|
|
3731
4410
|
};
|
|
@@ -3844,9 +4523,18 @@ class RecommendScreenCli {
|
|
|
3844
4523
|
? jsClickGreetFallbackFeatured
|
|
3845
4524
|
: jsClickGreetFallbackRecommend;
|
|
3846
4525
|
const greet = await this.evaluate(greetStateScript);
|
|
3847
|
-
if (!greet?.ok
|
|
4526
|
+
if (!greet?.ok) {
|
|
4527
|
+
if (greet?.error === "GREET_CONTINUE_BUTTON_FOUND") {
|
|
4528
|
+
throw this.buildError("GREET_CONTINUE_BUTTON_FOUND", "检测到“继续沟通”按钮,判定为已沟通过,跳过本次打招呼。");
|
|
4529
|
+
}
|
|
4530
|
+
if (greet?.error === "GREET_BUTTON_NOT_FOUND") {
|
|
4531
|
+
throw this.buildError("GREET_BUTTON_NOT_FOUND", "未找到可用的打招呼按钮,跳过本次打招呼。");
|
|
4532
|
+
}
|
|
3848
4533
|
throw this.buildError("GREET_BUTTON_FAILED", greet?.error || "打招呼按钮不可用");
|
|
3849
4534
|
}
|
|
4535
|
+
if (greet.disabled) {
|
|
4536
|
+
throw this.buildError("GREET_BUTTON_FAILED", "打招呼按钮不可用");
|
|
4537
|
+
}
|
|
3850
4538
|
|
|
3851
4539
|
try {
|
|
3852
4540
|
await this.simulateHumanClick(greet.x, greet.y);
|
|
@@ -3991,15 +4679,70 @@ class RecommendScreenCli {
|
|
|
3991
4679
|
}
|
|
3992
4680
|
|
|
3993
4681
|
saveCsv() {
|
|
3994
|
-
const lines = [
|
|
4682
|
+
const lines = [];
|
|
4683
|
+
const sanitizedInputSummary = sanitizeInputSummary(this.inputSummary);
|
|
4684
|
+
const inputSummaryRows = buildInputSummaryRows(sanitizedInputSummary);
|
|
4685
|
+
if (inputSummaryRows.length > 0) {
|
|
4686
|
+
lines.push(INPUT_SUMMARY_HEADER);
|
|
4687
|
+
for (const [key, value] of inputSummaryRows) {
|
|
4688
|
+
lines.push([csvEscape(key), csvEscape(value)].join(","));
|
|
4689
|
+
}
|
|
4690
|
+
lines.push("");
|
|
4691
|
+
}
|
|
4692
|
+
lines.push(CSV_HEADER);
|
|
4693
|
+
const passedByGeekId = new Map();
|
|
3995
4694
|
for (const item of this.passedCandidates) {
|
|
4695
|
+
const key = normalizeText(item?.geekId || "");
|
|
4696
|
+
if (!key) continue;
|
|
4697
|
+
passedByGeekId.set(key, item);
|
|
4698
|
+
}
|
|
4699
|
+
const auditRows = Array.isArray(this.candidateAudits) && this.candidateAudits.length > 0
|
|
4700
|
+
? this.candidateAudits
|
|
4701
|
+
: this.passedCandidates.map((item) => ({
|
|
4702
|
+
candidate_key: item?.geekId || "",
|
|
4703
|
+
geek_id: item?.geekId || "",
|
|
4704
|
+
candidate_name: item?.name || "",
|
|
4705
|
+
school: item?.school || "",
|
|
4706
|
+
major: item?.major || "",
|
|
4707
|
+
company: item?.company || "",
|
|
4708
|
+
position: item?.position || "",
|
|
4709
|
+
outcome: "passed",
|
|
4710
|
+
screening_reason: item?.reason || "",
|
|
4711
|
+
action_taken: item?.action || "none",
|
|
4712
|
+
resume_source: item?.resumeSource || "",
|
|
4713
|
+
raw_passed: true,
|
|
4714
|
+
final_passed: true,
|
|
4715
|
+
evidence_raw_count: null,
|
|
4716
|
+
evidence_matched_count: null,
|
|
4717
|
+
evidence_gate_demoted: false,
|
|
4718
|
+
error_code: "",
|
|
4719
|
+
error_message: ""
|
|
4720
|
+
}));
|
|
4721
|
+
for (const audit of auditRows) {
|
|
4722
|
+
const auditGeekId = normalizeText(audit?.geek_id || audit?.candidate_key || "");
|
|
4723
|
+
const passedItem = auditGeekId ? passedByGeekId.get(auditGeekId) : null;
|
|
4724
|
+
const finalPassed = audit?.final_passed === true || normalizeText(audit?.outcome || "") === "passed";
|
|
4725
|
+
const screeningReason = normalizeText(audit?.screening_reason || passedItem?.reason || "");
|
|
4726
|
+
const passReason = finalPassed ? screeningReason : "";
|
|
3996
4727
|
lines.push([
|
|
3997
|
-
csvEscape(
|
|
3998
|
-
csvEscape(
|
|
3999
|
-
csvEscape(
|
|
4000
|
-
csvEscape(
|
|
4001
|
-
csvEscape(
|
|
4002
|
-
csvEscape(
|
|
4728
|
+
csvEscape(audit?.candidate_name || passedItem?.name || ""),
|
|
4729
|
+
csvEscape(audit?.school || passedItem?.school || ""),
|
|
4730
|
+
csvEscape(audit?.major || passedItem?.major || ""),
|
|
4731
|
+
csvEscape(audit?.company || passedItem?.company || ""),
|
|
4732
|
+
csvEscape(audit?.position || passedItem?.position || ""),
|
|
4733
|
+
csvEscape(passReason),
|
|
4734
|
+
csvEscape(audit?.outcome || (finalPassed ? "passed" : "unknown")),
|
|
4735
|
+
csvEscape(screeningReason),
|
|
4736
|
+
csvEscape(audit?.action_taken || passedItem?.action || "none"),
|
|
4737
|
+
csvEscape(audit?.resume_source || passedItem?.resumeSource || ""),
|
|
4738
|
+
csvEscape(audit?.raw_passed === true ? "true" : audit?.raw_passed === false ? "false" : ""),
|
|
4739
|
+
csvEscape(finalPassed ? "true" : "false"),
|
|
4740
|
+
csvEscape(Number.isFinite(Number(audit?.evidence_raw_count)) ? Number(audit.evidence_raw_count) : ""),
|
|
4741
|
+
csvEscape(Number.isFinite(Number(audit?.evidence_matched_count)) ? Number(audit.evidence_matched_count) : ""),
|
|
4742
|
+
csvEscape(audit?.evidence_gate_demoted === true ? "true" : "false"),
|
|
4743
|
+
csvEscape(audit?.error_code || ""),
|
|
4744
|
+
csvEscape(audit?.error_message || ""),
|
|
4745
|
+
csvEscape(auditGeekId || passedItem?.geekId || "")
|
|
4003
4746
|
].join(","));
|
|
4004
4747
|
}
|
|
4005
4748
|
fs.mkdirSync(path.dirname(this.args.output), { recursive: true });
|
|
@@ -4156,6 +4899,17 @@ class RecommendScreenCli {
|
|
|
4156
4899
|
this.processedCount += 1;
|
|
4157
4900
|
log(`处理第 ${this.processedCount} 位候选人: ${nextCandidate.name || nextCandidate.geek_id}`);
|
|
4158
4901
|
let shouldMarkProcessed = true;
|
|
4902
|
+
let resumeSource = "";
|
|
4903
|
+
let resumeTextLength = null;
|
|
4904
|
+
let screening = null;
|
|
4905
|
+
let candidateProfile = {
|
|
4906
|
+
name: nextCandidate.name || "",
|
|
4907
|
+
school: nextCandidate.school || "",
|
|
4908
|
+
major: nextCandidate.major || "",
|
|
4909
|
+
company: nextCandidate.last_company || "",
|
|
4910
|
+
position: nextCandidate.last_position || ""
|
|
4911
|
+
};
|
|
4912
|
+
let allowDetailCloseFailure = false;
|
|
4159
4913
|
|
|
4160
4914
|
try {
|
|
4161
4915
|
this.currentCandidateKey = nextCandidate.key || nextCandidate.geek_id || null;
|
|
@@ -4166,33 +4920,42 @@ class RecommendScreenCli {
|
|
|
4166
4920
|
}
|
|
4167
4921
|
|
|
4168
4922
|
let capture = null;
|
|
4169
|
-
let screening = null;
|
|
4170
|
-
let resumeSource = "image_fallback";
|
|
4171
4923
|
const networkWaitMs = 4200;
|
|
4172
4924
|
const networkWaitStartedAt = Date.now();
|
|
4173
4925
|
const networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(nextCandidate, networkWaitMs);
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4926
|
+
let domCandidateInfo = null;
|
|
4927
|
+
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
4928
|
+
if (typeof this.logResumeNetworkMissDiagnostics === "function") {
|
|
4929
|
+
this.logResumeNetworkMissDiagnostics(nextCandidate, {
|
|
4930
|
+
timeoutMs: networkWaitMs,
|
|
4931
|
+
waitStartedAt: networkWaitStartedAt
|
|
4932
|
+
});
|
|
4933
|
+
}
|
|
4934
|
+
domCandidateInfo = await this.extractResumeTextFromDom(nextCandidate);
|
|
4179
4935
|
}
|
|
4180
|
-
const
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4936
|
+
const resumeCandidateInfo = networkCandidateInfo?.resumeText ? networkCandidateInfo : domCandidateInfo;
|
|
4937
|
+
candidateProfile = {
|
|
4938
|
+
name: resumeCandidateInfo?.name || nextCandidate.name || "",
|
|
4939
|
+
school: resumeCandidateInfo?.school || nextCandidate.school || "",
|
|
4940
|
+
major: resumeCandidateInfo?.major || nextCandidate.major || "",
|
|
4941
|
+
company: resumeCandidateInfo?.company || nextCandidate.last_company || "",
|
|
4942
|
+
position: resumeCandidateInfo?.position || nextCandidate.last_position || ""
|
|
4186
4943
|
};
|
|
4187
4944
|
|
|
4188
4945
|
if (networkCandidateInfo?.resumeText) {
|
|
4189
4946
|
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|
|
4190
4947
|
resumeSource = "network";
|
|
4948
|
+
resumeTextLength = normalizeText(networkCandidateInfo.resumeText).length;
|
|
4191
4949
|
this.resumeSourceStats.network += 1;
|
|
4950
|
+
} else if (domCandidateInfo?.resumeText) {
|
|
4951
|
+
screening = await this.callTextModel(domCandidateInfo.resumeText);
|
|
4952
|
+
resumeSource = "dom_fallback";
|
|
4953
|
+
resumeTextLength = normalizeText(domCandidateInfo.resumeText).length;
|
|
4954
|
+
this.resumeSourceStats.dom_fallback += 1;
|
|
4192
4955
|
} else {
|
|
4956
|
+
resumeSource = "image_fallback";
|
|
4193
4957
|
capture = await this.captureResumeImage(nextCandidate);
|
|
4194
4958
|
screening = await this.callVisionModel(capture.stitchedImage);
|
|
4195
|
-
resumeSource = "image_fallback";
|
|
4196
4959
|
this.resumeSourceStats.image_fallback += 1;
|
|
4197
4960
|
}
|
|
4198
4961
|
this.resetResumeCaptureFailureStreak();
|
|
@@ -4223,6 +4986,9 @@ class RecommendScreenCli {
|
|
|
4223
4986
|
throw postActionError;
|
|
4224
4987
|
}
|
|
4225
4988
|
log(`[POST_ACTION_WARN] ${effectiveAction} 失败,继续写入通过候选人: ${postActionError.message || postActionError}`);
|
|
4989
|
+
if (effectiveAction === "greet") {
|
|
4990
|
+
allowDetailCloseFailure = true;
|
|
4991
|
+
}
|
|
4226
4992
|
actionResult = {
|
|
4227
4993
|
actionTaken: `${effectiveAction}_failed`,
|
|
4228
4994
|
errorCode: postActionError.code || "POST_ACTION_FAILED",
|
|
@@ -4250,16 +5016,89 @@ class RecommendScreenCli {
|
|
|
4250
5016
|
imagePath: capture?.stitchedImage || "",
|
|
4251
5017
|
resumeSource
|
|
4252
5018
|
});
|
|
5019
|
+
this.recordCandidateAudit({
|
|
5020
|
+
candidate_key: nextCandidate.key || nextCandidate.geek_id || "",
|
|
5021
|
+
geek_id: nextCandidate.geek_id || nextCandidate.key || "",
|
|
5022
|
+
candidate_name: candidateProfile.name || nextCandidate.name || "",
|
|
5023
|
+
school: candidateProfile.school || "",
|
|
5024
|
+
major: candidateProfile.major || "",
|
|
5025
|
+
company: candidateProfile.company || "",
|
|
5026
|
+
position: candidateProfile.position || "",
|
|
5027
|
+
outcome: "passed",
|
|
5028
|
+
resume_source: resumeSource,
|
|
5029
|
+
resume_text_len: resumeTextLength,
|
|
5030
|
+
raw_passed: screening?.rawPassed === true || screening?.passed === true,
|
|
5031
|
+
final_passed: true,
|
|
5032
|
+
evidence_raw_count: Number.isFinite(Number(screening?.evidenceRawCount))
|
|
5033
|
+
? Number(screening.evidenceRawCount)
|
|
5034
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5035
|
+
evidence_matched_count: Number.isFinite(Number(screening?.evidenceMatchedCount))
|
|
5036
|
+
? Number(screening.evidenceMatchedCount)
|
|
5037
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5038
|
+
evidence_gate_demoted: screening?.evidenceGateDemoted === true,
|
|
5039
|
+
screening_reason: screeningReason,
|
|
5040
|
+
action_taken: actionResult.actionTaken || "none",
|
|
5041
|
+
chunk_index: Number.isFinite(Number(screening?.chunkIndex)) ? Number(screening.chunkIndex) : null,
|
|
5042
|
+
chunk_total: Number.isFinite(Number(screening?.chunkTotal)) ? Number(screening.chunkTotal) : null
|
|
5043
|
+
});
|
|
4253
5044
|
} else {
|
|
4254
5045
|
this.skippedCount += 1;
|
|
5046
|
+
this.recordCandidateAudit({
|
|
5047
|
+
candidate_key: nextCandidate.key || nextCandidate.geek_id || "",
|
|
5048
|
+
geek_id: nextCandidate.geek_id || nextCandidate.key || "",
|
|
5049
|
+
candidate_name: candidateProfile.name || nextCandidate.name || "",
|
|
5050
|
+
school: candidateProfile.school || "",
|
|
5051
|
+
major: candidateProfile.major || "",
|
|
5052
|
+
company: candidateProfile.company || "",
|
|
5053
|
+
position: candidateProfile.position || "",
|
|
5054
|
+
outcome: "skipped",
|
|
5055
|
+
resume_source: resumeSource,
|
|
5056
|
+
resume_text_len: resumeTextLength,
|
|
5057
|
+
raw_passed: screening?.rawPassed === true,
|
|
5058
|
+
final_passed: false,
|
|
5059
|
+
evidence_raw_count: Number.isFinite(Number(screening?.evidenceRawCount))
|
|
5060
|
+
? Number(screening.evidenceRawCount)
|
|
5061
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5062
|
+
evidence_matched_count: Number.isFinite(Number(screening?.evidenceMatchedCount))
|
|
5063
|
+
? Number(screening.evidenceMatchedCount)
|
|
5064
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5065
|
+
evidence_gate_demoted: screening?.evidenceGateDemoted === true,
|
|
5066
|
+
screening_reason: normalizeText(screening?.reason || screening?.summary || "模型判定不通过"),
|
|
5067
|
+
chunk_index: Number.isFinite(Number(screening?.chunkIndex)) ? Number(screening.chunkIndex) : null,
|
|
5068
|
+
chunk_total: Number.isFinite(Number(screening?.chunkTotal)) ? Number(screening.chunkTotal) : null
|
|
5069
|
+
});
|
|
4255
5070
|
}
|
|
4256
5071
|
} catch (error) {
|
|
4257
5072
|
this.skippedCount += 1;
|
|
5073
|
+
this.recordCandidateAudit({
|
|
5074
|
+
candidate_key: nextCandidate.key || nextCandidate.geek_id || "",
|
|
5075
|
+
geek_id: nextCandidate.geek_id || nextCandidate.key || "",
|
|
5076
|
+
candidate_name: nextCandidate.name || candidateProfile.name || "",
|
|
5077
|
+
school: candidateProfile.school || nextCandidate.school || "",
|
|
5078
|
+
major: candidateProfile.major || nextCandidate.major || "",
|
|
5079
|
+
company: candidateProfile.company || nextCandidate.last_company || "",
|
|
5080
|
+
position: candidateProfile.position || nextCandidate.last_position || "",
|
|
5081
|
+
outcome: "skipped_error",
|
|
5082
|
+
resume_source: resumeSource,
|
|
5083
|
+
resume_text_len: resumeTextLength,
|
|
5084
|
+
raw_passed: screening?.rawPassed === true,
|
|
5085
|
+
final_passed: false,
|
|
5086
|
+
evidence_raw_count: Number.isFinite(Number(screening?.evidenceRawCount))
|
|
5087
|
+
? Number(screening.evidenceRawCount)
|
|
5088
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5089
|
+
evidence_matched_count: Number.isFinite(Number(screening?.evidenceMatchedCount))
|
|
5090
|
+
? Number(screening.evidenceMatchedCount)
|
|
5091
|
+
: (Array.isArray(screening?.evidence) ? screening.evidence.length : null),
|
|
5092
|
+
evidence_gate_demoted: screening?.evidenceGateDemoted === true,
|
|
5093
|
+
screening_reason: normalizeText(screening?.reason || screening?.summary || ""),
|
|
5094
|
+
error_code: error?.code || "CANDIDATE_PROCESS_FAILED",
|
|
5095
|
+
error_message: normalizeText(error?.message || error)
|
|
5096
|
+
});
|
|
4258
5097
|
log(`候选人处理失败: ${error.code || error.message}`);
|
|
4259
5098
|
if (["RESUME_CAPTURE_FAILED", "RESUME_NETWORK_UNAVAILABLE"].includes(error.code)) {
|
|
4260
5099
|
this.recordResumeCaptureFailure(nextCandidate.key);
|
|
4261
5100
|
const failureLabel = error.code === "RESUME_NETWORK_UNAVAILABLE"
|
|
4262
|
-
? "简历 network 获取失败且截图回退未完成"
|
|
5101
|
+
? "简历 network/DOM 获取失败且截图回退未完成"
|
|
4263
5102
|
: "简历截图失败";
|
|
4264
5103
|
log(
|
|
4265
5104
|
`[候选人跳过] ${nextCandidate.name || nextCandidate.geek_id || "unknown"} ${failureLabel},` +
|
|
@@ -4268,7 +5107,7 @@ class RecommendScreenCli {
|
|
|
4268
5107
|
if (this.consecutiveResumeCaptureFailures >= MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES) {
|
|
4269
5108
|
shouldMarkProcessed = false;
|
|
4270
5109
|
const rollback = this.rollbackResumeCaptureFailureStreak(nextCandidate.key);
|
|
4271
|
-
const failureTypeText = "简历获取失败(network + 截图)";
|
|
5110
|
+
const failureTypeText = "简历获取失败(network + DOM + 截图)";
|
|
4272
5111
|
throw this.buildError(
|
|
4273
5112
|
"RESUME_CAPTURE_FAILED_CONSECUTIVE_LIMIT",
|
|
4274
5113
|
`连续 ${MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES} 位候选人${failureTypeText},已停止运行以避免错误跳过。` +
|
|
@@ -4308,7 +5147,11 @@ class RecommendScreenCli {
|
|
|
4308
5147
|
} finally {
|
|
4309
5148
|
const closed = await this.closeDetailPage();
|
|
4310
5149
|
if (!closed) {
|
|
4311
|
-
|
|
5150
|
+
if (allowDetailCloseFailure) {
|
|
5151
|
+
log("[详情关闭兜底] 本候选人 post_action 失败后详情页关闭未确认,已记录错误并继续下一位候选人。");
|
|
5152
|
+
} else {
|
|
5153
|
+
throw this.buildError("DETAIL_CLOSE_FAILED", "详情页未能正确关闭");
|
|
5154
|
+
}
|
|
4312
5155
|
}
|
|
4313
5156
|
if (shouldMarkProcessed) {
|
|
4314
5157
|
this.processedKeys.add(nextCandidate.key);
|
|
@@ -4381,7 +5224,7 @@ async function main() {
|
|
|
4381
5224
|
console.log(JSON.stringify({
|
|
4382
5225
|
status: "COMPLETED",
|
|
4383
5226
|
result: {
|
|
4384
|
-
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> --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
5227
|
+
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]"
|
|
4385
5228
|
}
|
|
4386
5229
|
}));
|
|
4387
5230
|
return;
|
|
@@ -4427,7 +5270,10 @@ if (require.main === module) {
|
|
|
4427
5270
|
parseFavoriteActionFromActionLog,
|
|
4428
5271
|
parseFavoriteActionFromWsPayload,
|
|
4429
5272
|
isRecoverablePostActionError,
|
|
4430
|
-
classifyFinishedWrapState
|
|
5273
|
+
classifyFinishedWrapState,
|
|
5274
|
+
formatResumeApiData,
|
|
5275
|
+
extractEvidenceTokens,
|
|
5276
|
+
matchEvidenceAgainstResume
|
|
4431
5277
|
}
|
|
4432
5278
|
};
|
|
4433
5279
|
}
|