@reconcrap/boss-recommend-mcp 1.3.18 → 1.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/config/screening-config.example.json +2 -1
- package/package.json +1 -1
- package/src/adapters.js +25 -0
- package/src/boss-chat.js +28 -1
- package/src/test-boss-chat.js +3 -3
- package/vendor/boss-chat-cli/src/runtime/interaction.js +1 -1
- package/vendor/boss-chat-cli/src/services/llm.js +3 -3
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +429 -70
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +46 -5
package/README.md
CHANGED
|
@@ -167,7 +167,8 @@ config/screening-config.example.json
|
|
|
167
167
|
- `openaiProject`
|
|
168
168
|
- `debugPort`
|
|
169
169
|
- `outputDir`
|
|
170
|
-
- `llmThinkingLevel`:默认 `
|
|
170
|
+
- `llmThinkingLevel`:默认 `low`。可设为 `off/minimal/low/medium/high/auto/current`,用于控制 OpenAI-compatible LLM 的 thinking/reasoning 强度。
|
|
171
|
+
- `humanRestEnabled`:默认 `false`。`false` 时 recommend-screen 随机休息/批次休息与 boss-chat 批次休息均为 `0ms`;`true` 时恢复随机休息节奏。
|
|
171
172
|
|
|
172
173
|
## 常用命令
|
|
173
174
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"baseUrl": "https://api.openai.com/v1",
|
|
3
3
|
"apiKey": "replace-with-openai-api-key",
|
|
4
4
|
"model": "gpt-4.1-mini",
|
|
5
|
-
"llmThinkingLevel": "
|
|
5
|
+
"llmThinkingLevel": "low",
|
|
6
|
+
"humanRestEnabled": false,
|
|
6
7
|
"openaiOrganization": "optional-org-id",
|
|
7
8
|
"openaiProject": "optional-project-id"
|
|
8
9
|
}
|
package/package.json
CHANGED
package/src/adapters.js
CHANGED
|
@@ -121,6 +121,30 @@ function normalizeText(value) {
|
|
|
121
121
|
return String(value || "").replace(/\s+/g, " ").trim();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
function parseBooleanValue(value) {
|
|
125
|
+
if (typeof value === "boolean") return value;
|
|
126
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
127
|
+
if (!normalized) return null;
|
|
128
|
+
if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
|
|
129
|
+
if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveHumanRestEnabled(config = {}) {
|
|
134
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
135
|
+
const candidates = [
|
|
136
|
+
config.humanRestEnabled,
|
|
137
|
+
config.human_rest_enabled,
|
|
138
|
+
config.humanLikeRestEnabled,
|
|
139
|
+
config.human_like_rest_enabled
|
|
140
|
+
];
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
const parsed = parseBooleanValue(candidate);
|
|
143
|
+
if (typeof parsed === "boolean") return parsed;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
124
148
|
function serializeInputSummary(value) {
|
|
125
149
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
126
150
|
try {
|
|
@@ -2937,6 +2961,7 @@ export async function runRecommendScreenCli({
|
|
|
2937
2961
|
if (llmThinkingLevel) {
|
|
2938
2962
|
args.push("--thinking-level", llmThinkingLevel);
|
|
2939
2963
|
}
|
|
2964
|
+
args.push("--human-rest", String(resolveHumanRestEnabled(loaded.config)));
|
|
2940
2965
|
if (Number.isInteger(screenParams.target_count) && screenParams.target_count > 0) {
|
|
2941
2966
|
args.push("--targetCount", String(screenParams.target_count));
|
|
2942
2967
|
}
|
package/src/boss-chat.js
CHANGED
|
@@ -40,6 +40,30 @@ function parsePositiveInteger(value, fallback = null) {
|
|
|
40
40
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function parseBooleanValue(value) {
|
|
44
|
+
if (typeof value === "boolean") return value;
|
|
45
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
46
|
+
if (!normalized) return null;
|
|
47
|
+
if (["1", "true", "yes", "y", "on", "是"].includes(normalized)) return true;
|
|
48
|
+
if (["0", "false", "no", "n", "off", "否"].includes(normalized)) return false;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveHumanRestEnabled(config = {}) {
|
|
53
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
54
|
+
const candidates = [
|
|
55
|
+
config.humanRestEnabled,
|
|
56
|
+
config.human_rest_enabled,
|
|
57
|
+
config.humanLikeRestEnabled,
|
|
58
|
+
config.human_like_rest_enabled
|
|
59
|
+
];
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
const parsed = parseBooleanValue(candidate);
|
|
62
|
+
if (typeof parsed === "boolean") return parsed;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
function isUnlimitedTargetCountToken(value) {
|
|
44
68
|
const token = normalizeText(value).toLowerCase();
|
|
45
69
|
if (!token) return false;
|
|
@@ -290,7 +314,8 @@ function resolveBossChatScreenConfig(workspaceRoot) {
|
|
|
290
314
|
apiKey: normalizeText(parsed.apiKey),
|
|
291
315
|
model: normalizeText(parsed.model),
|
|
292
316
|
llmThinkingLevel: resolveLlmThinkingLevel(parsed),
|
|
293
|
-
debugPort: parsePositiveInteger(parsed.debugPort, 9222)
|
|
317
|
+
debugPort: parsePositiveInteger(parsed.debugPort, 9222),
|
|
318
|
+
humanRestEnabled: resolveHumanRestEnabled(parsed)
|
|
294
319
|
},
|
|
295
320
|
config_path: configPath,
|
|
296
321
|
config_dir: path.dirname(configPath)
|
|
@@ -430,6 +455,8 @@ function buildBossChatCliArgs(command, input, resolvedConfig) {
|
|
|
430
455
|
}
|
|
431
456
|
if (typeof normalized.batchRestEnabled === "boolean") {
|
|
432
457
|
args.push("--batch-rest", String(normalized.batchRestEnabled));
|
|
458
|
+
} else if (typeof resolvedConfig?.humanRestEnabled === "boolean") {
|
|
459
|
+
args.push("--batch-rest", String(resolvedConfig.humanRestEnabled));
|
|
433
460
|
}
|
|
434
461
|
return args;
|
|
435
462
|
}
|
package/src/test-boss-chat.js
CHANGED
|
@@ -755,8 +755,8 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
|
|
|
755
755
|
},
|
|
756
756
|
});
|
|
757
757
|
await volcClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
|
|
758
|
-
assert.deepEqual(volcCompletionPayload.thinking, { type: "
|
|
759
|
-
assert.equal(volcCompletionPayload.reasoning_effort, "
|
|
758
|
+
assert.deepEqual(volcCompletionPayload.thinking, { type: "enabled" });
|
|
759
|
+
assert.equal(volcCompletionPayload.reasoning_effort, "low");
|
|
760
760
|
|
|
761
761
|
let lowCompletionPayload = null;
|
|
762
762
|
const lowClient = new LlmClient({
|
|
@@ -787,7 +787,7 @@ async function testBossChatLlmShouldApplyThinkingDefaultsAndOverrides() {
|
|
|
787
787
|
});
|
|
788
788
|
await openaiClient.requestCompletions({ prompt: "prompt", evidenceCorpus: "resume" });
|
|
789
789
|
assert.equal(openaiCompletionPayload.thinking, undefined);
|
|
790
|
-
assert.equal(openaiCompletionPayload.reasoning_effort, "
|
|
790
|
+
assert.equal(openaiCompletionPayload.reasoning_effort, "low");
|
|
791
791
|
|
|
792
792
|
let responsesPayload = null;
|
|
793
793
|
const responsesClient = new LlmClient({
|
|
@@ -47,7 +47,7 @@ export class InteractionController {
|
|
|
47
47
|
return 0;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const restMs =
|
|
50
|
+
const restMs = 4000 + Math.floor(Math.random() * 4000);
|
|
51
51
|
logger.log(`短暂休息 ${restMs}ms,保持处理节奏稳定...`);
|
|
52
52
|
await this.wait(restMs);
|
|
53
53
|
this.nextRestAt = processedCount + this.randomRestThreshold();
|
|
@@ -104,7 +104,7 @@ function resolveLlmThinkingLevel(config = {}, options = {}) {
|
|
|
104
104
|
normalizeLlmThinkingLevel(config.reasoningEffort) ||
|
|
105
105
|
normalizeLlmThinkingLevel(config.reasoning_effort) ||
|
|
106
106
|
getEnvLlmThinkingLevel() ||
|
|
107
|
-
'
|
|
107
|
+
'low'
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -118,7 +118,7 @@ function isVolcengineModel(baseUrl, model) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function applyChatCompletionThinking(payload, { baseUrl = '', model = '', thinkingLevel = '' } = {}) {
|
|
121
|
-
const level = normalizeLlmThinkingLevel(thinkingLevel) || '
|
|
121
|
+
const level = normalizeLlmThinkingLevel(thinkingLevel) || 'low';
|
|
122
122
|
if (isProviderDefaultThinkingLevel(level)) return payload;
|
|
123
123
|
const isVolc = isVolcengineModel(baseUrl, model);
|
|
124
124
|
if (isVolc) {
|
|
@@ -142,7 +142,7 @@ function applyChatCompletionThinking(payload, { baseUrl = '', model = '', thinki
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function applyResponsesThinking(payload, { thinkingLevel = '' } = {}) {
|
|
145
|
-
const level = normalizeLlmThinkingLevel(thinkingLevel) || '
|
|
145
|
+
const level = normalizeLlmThinkingLevel(thinkingLevel) || 'low';
|
|
146
146
|
if (isProviderDefaultThinkingLevel(level) || level === 'auto') return payload;
|
|
147
147
|
payload.reasoning = {
|
|
148
148
|
...(payload.reasoning || {}),
|
|
@@ -16,7 +16,7 @@ const CSV_HEADER = [
|
|
|
16
16
|
"最近工作职位",
|
|
17
17
|
"评估通过详细原因",
|
|
18
18
|
"处理结果",
|
|
19
|
-
"
|
|
19
|
+
"判断依据(CoT)",
|
|
20
20
|
"动作执行结果",
|
|
21
21
|
"简历来源",
|
|
22
22
|
"原始判定通过",
|
|
@@ -592,6 +592,101 @@ function mergeCandidateProfiles(...profiles) {
|
|
|
592
592
|
};
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
function buildCardProfileFallbackText(cardProfile = {}) {
|
|
596
|
+
const profile = cardProfile && typeof cardProfile === "object" ? cardProfile : {};
|
|
597
|
+
const educationList = Array.isArray(profile.educationList)
|
|
598
|
+
? profile.educationList
|
|
599
|
+
.map((item) => ({
|
|
600
|
+
school: normalizeText(item?.school || ""),
|
|
601
|
+
major: normalizeText(item?.major || ""),
|
|
602
|
+
degree: normalizeText(item?.degree || ""),
|
|
603
|
+
start: normalizeText(item?.start || ""),
|
|
604
|
+
end: normalizeText(item?.end || "")
|
|
605
|
+
}))
|
|
606
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
607
|
+
.slice(0, 2)
|
|
608
|
+
: [];
|
|
609
|
+
const hasCore = Boolean(
|
|
610
|
+
normalizeText(profile.name || "")
|
|
611
|
+
|| normalizeText(profile.age || "")
|
|
612
|
+
|| normalizeText(profile.gender || "")
|
|
613
|
+
|| normalizeText(profile.highestDegree || "")
|
|
614
|
+
|| normalizeText(profile.workYears || "")
|
|
615
|
+
|| normalizeText(profile.company || "")
|
|
616
|
+
|| normalizeText(profile.position || "")
|
|
617
|
+
|| normalizeText(profile.latestWorkStart || "")
|
|
618
|
+
|| normalizeText(profile.latestWorkEnd || "")
|
|
619
|
+
|| educationList.length > 0
|
|
620
|
+
);
|
|
621
|
+
if (!hasCore) return "";
|
|
622
|
+
|
|
623
|
+
const lines = ["=== 人选卡片兜底信息(仅在简历缺失时使用) ==="];
|
|
624
|
+
const pushField = (label, value) => {
|
|
625
|
+
const text = normalizeText(value);
|
|
626
|
+
if (!text) return;
|
|
627
|
+
lines.push(`${label}: ${text}`);
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
pushField("姓名", profile.name);
|
|
631
|
+
pushField("年龄", profile.age);
|
|
632
|
+
pushField("性别", profile.gender);
|
|
633
|
+
pushField("最高学历", profile.highestDegree);
|
|
634
|
+
pushField("工作年限", profile.workYears);
|
|
635
|
+
pushField("最近一份工作公司", profile.company);
|
|
636
|
+
pushField("最近一份职位", profile.position);
|
|
637
|
+
const workPeriod = formatResumeTimeRange(profile.latestWorkStart, profile.latestWorkEnd, "至今");
|
|
638
|
+
if (workPeriod) {
|
|
639
|
+
lines.push(`最近一份工作在职日期: ${workPeriod}`);
|
|
640
|
+
}
|
|
641
|
+
if (educationList.length > 0) {
|
|
642
|
+
lines.push("最近两段学校经历:");
|
|
643
|
+
educationList.forEach((item, index) => {
|
|
644
|
+
const eduPeriod = formatResumeTimeRange(item.start, item.end);
|
|
645
|
+
const detailParts = [
|
|
646
|
+
item.school ? `学校=${item.school}` : "",
|
|
647
|
+
item.major ? `专业=${item.major}` : "",
|
|
648
|
+
item.degree ? `学历=${item.degree}` : "",
|
|
649
|
+
eduPeriod ? `时间=${eduPeriod}` : ""
|
|
650
|
+
].filter(Boolean);
|
|
651
|
+
if (detailParts.length > 0) {
|
|
652
|
+
lines.push(`${index + 1}. ${detailParts.join(";")}`);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function enrichCandidateInfoWithCardProfile(candidateInfo = {}, cardProfile = null) {
|
|
660
|
+
const info = candidateInfo && typeof candidateInfo === "object" ? candidateInfo : {};
|
|
661
|
+
const profile = cardProfile && typeof cardProfile === "object" ? cardProfile : null;
|
|
662
|
+
if (!profile) return { ...info };
|
|
663
|
+
|
|
664
|
+
const educationList = Array.isArray(profile.educationList) ? profile.educationList : [];
|
|
665
|
+
const firstEducation = educationList[0] || {};
|
|
666
|
+
const merged = {
|
|
667
|
+
...info,
|
|
668
|
+
name: preferReadableName(info?.name || "", profile?.name || ""),
|
|
669
|
+
school: normalizeText(info?.school || "") || normalizeText(profile?.school || "") || normalizeText(firstEducation?.school || ""),
|
|
670
|
+
major: normalizeText(info?.major || "") || normalizeText(profile?.major || "") || normalizeText(firstEducation?.major || ""),
|
|
671
|
+
company: normalizeText(info?.company || "") || normalizeText(profile?.company || ""),
|
|
672
|
+
position: normalizeText(info?.position || "") || normalizeText(profile?.position || ""),
|
|
673
|
+
alreadyInterested: info?.alreadyInterested === true
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const baseResumeText = normalizeText(info?.resumeText || "");
|
|
677
|
+
const cardFallbackText = buildCardProfileFallbackText(profile);
|
|
678
|
+
if (cardFallbackText) {
|
|
679
|
+
merged.resumeText = baseResumeText.includes("=== 人选卡片兜底信息(仅在简历缺失时使用) ===")
|
|
680
|
+
? baseResumeText
|
|
681
|
+
: baseResumeText
|
|
682
|
+
? `${baseResumeText}\n\n${cardFallbackText}`
|
|
683
|
+
: cardFallbackText;
|
|
684
|
+
} else {
|
|
685
|
+
merged.resumeText = baseResumeText;
|
|
686
|
+
}
|
|
687
|
+
return merged;
|
|
688
|
+
}
|
|
689
|
+
|
|
595
690
|
function isDomProfileConsistentWithCard(cardProfile, domProfile) {
|
|
596
691
|
if (!cardProfile || !domProfile) return true;
|
|
597
692
|
let compared = 0;
|
|
@@ -809,6 +904,101 @@ function parseBoolean(value) {
|
|
|
809
904
|
return null;
|
|
810
905
|
}
|
|
811
906
|
|
|
907
|
+
function parsePassedDecision(value) {
|
|
908
|
+
if (typeof value === "boolean") return value;
|
|
909
|
+
if (typeof value === "number") {
|
|
910
|
+
if (value === 1) return true;
|
|
911
|
+
if (value === 0) return false;
|
|
912
|
+
}
|
|
913
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
914
|
+
if (!normalized) return null;
|
|
915
|
+
if (/不符合|不通过|未通过|未命中|不匹配|不满足/.test(normalized)) return false;
|
|
916
|
+
if (/符合|通过|命中|匹配|满足/.test(normalized)) return true;
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function parsePassedDecisionFromContent(content) {
|
|
921
|
+
const raw = String(content || "");
|
|
922
|
+
const explicit = raw.match(/"passed"\s*:\s*(true|false)/i);
|
|
923
|
+
if (explicit) {
|
|
924
|
+
return explicit[1].toLowerCase() === "true";
|
|
925
|
+
}
|
|
926
|
+
return parsePassedDecision(raw);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function flattenChatMessageContent(content) {
|
|
930
|
+
if (Array.isArray(content)) {
|
|
931
|
+
return content
|
|
932
|
+
.map((item) => {
|
|
933
|
+
if (typeof item === "string") return item;
|
|
934
|
+
if (item && typeof item === "object") {
|
|
935
|
+
return item.text || item.content || item.reasoning_content || "";
|
|
936
|
+
}
|
|
937
|
+
return "";
|
|
938
|
+
})
|
|
939
|
+
.filter(Boolean)
|
|
940
|
+
.join("\n");
|
|
941
|
+
}
|
|
942
|
+
return String(content || "");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function collectNestedText(value, out = [], depth = 0) {
|
|
946
|
+
if (depth > 6 || value === null || value === undefined) return out;
|
|
947
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
948
|
+
const normalized = normalizeText(String(value));
|
|
949
|
+
if (normalized) out.push(normalized);
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
if (Array.isArray(value)) {
|
|
953
|
+
for (const item of value) {
|
|
954
|
+
collectNestedText(item, out, depth + 1);
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
if (typeof value === "object") {
|
|
959
|
+
const priorityKeys = ["text", "reasoning_content", "summary_text", "summary", "content", "cot", "reason"];
|
|
960
|
+
const seen = new Set();
|
|
961
|
+
for (const key of priorityKeys) {
|
|
962
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
963
|
+
seen.add(key);
|
|
964
|
+
collectNestedText(value[key], out, depth + 1);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
968
|
+
if (seen.has(key)) continue;
|
|
969
|
+
collectNestedText(nested, out, depth + 1);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function extractCotFromChoice(choice, parsed = {}) {
|
|
976
|
+
const fragments = [];
|
|
977
|
+
const candidates = [
|
|
978
|
+
choice?.message?.reasoning_content,
|
|
979
|
+
choice?.message?.reasoning,
|
|
980
|
+
choice?.reasoning_content,
|
|
981
|
+
choice?.reasoning,
|
|
982
|
+
parsed?.cot,
|
|
983
|
+
parsed?.reasoning_content,
|
|
984
|
+
parsed?.reasoning,
|
|
985
|
+
parsed?.summary_text,
|
|
986
|
+
parsed?.summary,
|
|
987
|
+
parsed?.reason
|
|
988
|
+
];
|
|
989
|
+
for (const candidate of candidates) {
|
|
990
|
+
collectNestedText(candidate, fragments);
|
|
991
|
+
}
|
|
992
|
+
const deduped = [];
|
|
993
|
+
const seen = new Set();
|
|
994
|
+
for (const item of fragments) {
|
|
995
|
+
if (seen.has(item)) continue;
|
|
996
|
+
seen.add(item);
|
|
997
|
+
deduped.push(item);
|
|
998
|
+
}
|
|
999
|
+
return deduped.join("\n");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
812
1002
|
function normalizeCliOptionToken(rawToken) {
|
|
813
1003
|
const token = String(rawToken || "").trim();
|
|
814
1004
|
if (!token) {
|
|
@@ -1098,7 +1288,7 @@ function getEnvLlmThinkingLevel() {
|
|
|
1098
1288
|
}
|
|
1099
1289
|
|
|
1100
1290
|
function resolveLlmThinkingLevel(value) {
|
|
1101
|
-
return normalizeLlmThinkingLevel(value) || getEnvLlmThinkingLevel() || "
|
|
1291
|
+
return normalizeLlmThinkingLevel(value) || getEnvLlmThinkingLevel() || "low";
|
|
1102
1292
|
}
|
|
1103
1293
|
|
|
1104
1294
|
function isVolcengineModel(baseUrl, model) {
|
|
@@ -1149,6 +1339,7 @@ function parseArgs(argv) {
|
|
|
1149
1339
|
checkpointPath: null,
|
|
1150
1340
|
pauseControlPath: null,
|
|
1151
1341
|
resume: false,
|
|
1342
|
+
humanRestEnabled: false,
|
|
1152
1343
|
postAction: null,
|
|
1153
1344
|
postActionConfirmed: null,
|
|
1154
1345
|
help: false,
|
|
@@ -1163,6 +1354,7 @@ function parseArgs(argv) {
|
|
|
1163
1354
|
pageScope: false,
|
|
1164
1355
|
calibrationPath: false,
|
|
1165
1356
|
port: false,
|
|
1357
|
+
humanRest: false,
|
|
1166
1358
|
postAction: false,
|
|
1167
1359
|
postActionConfirmed: false
|
|
1168
1360
|
}
|
|
@@ -1231,6 +1423,11 @@ function parseArgs(argv) {
|
|
|
1231
1423
|
} else if (token === "--pause-control-path" && (inlineValue || next)) {
|
|
1232
1424
|
parsed.pauseControlPath = path.resolve(inlineValue || next);
|
|
1233
1425
|
if (!inlineValue) index += 1;
|
|
1426
|
+
} else if ((token === "--human-rest" || token === "--humanRest" || token === "--human_rest") && (inlineValue || next)) {
|
|
1427
|
+
const parsedBoolean = parseBoolean(inlineValue || next);
|
|
1428
|
+
parsed.humanRestEnabled = parsedBoolean === true;
|
|
1429
|
+
parsed.__provided.humanRest = parsedBoolean !== null;
|
|
1430
|
+
if (!inlineValue) index += 1;
|
|
1234
1431
|
} else if (token === "--resume") {
|
|
1235
1432
|
parsed.resume = true;
|
|
1236
1433
|
} else if ((token === "--post-action" || token === "--postAction") && (inlineValue || next)) {
|
|
@@ -1808,11 +2005,19 @@ function extractJsonObject(text) {
|
|
|
1808
2005
|
const start = raw.indexOf("{");
|
|
1809
2006
|
const end = raw.lastIndexOf("}");
|
|
1810
2007
|
if (start === -1 || end === -1 || end <= start) {
|
|
1811
|
-
throw new Error("
|
|
2008
|
+
throw new Error("Model response did not contain JSON");
|
|
1812
2009
|
}
|
|
1813
2010
|
return JSON.parse(raw.slice(start, end + 1));
|
|
1814
2011
|
}
|
|
1815
2012
|
|
|
2013
|
+
function tryExtractJsonObject(text) {
|
|
2014
|
+
try {
|
|
2015
|
+
return extractJsonObject(text);
|
|
2016
|
+
} catch {
|
|
2017
|
+
return {};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1816
2021
|
async function promptPostAction() {
|
|
1817
2022
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1818
2023
|
throw new Error("POST_ACTION_CONFIRMATION_REQUIRED");
|
|
@@ -4475,6 +4680,18 @@ class RecommendScreenCli {
|
|
|
4475
4680
|
}
|
|
4476
4681
|
return "";
|
|
4477
4682
|
};
|
|
4683
|
+
const rankDegree = (value) => {
|
|
4684
|
+
const text = normalize(value).toLowerCase();
|
|
4685
|
+
if (!text) return 0;
|
|
4686
|
+
if (/博士|phd|doctor/.test(text)) return 7;
|
|
4687
|
+
if (/硕士|master/.test(text)) return 6;
|
|
4688
|
+
if (/本科|学士|bachelor/.test(text)) return 5;
|
|
4689
|
+
if (/大专|专科/.test(text)) return 4;
|
|
4690
|
+
if (/高中/.test(text)) return 3;
|
|
4691
|
+
if (/中专|中技/.test(text)) return 2;
|
|
4692
|
+
if (/初中|小学/.test(text)) return 1;
|
|
4693
|
+
return 0;
|
|
4694
|
+
};
|
|
4478
4695
|
const recommendInner = Array.from(doc.querySelectorAll(".card-inner[data-geekid]"))
|
|
4479
4696
|
.find((item) => (item.getAttribute("data-geekid") || "") === String(candidateKey)) || null;
|
|
4480
4697
|
const latestInner = recommendInner
|
|
@@ -4500,13 +4717,84 @@ class RecommendScreenCli {
|
|
|
4500
4717
|
const workSpans = latestWork
|
|
4501
4718
|
? Array.from(latestWork.querySelectorAll(".join-text-wrap.content span")).map((item) => textOf(item)).filter(Boolean)
|
|
4502
4719
|
: [];
|
|
4720
|
+
const workTimeSpans = latestWork
|
|
4721
|
+
? Array.from(latestWork.querySelectorAll(".join-text-wrap.time span")).map((item) => textOf(item)).filter(Boolean)
|
|
4722
|
+
: [];
|
|
4723
|
+
const eduItems = Array.from(card.querySelectorAll(".timeline-wrap.edu-exps .timeline-item"))
|
|
4724
|
+
.map((item) => {
|
|
4725
|
+
const timeSpans = Array.from(item.querySelectorAll(".join-text-wrap.time span")).map((node) => textOf(node)).filter(Boolean);
|
|
4726
|
+
const contentSpans = Array.from(item.querySelectorAll(".join-text-wrap.content span")).map((node) => textOf(node)).filter(Boolean);
|
|
4727
|
+
return {
|
|
4728
|
+
school: contentSpans[0] || "",
|
|
4729
|
+
major: contentSpans[1] || "",
|
|
4730
|
+
degree: contentSpans[2] || "",
|
|
4731
|
+
start: timeSpans[0] || "",
|
|
4732
|
+
end: timeSpans[1] || ""
|
|
4733
|
+
};
|
|
4734
|
+
})
|
|
4735
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
4736
|
+
.slice(0, 2);
|
|
4737
|
+
if (eduItems.length === 0 && (eduSpans[0] || eduSpans[1] || eduSpans[2])) {
|
|
4738
|
+
eduItems.push({
|
|
4739
|
+
school: eduSpans[0] || "",
|
|
4740
|
+
major: eduSpans[1] || "",
|
|
4741
|
+
degree: eduSpans[2] || "",
|
|
4742
|
+
start: "",
|
|
4743
|
+
end: ""
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
const baseInfoTokens = Array.from(card.querySelectorAll(".join-text-wrap.base-info span, .base-info span"))
|
|
4747
|
+
.map((item) => textOf(item))
|
|
4748
|
+
.filter(Boolean);
|
|
4749
|
+
let age = "";
|
|
4750
|
+
let workYears = "";
|
|
4751
|
+
let highestDegree = "";
|
|
4752
|
+
for (const token of baseInfoTokens) {
|
|
4753
|
+
if (!age && /\d+\s*岁/u.test(token)) {
|
|
4754
|
+
age = token;
|
|
4755
|
+
continue;
|
|
4756
|
+
}
|
|
4757
|
+
if (!workYears && /(\d+\s*年|应届|在校)/u.test(token) && !/\d+\s*岁/u.test(token)) {
|
|
4758
|
+
workYears = token;
|
|
4759
|
+
continue;
|
|
4760
|
+
}
|
|
4761
|
+
if (!highestDegree && /(博士|硕士|本科|大专|专科|高中|中专|中技|初中)/u.test(token)) {
|
|
4762
|
+
highestDegree = token;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
const genderUse = card.querySelector("svg.gender use, .gender use, svg[class*='gender'] use");
|
|
4766
|
+
const genderHref = String(
|
|
4767
|
+
(genderUse && (genderUse.getAttribute("xlink:href") || genderUse.getAttribute("href") || ""))
|
|
4768
|
+
|| ""
|
|
4769
|
+
).toLowerCase();
|
|
4770
|
+
let gender = "";
|
|
4771
|
+
if (/(man|male|boy|icon-man|男)/.test(genderHref)) {
|
|
4772
|
+
gender = "男";
|
|
4773
|
+
} else if (/(woman|female|girl|icon-woman|女)/.test(genderHref)) {
|
|
4774
|
+
gender = "女";
|
|
4775
|
+
}
|
|
4776
|
+
if (!highestDegree) {
|
|
4777
|
+
const degreeFromEdu = eduItems
|
|
4778
|
+
.slice()
|
|
4779
|
+
.sort((a, b) => rankDegree(b.degree) - rankDegree(a.degree))[0];
|
|
4780
|
+
if (degreeFromEdu?.degree) {
|
|
4781
|
+
highestDegree = degreeFromEdu.degree;
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4503
4784
|
return {
|
|
4504
4785
|
ok: true,
|
|
4505
4786
|
name: pick(card, [".geek-name-wrap .name", ".name-wrap .name", "span.name", ".name"]),
|
|
4506
4787
|
school: eduSpans[0] || pick(card, [".edu-wrap .school-name", ".base-info .school-name", ".school-name"]),
|
|
4507
4788
|
major: eduSpans[1] || pick(card, [".edu-wrap .major", ".major"]),
|
|
4508
4789
|
company: workSpans[0] || pick(card, [".company-name-wrap .name", ".company-name"]),
|
|
4509
|
-
position: workSpans[1] || pick(card, [".position span", ".position"])
|
|
4790
|
+
position: workSpans[1] || pick(card, [".position span", ".position"]),
|
|
4791
|
+
age,
|
|
4792
|
+
gender,
|
|
4793
|
+
highest_degree: highestDegree,
|
|
4794
|
+
work_years: workYears,
|
|
4795
|
+
latest_work_start: workTimeSpans[0] || "",
|
|
4796
|
+
latest_work_end: workTimeSpans[1] || "",
|
|
4797
|
+
education_list: eduItems
|
|
4510
4798
|
};
|
|
4511
4799
|
})(${JSON.stringify(candidateKey)})`);
|
|
4512
4800
|
} catch {
|
|
@@ -4520,7 +4808,25 @@ class RecommendScreenCli {
|
|
|
4520
4808
|
school: normalizeText(profile?.school || ""),
|
|
4521
4809
|
major: normalizeText(profile?.major || ""),
|
|
4522
4810
|
company: normalizeText(profile?.company || ""),
|
|
4523
|
-
position: normalizeText(profile?.position || "")
|
|
4811
|
+
position: normalizeText(profile?.position || ""),
|
|
4812
|
+
age: normalizeText(profile?.age || ""),
|
|
4813
|
+
gender: normalizeText(profile?.gender || ""),
|
|
4814
|
+
highestDegree: normalizeText(profile?.highest_degree || ""),
|
|
4815
|
+
workYears: normalizeText(profile?.work_years || ""),
|
|
4816
|
+
latestWorkStart: normalizeText(profile?.latest_work_start || ""),
|
|
4817
|
+
latestWorkEnd: normalizeText(profile?.latest_work_end || ""),
|
|
4818
|
+
educationList: Array.isArray(profile?.education_list)
|
|
4819
|
+
? profile.education_list
|
|
4820
|
+
.map((item) => ({
|
|
4821
|
+
school: normalizeText(item?.school || ""),
|
|
4822
|
+
major: normalizeText(item?.major || ""),
|
|
4823
|
+
degree: normalizeText(item?.degree || ""),
|
|
4824
|
+
start: normalizeText(item?.start || ""),
|
|
4825
|
+
end: normalizeText(item?.end || "")
|
|
4826
|
+
}))
|
|
4827
|
+
.filter((item) => item.school || item.major || item.degree || item.start || item.end)
|
|
4828
|
+
.slice(0, 2)
|
|
4829
|
+
: []
|
|
4524
4830
|
};
|
|
4525
4831
|
}
|
|
4526
4832
|
|
|
@@ -4653,22 +4959,32 @@ class RecommendScreenCli {
|
|
|
4653
4959
|
applyVisionEvidenceGate(result) {
|
|
4654
4960
|
const parsed = result && typeof result === "object" ? result : {};
|
|
4655
4961
|
const rawPassed = parsed?.rawPassed === true || parsed?.passed === true;
|
|
4656
|
-
const
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
const
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4962
|
+
const evidenceGateEligible = parsed?.evidenceGateEligible === true
|
|
4963
|
+
|| Array.isArray(parsed?.evidence)
|
|
4964
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
4965
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
4966
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
|
|
4967
|
+
const evidenceRawCount = evidenceGateEligible
|
|
4968
|
+
? (Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
4969
|
+
? Number(parsed.evidenceRawCount)
|
|
4970
|
+
: parsedEvidence.length)
|
|
4971
|
+
: null;
|
|
4972
|
+
const evidenceMatchedCount = evidenceGateEligible
|
|
4973
|
+
? (Number.isFinite(Number(parsed?.evidenceMatchedCount))
|
|
4974
|
+
? Number(parsed.evidenceMatchedCount)
|
|
4975
|
+
: parsedEvidence.length)
|
|
4976
|
+
: null;
|
|
4977
|
+
const evidenceGateDemoted = parsed?.evidenceGateDemoted === true
|
|
4978
|
+
|| (evidenceGateEligible && rawPassed && evidenceMatchedCount <= 0);
|
|
4979
|
+
const cot = normalizeText(parsed?.cot || parsed?.reason || "");
|
|
4980
|
+
const summary = normalizeText(parsed?.summary || cot);
|
|
4666
4981
|
const finalReason = evidenceGateDemoted
|
|
4667
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${
|
|
4668
|
-
: (
|
|
4982
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${cot ? ` 原始判断依据(CoT): ${cot}` : ""}`
|
|
4983
|
+
: (cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。"));
|
|
4669
4984
|
return {
|
|
4670
4985
|
passed: evidenceGateDemoted ? false : rawPassed,
|
|
4671
4986
|
rawPassed,
|
|
4987
|
+
cot: finalReason,
|
|
4672
4988
|
reason: finalReason,
|
|
4673
4989
|
summary: summary || finalReason,
|
|
4674
4990
|
evidence: parsedEvidence,
|
|
@@ -4879,11 +5195,10 @@ class RecommendScreenCli {
|
|
|
4879
5195
|
"workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
4880
5196
|
"活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n\n" +
|
|
4881
5197
|
"要求:\n" +
|
|
4882
|
-
"1)
|
|
4883
|
-
"2)
|
|
4884
|
-
"3) evidence 至少给出 2 条可在简历中定位的原文短句。\n\n" +
|
|
5198
|
+
"1) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5199
|
+
"2) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
4885
5200
|
"请返回严格 JSON: " +
|
|
4886
|
-
"{\"passed\": true/false
|
|
5201
|
+
"{\"passed\": true/false}"
|
|
4887
5202
|
}
|
|
4888
5203
|
];
|
|
4889
5204
|
for (let index = 0; index < imagePaths.length; index += 1) {
|
|
@@ -4943,28 +5258,47 @@ class RecommendScreenCli {
|
|
|
4943
5258
|
throw this.buildError("VISION_MODEL_FAILED", `Vision model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
4944
5259
|
}
|
|
4945
5260
|
const json = await response.json();
|
|
4946
|
-
const
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
const
|
|
4950
|
-
const
|
|
4951
|
-
const
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
5261
|
+
const choice = json?.choices?.[0] || {};
|
|
5262
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
5263
|
+
const parsed = tryExtractJsonObject(content);
|
|
5264
|
+
const parsedPassed = parsePassedDecision(parsed?.passed);
|
|
5265
|
+
const fallbackPassed = parsePassedDecisionFromContent(content);
|
|
5266
|
+
const rawPassed = parsedPassed !== null ? parsedPassed : fallbackPassed;
|
|
5267
|
+
if (rawPassed === null) {
|
|
5268
|
+
throw this.buildError(
|
|
5269
|
+
"VISION_MODEL_FAILED",
|
|
5270
|
+
`Vision model response missing boolean passed decision. content=${truncateText(content, 180)}`
|
|
5271
|
+
);
|
|
5272
|
+
}
|
|
5273
|
+
const cot = normalizeText(extractCotFromChoice(choice, parsed));
|
|
5274
|
+
const reason = cot || (rawPassed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5275
|
+
const summary = reason;
|
|
5276
|
+
const evidenceGateEligible = Array.isArray(parsed?.evidence)
|
|
5277
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
5278
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
5279
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed?.evidence) : [];
|
|
5280
|
+
const evidenceRawCount = evidenceGateEligible
|
|
5281
|
+
? (Number.isFinite(Number(parsed?.evidenceRawCount)) ? Number(parsed.evidenceRawCount) : parsedEvidence.length)
|
|
5282
|
+
: null;
|
|
5283
|
+
const evidenceMatchedCount = evidenceGateEligible
|
|
5284
|
+
? (Number.isFinite(Number(parsed?.evidenceMatchedCount)) ? Number(parsed.evidenceMatchedCount) : parsedEvidence.length)
|
|
5285
|
+
: null;
|
|
5286
|
+
const evidenceGateDemoted = evidenceGateEligible && rawPassed && (evidenceMatchedCount ?? 0) <= 0;
|
|
4955
5287
|
const finalReason = evidenceGateDemoted
|
|
4956
|
-
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? `
|
|
4957
|
-
:
|
|
5288
|
+
? `模型未给出可在简历截图中引用的证据,按安全策略判为不通过。${reason ? ` 原始判断依据(CoT): ${reason}` : ""}`
|
|
5289
|
+
: reason;
|
|
4958
5290
|
const passed = evidenceGateDemoted ? false : rawPassed;
|
|
4959
|
-
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason,
|
|
5291
|
+
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, parsedEvidence, passed);
|
|
4960
5292
|
return {
|
|
4961
5293
|
passed,
|
|
4962
5294
|
rawPassed,
|
|
5295
|
+
cot: reason,
|
|
4963
5296
|
reason: enrichedReason,
|
|
4964
5297
|
summary: summary || enrichedReason,
|
|
4965
|
-
evidence,
|
|
4966
|
-
evidenceRawCount
|
|
4967
|
-
evidenceMatchedCount
|
|
5298
|
+
evidence: parsedEvidence,
|
|
5299
|
+
evidenceRawCount,
|
|
5300
|
+
evidenceMatchedCount,
|
|
5301
|
+
evidenceGateEligible,
|
|
4968
5302
|
evidenceGateDemoted
|
|
4969
5303
|
};
|
|
4970
5304
|
}
|
|
@@ -5064,17 +5398,17 @@ class RecommendScreenCli {
|
|
|
5064
5398
|
"要求:\n" +
|
|
5065
5399
|
"1) 必须完整阅读上面的全部简历文本。\n" +
|
|
5066
5400
|
"2) 只能依据简历中真实出现的信息判断,严禁编造不存在的经历/项目/学历/公司。\n" +
|
|
5067
|
-
"3)
|
|
5068
|
-
"4)
|
|
5401
|
+
"3) 若文本中包含“人选卡片兜底信息(仅在简历缺失时使用)”段落,只能在主简历缺失对应字段时引用该段,不可覆盖主简历已明确字段。\n" +
|
|
5402
|
+
"4) 若证据不足,必须返回 passed=false。\n\n" +
|
|
5403
|
+
"5) 当筛选条件涉及应届/毕业年份时,必须以最高学历毕业年份作为主依据;若简历中存在教育时间、毕业时间或可推断年份信息,必须先推断再判断;" +
|
|
5069
5404
|
"只有完全不存在时间线信息时才可写“无法判断”。\n" +
|
|
5070
|
-
"
|
|
5071
|
-
"
|
|
5072
|
-
"
|
|
5073
|
-
"
|
|
5074
|
-
"
|
|
5075
|
-
"10) evidence 至少给出 2 条可在简历原文定位的证据短句。\n\n" +
|
|
5405
|
+
"6) 当筛选条件提及“相关经验”时,必须以工作经历或项目经历作为硬性证据;教育/课程/论文/技能/个人优势只能作为补充,不能单独判定满足。\n" +
|
|
5406
|
+
"7) workExpCheckRes 等经历校验项仅作为“需追问”软风险,不得直接据此判定不通过。\n" +
|
|
5407
|
+
"8) 活跃度、沟通热度、受欢迎度等运营指标不参与筛选通过判定。\n" +
|
|
5408
|
+
"9) 只做结论判断:候选人是否符合筛选标准。\n" +
|
|
5409
|
+
"10) 只返回 passed 布尔值,不要在 JSON 中输出 reason/summary/evidence 等字段。\n\n" +
|
|
5076
5410
|
"请返回严格 JSON: " +
|
|
5077
|
-
"{\"passed\": true/false
|
|
5411
|
+
"{\"passed\": true/false}"
|
|
5078
5412
|
}
|
|
5079
5413
|
]
|
|
5080
5414
|
};
|
|
@@ -5100,32 +5434,43 @@ class RecommendScreenCli {
|
|
|
5100
5434
|
throw this.buildError("TEXT_MODEL_FAILED", `Text model request failed: ${response.status} ${body.slice(0, 400)}`);
|
|
5101
5435
|
}
|
|
5102
5436
|
const json = await response.json();
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
const
|
|
5107
|
-
const reason = normalizeText(parsed.reason);
|
|
5108
|
-
const summary = normalizeText(parsed.summary || reason);
|
|
5437
|
+
const choice = json?.choices?.[0] || {};
|
|
5438
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
5439
|
+
const parsed = tryExtractJsonObject(content);
|
|
5440
|
+
const cot = normalizeText(extractCotFromChoice(choice, parsed));
|
|
5109
5441
|
const normalizedResume = normalizeText(safeResumeText);
|
|
5110
5442
|
const normalizedResumeLower = toLowerSafe(normalizedResume);
|
|
5111
|
-
const
|
|
5443
|
+
const evidenceGateEligible = Array.isArray(parsed?.evidence)
|
|
5444
|
+
|| Number.isFinite(Number(parsed?.evidenceRawCount))
|
|
5445
|
+
|| Number.isFinite(Number(parsed?.evidenceMatchedCount));
|
|
5446
|
+
const parsedEvidence = evidenceGateEligible ? toStringArray(parsed.evidence) : [];
|
|
5112
5447
|
const evidence = [];
|
|
5113
5448
|
const unmatchedEvidence = [];
|
|
5114
|
-
|
|
5115
|
-
const
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5449
|
+
if (evidenceGateEligible) {
|
|
5450
|
+
for (const item of parsedEvidence) {
|
|
5451
|
+
const matched = matchEvidenceAgainstResume(item, safeResumeText, normalizedResume, normalizedResumeLower);
|
|
5452
|
+
if (matched.matched) {
|
|
5453
|
+
evidence.push(item);
|
|
5454
|
+
} else {
|
|
5455
|
+
unmatchedEvidence.push(item);
|
|
5456
|
+
}
|
|
5120
5457
|
}
|
|
5121
5458
|
}
|
|
5122
|
-
const
|
|
5459
|
+
const parsedPassed = parsePassedDecision(parsed?.passed);
|
|
5460
|
+
const fallbackPassed = parsePassedDecisionFromContent(content);
|
|
5461
|
+
const rawPassed = parsedPassed !== null ? parsedPassed : fallbackPassed;
|
|
5462
|
+
if (rawPassed === null) {
|
|
5463
|
+
throw this.buildError(
|
|
5464
|
+
"TEXT_MODEL_FAILED",
|
|
5465
|
+
`Text model response missing boolean passed decision. content=${truncateText(content, 180)}`
|
|
5466
|
+
);
|
|
5467
|
+
}
|
|
5123
5468
|
let passed = rawPassed;
|
|
5124
|
-
let finalReason =
|
|
5125
|
-
const evidenceGateDemoted = rawPassed && evidence.length <= 0;
|
|
5469
|
+
let finalReason = cot || (passed ? "模型判定符合筛选标准。" : "模型判定不符合筛选标准。");
|
|
5470
|
+
const evidenceGateDemoted = evidenceGateEligible && rawPassed && evidence.length <= 0;
|
|
5126
5471
|
if (evidenceGateDemoted) {
|
|
5127
5472
|
passed = false;
|
|
5128
|
-
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${
|
|
5473
|
+
finalReason = `模型未给出可在简历原文中校验的证据,按安全策略判为不通过。${finalReason ? ` 原始判断依据(CoT): ${finalReason}` : ""}`;
|
|
5129
5474
|
if (unmatchedEvidence.length > 0) {
|
|
5130
5475
|
log(
|
|
5131
5476
|
`[EVIDENCE_GATE] passed=true 但证据未命中简历原文,已降级为不通过;` +
|
|
@@ -5133,15 +5478,17 @@ class RecommendScreenCli {
|
|
|
5133
5478
|
);
|
|
5134
5479
|
}
|
|
5135
5480
|
}
|
|
5481
|
+
const summary = finalReason;
|
|
5136
5482
|
const enrichedReason = enrichReasonWithEvidence(finalReason, summary || finalReason, evidence, passed);
|
|
5137
5483
|
return {
|
|
5138
5484
|
passed,
|
|
5139
5485
|
rawPassed,
|
|
5486
|
+
cot: finalReason,
|
|
5140
5487
|
reason: enrichedReason,
|
|
5141
5488
|
summary: summary || enrichedReason,
|
|
5142
5489
|
evidence,
|
|
5143
|
-
evidenceRawCount: parsedEvidence.length,
|
|
5144
|
-
evidenceMatchedCount: evidence.length,
|
|
5490
|
+
evidenceRawCount: evidenceGateEligible ? parsedEvidence.length : null,
|
|
5491
|
+
evidenceMatchedCount: evidenceGateEligible ? evidence.length : null,
|
|
5145
5492
|
evidenceGateDemoted,
|
|
5146
5493
|
chunkIndex,
|
|
5147
5494
|
chunkTotal
|
|
@@ -5403,12 +5750,12 @@ class RecommendScreenCli {
|
|
|
5403
5750
|
async takeBreakIfNeeded() {
|
|
5404
5751
|
this.restCounter += 1;
|
|
5405
5752
|
if (Math.random() < 0.08) {
|
|
5406
|
-
const pauseMs = 0;
|
|
5753
|
+
const pauseMs = this.args.humanRestEnabled ? 3000 + Math.floor(Math.random() * 4000) : 0;
|
|
5407
5754
|
log(`[随机休息] 暂停 ${Math.round(pauseMs / 1000)} 秒`);
|
|
5408
5755
|
await sleep(pauseMs);
|
|
5409
5756
|
}
|
|
5410
5757
|
if (this.restCounter >= this.restThreshold) {
|
|
5411
|
-
const pauseMs = 0;
|
|
5758
|
+
const pauseMs = this.args.humanRestEnabled ? 15000 + Math.floor(Math.random() * 15000) : 0;
|
|
5412
5759
|
log(`[批次休息] 已连续处理 ${this.restCounter} 人,暂停 ${Math.round(pauseMs / 1000)} 秒`);
|
|
5413
5760
|
await sleep(pauseMs);
|
|
5414
5761
|
this.restCounter = 0;
|
|
@@ -5722,6 +6069,7 @@ class RecommendScreenCli {
|
|
|
5722
6069
|
let domCandidateInfo = null;
|
|
5723
6070
|
|
|
5724
6071
|
if (networkCandidateInfo?.resumeText) {
|
|
6072
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(networkCandidateInfo, cardProfile || null);
|
|
5725
6073
|
screening = await timeCandidateStage(
|
|
5726
6074
|
"text_model_ms",
|
|
5727
6075
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5761,7 +6109,10 @@ class RecommendScreenCli {
|
|
|
5761
6109
|
})
|
|
5762
6110
|
);
|
|
5763
6111
|
if (lateNetworkCandidateInfo?.resumeText) {
|
|
5764
|
-
networkCandidateInfo =
|
|
6112
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6113
|
+
lateNetworkCandidateInfo,
|
|
6114
|
+
cardProfile || null
|
|
6115
|
+
);
|
|
5765
6116
|
screening = await timeCandidateStage(
|
|
5766
6117
|
"text_model_ms",
|
|
5767
6118
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5786,7 +6137,10 @@ class RecommendScreenCli {
|
|
|
5786
6137
|
() => this.resolveDomResumeFallback(nextCandidate, cardProfile || null)
|
|
5787
6138
|
);
|
|
5788
6139
|
if (domFallback?.networkCandidateInfo?.resumeText) {
|
|
5789
|
-
networkCandidateInfo =
|
|
6140
|
+
networkCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6141
|
+
domFallback.networkCandidateInfo,
|
|
6142
|
+
cardProfile || null
|
|
6143
|
+
);
|
|
5790
6144
|
screening = await timeCandidateStage(
|
|
5791
6145
|
"text_model_ms",
|
|
5792
6146
|
() => this.callTextModel(networkCandidateInfo.resumeText)
|
|
@@ -5806,7 +6160,10 @@ class RecommendScreenCli {
|
|
|
5806
6160
|
}
|
|
5807
6161
|
);
|
|
5808
6162
|
} else if (domFallback?.domCandidateInfo?.resumeText) {
|
|
5809
|
-
domCandidateInfo =
|
|
6163
|
+
domCandidateInfo = enrichCandidateInfoWithCardProfile(
|
|
6164
|
+
domFallback.domCandidateInfo,
|
|
6165
|
+
cardProfile || null
|
|
6166
|
+
);
|
|
5810
6167
|
screening = await timeCandidateStage(
|
|
5811
6168
|
"text_model_ms",
|
|
5812
6169
|
() => this.callTextModel(domCandidateInfo.resumeText)
|
|
@@ -6119,7 +6476,7 @@ async function main() {
|
|
|
6119
6476
|
console.log(JSON.stringify({
|
|
6120
6477
|
status: "COMPLETED",
|
|
6121
6478
|
result: {
|
|
6122
|
-
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --thinking-level off|low|medium|high|current --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --output <csv-path> [--input-summary-json <json>] --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
6479
|
+
usage: "node boss-recommend-screen-cli.cjs --criteria \"有 MCP 开发经验\" --post-action <favorite|greet|none> --max-greet-count 10 --post-action-confirmed true --baseurl <url> --apikey <key> --model <model> --thinking-level off|low|medium|high|current --page-scope recommend|latest|featured --calibration <favorite-calibration.json> --port 9222 --human-rest <true|false> --output <csv-path> [--input-summary-json <json>] --checkpoint-path <checkpoint.json> --pause-control-path <pause-control.json> [--resume]"
|
|
6123
6480
|
}
|
|
6124
6481
|
}));
|
|
6125
6482
|
return;
|
|
@@ -6169,6 +6526,8 @@ if (require.main === module) {
|
|
|
6169
6526
|
isRecoverablePostActionError,
|
|
6170
6527
|
classifyFinishedWrapState,
|
|
6171
6528
|
formatResumeApiData,
|
|
6529
|
+
buildCardProfileFallbackText,
|
|
6530
|
+
enrichCandidateInfoWithCardProfile,
|
|
6172
6531
|
extractEvidenceTokens,
|
|
6173
6532
|
matchEvidenceAgainstResume
|
|
6174
6533
|
}
|
|
@@ -1013,6 +1013,46 @@ function testFormatResumeApiDataShouldIncludeStructuredJudgementHints() {
|
|
|
1013
1013
|
assert.equal(formatted.includes("判定忽略项: 活跃度/沟通热度/受欢迎度等运营指标不参与通过判定。"), true);
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
+
function testEnrichCandidateInfoWithCardProfileShouldAppendCardFallbackWhenDomInfoMissing() {
|
|
1017
|
+
const candidateInfo = {
|
|
1018
|
+
name: "",
|
|
1019
|
+
school: "",
|
|
1020
|
+
major: "",
|
|
1021
|
+
company: "",
|
|
1022
|
+
position: "",
|
|
1023
|
+
resumeText: "=== 基本信息 ===\n姓名: 赵梓轩\n"
|
|
1024
|
+
};
|
|
1025
|
+
const cardProfile = {
|
|
1026
|
+
name: "赵梓轩",
|
|
1027
|
+
age: "29岁",
|
|
1028
|
+
gender: "男",
|
|
1029
|
+
highestDegree: "硕士",
|
|
1030
|
+
workYears: "2年",
|
|
1031
|
+
company: "中科院",
|
|
1032
|
+
position: "科研助理",
|
|
1033
|
+
latestWorkStart: "2024.10",
|
|
1034
|
+
latestWorkEnd: "至今",
|
|
1035
|
+
educationList: [
|
|
1036
|
+
{ school: "科克大学", major: "理学", degree: "硕士", start: "2020", end: "2023" },
|
|
1037
|
+
{ school: "东北大学", major: "数学与应用数学", degree: "本科", start: "2014", end: "2018" }
|
|
1038
|
+
]
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const enriched = __testables.enrichCandidateInfoWithCardProfile(candidateInfo, cardProfile);
|
|
1042
|
+
assert.equal(enriched.name, "赵梓轩");
|
|
1043
|
+
assert.equal(enriched.company, "中科院");
|
|
1044
|
+
assert.equal(enriched.position, "科研助理");
|
|
1045
|
+
assert.equal(enriched.resumeText.includes("=== 人选卡片兜底信息(仅在简历缺失时使用) ==="), true);
|
|
1046
|
+
assert.equal(enriched.resumeText.includes("年龄: 29岁"), true);
|
|
1047
|
+
assert.equal(enriched.resumeText.includes("性别: 男"), true);
|
|
1048
|
+
assert.equal(enriched.resumeText.includes("最近一份工作在职日期: 2024.10 ~ 至今"), true);
|
|
1049
|
+
assert.equal(enriched.resumeText.includes("1. 学校=科克大学;专业=理学;学历=硕士;时间=2020 ~ 2023"), true);
|
|
1050
|
+
assert.equal(enriched.resumeText.includes("2. 学校=东北大学;专业=数学与应用数学;学历=本科;时间=2014 ~ 2018"), true);
|
|
1051
|
+
|
|
1052
|
+
const enrichedAgain = __testables.enrichCandidateInfoWithCardProfile(enriched, cardProfile);
|
|
1053
|
+
assert.equal((enrichedAgain.resumeText.match(/人选卡片兜底信息(仅在简历缺失时使用)/g) || []).length, 1);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1016
1056
|
function testEvidenceTokenMatcherShouldSupportParaphrasedEvidence() {
|
|
1017
1057
|
const resume = [
|
|
1018
1058
|
"南京大学 专业: 数学",
|
|
@@ -1485,8 +1525,8 @@ async function testCallTextModelShouldFallbackToChunkModeOnContextLimit() {
|
|
|
1485
1525
|
}
|
|
1486
1526
|
}
|
|
1487
1527
|
|
|
1488
|
-
async function
|
|
1489
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-
|
|
1528
|
+
async function testTextModelShouldDefaultThinkingLowForVolcengine() {
|
|
1529
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-thinking-low-default-"));
|
|
1490
1530
|
const cli = new RecommendScreenCli(createArgs(tempDir));
|
|
1491
1531
|
cli.args.baseUrl = "https://ark.cn-beijing.volces.com/api/v3";
|
|
1492
1532
|
cli.args.model = "doubao-seed-2-0-mini-260215";
|
|
@@ -1512,8 +1552,8 @@ async function testTextModelShouldDefaultThinkingOffForVolcengine() {
|
|
|
1512
1552
|
};
|
|
1513
1553
|
try {
|
|
1514
1554
|
await cli.callTextModel("resume");
|
|
1515
|
-
assert.deepEqual(capturedPayload?.thinking, { type: "
|
|
1516
|
-
assert.equal(capturedPayload?.reasoning_effort, "
|
|
1555
|
+
assert.deepEqual(capturedPayload?.thinking, { type: "enabled" });
|
|
1556
|
+
assert.equal(capturedPayload?.reasoning_effort, "low");
|
|
1517
1557
|
} finally {
|
|
1518
1558
|
global.fetch = originalFetch;
|
|
1519
1559
|
}
|
|
@@ -1757,6 +1797,7 @@ async function main() {
|
|
|
1757
1797
|
testFinishedWrapClassifierShouldNotTreatLoadMoreAsBottom();
|
|
1758
1798
|
testFormatResumeApiDataShouldPreserveEducationTagsAndProjectDescription();
|
|
1759
1799
|
testFormatResumeApiDataShouldIncludeStructuredJudgementHints();
|
|
1800
|
+
testEnrichCandidateInfoWithCardProfileShouldAppendCardFallbackWhenDomInfoMissing();
|
|
1760
1801
|
testEvidenceTokenMatcherShouldSupportParaphrasedEvidence();
|
|
1761
1802
|
testCheckpointPayloadShouldIncludeCandidateAudits();
|
|
1762
1803
|
testCheckpointShouldPersistAndRestoreInputSummary();
|
|
@@ -1771,7 +1812,7 @@ async function main() {
|
|
|
1771
1812
|
testParseArgsShouldSupportInputSummaryJson();
|
|
1772
1813
|
await testCallTextModelShouldNotTruncateLongResume();
|
|
1773
1814
|
await testCallTextModelShouldFallbackToChunkModeOnContextLimit();
|
|
1774
|
-
await
|
|
1815
|
+
await testTextModelShouldDefaultThinkingLowForVolcengine();
|
|
1775
1816
|
await testTextModelShouldSupportLowThinkingForVolcengine();
|
|
1776
1817
|
await testPrepareVisionImageSegmentsShouldSplitLongImage();
|
|
1777
1818
|
await testVisionEvidenceGateShouldDemoteImageFallbackWithoutEvidence();
|