@reconcrap/boss-recommend-mcp 1.3.12 → 1.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -32,6 +32,8 @@ const INPUT_SUMMARY_HEADER = ["运行输入字段", "运行输入值"].join(",")
|
|
|
32
32
|
const RESUME_CAPTURE_WAIT_MS = 60000;
|
|
33
33
|
const RESUME_CAPTURE_MAX_ATTEMPTS = 4;
|
|
34
34
|
const RESUME_CAPTURE_RETRY_DELAY_MS = 1200;
|
|
35
|
+
const NETWORK_RESUME_WAIT_MS = 4200;
|
|
36
|
+
const NETWORK_RESUME_RETRY_WAIT_MS = 2000;
|
|
35
37
|
const MAX_CONSECUTIVE_RESUME_CAPTURE_FAILURES = 10;
|
|
36
38
|
const DEFAULT_VISION_MAX_IMAGE_PIXELS = 36000000;
|
|
37
39
|
const DEFAULT_VISION_RETRY_MAX_IMAGE_PIXELS = 30000000;
|
|
@@ -519,6 +521,104 @@ function matchEvidenceAgainstResume(evidenceText, rawResumeText, normalizedResum
|
|
|
519
521
|
};
|
|
520
522
|
}
|
|
521
523
|
|
|
524
|
+
function truncateText(value, maxLength = 96) {
|
|
525
|
+
const text = normalizeText(value);
|
|
526
|
+
if (text.length <= maxLength) return text;
|
|
527
|
+
return `${text.slice(0, Math.max(12, maxLength - 1))}…`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function isMaskedName(value) {
|
|
531
|
+
return /[**]/.test(normalizeText(value));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function normalizeNameForCompare(value) {
|
|
535
|
+
return normalizeText(value).replace(/[**]/g, "");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function isLikelyNameMatch(expected, actual) {
|
|
539
|
+
const left = normalizeNameForCompare(expected);
|
|
540
|
+
const right = normalizeNameForCompare(actual);
|
|
541
|
+
if (!left || !right) return true;
|
|
542
|
+
if (left === right) return true;
|
|
543
|
+
if (left.includes(right) || right.includes(left)) return true;
|
|
544
|
+
return left[0] === right[0];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isLikelyTextMatch(expected, actual) {
|
|
548
|
+
const left = toLowerSafe(normalizeText(expected));
|
|
549
|
+
const right = toLowerSafe(normalizeText(actual));
|
|
550
|
+
if (!left || !right) return true;
|
|
551
|
+
return left === right || left.includes(right) || right.includes(left);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function preferReadableName(...values) {
|
|
555
|
+
const normalized = values.map((item) => normalizeText(item)).filter(Boolean);
|
|
556
|
+
if (normalized.length <= 0) return "";
|
|
557
|
+
const nonMasked = normalized.find((item) => !isMaskedName(item));
|
|
558
|
+
return nonMasked || normalized[0];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function mergeCandidateProfiles(...profiles) {
|
|
562
|
+
const list = profiles.filter((item) => item && typeof item === "object");
|
|
563
|
+
const takeFirst = (field) => {
|
|
564
|
+
for (const item of list) {
|
|
565
|
+
const text = normalizeText(item?.[field] || "");
|
|
566
|
+
if (text) return text;
|
|
567
|
+
}
|
|
568
|
+
return "";
|
|
569
|
+
};
|
|
570
|
+
return {
|
|
571
|
+
name: preferReadableName(...list.map((item) => item?.name || "")),
|
|
572
|
+
school: takeFirst("school"),
|
|
573
|
+
major: takeFirst("major"),
|
|
574
|
+
company: takeFirst("company"),
|
|
575
|
+
position: takeFirst("position")
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function isDomProfileConsistentWithCard(cardProfile, domProfile) {
|
|
580
|
+
if (!cardProfile || !domProfile) return true;
|
|
581
|
+
let compared = 0;
|
|
582
|
+
let mismatched = 0;
|
|
583
|
+
const compareField = (field, matcher) => {
|
|
584
|
+
const expected = normalizeText(cardProfile?.[field] || "");
|
|
585
|
+
const actual = normalizeText(domProfile?.[field] || "");
|
|
586
|
+
if (!expected || !actual) return;
|
|
587
|
+
compared += 1;
|
|
588
|
+
if (!matcher(expected, actual)) {
|
|
589
|
+
mismatched += 1;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
compareField("name", isLikelyNameMatch);
|
|
593
|
+
compareField("school", isLikelyTextMatch);
|
|
594
|
+
compareField("major", isLikelyTextMatch);
|
|
595
|
+
if (compared <= 0) return true;
|
|
596
|
+
return mismatched <= 1;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function isGenericReason(reason) {
|
|
600
|
+
const text = normalizeText(reason);
|
|
601
|
+
if (!text) return true;
|
|
602
|
+
if (text.length < 24) return true;
|
|
603
|
+
return /^(候选人同时满足全部筛选条件|满足筛选标准|不满足筛选标准|模型判定不通过|通过|不通过)[。!!?]?$/u.test(text);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function enrichReasonWithEvidence(reason, summary, evidence = [], passed = false) {
|
|
607
|
+
const normalizedReason = normalizeText(reason);
|
|
608
|
+
if (!isGenericReason(normalizedReason)) return normalizedReason;
|
|
609
|
+
const normalizedSummary = normalizeText(summary);
|
|
610
|
+
const evidenceItems = toStringArray(evidence, 4).map((item, index) => `${index + 1}) ${truncateText(item, 72)}`);
|
|
611
|
+
const evidenceText = evidenceItems.length > 0 ? evidenceItems.join(";") : "";
|
|
612
|
+
const base = normalizedReason || (passed ? "候选人满足筛选标准。" : "候选人未满足筛选标准。");
|
|
613
|
+
if (evidenceText) {
|
|
614
|
+
return `${base} 关键依据:${evidenceText}`;
|
|
615
|
+
}
|
|
616
|
+
if (normalizedSummary && normalizedSummary !== normalizedReason) {
|
|
617
|
+
return `${base} 摘要:${normalizedSummary}`;
|
|
618
|
+
}
|
|
619
|
+
return base;
|
|
620
|
+
}
|
|
621
|
+
|
|
522
622
|
function formatEducationDegree(edu) {
|
|
523
623
|
const degreeName = normalizeText(edu?.degreeName || edu?.degreeCategory || "");
|
|
524
624
|
if (degreeName) return degreeName;
|
|
@@ -1493,6 +1593,20 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1493
1593
|
const featuredCards = ${buildSelectorCollectionExpression(FEATURED_CARD_SELECTORS, "doc")};
|
|
1494
1594
|
const latestCards = ${buildSelectorCollectionExpression(LATEST_CARD_SELECTORS, "doc")};
|
|
1495
1595
|
const textOf = (el) => String(el ? el.textContent : '').replace(/\s+/g, ' ').trim();
|
|
1596
|
+
const pickText = (root, selectors) => {
|
|
1597
|
+
if (!root) return '';
|
|
1598
|
+
for (const selector of selectors || []) {
|
|
1599
|
+
let node = null;
|
|
1600
|
+
try {
|
|
1601
|
+
node = root.querySelector(selector);
|
|
1602
|
+
} catch {
|
|
1603
|
+
node = null;
|
|
1604
|
+
}
|
|
1605
|
+
const text = textOf(node);
|
|
1606
|
+
if (text) return text;
|
|
1607
|
+
}
|
|
1608
|
+
return '';
|
|
1609
|
+
};
|
|
1496
1610
|
const tabs = ${buildSelectorCollectionExpression(RECOMMEND_TAB_SELECTORS, "doc")};
|
|
1497
1611
|
const activeTab = tabs.find((node) => /(?:^|\\s)(?:curr|current|active|selected)(?:\\s|$)/i.test(String(node.className || ''))) || null;
|
|
1498
1612
|
const activeStatus = activeTab ? String(activeTab.getAttribute('data-status') || '') : '';
|
|
@@ -1502,7 +1616,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1502
1616
|
const geekId = inner.getAttribute('data-geekid');
|
|
1503
1617
|
if (!geekId) return null;
|
|
1504
1618
|
const rect = card.getBoundingClientRect();
|
|
1505
|
-
const
|
|
1619
|
+
const name = pickText(card, ['.geek-name-wrap .name', '.name-wrap .name', 'span.name', '.name']);
|
|
1506
1620
|
const eduSpans = Array.from(card.querySelectorAll('.edu-wrap .edu-exp span, .edu-wrap .content span, .edu-wrap span'))
|
|
1507
1621
|
.map((item) => textOf(item))
|
|
1508
1622
|
.filter(Boolean);
|
|
@@ -1515,7 +1629,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1515
1629
|
index,
|
|
1516
1630
|
key: geekId,
|
|
1517
1631
|
geek_id: geekId,
|
|
1518
|
-
name
|
|
1632
|
+
name,
|
|
1519
1633
|
school: eduSpans[0] || '',
|
|
1520
1634
|
major: eduSpans[1] || '',
|
|
1521
1635
|
degree: eduSpans[2] || '',
|
|
@@ -1534,7 +1648,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1534
1648
|
const geekId = anchor.getAttribute('data-geekid');
|
|
1535
1649
|
if (!geekId) return null;
|
|
1536
1650
|
const rect = card.getBoundingClientRect();
|
|
1537
|
-
const
|
|
1651
|
+
const name = pickText(card, ['.geek-name-wrap .name', '.name-wrap .name', '.name', '.geek-name']);
|
|
1538
1652
|
const tags = Array.from(card.querySelectorAll('.base-info span, .desc span, .tag-wrap span, .edu-wrap span'))
|
|
1539
1653
|
.map((item) => textOf(item))
|
|
1540
1654
|
.filter(Boolean);
|
|
@@ -1543,7 +1657,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1543
1657
|
index,
|
|
1544
1658
|
key: geekId,
|
|
1545
1659
|
geek_id: geekId,
|
|
1546
|
-
name
|
|
1660
|
+
name,
|
|
1547
1661
|
school: tags[0] || '',
|
|
1548
1662
|
major: tags[1] || '',
|
|
1549
1663
|
degree: tags[2] || '',
|
|
@@ -1562,7 +1676,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1562
1676
|
const geekId = inner.getAttribute('data-geek');
|
|
1563
1677
|
if (!geekId) return null;
|
|
1564
1678
|
const rect = card.getBoundingClientRect();
|
|
1565
|
-
const
|
|
1679
|
+
const name = pickText(card, ['.geek-name-wrap .name', '.name-wrap .name', '.name-wrap', '.name']);
|
|
1566
1680
|
const tags = Array.from(card.querySelectorAll('.base-info span, .edu-wrap span, .desc span, .tag-wrap span, .tag-item'))
|
|
1567
1681
|
.map((item) => textOf(item))
|
|
1568
1682
|
.filter(Boolean);
|
|
@@ -1575,7 +1689,7 @@ function buildListCandidatesExpr(processedKeys) {
|
|
|
1575
1689
|
index,
|
|
1576
1690
|
key: geekId,
|
|
1577
1691
|
geek_id: geekId,
|
|
1578
|
-
name
|
|
1692
|
+
name,
|
|
1579
1693
|
school: tags[0] || '',
|
|
1580
1694
|
major: tags[1] || '',
|
|
1581
1695
|
degree: tags[2] || '',
|
|
@@ -2945,6 +3059,12 @@ class RecommendScreenCli {
|
|
|
2945
3059
|
if (item.kind === "dom_fallback_error") {
|
|
2946
3060
|
return `${prefix} candidate=${item.candidate_key || "-"} error=${item.error || "unknown"}`;
|
|
2947
3061
|
}
|
|
3062
|
+
if (item.kind === "dom_profile_mismatch") {
|
|
3063
|
+
return `${prefix} candidate=${item.candidate_key || "-"} card=${item.card_name || "-"} dom=${item.dom_name || "-"}`;
|
|
3064
|
+
}
|
|
3065
|
+
if (item.kind === "dom_profile_mismatch_retry_failed") {
|
|
3066
|
+
return `${prefix} candidate=${item.candidate_key || "-"} error=${item.error || "unknown"}`;
|
|
3067
|
+
}
|
|
2948
3068
|
return `${prefix} ${item.url || item.reason || "n/a"}`;
|
|
2949
3069
|
});
|
|
2950
3070
|
}
|
|
@@ -3047,44 +3167,63 @@ class RecommendScreenCli {
|
|
|
3047
3167
|
}
|
|
3048
3168
|
}
|
|
3049
3169
|
|
|
3050
|
-
tryExtractNetworkResumeForCandidate(candidate) {
|
|
3170
|
+
tryExtractNetworkResumeForCandidate(candidate, options = {}) {
|
|
3051
3171
|
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
3172
|
+
const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
|
|
3052
3173
|
if (candidateKey && this.resumeNetworkByGeekId.has(candidateKey)) {
|
|
3053
|
-
|
|
3174
|
+
const wrapped = this.resumeNetworkByGeekId.get(candidateKey);
|
|
3175
|
+
const payloadTs = Number(wrapped?.ts || 0);
|
|
3176
|
+
if (payloadTs >= minTs) {
|
|
3177
|
+
return {
|
|
3178
|
+
candidateInfo: wrapped?.candidateInfo || null,
|
|
3179
|
+
source: "geek_id_map",
|
|
3180
|
+
ts: payloadTs
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3054
3183
|
}
|
|
3055
3184
|
if (this.latestResumeNetworkPayload) {
|
|
3056
|
-
const
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3185
|
+
const wrapped = this.latestResumeNetworkPayload;
|
|
3186
|
+
const payloadTs = Number(wrapped?.ts || 0);
|
|
3187
|
+
const ageMs = Date.now() - payloadTs;
|
|
3188
|
+
const latestGeekIds = Array.isArray(wrapped?.geekIds)
|
|
3189
|
+
? wrapped.geekIds.map((id) => normalizeText(id)).filter(Boolean)
|
|
3059
3190
|
: [];
|
|
3060
|
-
|
|
3061
|
-
|
|
3191
|
+
const withinAge = ageMs <= 12000;
|
|
3192
|
+
const withinTs = payloadTs >= minTs;
|
|
3193
|
+
if (!candidateKey && withinAge && withinTs) {
|
|
3194
|
+
return {
|
|
3195
|
+
candidateInfo: wrapped?.candidateInfo || null,
|
|
3196
|
+
source: "latest_payload",
|
|
3197
|
+
ts: payloadTs
|
|
3198
|
+
};
|
|
3062
3199
|
}
|
|
3063
|
-
if (candidateKey &&
|
|
3064
|
-
return
|
|
3200
|
+
if (candidateKey && withinAge && withinTs && latestGeekIds.includes(candidateKey)) {
|
|
3201
|
+
return {
|
|
3202
|
+
candidateInfo: wrapped?.candidateInfo || null,
|
|
3203
|
+
source: "latest_payload_key_match",
|
|
3204
|
+
ts: payloadTs
|
|
3205
|
+
};
|
|
3065
3206
|
}
|
|
3066
3207
|
}
|
|
3067
3208
|
return null;
|
|
3068
3209
|
}
|
|
3069
3210
|
|
|
3070
|
-
async waitForNetworkResumeCandidateInfo(candidate, timeoutMs = 2200) {
|
|
3211
|
+
async waitForNetworkResumeCandidateInfo(candidate, timeoutMs = 2200, options = {}) {
|
|
3071
3212
|
const waitStartedAt = Date.now();
|
|
3072
3213
|
const candidateKey = normalizeText(candidate?.key || candidate?.geek_id || "");
|
|
3214
|
+
const minTs = Number.isFinite(Number(options?.minTs)) ? Number(options.minTs) : 0;
|
|
3073
3215
|
const deadline = Date.now() + timeoutMs;
|
|
3074
3216
|
while (Date.now() < deadline) {
|
|
3075
|
-
const
|
|
3217
|
+
const match = this.tryExtractNetworkResumeForCandidate(candidate, { minTs });
|
|
3218
|
+
const info = match?.candidateInfo || null;
|
|
3076
3219
|
if (info && normalizeText(info.resumeText)) {
|
|
3077
|
-
const latestGeekIds = Array.isArray(this.latestResumeNetworkPayload?.geekIds)
|
|
3078
|
-
? this.latestResumeNetworkPayload.geekIds.map((id) => normalizeText(id)).filter(Boolean)
|
|
3079
|
-
: [];
|
|
3080
|
-
const source = candidateKey && this.resumeNetworkByGeekId.has(candidateKey)
|
|
3081
|
-
? "geek_id_map"
|
|
3082
|
-
: (candidateKey && latestGeekIds.includes(candidateKey) ? "latest_payload_key_match" : "latest_payload");
|
|
3083
3220
|
this.recordResumeNetworkDiagnostic({
|
|
3084
3221
|
kind: "wait_hit",
|
|
3085
3222
|
candidate_key: candidateKey,
|
|
3086
|
-
source,
|
|
3223
|
+
source: match?.source || "unknown",
|
|
3087
3224
|
waited_ms: Date.now() - waitStartedAt,
|
|
3225
|
+
min_ts: minTs || null,
|
|
3226
|
+
payload_ts: Number(match?.ts || 0) || null,
|
|
3088
3227
|
resume_text_len: normalizeText(info.resumeText).length
|
|
3089
3228
|
});
|
|
3090
3229
|
return info;
|
|
@@ -3095,6 +3234,7 @@ class RecommendScreenCli {
|
|
|
3095
3234
|
kind: "wait_timeout",
|
|
3096
3235
|
candidate_key: candidateKey,
|
|
3097
3236
|
waited_ms: Date.now() - waitStartedAt,
|
|
3237
|
+
min_ts: minTs || null,
|
|
3098
3238
|
reason: "resume_text_not_ready"
|
|
3099
3239
|
});
|
|
3100
3240
|
return null;
|
|
@@ -3142,7 +3282,7 @@ class RecommendScreenCli {
|
|
|
3142
3282
|
}
|
|
3143
3283
|
|
|
3144
3284
|
const info = {
|
|
3145
|
-
name:
|
|
3285
|
+
name: preferReadableName(extracted.name || "", candidate?.name || ""),
|
|
3146
3286
|
school: normalizeText(extracted.school || candidate?.school || ""),
|
|
3147
3287
|
major: normalizeText(extracted.major || candidate?.major || ""),
|
|
3148
3288
|
company: normalizeText(extracted.company || candidate?.last_company || ""),
|
|
@@ -3888,6 +4028,83 @@ class RecommendScreenCli {
|
|
|
3888
4028
|
})(${JSON.stringify(candidateKey)})`);
|
|
3889
4029
|
}
|
|
3890
4030
|
|
|
4031
|
+
async extractCandidateProfileFromCard(candidate) {
|
|
4032
|
+
const candidateKey = candidate?.key || candidate?.geek_id || null;
|
|
4033
|
+
if (!candidateKey) {
|
|
4034
|
+
return null;
|
|
4035
|
+
}
|
|
4036
|
+
let profile = null;
|
|
4037
|
+
try {
|
|
4038
|
+
profile = await this.evaluate(`((candidateKey) => {
|
|
4039
|
+
const frame = ${buildFirstSelectorLookupExpression(RECOMMEND_IFRAME_SELECTORS)};
|
|
4040
|
+
if (!frame || !frame.contentDocument) {
|
|
4041
|
+
return { ok: false, error: "NO_RECOMMEND_IFRAME" };
|
|
4042
|
+
}
|
|
4043
|
+
const doc = frame.contentDocument;
|
|
4044
|
+
const textOf = (el) => String(el ? el.textContent : "").replace(/\\s+/g, " ").trim();
|
|
4045
|
+
const pick = (root, selectors) => {
|
|
4046
|
+
if (!root) return "";
|
|
4047
|
+
for (const selector of selectors || []) {
|
|
4048
|
+
let node = null;
|
|
4049
|
+
try {
|
|
4050
|
+
node = root.querySelector(selector);
|
|
4051
|
+
} catch {
|
|
4052
|
+
node = null;
|
|
4053
|
+
}
|
|
4054
|
+
const text = textOf(node);
|
|
4055
|
+
if (text) return text;
|
|
4056
|
+
}
|
|
4057
|
+
return "";
|
|
4058
|
+
};
|
|
4059
|
+
const recommendInner = Array.from(doc.querySelectorAll(".card-inner[data-geekid]"))
|
|
4060
|
+
.find((item) => (item.getAttribute("data-geekid") || "") === String(candidateKey)) || null;
|
|
4061
|
+
const latestInner = recommendInner
|
|
4062
|
+
? null
|
|
4063
|
+
: ${buildSelectorCollectionExpression([".candidate-card-wrap .card-inner[data-geek]", ".candidate-card-wrap [data-geek]"], "doc")}
|
|
4064
|
+
.find((item) => (item.getAttribute("data-geek") || "") === String(candidateKey)) || null;
|
|
4065
|
+
const featuredAnchor = (recommendInner || latestInner)
|
|
4066
|
+
? null
|
|
4067
|
+
: ${buildSelectorCollectionExpression(["li.geek-info-card a[data-geekid]", "a[data-geekid]"], "doc")}
|
|
4068
|
+
.find((item) => (item.getAttribute("data-geekid") || "") === String(candidateKey)) || null;
|
|
4069
|
+
const card = recommendInner
|
|
4070
|
+
? (recommendInner.closest("li.card-item") || recommendInner.closest(".card-item"))
|
|
4071
|
+
: latestInner
|
|
4072
|
+
? (latestInner.closest(".candidate-card-wrap") || latestInner.closest("li.card-item") || latestInner.closest(".card-item"))
|
|
4073
|
+
: (featuredAnchor ? (featuredAnchor.closest("li.geek-info-card") || featuredAnchor.closest(".geek-info-card")) : null);
|
|
4074
|
+
if (!card) {
|
|
4075
|
+
return { ok: false, error: "CANDIDATE_CARD_NOT_FOUND" };
|
|
4076
|
+
}
|
|
4077
|
+
const eduSpans = Array.from(card.querySelectorAll(".edu-wrap .edu-exp span, .edu-wrap .content span, .edu-wrap span"))
|
|
4078
|
+
.map((item) => textOf(item))
|
|
4079
|
+
.filter(Boolean);
|
|
4080
|
+
const latestWork = card.querySelector(".timeline-wrap.work-exps .timeline-item");
|
|
4081
|
+
const workSpans = latestWork
|
|
4082
|
+
? Array.from(latestWork.querySelectorAll(".join-text-wrap.content span")).map((item) => textOf(item)).filter(Boolean)
|
|
4083
|
+
: [];
|
|
4084
|
+
return {
|
|
4085
|
+
ok: true,
|
|
4086
|
+
name: pick(card, [".geek-name-wrap .name", ".name-wrap .name", "span.name", ".name"]),
|
|
4087
|
+
school: eduSpans[0] || pick(card, [".edu-wrap .school-name", ".base-info .school-name", ".school-name"]),
|
|
4088
|
+
major: eduSpans[1] || pick(card, [".edu-wrap .major", ".major"]),
|
|
4089
|
+
company: workSpans[0] || pick(card, [".company-name-wrap .name", ".company-name"]),
|
|
4090
|
+
position: workSpans[1] || pick(card, [".position span", ".position"])
|
|
4091
|
+
};
|
|
4092
|
+
})(${JSON.stringify(candidateKey)})`);
|
|
4093
|
+
} catch {
|
|
4094
|
+
profile = null;
|
|
4095
|
+
}
|
|
4096
|
+
if (!profile?.ok) {
|
|
4097
|
+
return null;
|
|
4098
|
+
}
|
|
4099
|
+
return {
|
|
4100
|
+
name: normalizeText(profile?.name || ""),
|
|
4101
|
+
school: normalizeText(profile?.school || ""),
|
|
4102
|
+
major: normalizeText(profile?.major || ""),
|
|
4103
|
+
company: normalizeText(profile?.company || ""),
|
|
4104
|
+
position: normalizeText(profile?.position || "")
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
|
|
3891
4108
|
async clickCandidate(candidate) {
|
|
3892
4109
|
const centered = await this.getCenteredCandidateClickPoint(candidate);
|
|
3893
4110
|
if (centered?.ok) {
|
|
@@ -4204,6 +4421,10 @@ class RecommendScreenCli {
|
|
|
4204
4421
|
`筛选标准:\n${this.args.criteria}\n\n` +
|
|
4205
4422
|
"你将收到候选人完整简历的一个或多个顺序分段图片。必须完整阅读全部分段后再判断," +
|
|
4206
4423
|
"严禁编造任何不存在的经历、项目、学校、公司或时间线;证据不足时必须判定为不通过。\n\n" +
|
|
4424
|
+
"要求:\n" +
|
|
4425
|
+
"1) reason 必须写出可审计的判定依据,至少包含 2 条与筛选标准直接相关的事实。\n" +
|
|
4426
|
+
"2) 禁止只写“候选人同时满足全部筛选条件/不满足筛选条件”等泛化句。\n" +
|
|
4427
|
+
"3) evidence 至少给出 2 条可在简历中定位的原文短句。\n\n" +
|
|
4207
4428
|
"请返回严格 JSON: " +
|
|
4208
4429
|
"{\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\", \"evidence\": [\"证据原文1\", \"证据原文2\"]}"
|
|
4209
4430
|
}
|
|
@@ -4272,11 +4493,13 @@ class RecommendScreenCli {
|
|
|
4272
4493
|
const finalReason = evidenceGateDemoted
|
|
4273
4494
|
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? ` 原始原因: ${reason}` : ""}`
|
|
4274
4495
|
: (reason || (rawPassed ? "满足筛选标准。" : "未满足筛选标准。"));
|
|
4496
|
+
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
4497
|
+
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
4275
4498
|
return {
|
|
4276
|
-
passed
|
|
4499
|
+
passed,
|
|
4277
4500
|
rawPassed,
|
|
4278
|
-
reason:
|
|
4279
|
-
summary: summary ||
|
|
4501
|
+
reason: enrichedReason,
|
|
4502
|
+
summary: summary || enrichedReason,
|
|
4280
4503
|
evidence,
|
|
4281
4504
|
evidenceRawCount: evidence.length,
|
|
4282
4505
|
evidenceMatchedCount: evidence.length,
|
|
@@ -4380,6 +4603,9 @@ class RecommendScreenCli {
|
|
|
4380
4603
|
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
4381
4604
|
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
4382
4605
|
"3) 若证据不足,必须返回 passed=false。\n\n" +
|
|
4606
|
+
"4) reason 必须写出可审计依据,至少包含 2 条与筛选标准直接相关的事实。\n" +
|
|
4607
|
+
"5) 禁止只写“候选人同时满足全部筛选条件/不满足筛选条件”等泛化句。\n" +
|
|
4608
|
+
"6) evidence 至少给出 2 条可在简历原文定位的证据短句。\n\n" +
|
|
4383
4609
|
"请返回严格 JSON: " +
|
|
4384
4610
|
"{\"passed\": true/false, \"reason\": \"...\", \"summary\": \"...\", \"evidence\": [\"证据原文1\", \"证据原文2\"]}"
|
|
4385
4611
|
}
|
|
@@ -4435,11 +4661,12 @@ class RecommendScreenCli {
|
|
|
4435
4661
|
);
|
|
4436
4662
|
}
|
|
4437
4663
|
}
|
|
4664
|
+
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
4438
4665
|
return {
|
|
4439
4666
|
passed,
|
|
4440
4667
|
rawPassed,
|
|
4441
|
-
reason:
|
|
4442
|
-
summary: summary ||
|
|
4668
|
+
reason: enrichedReason,
|
|
4669
|
+
summary: summary || enrichedReason,
|
|
4443
4670
|
evidence,
|
|
4444
4671
|
evidenceRawCount: parsedEvidence.length,
|
|
4445
4672
|
evidenceMatchedCount: evidence.length,
|
|
@@ -4941,17 +5168,31 @@ class RecommendScreenCli {
|
|
|
4941
5168
|
let resumeSource = "";
|
|
4942
5169
|
let resumeTextLength = null;
|
|
4943
5170
|
let screening = null;
|
|
4944
|
-
let candidateProfile =
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
5171
|
+
let candidateProfile = mergeCandidateProfiles(
|
|
5172
|
+
{
|
|
5173
|
+
name: nextCandidate.name || "",
|
|
5174
|
+
school: nextCandidate.school || "",
|
|
5175
|
+
major: nextCandidate.major || "",
|
|
5176
|
+
company: nextCandidate.last_company || "",
|
|
5177
|
+
position: nextCandidate.last_position || ""
|
|
5178
|
+
}
|
|
5179
|
+
);
|
|
4951
5180
|
let allowDetailCloseFailure = false;
|
|
4952
5181
|
|
|
4953
5182
|
try {
|
|
4954
5183
|
this.currentCandidateKey = nextCandidate.key || nextCandidate.geek_id || null;
|
|
5184
|
+
const cardProfile = await this.extractCandidateProfileFromCard(nextCandidate);
|
|
5185
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5186
|
+
cardProfile || null,
|
|
5187
|
+
{
|
|
5188
|
+
name: nextCandidate.name || "",
|
|
5189
|
+
school: nextCandidate.school || "",
|
|
5190
|
+
major: nextCandidate.major || "",
|
|
5191
|
+
company: nextCandidate.last_company || "",
|
|
5192
|
+
position: nextCandidate.last_position || ""
|
|
5193
|
+
}
|
|
5194
|
+
);
|
|
5195
|
+
const candidateCaptureStartedAt = Date.now();
|
|
4955
5196
|
await this.clickCandidate(nextCandidate);
|
|
4956
5197
|
const detailOpen = await this.ensureDetailOpen();
|
|
4957
5198
|
if (!detailOpen) {
|
|
@@ -4959,9 +5200,11 @@ class RecommendScreenCli {
|
|
|
4959
5200
|
}
|
|
4960
5201
|
|
|
4961
5202
|
let capture = null;
|
|
4962
|
-
const networkWaitMs =
|
|
5203
|
+
const networkWaitMs = NETWORK_RESUME_WAIT_MS;
|
|
4963
5204
|
const networkWaitStartedAt = Date.now();
|
|
4964
|
-
|
|
5205
|
+
let networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(nextCandidate, networkWaitMs, {
|
|
5206
|
+
minTs: candidateCaptureStartedAt
|
|
5207
|
+
});
|
|
4965
5208
|
let domCandidateInfo = null;
|
|
4966
5209
|
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
4967
5210
|
if (typeof this.logResumeNetworkMissDiagnostics === "function") {
|
|
@@ -4970,16 +5213,75 @@ class RecommendScreenCli {
|
|
|
4970
5213
|
waitStartedAt: networkWaitStartedAt
|
|
4971
5214
|
});
|
|
4972
5215
|
}
|
|
5216
|
+
await sleep(NETWORK_RESUME_RETRY_WAIT_MS);
|
|
5217
|
+
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
5218
|
+
nextCandidate,
|
|
5219
|
+
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
5220
|
+
{
|
|
5221
|
+
minTs: candidateCaptureStartedAt
|
|
5222
|
+
}
|
|
5223
|
+
);
|
|
5224
|
+
}
|
|
5225
|
+
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
4973
5226
|
domCandidateInfo = await this.extractResumeTextFromDom(nextCandidate);
|
|
5227
|
+
if (domCandidateInfo && !isDomProfileConsistentWithCard(cardProfile, domCandidateInfo)) {
|
|
5228
|
+
this.recordResumeNetworkDiagnostic({
|
|
5229
|
+
kind: "dom_profile_mismatch",
|
|
5230
|
+
candidate_key: normalizeText(nextCandidate?.key || nextCandidate?.geek_id || ""),
|
|
5231
|
+
card_name: normalizeText(cardProfile?.name || ""),
|
|
5232
|
+
dom_name: normalizeText(domCandidateInfo?.name || ""),
|
|
5233
|
+
card_school: normalizeText(cardProfile?.school || ""),
|
|
5234
|
+
dom_school: normalizeText(domCandidateInfo?.school || "")
|
|
5235
|
+
});
|
|
5236
|
+
log(
|
|
5237
|
+
`[DOM简历疑似错位] candidate=${nextCandidate?.key || nextCandidate?.geek_id || "unknown"} ` +
|
|
5238
|
+
`card=${normalizeText(cardProfile?.name || "-")} dom=${normalizeText(domCandidateInfo?.name || "-")},尝试重试一次点击+监听。`
|
|
5239
|
+
);
|
|
5240
|
+
try {
|
|
5241
|
+
const retryCaptureStartedAt = Date.now();
|
|
5242
|
+
await this.clickCandidate(nextCandidate);
|
|
5243
|
+
const retryDetailOpen = await this.ensureDetailOpen();
|
|
5244
|
+
if (retryDetailOpen) {
|
|
5245
|
+
networkCandidateInfo = await this.waitForNetworkResumeCandidateInfo(
|
|
5246
|
+
nextCandidate,
|
|
5247
|
+
NETWORK_RESUME_RETRY_WAIT_MS,
|
|
5248
|
+
{ minTs: retryCaptureStartedAt }
|
|
5249
|
+
);
|
|
5250
|
+
if (!normalizeText(networkCandidateInfo?.resumeText)) {
|
|
5251
|
+
const retryDomCandidateInfo = await this.extractResumeTextFromDom(nextCandidate);
|
|
5252
|
+
if (retryDomCandidateInfo && isDomProfileConsistentWithCard(cardProfile, retryDomCandidateInfo)) {
|
|
5253
|
+
domCandidateInfo = retryDomCandidateInfo;
|
|
5254
|
+
} else {
|
|
5255
|
+
domCandidateInfo = null;
|
|
5256
|
+
}
|
|
5257
|
+
} else {
|
|
5258
|
+
domCandidateInfo = null;
|
|
5259
|
+
}
|
|
5260
|
+
} else {
|
|
5261
|
+
domCandidateInfo = null;
|
|
5262
|
+
}
|
|
5263
|
+
} catch (retryError) {
|
|
5264
|
+
domCandidateInfo = null;
|
|
5265
|
+
this.recordResumeNetworkDiagnostic({
|
|
5266
|
+
kind: "dom_profile_mismatch_retry_failed",
|
|
5267
|
+
candidate_key: normalizeText(nextCandidate?.key || nextCandidate?.geek_id || ""),
|
|
5268
|
+
error: normalizeText(retryError?.message || retryError)
|
|
5269
|
+
});
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
4974
5272
|
}
|
|
4975
5273
|
const resumeCandidateInfo = networkCandidateInfo?.resumeText ? networkCandidateInfo : domCandidateInfo;
|
|
4976
|
-
candidateProfile =
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5274
|
+
candidateProfile = mergeCandidateProfiles(
|
|
5275
|
+
resumeCandidateInfo || null,
|
|
5276
|
+
cardProfile || null,
|
|
5277
|
+
{
|
|
5278
|
+
name: nextCandidate.name || "",
|
|
5279
|
+
school: nextCandidate.school || "",
|
|
5280
|
+
major: nextCandidate.major || "",
|
|
5281
|
+
company: nextCandidate.last_company || "",
|
|
5282
|
+
position: nextCandidate.last_position || ""
|
|
5283
|
+
}
|
|
5284
|
+
);
|
|
4983
5285
|
|
|
4984
5286
|
if (networkCandidateInfo?.resumeText) {
|
|
4985
5287
|
screening = await this.callTextModel(networkCandidateInfo.resumeText);
|