@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.22
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 +5 -2
- package/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +33 -33
- package/package.json +8 -8
- package/scripts/install-macos.sh +280 -280
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +42 -42
- package/skills/boss-chat/SKILL.md +106 -106
- package/skills/boss-recommend-pipeline/README.md +13 -13
- package/skills/boss-recommend-pipeline/SKILL.md +219 -214
- package/skills/boss-recruit-pipeline/README.md +19 -19
- package/skills/boss-recruit-pipeline/SKILL.md +89 -89
- package/src/chat-mcp.js +127 -127
- package/src/chat-runtime-config.js +775 -775
- package/src/cli.js +573 -573
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +2419 -2415
- package/src/core/capture/index.js +1201 -1201
- package/src/core/cv-acquisition/index.js +238 -238
- package/src/core/cv-capture-target/index.js +299 -299
- package/src/core/greet-quota/index.js +71 -71
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +334 -332
- package/src/core/run/index.js +32 -32
- package/src/core/run/timing.js +33 -33
- package/src/core/screening/index.js +2135 -2135
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/detached-worker.js +99 -99
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +9 -9
- package/src/domains/chat/detail.js +113 -113
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +620 -620
- package/src/domains/chat/page-guard.js +122 -122
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +571 -571
- package/src/domains/common/account-rights-panel.js +314 -314
- package/src/domains/common/recovery-settle.js +159 -159
- package/src/domains/recommend/actions.js +472 -472
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/colleague-contact.js +333 -333
- package/src/domains/recommend/constants.js +228 -159
- package/src/domains/recommend/detail.js +650 -650
- package/src/domains/recommend/filters.js +748 -377
- package/src/domains/recommend/index.js +4 -3
- package/src/domains/recommend/jobs.js +542 -542
- package/src/domains/recommend/location.js +736 -0
- package/src/domains/recommend/refresh.js +504 -361
- package/src/domains/recommend/roots.js +80 -80
- package/src/domains/recommend/run-service.js +987 -854
- package/src/domains/recommend/scopes.js +246 -246
- package/src/domains/recruit/actions.js +277 -277
- package/src/domains/recruit/cards.js +74 -74
- package/src/domains/recruit/constants.js +236 -236
- package/src/domains/recruit/detail.js +588 -588
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +866 -866
- package/src/domains/recruit/refresh.js +45 -45
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1620 -1620
- package/src/domains/recruit/search.js +3229 -3229
- package/src/index.js +13 -0
- package/src/parser.js +376 -8
- package/src/recommend-mcp.js +929 -915
- package/src/recommend-scheduler.js +496 -496
- package/src/recruit-mcp.js +2121 -2121
|
@@ -1,2135 +1,2135 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const SUPPORTED_DOMAINS = new Set(["recommend", "recruit", "chat"]);
|
|
5
|
-
|
|
6
|
-
const DEGREE_RANK = {
|
|
7
|
-
"初中及以下": 1,
|
|
8
|
-
"中专/中技": 2,
|
|
9
|
-
"高中": 3,
|
|
10
|
-
"大专": 4,
|
|
11
|
-
"本科": 5,
|
|
12
|
-
"硕士": 6,
|
|
13
|
-
"博士": 7
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const DEGREE_PATTERNS = [
|
|
17
|
-
{ value: "博士", regex: /博士|phd|doctor/i },
|
|
18
|
-
{ value: "硕士", regex: /硕士|研究生|master/i },
|
|
19
|
-
{ value: "本科", regex: /本科|学士|bachelor/i },
|
|
20
|
-
{ value: "大专", regex: /大专|专科|college/i },
|
|
21
|
-
{ value: "高中", regex: /高中/i },
|
|
22
|
-
{ value: "中专/中技", regex: /中专|中技/i },
|
|
23
|
-
{ value: "初中及以下", regex: /初中及以下|初中以下/i }
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const ENTITY_MAP = {
|
|
27
|
-
amp: "&",
|
|
28
|
-
lt: "<",
|
|
29
|
-
gt: ">",
|
|
30
|
-
quot: "\"",
|
|
31
|
-
apos: "'",
|
|
32
|
-
nbsp: " "
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const GENDER_CODE_MAP = {
|
|
36
|
-
1: "男",
|
|
37
|
-
2: "女"
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
|
|
41
|
-
const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
|
|
42
|
-
const FAST_FIRST_DEFAULT_FAST_MAX_TOKENS = 384;
|
|
43
|
-
const FATAL_LLM_PROVIDER_MAX_RETRIES = 2;
|
|
44
|
-
|
|
45
|
-
function nowIso() {
|
|
46
|
-
return new Date().toISOString();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizeLlmThinkingLevel(value) {
|
|
50
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
51
|
-
return LLM_THINKING_LEVELS.has(normalized) ? normalized : "";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizeLlmScreeningStrategy(value) {
|
|
55
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
56
|
-
return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : "single_pass";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizeBaseUrl(baseUrl) {
|
|
60
|
-
return String(baseUrl || "").replace(/\/+$/, "");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildChatCompletionsUrl(baseUrl) {
|
|
64
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
65
|
-
if (/\/chat\/completions$/i.test(normalized)) return normalized;
|
|
66
|
-
return `${normalized}/chat/completions`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function redactBaseUrl(baseUrl) {
|
|
70
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
71
|
-
return normalized ? normalized.replace(/\/\/[^/]+/, "//[redacted-host]") : "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function firstConfiguredValue(...values) {
|
|
75
|
-
for (const value of values) {
|
|
76
|
-
if (value === undefined || value === null) continue;
|
|
77
|
-
if (typeof value === "string" && !value.trim()) continue;
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
return "";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizeLlmProviderEntry(rawEntry, inherited = {}, index = 0) {
|
|
84
|
-
const entry = typeof rawEntry === "string"
|
|
85
|
-
? { model: rawEntry }
|
|
86
|
-
: (rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {});
|
|
87
|
-
const providerName = firstConfiguredValue(
|
|
88
|
-
entry.name,
|
|
89
|
-
entry.label,
|
|
90
|
-
entry.id,
|
|
91
|
-
entry.providerName,
|
|
92
|
-
entry.provider,
|
|
93
|
-
""
|
|
94
|
-
);
|
|
95
|
-
const next = {
|
|
96
|
-
...inherited,
|
|
97
|
-
...entry,
|
|
98
|
-
baseUrl: firstConfiguredValue(entry.baseUrl, entry.base_url, inherited.baseUrl, inherited.base_url),
|
|
99
|
-
apiKey: firstConfiguredValue(entry.apiKey, entry.api_key, inherited.apiKey, inherited.api_key),
|
|
100
|
-
model: firstConfiguredValue(entry.model, entry.modelName, entry.model_name, typeof rawEntry === "string" ? rawEntry : "", inherited.model),
|
|
101
|
-
openaiOrganization: firstConfiguredValue(entry.openaiOrganization, entry.organization, inherited.openaiOrganization, inherited.organization),
|
|
102
|
-
openaiProject: firstConfiguredValue(entry.openaiProject, entry.project, inherited.openaiProject, inherited.project),
|
|
103
|
-
topP: firstConfiguredValue(entry.topP, entry.top_p, inherited.topP, inherited.top_p),
|
|
104
|
-
llmProviderName: normalizeText(providerName),
|
|
105
|
-
llmProviderIndex: index
|
|
106
|
-
};
|
|
107
|
-
delete next.llmModels;
|
|
108
|
-
delete next.models;
|
|
109
|
-
return next;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function normalizeLlmProviderConfigs(config = {}) {
|
|
113
|
-
if (Array.isArray(config)) {
|
|
114
|
-
return config.map((entry, index) => normalizeLlmProviderEntry(entry, {}, index));
|
|
115
|
-
}
|
|
116
|
-
const inherited = config && typeof config === "object" && !Array.isArray(config) ? { ...config } : {};
|
|
117
|
-
const rawProviders = Array.isArray(inherited.llmModels) && inherited.llmModels.length > 0
|
|
118
|
-
? inherited.llmModels
|
|
119
|
-
: (Array.isArray(inherited.models) && inherited.models.length > 0 ? inherited.models : [inherited]);
|
|
120
|
-
delete inherited.llmModels;
|
|
121
|
-
delete inherited.models;
|
|
122
|
-
return rawProviders.map((entry, index) => normalizeLlmProviderEntry(entry, inherited, index));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function compactLlmProviderFailure(error, providerConfig = {}, providerIndex = 0) {
|
|
126
|
-
return {
|
|
127
|
-
index: providerIndex + 1,
|
|
128
|
-
name: normalizeText(providerConfig.llmProviderName || providerConfig.name || providerConfig.label || providerConfig.id) || null,
|
|
129
|
-
baseUrl: redactBaseUrl(providerConfig.baseUrl),
|
|
130
|
-
model: normalizeText(providerConfig.model) || null,
|
|
131
|
-
status: Number.isFinite(Number(error?.status)) ? Number(error.status) : null,
|
|
132
|
-
attempts: Number(error?.llm_attempt_count) || 0,
|
|
133
|
-
message: String(error?.message || error || "").slice(0, 500)
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function classifyFatalLlmProviderError(error) {
|
|
138
|
-
if (!error) return null;
|
|
139
|
-
if (error.llm_fatal_provider_error) {
|
|
140
|
-
return {
|
|
141
|
-
code: error.code || "LLM_FATAL_PROVIDER_ERROR",
|
|
142
|
-
reason: error.llm_fatal_reason || "fatal_provider_error"
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
const status = Number(error?.status);
|
|
146
|
-
const message = String(error?.message || error || "");
|
|
147
|
-
const providerCode = normalizeText(error?.provider_error_code).toLowerCase();
|
|
148
|
-
const providerType = normalizeText(error?.provider_error_type).toLowerCase();
|
|
149
|
-
const searchable = `${providerCode} ${providerType} ${message}`.toLowerCase();
|
|
150
|
-
if (/(?:budget[_\s-]*exceeded|budget has been exceeded|max budget|spend cap|hard limit|credit limit)/i.test(searchable)) {
|
|
151
|
-
return { code: "LLM_BUDGET_EXCEEDED", reason: "budget_exceeded" };
|
|
152
|
-
}
|
|
153
|
-
if (/(?:insufficient[_\s-]*quota|quota[_\s-]*(?:exceeded|exhausted)|(?:exceeded|exhausted).*quota|out of quota|usage quota|token quota|monthly quota|rate limit quota)/i.test(searchable)) {
|
|
154
|
-
return { code: "LLM_QUOTA_EXCEEDED", reason: "quota_exceeded" };
|
|
155
|
-
}
|
|
156
|
-
if (/(?:insufficient[_\s-]*(?:balance|credit|credits)|no credits|credit balance|billing|payment required|unpaid|account balance)/i.test(searchable)) {
|
|
157
|
-
return { code: "LLM_BILLING_REQUIRED", reason: "billing_required" };
|
|
158
|
-
}
|
|
159
|
-
if (status === 401 || /(?:unauthorized|unauthorised|invalid api key|incorrect api key|authentication failed|invalid authentication|api key invalid|no api key)/i.test(searchable)) {
|
|
160
|
-
return { code: "LLM_AUTH_FAILED", reason: "auth_failed" };
|
|
161
|
-
}
|
|
162
|
-
if (status === 403 || /(?:forbidden|permission denied|access denied|not authorized|not authorised|model access denied)/i.test(searchable)) {
|
|
163
|
-
return { code: "LLM_PERMISSION_DENIED", reason: "permission_denied" };
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function isFatalLlmProviderError(error) {
|
|
169
|
-
return Boolean(classifyFatalLlmProviderError(error));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function createFatalLlmRunError(error, { domain = "", candidate = null } = {}) {
|
|
173
|
-
const classification = classifyFatalLlmProviderError(error) || {
|
|
174
|
-
code: "LLM_FATAL_PROVIDER_ERROR",
|
|
175
|
-
reason: "fatal_provider_error"
|
|
176
|
-
};
|
|
177
|
-
const attempts = Number(error?.llm_attempt_count) || 0;
|
|
178
|
-
const suffix = attempts ? ` after ${attempts} attempts` : "";
|
|
179
|
-
const fatal = new Error(`Fatal LLM provider error${suffix}: ${error?.message || String(error || "unknown")}`);
|
|
180
|
-
fatal.name = "FatalLlmProviderError";
|
|
181
|
-
fatal.code = classification.code;
|
|
182
|
-
fatal.llm_fatal_provider_error = true;
|
|
183
|
-
fatal.llm_fatal_reason = classification.reason;
|
|
184
|
-
fatal.llm_attempt_count = attempts;
|
|
185
|
-
fatal.status = Number.isFinite(Number(error?.status)) ? Number(error.status) : null;
|
|
186
|
-
fatal.provider_error_code = error?.provider_error_code || null;
|
|
187
|
-
fatal.provider_error_type = error?.provider_error_type || null;
|
|
188
|
-
fatal.provider_error_message = error?.provider_error_message || null;
|
|
189
|
-
fatal.domain = domain || null;
|
|
190
|
-
fatal.candidate_id = candidate?.id || null;
|
|
191
|
-
fatal.candidate_name = candidate?.identity?.name || null;
|
|
192
|
-
fatal.cause = error || null;
|
|
193
|
-
return fatal;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function isVolcengineModel(baseUrl, model) {
|
|
197
|
-
return /volces|volcengine|ark\.cn|doubao|seed/i.test(`${baseUrl || ""} ${model || ""}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function isNativeVolcengineBaseUrl(baseUrl) {
|
|
201
|
-
return /volces|volcengine|ark\.cn/i.test(String(baseUrl || ""));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isOpenAiCompatibleV1BaseUrl(baseUrl) {
|
|
205
|
-
return /(?:^|\/)v1(?:\/)?$/i.test(normalizeBaseUrl(baseUrl));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
|
|
209
|
-
const level = normalizeLlmThinkingLevel(thinkingLevel);
|
|
210
|
-
if (!level || level === "current" || level === "auto") return payload;
|
|
211
|
-
if (isVolcengineModel(baseUrl, model)) {
|
|
212
|
-
if (level === "off" || level === "minimal") {
|
|
213
|
-
payload.thinking = { type: "disabled" };
|
|
214
|
-
} else {
|
|
215
|
-
payload.thinking = { type: "enabled" };
|
|
216
|
-
}
|
|
217
|
-
if (!isNativeVolcengineBaseUrl(baseUrl) && isOpenAiCompatibleV1BaseUrl(baseUrl)) {
|
|
218
|
-
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
219
|
-
}
|
|
220
|
-
return payload;
|
|
221
|
-
}
|
|
222
|
-
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
223
|
-
return payload;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function parsePositiveNumber(value, fallback = null) {
|
|
227
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
228
|
-
const parsed = Number(value);
|
|
229
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function parseFiniteNumber(value, fallback = null) {
|
|
233
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
234
|
-
const parsed = Number(value);
|
|
235
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function resolveLlmOutputTokenBudget(config = {}, thinkingLevel = "", options = {}) {
|
|
239
|
-
const explicit = parsePositiveNumber(
|
|
240
|
-
config.llmMaxCompletionTokens
|
|
241
|
-
?? config.maxCompletionTokens
|
|
242
|
-
?? config.llmMaxTokens
|
|
243
|
-
?? config.maxTokens,
|
|
244
|
-
null
|
|
245
|
-
);
|
|
246
|
-
if (explicit) return Math.max(1, Math.floor(explicit));
|
|
247
|
-
if (options.requireReviewDecision) return 384;
|
|
248
|
-
const normalizedThinking = normalizeLlmThinkingLevel(thinkingLevel || "low") || "low";
|
|
249
|
-
return normalizedThinking === "off" || normalizedThinking === "minimal" ? 64 : 512;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function parsePositiveInteger(value, fallback = null) {
|
|
253
|
-
const parsed = parsePositiveNumber(value, fallback);
|
|
254
|
-
return parsed ? Math.max(1, Math.floor(parsed)) : fallback;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function normalizeText(input) {
|
|
258
|
-
return String(input || "").replace(/\s+/g, " ").trim();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function normalizeBlockText(input) {
|
|
262
|
-
return String(input ?? "").trim();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function normalizeReasoningKey(input) {
|
|
266
|
-
return normalizeBlockText(input).replace(/\s+/g, " ");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function collapseRepeatedReasoningText(input) {
|
|
270
|
-
const text = normalizeBlockText(input);
|
|
271
|
-
if (!text) return "";
|
|
272
|
-
const chunks = text.split(/\n{2,}/).map(normalizeBlockText).filter(Boolean);
|
|
273
|
-
if (chunks.length >= 2 && chunks.length % 2 === 0) {
|
|
274
|
-
const midpoint = chunks.length / 2;
|
|
275
|
-
const first = chunks.slice(0, midpoint);
|
|
276
|
-
const second = chunks.slice(midpoint);
|
|
277
|
-
if (normalizeReasoningKey(first.join("\n\n")) === normalizeReasoningKey(second.join("\n\n"))) {
|
|
278
|
-
return first.join("\n\n");
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const compacted = normalizeReasoningKey(text);
|
|
283
|
-
if (compacted.length < 160) return text;
|
|
284
|
-
const midpoint = Math.floor(compacted.length / 2);
|
|
285
|
-
for (let offset = -32; offset <= 32; offset += 1) {
|
|
286
|
-
const split = midpoint + offset;
|
|
287
|
-
if (split <= 80 || split >= compacted.length - 80) continue;
|
|
288
|
-
const first = compacted.slice(0, split).trim();
|
|
289
|
-
const second = compacted.slice(split).trim();
|
|
290
|
-
if (first && first === second) return first;
|
|
291
|
-
}
|
|
292
|
-
return text;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function firstReasoningText(lines) {
|
|
296
|
-
for (const line of lines) {
|
|
297
|
-
const cleaned = collapseRepeatedReasoningText(line);
|
|
298
|
-
if (cleaned) return cleaned;
|
|
299
|
-
}
|
|
300
|
-
return "";
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function compact(input) {
|
|
304
|
-
return normalizeText(input).toLowerCase();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function decodeHtmlEntities(input) {
|
|
308
|
-
return String(input || "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
|
|
309
|
-
const key = String(entity).toLowerCase();
|
|
310
|
-
if (key.startsWith("#x")) {
|
|
311
|
-
const value = Number.parseInt(key.slice(2), 16);
|
|
312
|
-
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
313
|
-
}
|
|
314
|
-
if (key.startsWith("#")) {
|
|
315
|
-
const value = Number.parseInt(key.slice(1), 10);
|
|
316
|
-
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
317
|
-
}
|
|
318
|
-
return ENTITY_MAP[key] || match;
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
export function htmlToText(html) {
|
|
323
|
-
const withoutScripts = String(html || "")
|
|
324
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
325
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
|
|
326
|
-
const withBreaks = withoutScripts
|
|
327
|
-
.replace(/<\/(?:div|li|p|section|article|header|footer|h[1-6]|tr)>/gi, "\n")
|
|
328
|
-
.replace(/<br\s*\/?>/gi, "\n");
|
|
329
|
-
return decodeHtmlEntities(withBreaks.replace(/<[^>]+>/g, " "))
|
|
330
|
-
.split(/\r?\n/)
|
|
331
|
-
.map((line) => normalizeText(line))
|
|
332
|
-
.filter(Boolean)
|
|
333
|
-
.join("\n");
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function parseHtmlAttributes(html) {
|
|
337
|
-
const attributes = {};
|
|
338
|
-
const openTag = String(html || "").match(/^<[^>]+>/s)?.[0] || "";
|
|
339
|
-
const regex = /([:@A-Za-z0-9_-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
340
|
-
let match;
|
|
341
|
-
while ((match = regex.exec(openTag))) {
|
|
342
|
-
const name = match[1];
|
|
343
|
-
if (!name || name.startsWith("<")) continue;
|
|
344
|
-
attributes[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
345
|
-
}
|
|
346
|
-
return attributes;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function unique(values) {
|
|
350
|
-
return Array.from(new Set(values.map(normalizeText).filter(Boolean)));
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function normalizeDomain(domain) {
|
|
354
|
-
const normalized = compact(domain);
|
|
355
|
-
if (!SUPPORTED_DOMAINS.has(normalized)) {
|
|
356
|
-
throw new Error(`Unsupported screening domain: ${domain}`);
|
|
357
|
-
}
|
|
358
|
-
return normalized;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function collectTextParts(candidate = {}) {
|
|
362
|
-
return unique([
|
|
363
|
-
candidate.text,
|
|
364
|
-
candidate.raw_text,
|
|
365
|
-
candidate.summary,
|
|
366
|
-
candidate.resume_text,
|
|
367
|
-
candidate.identity?.name,
|
|
368
|
-
candidate.identity?.title,
|
|
369
|
-
candidate.identity?.current_position,
|
|
370
|
-
candidate.identity?.current_company,
|
|
371
|
-
candidate.identity?.school,
|
|
372
|
-
candidate.identity?.major,
|
|
373
|
-
...(candidate.tags || [])
|
|
374
|
-
]);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function parseDegree(text) {
|
|
378
|
-
for (const item of DEGREE_PATTERNS) {
|
|
379
|
-
if (item.regex.test(text)) return item.value;
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function parseYearsExperience(text) {
|
|
385
|
-
const normalized = normalizeText(text);
|
|
386
|
-
const match = normalized.match(/(?<!\d)(\d{1,2})\s*(?:年以上?|年)\s*(?:经验|工作经验)/i)
|
|
387
|
-
|| normalized.match(/(?:经验|工作经验|工作)\s*(?<!\d)(\d{1,2})\s*(?:年以上?|年)/i)
|
|
388
|
-
|| normalized.match(/(?<!\d)(\d{1,2})\s*years?\s*(?:of\s*)?(?:experience|work)?/i);
|
|
389
|
-
if (!match) return null;
|
|
390
|
-
const value = Number.parseInt(match[1], 10);
|
|
391
|
-
return Number.isFinite(value) ? value : null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function parseAge(text) {
|
|
395
|
-
const match = normalizeText(text).match(/(\d{2})\s*岁/);
|
|
396
|
-
if (!match) return null;
|
|
397
|
-
const value = Number.parseInt(match[1], 10);
|
|
398
|
-
return Number.isFinite(value) ? value : null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function parseGender(text) {
|
|
402
|
-
const normalized = normalizeText(text);
|
|
403
|
-
if (/(?:^|[\s||,,])男(?:$|[\s||,,])/.test(normalized)) return "男";
|
|
404
|
-
if (/(?:^|[\s||,,])女(?:$|[\s||,,])/.test(normalized)) return "女";
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function normalizeGenderValue(value) {
|
|
409
|
-
if (value == null || value === "") return null;
|
|
410
|
-
if (GENDER_CODE_MAP[value]) return GENDER_CODE_MAP[value];
|
|
411
|
-
const normalized = normalizeText(value);
|
|
412
|
-
if (normalized === "男" || normalized === "女") return normalized;
|
|
413
|
-
return parseGender(normalized);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function parseDateLike(value) {
|
|
417
|
-
const normalized = normalizeText(value);
|
|
418
|
-
if (!normalized || normalized === "0") return "";
|
|
419
|
-
if (/^\d{6}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
420
|
-
if (/^\d{8}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
421
|
-
return normalized;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function isLikelySalaryLine(value = "") {
|
|
425
|
-
const normalized = normalizeText(value);
|
|
426
|
-
return Boolean(
|
|
427
|
-
/^(?:面议|薪资面议)$/i.test(normalized)
|
|
428
|
-
|| /^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?$/.test(normalized)
|
|
429
|
-
|| /^\d+\s*-\s*\d+\s*元\s*\/\s*天$/.test(normalized)
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function isLikelyStatusLine(value = "") {
|
|
434
|
-
const normalized = normalizeText(value);
|
|
435
|
-
return Boolean(
|
|
436
|
-
!normalized
|
|
437
|
-
|| /^沟通|^收藏|^查看|^不合适/.test(normalized)
|
|
438
|
-
|| /^(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/.test(normalized)
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function stripLeadingSalaryToken(value = "") {
|
|
443
|
-
return normalizeText(value)
|
|
444
|
-
.replace(/^(?:面议|薪资面议)\s+/i, "")
|
|
445
|
-
.replace(/^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?\s+/, "")
|
|
446
|
-
.replace(/^\d+\s*-\s*\d+\s*元\s*\/\s*天\s+/, "")
|
|
447
|
-
.trim();
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function stripTrailingStatusToken(value = "") {
|
|
451
|
-
return normalizeText(value)
|
|
452
|
-
.replace(/\s*(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/u, "")
|
|
453
|
-
.trim();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function cleanInferredNameLine(value = "") {
|
|
457
|
-
const withoutSalary = stripLeadingSalaryToken(value);
|
|
458
|
-
const withoutStatus = stripTrailingStatusToken(withoutSalary);
|
|
459
|
-
return withoutStatus && !isLikelyStatusLine(withoutStatus) && !isLikelySalaryLine(withoutStatus)
|
|
460
|
-
? withoutStatus
|
|
461
|
-
: "";
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function firstUsefulLine(lines) {
|
|
465
|
-
for (const line of lines) {
|
|
466
|
-
const cleaned = cleanInferredNameLine(line);
|
|
467
|
-
if (cleaned) return cleaned;
|
|
468
|
-
}
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function parseNetworkBodyText(networkBody = {}) {
|
|
473
|
-
const bodyResult = networkBody.body || networkBody;
|
|
474
|
-
let body = String(bodyResult?.body || "");
|
|
475
|
-
if (bodyResult?.base64Encoded) {
|
|
476
|
-
body = Buffer.from(body, "base64").toString("utf8");
|
|
477
|
-
}
|
|
478
|
-
return body;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function tryParseJson(text) {
|
|
482
|
-
try {
|
|
483
|
-
return JSON.parse(text);
|
|
484
|
-
} catch {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function tryExtractJsonObject(text) {
|
|
490
|
-
const normalized = String(text || "").trim();
|
|
491
|
-
const direct = tryParseJson(normalized);
|
|
492
|
-
if (direct && typeof direct === "object") return direct;
|
|
493
|
-
const fenced = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
494
|
-
if (fenced) {
|
|
495
|
-
const parsed = tryParseJson(fenced[1].trim());
|
|
496
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
497
|
-
}
|
|
498
|
-
const start = normalized.indexOf("{");
|
|
499
|
-
const end = normalized.lastIndexOf("}");
|
|
500
|
-
if (start >= 0 && end > start) {
|
|
501
|
-
const parsed = tryParseJson(normalized.slice(start, end + 1));
|
|
502
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
503
|
-
}
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function extractBalancedJsonAt(text = "", startIndex = 0) {
|
|
508
|
-
const source = String(text || "");
|
|
509
|
-
const start = source.indexOf("{", Math.max(0, Number(startIndex) || 0));
|
|
510
|
-
if (start < 0) return "";
|
|
511
|
-
let depth = 0;
|
|
512
|
-
let inString = false;
|
|
513
|
-
let quote = "";
|
|
514
|
-
let escaped = false;
|
|
515
|
-
for (let index = start; index < source.length; index += 1) {
|
|
516
|
-
const char = source[index];
|
|
517
|
-
if (inString) {
|
|
518
|
-
if (escaped) {
|
|
519
|
-
escaped = false;
|
|
520
|
-
} else if (char === "\\") {
|
|
521
|
-
escaped = true;
|
|
522
|
-
} else if (char === quote) {
|
|
523
|
-
inString = false;
|
|
524
|
-
quote = "";
|
|
525
|
-
}
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (char === "\"" || char === "'") {
|
|
529
|
-
inString = true;
|
|
530
|
-
quote = char;
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
if (char === "{") depth += 1;
|
|
534
|
-
if (char === "}") {
|
|
535
|
-
depth -= 1;
|
|
536
|
-
if (depth === 0) return source.slice(start, index + 1);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
return "";
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function tryParseEmbeddedJsonObjects(text = "") {
|
|
543
|
-
const source = decodeHtmlEntities(String(text || ""));
|
|
544
|
-
const objects = [];
|
|
545
|
-
const anchors = [
|
|
546
|
-
"__INITIAL_STATE__",
|
|
547
|
-
"__NEXT_DATA__",
|
|
548
|
-
"geekDetailInfo",
|
|
549
|
-
"geekDetail",
|
|
550
|
-
"geekBaseInfo",
|
|
551
|
-
"geekEduExpList",
|
|
552
|
-
"geekWorkExpList",
|
|
553
|
-
"resume"
|
|
554
|
-
];
|
|
555
|
-
for (const anchor of anchors) {
|
|
556
|
-
let searchIndex = 0;
|
|
557
|
-
while (searchIndex >= 0 && searchIndex < source.length) {
|
|
558
|
-
const anchorIndex = source.indexOf(anchor, searchIndex);
|
|
559
|
-
if (anchorIndex < 0) break;
|
|
560
|
-
const windowStart = Math.max(0, anchorIndex - 4000);
|
|
561
|
-
const braceIndex = source.lastIndexOf("{", anchorIndex);
|
|
562
|
-
if (braceIndex >= windowStart) {
|
|
563
|
-
const jsonText = extractBalancedJsonAt(source, braceIndex);
|
|
564
|
-
const parsed = tryParseJson(jsonText);
|
|
565
|
-
if (parsed && typeof parsed === "object") objects.push(parsed);
|
|
566
|
-
}
|
|
567
|
-
searchIndex = anchorIndex + anchor.length;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return objects;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function flattenChatMessageContent(content) {
|
|
574
|
-
if (typeof content === "string") return content;
|
|
575
|
-
if (Array.isArray(content)) {
|
|
576
|
-
return content.map((item) => {
|
|
577
|
-
if (typeof item === "string") return item;
|
|
578
|
-
return item?.text || item?.content || item?.reasoning_content || "";
|
|
579
|
-
}).filter(Boolean).join("\n");
|
|
580
|
-
}
|
|
581
|
-
return "";
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function collectLlmReasoningText(choice = {}) {
|
|
585
|
-
const message = choice?.message || {};
|
|
586
|
-
const seen = new Set();
|
|
587
|
-
const unique = [];
|
|
588
|
-
for (const item of [
|
|
589
|
-
message.reasoning_content,
|
|
590
|
-
message.provider_specific_fields?.reasoning_content,
|
|
591
|
-
message.reasoning,
|
|
592
|
-
message.provider_specific_fields?.reasoning,
|
|
593
|
-
message.cot,
|
|
594
|
-
message.provider_specific_fields?.cot,
|
|
595
|
-
message.chain_of_thought,
|
|
596
|
-
message.provider_specific_fields?.chain_of_thought,
|
|
597
|
-
choice.reasoning_content,
|
|
598
|
-
choice.provider_specific_fields?.reasoning_content,
|
|
599
|
-
choice.reasoning,
|
|
600
|
-
choice.provider_specific_fields?.reasoning,
|
|
601
|
-
choice.cot,
|
|
602
|
-
choice.provider_specific_fields?.cot,
|
|
603
|
-
choice.chain_of_thought,
|
|
604
|
-
choice.provider_specific_fields?.chain_of_thought
|
|
605
|
-
]) {
|
|
606
|
-
const text = collapseRepeatedReasoningText(flattenChatMessageContent(item));
|
|
607
|
-
const key = normalizeReasoningKey(text);
|
|
608
|
-
if (!key || seen.has(key)) continue;
|
|
609
|
-
seen.add(key);
|
|
610
|
-
unique.push(text);
|
|
611
|
-
}
|
|
612
|
-
return collapseRepeatedReasoningText(unique.join("\n\n"));
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function mimeTypeForImagePath(filePath) {
|
|
616
|
-
const extension = path.extname(String(filePath || "")).toLowerCase();
|
|
617
|
-
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
|
|
618
|
-
if (extension === ".webp") return "image/webp";
|
|
619
|
-
return "image/png";
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function normalizeImagePaths({ imageEvidence = null, imagePaths = [] } = {}) {
|
|
623
|
-
const paths = [];
|
|
624
|
-
if (Array.isArray(imagePaths)) {
|
|
625
|
-
paths.push(...imagePaths);
|
|
626
|
-
}
|
|
627
|
-
const evidenceLlmPaths = Array.isArray(imageEvidence?.llm_file_paths)
|
|
628
|
-
? imageEvidence.llm_file_paths
|
|
629
|
-
: [];
|
|
630
|
-
if (evidenceLlmPaths.length) {
|
|
631
|
-
paths.push(...evidenceLlmPaths);
|
|
632
|
-
} else {
|
|
633
|
-
if (Array.isArray(imageEvidence?.file_paths)) {
|
|
634
|
-
paths.push(...imageEvidence.file_paths);
|
|
635
|
-
}
|
|
636
|
-
if (Array.isArray(imageEvidence?.screenshots)) {
|
|
637
|
-
paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return unique(paths.map((filePath) => String(filePath || "").trim()).filter(Boolean));
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function imagePathToLlmInput(filePath, {
|
|
644
|
-
detail = "high"
|
|
645
|
-
} = {}) {
|
|
646
|
-
const resolved = path.resolve(filePath);
|
|
647
|
-
const buffer = fs.readFileSync(resolved);
|
|
648
|
-
const mimeType = mimeTypeForImagePath(resolved);
|
|
649
|
-
return {
|
|
650
|
-
type: "image_url",
|
|
651
|
-
image_url: {
|
|
652
|
-
url: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
653
|
-
detail
|
|
654
|
-
},
|
|
655
|
-
metadata: {
|
|
656
|
-
file_path: resolved,
|
|
657
|
-
mime_type: mimeType,
|
|
658
|
-
byte_length: buffer.length
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
export function buildScreeningLlmImageInputs({
|
|
664
|
-
imageEvidence = null,
|
|
665
|
-
imagePaths = [],
|
|
666
|
-
maxImages = 8,
|
|
667
|
-
detail = "high"
|
|
668
|
-
} = {}) {
|
|
669
|
-
const paths = normalizeImagePaths({ imageEvidence, imagePaths });
|
|
670
|
-
const limit = Math.max(1, Number(maxImages) || 8);
|
|
671
|
-
return paths.slice(0, limit).map((filePath) => imagePathToLlmInput(filePath, { detail }));
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function summarizeLlmImageInputs(imageInputs = []) {
|
|
675
|
-
return imageInputs.map((input, index) => ({
|
|
676
|
-
index,
|
|
677
|
-
file_path: input.metadata?.file_path || null,
|
|
678
|
-
mime_type: input.metadata?.mime_type || null,
|
|
679
|
-
byte_length: input.metadata?.byte_length || 0
|
|
680
|
-
}));
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function parsePassedDecision(value) {
|
|
684
|
-
if (typeof value === "boolean") return value;
|
|
685
|
-
if (typeof value === "number") return value !== 0;
|
|
686
|
-
const normalized = normalizeText(value).toLowerCase();
|
|
687
|
-
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(normalized)) return true;
|
|
688
|
-
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(normalized)) return false;
|
|
689
|
-
return null;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function pickFirst(...values) {
|
|
693
|
-
for (const value of values) {
|
|
694
|
-
const normalized = normalizeText(value);
|
|
695
|
-
if (normalized && normalized !== "0") return normalized;
|
|
696
|
-
}
|
|
697
|
-
return "";
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function isPlainObject(value) {
|
|
701
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function isBossGeekDetailShape(value) {
|
|
705
|
-
if (!isPlainObject(value)) return false;
|
|
706
|
-
return Boolean(
|
|
707
|
-
isPlainObject(value.geekBaseInfo)
|
|
708
|
-
|| value.geekName
|
|
709
|
-
|| value.geekAdvantage
|
|
710
|
-
|| Array.isArray(value.geekEduExpList)
|
|
711
|
-
|| Array.isArray(value.geekEducationList)
|
|
712
|
-
|| Array.isArray(value.geekWorkExpList)
|
|
713
|
-
|| Array.isArray(value.geekProjExpList)
|
|
714
|
-
|| Array.isArray(value.geekCertificationList)
|
|
715
|
-
|| Array.isArray(value.geekSkillList)
|
|
716
|
-
|| isPlainObject(value.highestEduExp)
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
function isBossChatProfileShape(value) {
|
|
721
|
-
if (!isPlainObject(value)) return false;
|
|
722
|
-
return Boolean(
|
|
723
|
-
(value.name || value.encryptGeekId || value.uid)
|
|
724
|
-
&& (
|
|
725
|
-
Array.isArray(value.eduExpList)
|
|
726
|
-
|| Array.isArray(value.workExpList)
|
|
727
|
-
|| value.school
|
|
728
|
-
|| value.major
|
|
729
|
-
|| value.lastCompany
|
|
730
|
-
|| value.positionName
|
|
731
|
-
)
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function collectObjects(root, {
|
|
736
|
-
maxObjects = 500,
|
|
737
|
-
maxDepth = 8
|
|
738
|
-
} = {}) {
|
|
739
|
-
if (!root || typeof root !== "object") return [];
|
|
740
|
-
const queue = [{ value: root, depth: 0 }];
|
|
741
|
-
const seen = new WeakSet();
|
|
742
|
-
const objects = [];
|
|
743
|
-
while (queue.length && objects.length < maxObjects) {
|
|
744
|
-
const { value, depth } = queue.shift();
|
|
745
|
-
if (!value || typeof value !== "object" || seen.has(value)) continue;
|
|
746
|
-
seen.add(value);
|
|
747
|
-
if (isPlainObject(value)) objects.push(value);
|
|
748
|
-
if (depth >= maxDepth) continue;
|
|
749
|
-
const children = Array.isArray(value) ? value : Object.values(value);
|
|
750
|
-
for (const child of children) {
|
|
751
|
-
if (child && typeof child === "object") {
|
|
752
|
-
queue.push({ value: child, depth: depth + 1 });
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
return objects;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function joinRange(start, end, fallback = "") {
|
|
760
|
-
const left = parseDateLike(start);
|
|
761
|
-
const right = parseDateLike(end);
|
|
762
|
-
if (left && right) return `${left}-${right}`;
|
|
763
|
-
if (left) return `${left}-至今`;
|
|
764
|
-
if (right) return right;
|
|
765
|
-
return normalizeText(fallback);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function normalizeList(value) {
|
|
769
|
-
if (!value) return [];
|
|
770
|
-
if (Array.isArray(value)) return value;
|
|
771
|
-
return [];
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function normalizeTagList(value) {
|
|
775
|
-
if (!Array.isArray(value)) return [];
|
|
776
|
-
return value.map((item) => {
|
|
777
|
-
if (typeof item === "string") return item;
|
|
778
|
-
return pickFirst(item?.name, item?.label, item?.tagName, item?.text, item?.value);
|
|
779
|
-
}).filter(Boolean);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function formatNamedSection(title, lines = []) {
|
|
783
|
-
const normalized = lines.map(normalizeText).filter(Boolean);
|
|
784
|
-
if (!normalized.length) return "";
|
|
785
|
-
return [`【${title}】`, ...normalized].join("\n");
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function formatWorkExperience(item = {}, index = 0) {
|
|
789
|
-
const company = pickFirst(item.formattedCompany, item.company);
|
|
790
|
-
const position = pickFirst(item.positionName, item.positionTitle, item.position);
|
|
791
|
-
const period = joinRange(item.startYearMonStr || item.startDate, item.endYearMonStr || item.endDate, item.workYearDesc);
|
|
792
|
-
const emphasis = [
|
|
793
|
-
...normalizeTagList(item.workEmphasisList),
|
|
794
|
-
...normalizeTagList(item.respHighlightList),
|
|
795
|
-
...normalizeTagList(item.workPerfHighlightList)
|
|
796
|
-
];
|
|
797
|
-
return [
|
|
798
|
-
`${index + 1}. ${[company, position, period].filter(Boolean).join(" / ")}`,
|
|
799
|
-
item.department ? `部门:${normalizeText(item.department)}` : "",
|
|
800
|
-
item.responsibility ? `职责:${normalizeText(item.responsibility)}` : "",
|
|
801
|
-
item.workPerformance ? `业绩:${normalizeText(item.workPerformance)}` : "",
|
|
802
|
-
item.workEmphasis ? `重点:${normalizeText(item.workEmphasis)}` : "",
|
|
803
|
-
emphasis.length ? `亮点:${unique(emphasis).join("、")}` : ""
|
|
804
|
-
].filter(Boolean).join("\n");
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function formatProjectExperience(item = {}, index = 0) {
|
|
808
|
-
const period = joinRange(item.startYearMonStr || item.startDateDesc || item.startDate, item.endYearMonStr || item.endDateDesc || item.endDate);
|
|
809
|
-
return [
|
|
810
|
-
`${index + 1}. ${[pickFirst(item.name), pickFirst(item.roleName), period].filter(Boolean).join(" / ")}`,
|
|
811
|
-
pickFirst(item.projectDescription, item.description) ? `项目描述:${pickFirst(item.projectDescription, item.description)}` : "",
|
|
812
|
-
item.performance ? `项目业绩:${normalizeText(item.performance)}` : ""
|
|
813
|
-
].filter(Boolean).join("\n");
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function formatEducation(item = {}, index = 0) {
|
|
817
|
-
const period = joinRange(
|
|
818
|
-
item.startDateDesc || item.startDate || item.startYearStr,
|
|
819
|
-
item.endDateDesc || item.endDate || item.endYearStr
|
|
820
|
-
);
|
|
821
|
-
const tags = [
|
|
822
|
-
...normalizeTagList(item.tags),
|
|
823
|
-
...normalizeTagList(item.schoolTags),
|
|
824
|
-
...normalizeTagList(item.keySubjectList)
|
|
825
|
-
];
|
|
826
|
-
return [
|
|
827
|
-
`${index + 1}. ${[
|
|
828
|
-
pickFirst(item.school, item.schoolName),
|
|
829
|
-
pickFirst(item.major, item.majorName),
|
|
830
|
-
pickFirst(item.degreeName, item.degree),
|
|
831
|
-
period
|
|
832
|
-
].filter(Boolean).join(" / ")}`,
|
|
833
|
-
tags.length ? `标签:${unique(tags).join("、")}` : "",
|
|
834
|
-
item.courseDesc ? `课程:${normalizeText(item.courseDesc)}` : "",
|
|
835
|
-
item.eduDescription ? `教育描述:${normalizeText(item.eduDescription)}` : "",
|
|
836
|
-
item.thesisTitle ? `论文:${normalizeText(item.thesisTitle)}` : "",
|
|
837
|
-
item.thesisDesc ? `论文描述:${normalizeText(item.thesisDesc)}` : ""
|
|
838
|
-
].filter(Boolean).join("\n");
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function formatExpectation(item = {}, index = 0) {
|
|
842
|
-
return `${index + 1}. ${[
|
|
843
|
-
pickFirst(item.positionName, item.position),
|
|
844
|
-
pickFirst(item.locationName, item.location),
|
|
845
|
-
pickFirst(item.salaryDesc),
|
|
846
|
-
pickFirst(item.industryDesc)
|
|
847
|
-
].filter(Boolean).join(" / ")}`;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function formatChatWorkExperience(item = {}, index = 0) {
|
|
851
|
-
return [
|
|
852
|
-
`${index + 1}. ${[
|
|
853
|
-
pickFirst(item.company, item.brandName),
|
|
854
|
-
pickFirst(item.positionName, item.position),
|
|
855
|
-
pickFirst(item.workYear, item.workYearDesc, item.dateRange)
|
|
856
|
-
].filter(Boolean).join(" / ")}`,
|
|
857
|
-
pickFirst(item.description, item.performance, item.content) ? `描述:${pickFirst(item.description, item.performance, item.content)}` : ""
|
|
858
|
-
].filter(Boolean).join("\n");
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function formatChatEducation(item = {}, index = 0) {
|
|
862
|
-
return `${index + 1}. ${[
|
|
863
|
-
pickFirst(item.school),
|
|
864
|
-
pickFirst(item.major),
|
|
865
|
-
pickFirst(item.degree, item.degreeName),
|
|
866
|
-
pickFirst(item.year, item.dateRange)
|
|
867
|
-
].filter(Boolean).join(" / ")}`;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function resolveBossGeekDetail(payload = {}) {
|
|
871
|
-
const candidates = [
|
|
872
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.geekDetailInfo },
|
|
873
|
-
{ sourceKey: "geekDetail", detail: payload?.zpData?.geekDetail },
|
|
874
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.geekDetailInfo },
|
|
875
|
-
{ sourceKey: "geekDetail", detail: payload?.zpData?.data?.geekDetail },
|
|
876
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.detailInfo },
|
|
877
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.resumeDetail },
|
|
878
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data },
|
|
879
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.data?.geekDetailInfo },
|
|
880
|
-
{ sourceKey: "geekDetail", detail: payload?.data?.geekDetail },
|
|
881
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.data?.detailInfo },
|
|
882
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.data?.resumeDetail },
|
|
883
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.data },
|
|
884
|
-
{ sourceKey: "geekDetailInfo", detail: payload?.geekDetailInfo },
|
|
885
|
-
{ sourceKey: "geekDetail", detail: payload?.geekDetail },
|
|
886
|
-
{ sourceKey: "geekDetailInfo", detail: payload }
|
|
887
|
-
];
|
|
888
|
-
const found = candidates.find((item) => isBossGeekDetailShape(item.detail));
|
|
889
|
-
return found || { sourceKey: "", detail: null };
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function extractBossChatGeekInfo(payload = {}) {
|
|
893
|
-
const data = payload?.zpData?.data || payload?.data || payload?.zpData?.geekInfo || payload?.geekInfo;
|
|
894
|
-
if (!data || typeof data !== "object") return null;
|
|
895
|
-
if (!isBossChatProfileShape(data)) return null;
|
|
896
|
-
const educationList = normalizeList(data.eduExpList);
|
|
897
|
-
const workList = normalizeList(data.workExpList);
|
|
898
|
-
const firstEducation = educationList[0] || {};
|
|
899
|
-
const firstWork = workList[0] || {};
|
|
900
|
-
const tags = unique([
|
|
901
|
-
...normalizeTagList(data.highLightGeekResumeWords),
|
|
902
|
-
...normalizeTagList(data.highLightWords),
|
|
903
|
-
...normalizeTagList(data.skillTags),
|
|
904
|
-
...normalizeTagList(data.labels),
|
|
905
|
-
pickFirst(data.positionCategory),
|
|
906
|
-
pickFirst(data.positionName, data.position, data.toPosition)
|
|
907
|
-
]);
|
|
908
|
-
const salary = data.salaryDesc || (
|
|
909
|
-
data.lowSalary && data.highSalary ? `${data.lowSalary}-${data.highSalary}K` : ""
|
|
910
|
-
);
|
|
911
|
-
const sections = {
|
|
912
|
-
base: [
|
|
913
|
-
pickFirst(data.name) ? `姓名:${pickFirst(data.name)}` : "",
|
|
914
|
-
pickFirst(data.gender) ? `性别:${pickFirst(data.gender)}` : "",
|
|
915
|
-
pickFirst(data.age) ? `年龄:${pickFirst(data.age)}` : "",
|
|
916
|
-
pickFirst(data.year, data.workYear, data.workYearDesc) ? `工作年限:${pickFirst(data.year, data.workYear, data.workYearDesc)}` : "",
|
|
917
|
-
pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) ? `最高学历:${pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName)}` : "",
|
|
918
|
-
pickFirst(data.positionStatus, data.positionStatusDesc) ? `求职状态:${pickFirst(data.positionStatus, data.positionStatusDesc)}` : ""
|
|
919
|
-
].filter(Boolean),
|
|
920
|
-
expectation: [
|
|
921
|
-
[pickFirst(data.toPosition, data.positionName, data.position), salary].filter(Boolean).join(" / ")
|
|
922
|
-
].filter(Boolean),
|
|
923
|
-
current: [
|
|
924
|
-
[pickFirst(data.lastCompany, data.lastCompany2), pickFirst(data.lastPosition, data.lastPosition2)].filter(Boolean).join(" / ")
|
|
925
|
-
].filter(Boolean),
|
|
926
|
-
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
927
|
-
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
928
|
-
highlights: tags
|
|
929
|
-
};
|
|
930
|
-
const text = [
|
|
931
|
-
formatNamedSection("基础信息", sections.base),
|
|
932
|
-
formatNamedSection("求职期望", sections.expectation),
|
|
933
|
-
formatNamedSection("最近经历", sections.current),
|
|
934
|
-
formatNamedSection("工作经历", sections.work),
|
|
935
|
-
formatNamedSection("教育经历", sections.education),
|
|
936
|
-
formatNamedSection("亮点标签", sections.highlights)
|
|
937
|
-
].filter(Boolean).join("\n\n");
|
|
938
|
-
return {
|
|
939
|
-
text,
|
|
940
|
-
identity: {
|
|
941
|
-
name: pickFirst(data.name) || null,
|
|
942
|
-
title: pickFirst(data.positionName, data.position, data.toPosition) || null,
|
|
943
|
-
current_position: pickFirst(data.lastPosition, data.lastPosition2, firstWork.positionName, firstWork.position) || null,
|
|
944
|
-
current_company: pickFirst(data.lastCompany, data.lastCompany2, firstWork.company, firstWork.brandName) || null,
|
|
945
|
-
school: pickFirst(data.school, firstEducation.school) || null,
|
|
946
|
-
major: pickFirst(data.major, firstEducation.major) || null,
|
|
947
|
-
degree: pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) || parseDegree(text),
|
|
948
|
-
years_experience: parseYearsExperience(pickFirst(data.year, data.workYear, data.workYearDesc)) ?? null,
|
|
949
|
-
age: parseAge(String(data.age || "")) ?? null,
|
|
950
|
-
gender: normalizeGenderValue(data.gender)
|
|
951
|
-
},
|
|
952
|
-
tags,
|
|
953
|
-
source_keys: {
|
|
954
|
-
chat_geek_info: true,
|
|
955
|
-
geek_detail_info: false,
|
|
956
|
-
geek_detail: false,
|
|
957
|
-
education_count: educationList.length,
|
|
958
|
-
work_count: workList.length
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
function extractBossChatHistoryResume(payload = {}) {
|
|
964
|
-
const messages = normalizeList(payload?.zpData?.messages).length
|
|
965
|
-
? normalizeList(payload?.zpData?.messages)
|
|
966
|
-
: normalizeList(payload?.messages).length
|
|
967
|
-
? normalizeList(payload?.messages)
|
|
968
|
-
: normalizeList(payload?.data?.messages).length
|
|
969
|
-
? normalizeList(payload?.data?.messages)
|
|
970
|
-
: normalizeList(payload?.zpData?.data?.messages);
|
|
971
|
-
const resumes = messages
|
|
972
|
-
.map((message) => message?.body?.resume)
|
|
973
|
-
.filter((resume) => resume && typeof resume === "object");
|
|
974
|
-
const resume = resumes[0];
|
|
975
|
-
if (!resume) return null;
|
|
976
|
-
const user = resume.user || {};
|
|
977
|
-
const educationList = normalizeList(resume.education);
|
|
978
|
-
const workList = normalizeList(resume.experiences);
|
|
979
|
-
const firstEducation = educationList[0] || {};
|
|
980
|
-
const firstWork = workList[0] || {};
|
|
981
|
-
const tags = unique([
|
|
982
|
-
pickFirst(resume.position),
|
|
983
|
-
pickFirst(resume.positionCategory),
|
|
984
|
-
...normalizeTagList(resume.skills),
|
|
985
|
-
...normalizeTagList(resume.tags)
|
|
986
|
-
]);
|
|
987
|
-
const sections = {
|
|
988
|
-
base: [
|
|
989
|
-
pickFirst(user.name) ? `姓名:${pickFirst(user.name)}` : "",
|
|
990
|
-
pickFirst(resume.workYear) ? `工作年限:${pickFirst(resume.workYear)}` : "",
|
|
991
|
-
pickFirst(firstEducation.degree, resume.degree) ? `最高学历:${pickFirst(firstEducation.degree, resume.degree)}` : ""
|
|
992
|
-
].filter(Boolean),
|
|
993
|
-
expectation: [
|
|
994
|
-
[pickFirst(resume.position), pickFirst(resume.positionCategory)].filter(Boolean).join(" / ")
|
|
995
|
-
].filter(Boolean),
|
|
996
|
-
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
997
|
-
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
998
|
-
highlights: tags
|
|
999
|
-
};
|
|
1000
|
-
const text = [
|
|
1001
|
-
formatNamedSection("基础信息", sections.base),
|
|
1002
|
-
formatNamedSection("求职期望", sections.expectation),
|
|
1003
|
-
formatNamedSection("工作经历", sections.work),
|
|
1004
|
-
formatNamedSection("教育经历", sections.education),
|
|
1005
|
-
formatNamedSection("亮点标签", sections.highlights)
|
|
1006
|
-
].filter(Boolean).join("\n\n");
|
|
1007
|
-
return {
|
|
1008
|
-
text,
|
|
1009
|
-
identity: {
|
|
1010
|
-
name: pickFirst(user.name) || null,
|
|
1011
|
-
title: pickFirst(resume.position) || null,
|
|
1012
|
-
current_position: pickFirst(firstWork.positionName, firstWork.position) || null,
|
|
1013
|
-
current_company: pickFirst(firstWork.company, firstWork.brandName, user.company) || null,
|
|
1014
|
-
school: pickFirst(firstEducation.school) || null,
|
|
1015
|
-
major: pickFirst(firstEducation.major) || null,
|
|
1016
|
-
degree: pickFirst(firstEducation.degree, firstEducation.degreeName, resume.degree) || parseDegree(text),
|
|
1017
|
-
years_experience: parseYearsExperience(pickFirst(resume.workYear)) ?? null,
|
|
1018
|
-
age: null,
|
|
1019
|
-
gender: null
|
|
1020
|
-
},
|
|
1021
|
-
tags,
|
|
1022
|
-
source_keys: {
|
|
1023
|
-
chat_history_resume: true,
|
|
1024
|
-
geek_detail_info: false,
|
|
1025
|
-
geek_detail: false,
|
|
1026
|
-
education_count: educationList.length,
|
|
1027
|
-
work_count: workList.length
|
|
1028
|
-
}
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function extractBossProfileRecursively(payload = {}) {
|
|
1033
|
-
for (const object of collectObjects(payload)) {
|
|
1034
|
-
if (isBossGeekDetailShape(object)) {
|
|
1035
|
-
const profile = extractBossGeekDetailInfo({ geekDetailInfo: object });
|
|
1036
|
-
if (profile?.text || profile?.identity?.name) {
|
|
1037
|
-
return {
|
|
1038
|
-
...profile,
|
|
1039
|
-
source_keys: {
|
|
1040
|
-
...(profile.source_keys || {}),
|
|
1041
|
-
recursive_profile_match: true
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
if (isBossChatProfileShape(object)) {
|
|
1047
|
-
const profile = extractBossChatGeekInfo({ zpData: { data: object } });
|
|
1048
|
-
if (profile?.text || profile?.identity?.name) {
|
|
1049
|
-
return {
|
|
1050
|
-
...profile,
|
|
1051
|
-
source_keys: {
|
|
1052
|
-
...(profile.source_keys || {}),
|
|
1053
|
-
recursive_profile_match: true
|
|
1054
|
-
}
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
if (isPlainObject(object.resume)) {
|
|
1059
|
-
const profile = extractBossChatHistoryResume({ zpData: { messages: [{ body: { resume: object.resume } }] } });
|
|
1060
|
-
if (profile?.text || profile?.identity?.name) {
|
|
1061
|
-
return {
|
|
1062
|
-
...profile,
|
|
1063
|
-
source_keys: {
|
|
1064
|
-
...(profile.source_keys || {}),
|
|
1065
|
-
recursive_profile_match: true
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return null;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function extractBossGeekDetailInfo(payload = {}) {
|
|
1075
|
-
const { sourceKey, detail } = resolveBossGeekDetail(payload);
|
|
1076
|
-
if (!detail || typeof detail !== "object") return null;
|
|
1077
|
-
|
|
1078
|
-
const base = detail.geekBaseInfo || detail.baseInfo || detail.base || {};
|
|
1079
|
-
const educationList = normalizeList(detail.geekEduExpList).length
|
|
1080
|
-
? normalizeList(detail.geekEduExpList)
|
|
1081
|
-
: normalizeList(detail.geekEducationList);
|
|
1082
|
-
const firstEducation = educationList[0] || detail.highestEduExp || {};
|
|
1083
|
-
const workList = normalizeList(detail.geekWorkExpList);
|
|
1084
|
-
const firstWork = workList[0] || {};
|
|
1085
|
-
const projectList = normalizeList(detail.geekProjExpList);
|
|
1086
|
-
const expectationList = normalizeList(detail.geekExpPosList).length
|
|
1087
|
-
? normalizeList(detail.geekExpPosList)
|
|
1088
|
-
: normalizeList(detail.geekExpectList);
|
|
1089
|
-
const expectationFallback = detail.showExpectPosition && typeof detail.showExpectPosition === "object"
|
|
1090
|
-
? [detail.showExpectPosition]
|
|
1091
|
-
: [];
|
|
1092
|
-
const normalizedExpectationList = expectationList.length ? expectationList : expectationFallback;
|
|
1093
|
-
const certifications = normalizeList(detail.geekCertificationList);
|
|
1094
|
-
const skillTags = [
|
|
1095
|
-
...normalizeTagList(detail.geekSkillList),
|
|
1096
|
-
...normalizeTagList(detail.skillList),
|
|
1097
|
-
...normalizeTagList(detail.blueGeekSkills),
|
|
1098
|
-
...normalizeTagList(base.userHighlightList),
|
|
1099
|
-
...normalizeTagList(base.userDescHighlightList),
|
|
1100
|
-
...normalizeTagList(base.userDescHighLightList),
|
|
1101
|
-
...normalizeTagList(detail.geekPersonalLabelList),
|
|
1102
|
-
...normalizeTagList(detail.professionalSkill)
|
|
1103
|
-
];
|
|
1104
|
-
const summaryParts = [
|
|
1105
|
-
pickFirst(detail.geekAdvantage),
|
|
1106
|
-
pickFirst(base.userDescription),
|
|
1107
|
-
pickFirst(base.userDesc),
|
|
1108
|
-
pickFirst(base.workEduDesc),
|
|
1109
|
-
pickFirst(detail.resumeSummary?.content, detail.resumeSummary?.text, detail.resumeSummary?.summary)
|
|
1110
|
-
].filter(Boolean);
|
|
1111
|
-
const sections = {
|
|
1112
|
-
base: [
|
|
1113
|
-
pickFirst(base.name, detail.geekName, detail.name) ? `姓名:${pickFirst(base.name, detail.geekName, detail.name)}` : "",
|
|
1114
|
-
normalizeGenderValue(base.gender) ? `性别:${normalizeGenderValue(base.gender)}` : "",
|
|
1115
|
-
pickFirst(base.ageDesc, base.age) ? `年龄:${pickFirst(base.ageDesc, base.age)}` : "",
|
|
1116
|
-
pickFirst(base.degreeCategory) ? `最高学历:${pickFirst(base.degreeCategory)}` : "",
|
|
1117
|
-
pickFirst(base.workYearDesc, base.workYearsDesc) ? `工作年限:${pickFirst(base.workYearDesc, base.workYearsDesc)}` : "",
|
|
1118
|
-
pickFirst(base.activeTimeDesc) ? `活跃状态:${pickFirst(base.activeTimeDesc)}` : "",
|
|
1119
|
-
pickFirst(base.applyStatusDesc, base.applyStatusContent) ? `求职状态:${pickFirst(base.applyStatusDesc, base.applyStatusContent)}` : ""
|
|
1120
|
-
].filter(Boolean),
|
|
1121
|
-
summary: summaryParts,
|
|
1122
|
-
expectations: normalizedExpectationList.map(formatExpectation).filter(Boolean),
|
|
1123
|
-
work_experience: workList.map(formatWorkExperience).filter(Boolean),
|
|
1124
|
-
project_experience: projectList.map(formatProjectExperience).filter(Boolean),
|
|
1125
|
-
education: educationList.map(formatEducation).filter(Boolean),
|
|
1126
|
-
certifications: certifications.map((item, index) => `${index + 1}. ${pickFirst(item.certName, item.name)}`).filter(Boolean),
|
|
1127
|
-
skills: unique(skillTags)
|
|
1128
|
-
};
|
|
1129
|
-
const text = [
|
|
1130
|
-
formatNamedSection("基础信息", sections.base),
|
|
1131
|
-
formatNamedSection("个人总结", sections.summary),
|
|
1132
|
-
formatNamedSection("求职期望", sections.expectations),
|
|
1133
|
-
formatNamedSection("工作经历", sections.work_experience),
|
|
1134
|
-
formatNamedSection("项目经历", sections.project_experience),
|
|
1135
|
-
formatNamedSection("教育经历", sections.education),
|
|
1136
|
-
formatNamedSection("证书", sections.certifications),
|
|
1137
|
-
formatNamedSection("技能/亮点", sections.skills)
|
|
1138
|
-
].filter(Boolean).join("\n\n");
|
|
1139
|
-
|
|
1140
|
-
return {
|
|
1141
|
-
identity: {
|
|
1142
|
-
name: pickFirst(base.name, detail.geekName, detail.name),
|
|
1143
|
-
current_position: pickFirst(firstWork.positionName, firstWork.positionTitle, firstWork.position),
|
|
1144
|
-
current_company: pickFirst(firstWork.formattedCompany, firstWork.company, firstWork.brandName),
|
|
1145
|
-
school: pickFirst(firstEducation.school, firstEducation.schoolName),
|
|
1146
|
-
major: pickFirst(firstEducation.major, firstEducation.majorName),
|
|
1147
|
-
degree: pickFirst(base.degreeCategory, firstEducation.degreeName, firstEducation.degree),
|
|
1148
|
-
years_experience: parseYearsExperience(pickFirst(base.workYearDesc, base.workYearsDesc)) ?? null,
|
|
1149
|
-
age: parseAge(pickFirst(base.ageDesc, base.age)) ?? null,
|
|
1150
|
-
gender: normalizeGenderValue(base.gender)
|
|
1151
|
-
},
|
|
1152
|
-
tags: unique([
|
|
1153
|
-
...sections.skills,
|
|
1154
|
-
...educationList.flatMap((item) => [
|
|
1155
|
-
...normalizeTagList(item.tags),
|
|
1156
|
-
...normalizeTagList(item.schoolTags)
|
|
1157
|
-
])
|
|
1158
|
-
]),
|
|
1159
|
-
sections,
|
|
1160
|
-
text,
|
|
1161
|
-
source_keys: {
|
|
1162
|
-
source_key: sourceKey,
|
|
1163
|
-
geek_detail_info: sourceKey === "geekDetailInfo",
|
|
1164
|
-
geek_detail: sourceKey === "geekDetail",
|
|
1165
|
-
work_count: workList.length,
|
|
1166
|
-
project_count: projectList.length,
|
|
1167
|
-
education_count: educationList.length,
|
|
1168
|
-
expectation_count: normalizedExpectationList.length,
|
|
1169
|
-
certification_count: certifications.length
|
|
1170
|
-
}
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
export function extractBossProfileFromNetworkBody(networkBody = {}) {
|
|
1175
|
-
const text = parseNetworkBodyText(networkBody);
|
|
1176
|
-
const parsedObjects = [
|
|
1177
|
-
tryParseJson(text),
|
|
1178
|
-
...tryParseEmbeddedJsonObjects(text)
|
|
1179
|
-
].filter((item) => item && typeof item === "object");
|
|
1180
|
-
if (!parsedObjects.length) {
|
|
1181
|
-
const htmlText = /<html|<body|<div|<section|<script/i.test(text) ? htmlToText(text) : "";
|
|
1182
|
-
if (htmlText && htmlText.length > 80) {
|
|
1183
|
-
const candidate = normalizeCandidateProfile({
|
|
1184
|
-
domain: "recommend",
|
|
1185
|
-
source: "network-html-fallback",
|
|
1186
|
-
text: htmlText
|
|
1187
|
-
});
|
|
1188
|
-
return {
|
|
1189
|
-
ok: true,
|
|
1190
|
-
url: networkBody.url || null,
|
|
1191
|
-
status: networkBody.status ?? null,
|
|
1192
|
-
mimeType: networkBody.mimeType || null,
|
|
1193
|
-
text_length: text.length,
|
|
1194
|
-
profile: {
|
|
1195
|
-
identity: candidate.identity,
|
|
1196
|
-
tags: candidate.tags,
|
|
1197
|
-
sections: { html_text: [htmlText] },
|
|
1198
|
-
text: htmlText,
|
|
1199
|
-
source_keys: {
|
|
1200
|
-
network_html_text: true,
|
|
1201
|
-
html_text_length: htmlText.length
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
return {
|
|
1207
|
-
ok: false,
|
|
1208
|
-
error: "NETWORK_BODY_NOT_JSON",
|
|
1209
|
-
text_length: text.length
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
let profile = null;
|
|
1213
|
-
let parsed = parsedObjects[0];
|
|
1214
|
-
for (const candidateObject of parsedObjects) {
|
|
1215
|
-
profile = extractBossGeekDetailInfo(candidateObject)
|
|
1216
|
-
|| extractBossChatGeekInfo(candidateObject)
|
|
1217
|
-
|| extractBossChatHistoryResume(candidateObject)
|
|
1218
|
-
|| extractBossProfileRecursively(candidateObject);
|
|
1219
|
-
if (profile) {
|
|
1220
|
-
parsed = candidateObject;
|
|
1221
|
-
break;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (!profile) {
|
|
1225
|
-
const encryptedPayload = parsedObjects.find((item) => (
|
|
1226
|
-
normalizeText(item?.zpData?.encryptGeekDetailInfo || item?.encryptGeekDetailInfo || "")
|
|
1227
|
-
));
|
|
1228
|
-
return {
|
|
1229
|
-
ok: false,
|
|
1230
|
-
error: encryptedPayload ? "BOSS_GEEK_DETAIL_INFO_ENCRYPTED" : "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
|
|
1231
|
-
text_length: text.length,
|
|
1232
|
-
parsed_object_count: parsedObjects.length,
|
|
1233
|
-
top_level_keys: Object.keys(parsed || {}).slice(0, 30),
|
|
1234
|
-
zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50),
|
|
1235
|
-
data_keys: Object.keys(parsed?.data || parsed?.zpData?.data || {}).slice(0, 50),
|
|
1236
|
-
encrypted_resume: Boolean(encryptedPayload),
|
|
1237
|
-
encrypted_resume_length: normalizeText(encryptedPayload?.zpData?.encryptGeekDetailInfo || encryptedPayload?.encryptGeekDetailInfo || "").length
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
return {
|
|
1241
|
-
ok: true,
|
|
1242
|
-
url: networkBody.url || null,
|
|
1243
|
-
status: networkBody.status ?? null,
|
|
1244
|
-
mimeType: networkBody.mimeType || null,
|
|
1245
|
-
text_length: text.length,
|
|
1246
|
-
profile
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
export function mergeCandidateProfiles(...profiles) {
|
|
1251
|
-
const base = {};
|
|
1252
|
-
for (const profile of profiles) {
|
|
1253
|
-
if (!profile) continue;
|
|
1254
|
-
for (const [key, value] of Object.entries(profile)) {
|
|
1255
|
-
if (value == null || value === "") continue;
|
|
1256
|
-
if (base[key] == null || base[key] === "") {
|
|
1257
|
-
base[key] = value;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
return base;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
export function buildScreeningCandidateFromDetail({
|
|
1265
|
-
cardCandidate,
|
|
1266
|
-
detailText = "",
|
|
1267
|
-
networkBodies = [],
|
|
1268
|
-
domain = "recommend",
|
|
1269
|
-
source = "live-cdp-detail",
|
|
1270
|
-
metadata = {}
|
|
1271
|
-
} = {}) {
|
|
1272
|
-
const parsedNetworkProfiles = networkBodies.map(extractBossProfileFromNetworkBody);
|
|
1273
|
-
const successfulProfiles = parsedNetworkProfiles.filter((item) => item.ok).map((item) => item.profile);
|
|
1274
|
-
const networkText = successfulProfiles.map((profile) => profile.text).filter(Boolean).join("\n\n");
|
|
1275
|
-
const networkIdentity = mergeCandidateProfiles(
|
|
1276
|
-
...successfulProfiles.map((profile) => profile.identity)
|
|
1277
|
-
);
|
|
1278
|
-
const networkTags = unique(successfulProfiles.flatMap((profile) => profile.tags || []));
|
|
1279
|
-
const combinedIdentity = mergeCandidateProfiles(
|
|
1280
|
-
networkIdentity,
|
|
1281
|
-
cardCandidate?.identity
|
|
1282
|
-
);
|
|
1283
|
-
const candidate = normalizeCandidateProfile({
|
|
1284
|
-
domain,
|
|
1285
|
-
source,
|
|
1286
|
-
id: cardCandidate?.id,
|
|
1287
|
-
href: cardCandidate?.links?.href,
|
|
1288
|
-
text: [
|
|
1289
|
-
networkText,
|
|
1290
|
-
detailText,
|
|
1291
|
-
cardCandidate?.text?.raw
|
|
1292
|
-
].filter(Boolean).join("\n\n"),
|
|
1293
|
-
attributes: cardCandidate?.metadata?.attributes || {},
|
|
1294
|
-
identity: combinedIdentity,
|
|
1295
|
-
tags: unique([
|
|
1296
|
-
...(cardCandidate?.tags || []),
|
|
1297
|
-
...networkTags
|
|
1298
|
-
]),
|
|
1299
|
-
metadata: {
|
|
1300
|
-
...metadata,
|
|
1301
|
-
card_candidate_source: cardCandidate?.source || null,
|
|
1302
|
-
network_profile_count: successfulProfiles.length,
|
|
1303
|
-
network_profiles: parsedNetworkProfiles.map((item) => ({
|
|
1304
|
-
ok: item.ok,
|
|
1305
|
-
url: item.url,
|
|
1306
|
-
status: item.status,
|
|
1307
|
-
error: item.error,
|
|
1308
|
-
text_length: item.text_length,
|
|
1309
|
-
source_keys: item.profile?.source_keys || null
|
|
1310
|
-
}))
|
|
1311
|
-
}
|
|
1312
|
-
});
|
|
1313
|
-
return {
|
|
1314
|
-
candidate,
|
|
1315
|
-
parsed_network_profiles: parsedNetworkProfiles
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
export function normalizeCandidateProfile(input = {}) {
|
|
1320
|
-
const domain = normalizeDomain(input.domain || "recommend");
|
|
1321
|
-
const rawText = String(input.text || input.raw_text || input.resume_text || "")
|
|
1322
|
-
.split(/\r?\n/)
|
|
1323
|
-
.map((line) => normalizeText(line))
|
|
1324
|
-
.filter(Boolean)
|
|
1325
|
-
.join("\n");
|
|
1326
|
-
const lines = rawText.split(/\r?\n/).map(normalizeText).filter(Boolean);
|
|
1327
|
-
const attrs = {
|
|
1328
|
-
...(input.attributes || {}),
|
|
1329
|
-
...(input.metadata?.attributes || {})
|
|
1330
|
-
};
|
|
1331
|
-
const sourceId = normalizeText(
|
|
1332
|
-
input.id
|
|
1333
|
-
|| attrs["data-geek"]
|
|
1334
|
-
|| attrs["data-geekid"]
|
|
1335
|
-
|| attrs["data-expect"]
|
|
1336
|
-
|| attrs["data-uid"]
|
|
1337
|
-
|| attrs["data-securityid"]
|
|
1338
|
-
|| attrs.encryptgeekid
|
|
1339
|
-
|| attrs["data-lid"]
|
|
1340
|
-
|| attrs["data-jid"]
|
|
1341
|
-
|| attrs["data-itemid"]
|
|
1342
|
-
|| attrs.geekid
|
|
1343
|
-
|| attrs.expect
|
|
1344
|
-
|| attrs.uid
|
|
1345
|
-
|| attrs.securityid
|
|
1346
|
-
|| attrs.jid
|
|
1347
|
-
|| attrs.lid
|
|
1348
|
-
|| attrs.href
|
|
1349
|
-
|| ""
|
|
1350
|
-
) || null;
|
|
1351
|
-
const explicitName = cleanInferredNameLine(input.identity?.name || input.name || "");
|
|
1352
|
-
const inferredName = explicitName || firstUsefulLine(lines) || null;
|
|
1353
|
-
const fullText = collectTextParts({
|
|
1354
|
-
...input,
|
|
1355
|
-
text: rawText,
|
|
1356
|
-
raw_text: rawText,
|
|
1357
|
-
identity: {
|
|
1358
|
-
...(input.identity || {}),
|
|
1359
|
-
name: inferredName
|
|
1360
|
-
}
|
|
1361
|
-
}).join("\n");
|
|
1362
|
-
const degree = input.identity?.degree || input.degree || parseDegree(fullText);
|
|
1363
|
-
|
|
1364
|
-
return {
|
|
1365
|
-
schema_version: 1,
|
|
1366
|
-
domain,
|
|
1367
|
-
source: normalizeText(input.source || "unknown") || "unknown",
|
|
1368
|
-
id: sourceId,
|
|
1369
|
-
identity: {
|
|
1370
|
-
name: inferredName,
|
|
1371
|
-
title: normalizeText(input.identity?.title || input.title || "") || null,
|
|
1372
|
-
current_position: normalizeText(input.identity?.current_position || input.current_position || "") || null,
|
|
1373
|
-
current_company: normalizeText(input.identity?.current_company || input.current_company || "") || null,
|
|
1374
|
-
school: normalizeText(input.identity?.school || input.school || "") || null,
|
|
1375
|
-
major: normalizeText(input.identity?.major || input.major || "") || null,
|
|
1376
|
-
degree,
|
|
1377
|
-
years_experience: input.identity?.years_experience ?? input.years_experience ?? parseYearsExperience(fullText),
|
|
1378
|
-
age: input.identity?.age ?? input.age ?? parseAge(fullText),
|
|
1379
|
-
gender: input.identity?.gender || input.gender || parseGender(fullText)
|
|
1380
|
-
},
|
|
1381
|
-
tags: unique(input.tags || []),
|
|
1382
|
-
text: {
|
|
1383
|
-
summary: lines.slice(0, 8).join("\n"),
|
|
1384
|
-
raw: rawText
|
|
1385
|
-
},
|
|
1386
|
-
links: {
|
|
1387
|
-
href: normalizeText(input.href || attrs.href || "") || null
|
|
1388
|
-
},
|
|
1389
|
-
metadata: {
|
|
1390
|
-
...(input.metadata || {}),
|
|
1391
|
-
attributes: attrs,
|
|
1392
|
-
normalized_at: input.normalized_at || nowIso()
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
export function normalizeCandidateFromHtml({
|
|
1398
|
-
domain = "recommend",
|
|
1399
|
-
source = "dom",
|
|
1400
|
-
html,
|
|
1401
|
-
attributes = {},
|
|
1402
|
-
metadata = {}
|
|
1403
|
-
} = {}) {
|
|
1404
|
-
const parsedAttributes = parseHtmlAttributes(html);
|
|
1405
|
-
return normalizeCandidateProfile({
|
|
1406
|
-
domain,
|
|
1407
|
-
source,
|
|
1408
|
-
text: htmlToText(html),
|
|
1409
|
-
attributes: {
|
|
1410
|
-
...parsedAttributes,
|
|
1411
|
-
...attributes
|
|
1412
|
-
},
|
|
1413
|
-
metadata: {
|
|
1414
|
-
...metadata,
|
|
1415
|
-
html_length: String(html || "").length
|
|
1416
|
-
}
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
function normalizeKeywordList(value) {
|
|
1421
|
-
if (!value) return [];
|
|
1422
|
-
if (Array.isArray(value)) return unique(value);
|
|
1423
|
-
return unique(String(value).split(/[,\n,、|/]/));
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function keywordMatches(text, keywords) {
|
|
1427
|
-
const haystack = compact(text);
|
|
1428
|
-
return keywords.filter((keyword) => haystack.includes(compact(keyword)));
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
function degreeAtLeast(candidateDegree, minimumDegree) {
|
|
1432
|
-
if (!minimumDegree) return true;
|
|
1433
|
-
const candidateRank = DEGREE_RANK[candidateDegree] || 0;
|
|
1434
|
-
const minimumRank = DEGREE_RANK[minimumDegree] || 0;
|
|
1435
|
-
return candidateRank >= minimumRank;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
export function screenCandidate(candidateInput, criteria = {}) {
|
|
1439
|
-
const candidate = candidateInput?.schema_version
|
|
1440
|
-
? candidateInput
|
|
1441
|
-
: normalizeCandidateProfile(candidateInput);
|
|
1442
|
-
const text = [
|
|
1443
|
-
candidate.text?.raw,
|
|
1444
|
-
candidate.text?.summary,
|
|
1445
|
-
...Object.values(candidate.identity || {}).map((value) => value == null ? "" : String(value)),
|
|
1446
|
-
...(candidate.tags || [])
|
|
1447
|
-
].join("\n");
|
|
1448
|
-
const requiredKeywords = normalizeKeywordList(criteria.required_keywords || criteria.requiredKeywords);
|
|
1449
|
-
const preferredKeywords = normalizeKeywordList(criteria.preferred_keywords || criteria.preferredKeywords || criteria.criteria);
|
|
1450
|
-
const excludedKeywords = normalizeKeywordList(criteria.excluded_keywords || criteria.excludedKeywords);
|
|
1451
|
-
const matchedRequired = keywordMatches(text, requiredKeywords);
|
|
1452
|
-
const matchedPreferred = keywordMatches(text, preferredKeywords);
|
|
1453
|
-
const matchedExcluded = keywordMatches(text, excludedKeywords);
|
|
1454
|
-
const reasons = [];
|
|
1455
|
-
let score = 0;
|
|
1456
|
-
|
|
1457
|
-
if (requiredKeywords.length > 0) {
|
|
1458
|
-
if (matchedRequired.length === requiredKeywords.length) {
|
|
1459
|
-
score += 60;
|
|
1460
|
-
reasons.push(`Matched all required keywords: ${matchedRequired.join(", ")}`);
|
|
1461
|
-
} else {
|
|
1462
|
-
const missing = requiredKeywords.filter((keyword) => !matchedRequired.includes(keyword));
|
|
1463
|
-
reasons.push(`Missing required keywords: ${missing.join(", ")}`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
if (preferredKeywords.length > 0) {
|
|
1468
|
-
score += Math.round((matchedPreferred.length / preferredKeywords.length) * 30);
|
|
1469
|
-
if (matchedPreferred.length) {
|
|
1470
|
-
reasons.push(`Matched preferred keywords: ${matchedPreferred.join(", ")}`);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if (matchedExcluded.length > 0) {
|
|
1475
|
-
score -= 80;
|
|
1476
|
-
reasons.push(`Matched excluded keywords: ${matchedExcluded.join(", ")}`);
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
const minimumDegree = criteria.minimum_degree || criteria.minimumDegree || null;
|
|
1480
|
-
const degreeOk = degreeAtLeast(candidate.identity?.degree, minimumDegree);
|
|
1481
|
-
if (minimumDegree) {
|
|
1482
|
-
if (degreeOk) {
|
|
1483
|
-
score += 10;
|
|
1484
|
-
reasons.push(`Degree meets minimum: ${candidate.identity?.degree || "unknown"} >= ${minimumDegree}`);
|
|
1485
|
-
} else {
|
|
1486
|
-
reasons.push(`Degree below or unknown for minimum: ${minimumDegree}`);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const hasCriteria = (
|
|
1491
|
-
requiredKeywords.length > 0
|
|
1492
|
-
|| preferredKeywords.length > 0
|
|
1493
|
-
|| excludedKeywords.length > 0
|
|
1494
|
-
|| Boolean(minimumDegree)
|
|
1495
|
-
);
|
|
1496
|
-
const hasRequired = requiredKeywords.length === 0 || matchedRequired.length === requiredKeywords.length;
|
|
1497
|
-
const passed = hasCriteria && hasRequired && degreeOk && matchedExcluded.length === 0;
|
|
1498
|
-
const boundedScore = Math.max(0, Math.min(100, hasCriteria ? score : 0));
|
|
1499
|
-
|
|
1500
|
-
return {
|
|
1501
|
-
schema_version: 1,
|
|
1502
|
-
status: passed ? "pass" : "review",
|
|
1503
|
-
passed,
|
|
1504
|
-
score: boundedScore,
|
|
1505
|
-
reasons: reasons.length ? reasons : ["No explicit screening criteria supplied; candidate normalized for review."],
|
|
1506
|
-
matched: {
|
|
1507
|
-
required_keywords: matchedRequired,
|
|
1508
|
-
preferred_keywords: matchedPreferred,
|
|
1509
|
-
excluded_keywords: matchedExcluded
|
|
1510
|
-
},
|
|
1511
|
-
candidate: {
|
|
1512
|
-
domain: candidate.domain,
|
|
1513
|
-
source: candidate.source,
|
|
1514
|
-
id: candidate.id,
|
|
1515
|
-
identity: candidate.identity
|
|
1516
|
-
},
|
|
1517
|
-
screened_at: nowIso()
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
export function compactScreeningLlmResult(llmResult) {
|
|
1522
|
-
if (!llmResult) return null;
|
|
1523
|
-
return {
|
|
1524
|
-
ok: Boolean(llmResult.ok),
|
|
1525
|
-
provider: llmResult.provider || null,
|
|
1526
|
-
passed: llmResult.passed,
|
|
1527
|
-
review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
|
|
1528
|
-
cot: llmResult.cot || llmResult.decision_cot || "",
|
|
1529
|
-
reasoning_content: llmResult.reasoning_content || "",
|
|
1530
|
-
raw_model_output: llmResult.raw_model_output || "",
|
|
1531
|
-
evidence_count: Array.isArray(llmResult.evidence) ? llmResult.evidence.length : 0,
|
|
1532
|
-
usage: llmResult.usage || null,
|
|
1533
|
-
finish_reason: llmResult.finish_reason || null,
|
|
1534
|
-
image_input_count: llmResult.image_input_count || 0,
|
|
1535
|
-
attempt_count: llmResult.attempt_count || 0,
|
|
1536
|
-
fallback_count: llmResult.fallback_count || 0,
|
|
1537
|
-
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
1538
|
-
screening_strategy: llmResult.screening_strategy || "",
|
|
1539
|
-
fast_thinking_level: llmResult.fast_thinking_level || "",
|
|
1540
|
-
verify_thinking_level: llmResult.verify_thinking_level || "",
|
|
1541
|
-
verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
|
|
1542
|
-
verification_reason: llmResult.verification_reason || "",
|
|
1543
|
-
decision_source: llmResult.decision_source || "",
|
|
1544
|
-
fast_result: llmResult.fast_result || null,
|
|
1545
|
-
verify_result: llmResult.verify_result || null,
|
|
1546
|
-
error_code: llmResult.error_code || null,
|
|
1547
|
-
fatal: Boolean(llmResult.fatal),
|
|
1548
|
-
fatal_reason: llmResult.fatal_reason || "",
|
|
1549
|
-
error: llmResult.error || null,
|
|
1550
|
-
screened_at: llmResult.screened_at || null
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
export function llmResultToScreening(llmResult, candidate) {
|
|
1555
|
-
return {
|
|
1556
|
-
status: llmResult?.passed ? "pass" : "fail",
|
|
1557
|
-
passed: Boolean(llmResult?.passed),
|
|
1558
|
-
score: llmResult?.passed ? 100 : 0,
|
|
1559
|
-
reasons: llmResult?.error ? ["llm_invalid_response"] : [],
|
|
1560
|
-
candidate
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
export function isRecoverableLlmScreeningError(error) {
|
|
1565
|
-
return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response missing boolean review_required decision|LLM response was not valid JSON)/i
|
|
1566
|
-
.test(String(error?.message || error || ""));
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
export function createFailedLlmScreeningResult(error) {
|
|
1570
|
-
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1571
|
-
return {
|
|
1572
|
-
ok: false,
|
|
1573
|
-
passed: false,
|
|
1574
|
-
reason: "",
|
|
1575
|
-
evidence: [],
|
|
1576
|
-
cot: "",
|
|
1577
|
-
decision_cot: "",
|
|
1578
|
-
reasoning_content: "",
|
|
1579
|
-
raw_model_output: "",
|
|
1580
|
-
image_input_count: Number(error?.image_input_count) || 0,
|
|
1581
|
-
image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
|
|
1582
|
-
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
1583
|
-
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
1584
|
-
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
1585
|
-
error_code: fatalClassification?.code || error?.code || null,
|
|
1586
|
-
fatal: Boolean(fatalClassification),
|
|
1587
|
-
fatal_reason: fatalClassification?.reason || "",
|
|
1588
|
-
error: error?.message || String(error || "unknown"),
|
|
1589
|
-
screened_at: nowIso()
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
export function buildScreeningLlmMessages({
|
|
1594
|
-
candidate,
|
|
1595
|
-
criteria,
|
|
1596
|
-
thinkingLevel = "low",
|
|
1597
|
-
requireReviewDecision = false,
|
|
1598
|
-
imageEvidence = null,
|
|
1599
|
-
imagePaths = [],
|
|
1600
|
-
imageInputs = null,
|
|
1601
|
-
maxImages = 8,
|
|
1602
|
-
imageDetail = "high"
|
|
1603
|
-
}) {
|
|
1604
|
-
const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
|
|
1605
|
-
const safeText = String(candidate?.text?.raw || candidate?.text || "");
|
|
1606
|
-
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1607
|
-
const requestReviewDecision = Boolean(requireReviewDecision);
|
|
1608
|
-
const requestSummary = requestReviewDecision || normalizedThinkingLevel === "current";
|
|
1609
|
-
const images = Array.isArray(imageInputs)
|
|
1610
|
-
? imageInputs
|
|
1611
|
-
: buildScreeningLlmImageInputs({
|
|
1612
|
-
imageEvidence,
|
|
1613
|
-
imagePaths,
|
|
1614
|
-
maxImages,
|
|
1615
|
-
detail: imageDetail
|
|
1616
|
-
});
|
|
1617
|
-
const outputShape = requestReviewDecision
|
|
1618
|
-
? "最后只返回 JSON,格式为:"
|
|
1619
|
-
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\", \"review_required\": true/false}"
|
|
1620
|
-
: requestSummary
|
|
1621
|
-
? "7) 只返回 JSON,格式为:"
|
|
1622
|
-
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
|
|
1623
|
-
: "7) 只返回 JSON,格式为:"
|
|
1624
|
-
+ "{\"passed\": true/false}";
|
|
1625
|
-
const failFastInstructions = [
|
|
1626
|
-
"3) 按筛选标准原文顺序拆解并检查硬性淘汰项;一旦某项可确定不满足,必须立即返回 passed=false,不要继续评估后续条件。",
|
|
1627
|
-
"4) 若筛选标准规定证据不足、业务线无法判断、任期无法判断、学校无法判断等情况不通过,这类缺失证据就是可确定不满足。",
|
|
1628
|
-
"5) 只有当前淘汰项本身存在截图不清、信息冲突、或可能被后续可见简历内容澄清时,才继续阅读以核实该项。"
|
|
1629
|
-
].join("\n");
|
|
1630
|
-
const fastReviewInstructions = requestReviewDecision
|
|
1631
|
-
? [
|
|
1632
|
-
"7) review_required 必须是布尔值;当证据缺失、证据冲突、截图/文本不完整或不清晰、结论接近规则边界、或你依赖假设时设为 true;若结论明确且无需更深推理核验,设为 false。",
|
|
1633
|
-
"8) 只有当某个硬性不通过条件有直接、明确、无可见反向证据的简历证据时,才可返回 review_required=false。",
|
|
1634
|
-
"9) 如果简历中存在任何可能满足当前被否定条件的反向证据、边界证据或可计入经历,即使你倾向 passed=false,也必须 review_required=true。",
|
|
1635
|
-
"10) 反向证据包括但不限于:可能合格的学校/学历、可能合格的产品或行业、可能合格的职责或指标、可能合格的海外/语言证据、或需要精确计算的任期/年限证据。",
|
|
1636
|
-
"11) 不要因为候选人其他经历不匹配,就忽略某一段可能匹配的经历;只要任一可见经历可能满足被否定条件,就必须交给复核。",
|
|
1637
|
-
"12) 当 review_required=false 时,summary 必须点明确切的决定性硬性失败,并说明没有可见反向证据或边界证据。"
|
|
1638
|
-
].join("\n")
|
|
1639
|
-
: "";
|
|
1640
|
-
const prompt =
|
|
1641
|
-
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1642
|
-
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
1643
|
-
+ (images.length
|
|
1644
|
-
? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。若截图是拼接长图,请按图内从上到下顺序阅读;若已出现明确硬性淘汰项,可停止后续评估。\n\n`
|
|
1645
|
-
: "")
|
|
1646
|
-
+ "要求:\n"
|
|
1647
|
-
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
1648
|
-
+ "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
|
|
1649
|
-
+ failFastInstructions + "\n"
|
|
1650
|
-
+ (requestSummary
|
|
1651
|
-
? "6) summary 必须为少于100个中文词的简短筛选总结,可包含核心依据和主要风险;不要输出推理过程。\n"
|
|
1652
|
-
: "6) 不要输出评估原因、证据列表、解释或额外文字。\n")
|
|
1653
|
-
+ (fastReviewInstructions ? `${fastReviewInstructions}\n` : "")
|
|
1654
|
-
+ outputShape;
|
|
1655
|
-
const userContent = images.length
|
|
1656
|
-
? [
|
|
1657
|
-
{ type: "text", text: prompt },
|
|
1658
|
-
...images.map((image) => ({
|
|
1659
|
-
type: "image_url",
|
|
1660
|
-
image_url: image.image_url
|
|
1661
|
-
}))
|
|
1662
|
-
]
|
|
1663
|
-
: prompt;
|
|
1664
|
-
return [
|
|
1665
|
-
{
|
|
1666
|
-
role: "system",
|
|
1667
|
-
content:
|
|
1668
|
-
"你是一位严谨的招聘筛选助手。必须按筛选标准顺序严格阅读和判断,严禁编造不存在的候选人经历;一旦确定命中硬性淘汰项,可立即给出最终不通过结论。"
|
|
1669
|
-
+ (requestReviewDecision
|
|
1670
|
-
? "只能返回严格 JSON。必须包含 passed、summary 和 review_required;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程;review_required 只能是 true 或 false。只有在硬性失败直接明确且无可见反向证据时,review_required 才能为 false。"
|
|
1671
|
-
: requestSummary
|
|
1672
|
-
? "只能返回严格 JSON。必须包含 passed 和 summary;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程。"
|
|
1673
|
-
: "只能返回严格 JSON,不要输出原因、证据或额外文字。")
|
|
1674
|
-
},
|
|
1675
|
-
{
|
|
1676
|
-
role: "user",
|
|
1677
|
-
content: userContent
|
|
1678
|
-
}
|
|
1679
|
-
];
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function normalizeLlmMaxRetries(value) {
|
|
1683
|
-
if (value == null || value === "") return 1;
|
|
1684
|
-
const parsed = Number(value);
|
|
1685
|
-
if (!Number.isFinite(parsed) || parsed < 0) return 1;
|
|
1686
|
-
return Math.min(3, Math.floor(parsed));
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
function isRetryableLlmRequestError(error) {
|
|
1690
|
-
const status = Number(error?.status);
|
|
1691
|
-
if ([408, 409, 425, 429].includes(status) || status >= 500) return true;
|
|
1692
|
-
return /(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i
|
|
1693
|
-
.test(String(error?.message || error || ""));
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function sleepMs(ms) {
|
|
1697
|
-
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
async function callScreeningLlmWithProvider({
|
|
1701
|
-
candidate,
|
|
1702
|
-
criteria,
|
|
1703
|
-
config = {},
|
|
1704
|
-
timeoutMs = 60000,
|
|
1705
|
-
requireReviewDecision = false,
|
|
1706
|
-
imageEvidence = null,
|
|
1707
|
-
imagePaths = [],
|
|
1708
|
-
maxImages = 8,
|
|
1709
|
-
imageDetail = "high"
|
|
1710
|
-
} = {}) {
|
|
1711
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
1712
|
-
const apiKey = normalizeText(config.apiKey);
|
|
1713
|
-
const model = normalizeText(config.model);
|
|
1714
|
-
if (!baseUrl || !apiKey || !model) {
|
|
1715
|
-
throw new Error("Missing LLM config fields: baseUrl/apiKey/model");
|
|
1716
|
-
}
|
|
1717
|
-
const imageInputs = buildScreeningLlmImageInputs({
|
|
1718
|
-
imageEvidence,
|
|
1719
|
-
imagePaths,
|
|
1720
|
-
maxImages: config.llmImageLimit || config.imageLimit || maxImages,
|
|
1721
|
-
detail: config.llmImageDetail || config.imageDetail || imageDetail
|
|
1722
|
-
});
|
|
1723
|
-
if (!candidate?.text?.raw && !candidate?.text && !imageInputs.length) {
|
|
1724
|
-
throw new Error("Candidate text and image evidence are empty");
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
const thinkingLevel = config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "low";
|
|
1728
|
-
const outputTokenBudget = resolveLlmOutputTokenBudget(config, thinkingLevel, { requireReviewDecision });
|
|
1729
|
-
const payload = {
|
|
1730
|
-
model,
|
|
1731
|
-
temperature: parseFiniteNumber(config.temperature, 0.1),
|
|
1732
|
-
max_tokens: outputTokenBudget,
|
|
1733
|
-
messages: buildScreeningLlmMessages({
|
|
1734
|
-
candidate,
|
|
1735
|
-
criteria,
|
|
1736
|
-
thinkingLevel,
|
|
1737
|
-
requireReviewDecision,
|
|
1738
|
-
imageInputs
|
|
1739
|
-
})
|
|
1740
|
-
};
|
|
1741
|
-
const topP = parseFiniteNumber(config.topP ?? config.top_p, null);
|
|
1742
|
-
if (topP !== null) payload.top_p = topP;
|
|
1743
|
-
const maxCompletionTokens = parsePositiveNumber(
|
|
1744
|
-
config.llmMaxCompletionTokens ?? config.maxCompletionTokens,
|
|
1745
|
-
null
|
|
1746
|
-
);
|
|
1747
|
-
if (maxCompletionTokens !== null) {
|
|
1748
|
-
payload.max_completion_tokens = Math.max(1, Math.floor(maxCompletionTokens));
|
|
1749
|
-
}
|
|
1750
|
-
applyChatCompletionThinking(payload, {
|
|
1751
|
-
baseUrl,
|
|
1752
|
-
model,
|
|
1753
|
-
thinkingLevel
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
|
-
const effectiveTimeoutMs = parsePositiveNumber(config.llmTimeoutMs ?? config.timeoutMs, timeoutMs) || timeoutMs;
|
|
1757
|
-
const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
|
|
1758
|
-
const maxAttempts = maxRetries + 1;
|
|
1759
|
-
let lastError = null;
|
|
1760
|
-
let attempt = 0;
|
|
1761
|
-
let fatalProviderAttempts = 0;
|
|
1762
|
-
while (true) {
|
|
1763
|
-
attempt += 1;
|
|
1764
|
-
const controller = new AbortController();
|
|
1765
|
-
const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs);
|
|
1766
|
-
try {
|
|
1767
|
-
const headers = {
|
|
1768
|
-
"Content-Type": "application/json",
|
|
1769
|
-
Authorization: `Bearer ${apiKey}`
|
|
1770
|
-
};
|
|
1771
|
-
if (config.openaiOrganization) headers["OpenAI-Organization"] = config.openaiOrganization;
|
|
1772
|
-
if (config.openaiProject) headers["OpenAI-Project"] = config.openaiProject;
|
|
1773
|
-
|
|
1774
|
-
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
1775
|
-
method: "POST",
|
|
1776
|
-
headers,
|
|
1777
|
-
body: JSON.stringify(payload),
|
|
1778
|
-
signal: controller.signal
|
|
1779
|
-
});
|
|
1780
|
-
const responseText = await response.text();
|
|
1781
|
-
if (!response.ok) {
|
|
1782
|
-
const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1783
|
-
error.status = response.status;
|
|
1784
|
-
const providerError = tryParseJson(responseText)?.error || {};
|
|
1785
|
-
error.provider_error_code = providerError.code || null;
|
|
1786
|
-
error.provider_error_type = providerError.type || null;
|
|
1787
|
-
error.provider_error_message = providerError.message || null;
|
|
1788
|
-
throw error;
|
|
1789
|
-
}
|
|
1790
|
-
const json = tryParseJson(responseText);
|
|
1791
|
-
if (!json) {
|
|
1792
|
-
throw new Error("LLM response was not valid JSON");
|
|
1793
|
-
}
|
|
1794
|
-
const choice = json?.choices?.[0] || {};
|
|
1795
|
-
const content = flattenChatMessageContent(choice?.message?.content);
|
|
1796
|
-
const reasoningContent = collectLlmReasoningText(choice);
|
|
1797
|
-
const parsed = tryExtractJsonObject(content) || tryExtractJsonObject(reasoningContent);
|
|
1798
|
-
const passed = parsePassedDecision(parsed?.passed);
|
|
1799
|
-
if (passed === null) {
|
|
1800
|
-
throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
|
|
1801
|
-
}
|
|
1802
|
-
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1803
|
-
const summary = normalizeBlockText(parsed?.summary || parsed?.screen_summary || parsed?.brief_summary);
|
|
1804
|
-
if ((normalizedThinkingLevel === "current" || requireReviewDecision) && !summary) {
|
|
1805
|
-
throw new Error(`LLM response missing brief summary for current thinking level or fast-first strategy: ${content.slice(0, 240)}`);
|
|
1806
|
-
}
|
|
1807
|
-
const reviewRequired = requireReviewDecision
|
|
1808
|
-
? parsePassedDecision(parsed?.review_required ?? parsed?.reviewRequired ?? parsed?.needs_review ?? parsed?.needsReview)
|
|
1809
|
-
: null;
|
|
1810
|
-
if (requireReviewDecision && reviewRequired === null) {
|
|
1811
|
-
throw new Error(`LLM response missing boolean review_required decision: ${content.slice(0, 240)}`);
|
|
1812
|
-
}
|
|
1813
|
-
const evidence = Array.isArray(parsed?.evidence)
|
|
1814
|
-
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1815
|
-
: [];
|
|
1816
|
-
const decisionCot = (normalizedThinkingLevel === "current" || requireReviewDecision)
|
|
1817
|
-
? summary
|
|
1818
|
-
: (firstReasoningText([
|
|
1819
|
-
parsed?.cot,
|
|
1820
|
-
parsed?.decision_cot,
|
|
1821
|
-
parsed?.reasoning,
|
|
1822
|
-
parsed?.chain_of_thought,
|
|
1823
|
-
reasoningContent
|
|
1824
|
-
].map(normalizeBlockText).filter(Boolean)) || reasoningContent);
|
|
1825
|
-
const providerName = normalizeText(config.llmProviderName || config.name || config.label || config.id);
|
|
1826
|
-
const providerIndex = Number.isFinite(Number(config.llmProviderIndex)) ? Number(config.llmProviderIndex) : 0;
|
|
1827
|
-
const providerCount = Number.isFinite(Number(config.llmProviderCount)) ? Number(config.llmProviderCount) : 1;
|
|
1828
|
-
const result = {
|
|
1829
|
-
ok: true,
|
|
1830
|
-
provider: {
|
|
1831
|
-
baseUrl: redactBaseUrl(baseUrl),
|
|
1832
|
-
model,
|
|
1833
|
-
name: providerName || null,
|
|
1834
|
-
index: providerIndex + 1,
|
|
1835
|
-
total: providerCount,
|
|
1836
|
-
thinking_level: normalizeLlmThinkingLevel(thinkingLevel) || "low",
|
|
1837
|
-
thinking: payload.thinking || null,
|
|
1838
|
-
reasoning_effort: payload.reasoning_effort || null,
|
|
1839
|
-
max_tokens: payload.max_tokens,
|
|
1840
|
-
max_completion_tokens: payload.max_completion_tokens || null
|
|
1841
|
-
},
|
|
1842
|
-
passed,
|
|
1843
|
-
review_required: reviewRequired,
|
|
1844
|
-
reason: "",
|
|
1845
|
-
evidence,
|
|
1846
|
-
cot: decisionCot,
|
|
1847
|
-
decision_cot: decisionCot,
|
|
1848
|
-
reasoning_content: reasoningContent,
|
|
1849
|
-
raw_model_output: content,
|
|
1850
|
-
usage: json.usage || null,
|
|
1851
|
-
finish_reason: choice.finish_reason || null,
|
|
1852
|
-
raw_content_length: content.length,
|
|
1853
|
-
image_input_count: imageInputs.length,
|
|
1854
|
-
image_inputs: summarizeLlmImageInputs(imageInputs),
|
|
1855
|
-
attempt_count: attempt,
|
|
1856
|
-
provider_attempt_count: attempt,
|
|
1857
|
-
screened_at: nowIso()
|
|
1858
|
-
};
|
|
1859
|
-
return result;
|
|
1860
|
-
} catch (error) {
|
|
1861
|
-
lastError = error;
|
|
1862
|
-
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1863
|
-
if (fatalClassification) {
|
|
1864
|
-
fatalProviderAttempts += 1;
|
|
1865
|
-
error.code = fatalClassification.code;
|
|
1866
|
-
error.llm_fatal_provider_error = true;
|
|
1867
|
-
error.llm_fatal_reason = fatalClassification.reason;
|
|
1868
|
-
if (fatalProviderAttempts <= FATAL_LLM_PROVIDER_MAX_RETRIES) {
|
|
1869
|
-
await sleepMs(Math.min(2500, 500 * fatalProviderAttempts));
|
|
1870
|
-
continue;
|
|
1871
|
-
}
|
|
1872
|
-
error.image_input_count = imageInputs.length;
|
|
1873
|
-
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1874
|
-
error.llm_attempt_count = attempt;
|
|
1875
|
-
throw error;
|
|
1876
|
-
}
|
|
1877
|
-
const retryable = isRetryableLlmRequestError(error) || isRecoverableLlmScreeningError(error);
|
|
1878
|
-
if (attempt >= maxAttempts || !retryable) {
|
|
1879
|
-
error.image_input_count = imageInputs.length;
|
|
1880
|
-
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1881
|
-
error.llm_attempt_count = attempt;
|
|
1882
|
-
throw error;
|
|
1883
|
-
}
|
|
1884
|
-
await sleepMs(Math.min(2500, 500 * attempt));
|
|
1885
|
-
} finally {
|
|
1886
|
-
clearTimeout(timer);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
lastError = lastError || new Error("LLM request failed without response");
|
|
1890
|
-
lastError.image_input_count = imageInputs.length;
|
|
1891
|
-
lastError.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1892
|
-
lastError.llm_attempt_count = maxAttempts;
|
|
1893
|
-
throw lastError;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
function compactStrategyLlmResult(result) {
|
|
1897
|
-
if (!result) return null;
|
|
1898
|
-
const compact = compactScreeningLlmResult(result);
|
|
1899
|
-
if (compact) {
|
|
1900
|
-
compact.fast_result = null;
|
|
1901
|
-
compact.verify_result = null;
|
|
1902
|
-
}
|
|
1903
|
-
return compact;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
function attachFastFirstScreeningMetadata(result, {
|
|
1907
|
-
fastThinkingLevel = "current",
|
|
1908
|
-
verifyThinkingLevel = "low",
|
|
1909
|
-
verified = false,
|
|
1910
|
-
verificationReason = "",
|
|
1911
|
-
decisionSource = "fast",
|
|
1912
|
-
fastResult = null,
|
|
1913
|
-
verifyResult = null
|
|
1914
|
-
} = {}) {
|
|
1915
|
-
return {
|
|
1916
|
-
...result,
|
|
1917
|
-
screening_strategy: "fast_first_verified",
|
|
1918
|
-
fast_thinking_level: fastThinkingLevel,
|
|
1919
|
-
verify_thinking_level: verifyThinkingLevel,
|
|
1920
|
-
verified: Boolean(verified),
|
|
1921
|
-
verification_reason: verificationReason,
|
|
1922
|
-
decision_source: decisionSource,
|
|
1923
|
-
fast_result: compactStrategyLlmResult(fastResult),
|
|
1924
|
-
verify_result: compactStrategyLlmResult(verifyResult)
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
function normalizeStrategyThinkingLevel(value, fallback) {
|
|
1929
|
-
return normalizeLlmThinkingLevel(value) || normalizeLlmThinkingLevel(fallback) || fallback;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
function resolveStrategyPassMaxTokens(entry = {}, inherited = {}, pass = "fast", fallback = null) {
|
|
1933
|
-
const value = pass === "verify"
|
|
1934
|
-
? firstConfiguredValue(
|
|
1935
|
-
entry.llmVerifyMaxTokens,
|
|
1936
|
-
entry.verifyMaxTokens,
|
|
1937
|
-
entry.verify_max_tokens,
|
|
1938
|
-
inherited.llmVerifyMaxTokens,
|
|
1939
|
-
inherited.verifyMaxTokens,
|
|
1940
|
-
inherited.verify_max_tokens
|
|
1941
|
-
)
|
|
1942
|
-
: firstConfiguredValue(
|
|
1943
|
-
entry.llmFastMaxTokens,
|
|
1944
|
-
entry.fastMaxTokens,
|
|
1945
|
-
entry.fast_max_tokens,
|
|
1946
|
-
inherited.llmFastMaxTokens,
|
|
1947
|
-
inherited.fastMaxTokens,
|
|
1948
|
-
inherited.fast_max_tokens
|
|
1949
|
-
);
|
|
1950
|
-
return parsePositiveInteger(value, fallback);
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
function applyForcedOutputTokenBudget(config = {}, outputTokenBudget = null) {
|
|
1954
|
-
const budget = parsePositiveInteger(outputTokenBudget, null);
|
|
1955
|
-
if (!budget) return config;
|
|
1956
|
-
const next = {
|
|
1957
|
-
...config,
|
|
1958
|
-
llmMaxTokens: budget,
|
|
1959
|
-
maxTokens: budget
|
|
1960
|
-
};
|
|
1961
|
-
delete next.llmMaxCompletionTokens;
|
|
1962
|
-
delete next.maxCompletionTokens;
|
|
1963
|
-
return next;
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
function withForcedLlmThinkingLevel(config = {}, thinkingLevel = "low", options = {}) {
|
|
1967
|
-
const normalizedThinkingLevel = normalizeStrategyThinkingLevel(thinkingLevel, "low");
|
|
1968
|
-
const forceEntry = (entry) => {
|
|
1969
|
-
const objectEntry = typeof entry === "string" ? { model: entry } : { ...(entry || {}) };
|
|
1970
|
-
const forced = {
|
|
1971
|
-
...objectEntry,
|
|
1972
|
-
llmThinkingLevel: normalizedThinkingLevel,
|
|
1973
|
-
thinkingLevel: normalizedThinkingLevel,
|
|
1974
|
-
reasoningEffort: normalizedThinkingLevel
|
|
1975
|
-
};
|
|
1976
|
-
const entryBudget = options.pass
|
|
1977
|
-
? resolveStrategyPassMaxTokens(objectEntry, config, options.pass, options.defaultMaxTokens)
|
|
1978
|
-
: options.outputTokenBudget;
|
|
1979
|
-
return applyForcedOutputTokenBudget(forced, entryBudget);
|
|
1980
|
-
};
|
|
1981
|
-
const baseBudget = options.pass
|
|
1982
|
-
? resolveStrategyPassMaxTokens(config, {}, options.pass, options.defaultMaxTokens)
|
|
1983
|
-
: options.outputTokenBudget;
|
|
1984
|
-
const next = applyForcedOutputTokenBudget({
|
|
1985
|
-
...(config || {}),
|
|
1986
|
-
llmThinkingLevel: normalizedThinkingLevel,
|
|
1987
|
-
thinkingLevel: normalizedThinkingLevel,
|
|
1988
|
-
reasoningEffort: normalizedThinkingLevel
|
|
1989
|
-
}, baseBudget);
|
|
1990
|
-
if (Array.isArray(config?.llmModels)) {
|
|
1991
|
-
next.llmModels = config.llmModels.map(forceEntry);
|
|
1992
|
-
}
|
|
1993
|
-
if (Array.isArray(config?.models)) {
|
|
1994
|
-
next.models = config.models.map(forceEntry);
|
|
1995
|
-
}
|
|
1996
|
-
return next;
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
async function callSinglePassScreeningLlm(args = {}) {
|
|
2000
|
-
const providers = normalizeLlmProviderConfigs(args.config || {});
|
|
2001
|
-
if (providers.length <= 1) {
|
|
2002
|
-
return callScreeningLlmWithProvider({
|
|
2003
|
-
...args,
|
|
2004
|
-
config: {
|
|
2005
|
-
...(providers[0] || args.config || {}),
|
|
2006
|
-
llmProviderCount: 1
|
|
2007
|
-
}
|
|
2008
|
-
});
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
const providerFailures = [];
|
|
2012
|
-
let lastError = null;
|
|
2013
|
-
for (let index = 0; index < providers.length; index += 1) {
|
|
2014
|
-
const providerConfig = {
|
|
2015
|
-
...providers[index],
|
|
2016
|
-
llmProviderIndex: index,
|
|
2017
|
-
llmProviderCount: providers.length
|
|
2018
|
-
};
|
|
2019
|
-
try {
|
|
2020
|
-
const previousAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
2021
|
-
const result = await callScreeningLlmWithProvider({
|
|
2022
|
-
...args,
|
|
2023
|
-
config: providerConfig
|
|
2024
|
-
});
|
|
2025
|
-
const providerAttempts = Number(result.provider_attempt_count ?? result.attempt_count) || 0;
|
|
2026
|
-
return {
|
|
2027
|
-
...result,
|
|
2028
|
-
attempt_count: previousAttempts + providerAttempts,
|
|
2029
|
-
llm_model_failures: providerFailures,
|
|
2030
|
-
fallback_count: providerFailures.length
|
|
2031
|
-
};
|
|
2032
|
-
} catch (error) {
|
|
2033
|
-
if (isFatalLlmProviderError(error)) throw error;
|
|
2034
|
-
lastError = error;
|
|
2035
|
-
providerFailures.push(compactLlmProviderFailure(error, providerConfig, index));
|
|
2036
|
-
if (index < providers.length - 1) {
|
|
2037
|
-
await sleepMs(Math.min(1500, 250 * (index + 1)));
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
const totalAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
2043
|
-
const finalError = new Error(
|
|
2044
|
-
`All configured LLM models failed (${providers.length}); last error: ${lastError?.message || "unknown error"}`
|
|
2045
|
-
);
|
|
2046
|
-
finalError.cause = lastError || null;
|
|
2047
|
-
finalError.llm_provider_failures = providerFailures;
|
|
2048
|
-
finalError.llm_model_failures = providerFailures;
|
|
2049
|
-
finalError.llm_attempt_count = totalAttempts;
|
|
2050
|
-
finalError.image_input_count = lastError?.image_input_count || 0;
|
|
2051
|
-
finalError.image_inputs = lastError?.image_inputs || [];
|
|
2052
|
-
throw finalError;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
async function callFastFirstVerifiedScreeningLlm(args = {}) {
|
|
2056
|
-
const config = args.config || {};
|
|
2057
|
-
const fastThinkingLevel = normalizeStrategyThinkingLevel(config.llmFastThinkingLevel ?? config.fastThinkingLevel, "current");
|
|
2058
|
-
const verifyThinkingLevel = normalizeStrategyThinkingLevel(config.llmVerifyThinkingLevel ?? config.verifyThinkingLevel, "low");
|
|
2059
|
-
const fastArgs = {
|
|
2060
|
-
...args,
|
|
2061
|
-
config: withForcedLlmThinkingLevel(config, fastThinkingLevel, {
|
|
2062
|
-
pass: "fast",
|
|
2063
|
-
defaultMaxTokens: FAST_FIRST_DEFAULT_FAST_MAX_TOKENS
|
|
2064
|
-
}),
|
|
2065
|
-
requireReviewDecision: true
|
|
2066
|
-
};
|
|
2067
|
-
let fastResult = null;
|
|
2068
|
-
let verifyReason = "";
|
|
2069
|
-
try {
|
|
2070
|
-
fastResult = await callSinglePassScreeningLlm(fastArgs);
|
|
2071
|
-
if (fastResult.passed === true) {
|
|
2072
|
-
verifyReason = fastResult.review_required === true ? "fast_passed_review_required" : "fast_passed";
|
|
2073
|
-
} else if (fastResult.review_required === true) {
|
|
2074
|
-
verifyReason = "fast_review_required";
|
|
2075
|
-
}
|
|
2076
|
-
} catch (error) {
|
|
2077
|
-
if (isFatalLlmProviderError(error)) throw error;
|
|
2078
|
-
fastResult = createFailedLlmScreeningResult(error);
|
|
2079
|
-
verifyReason = "fast_invalid_response";
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
if (!verifyReason) {
|
|
2083
|
-
return attachFastFirstScreeningMetadata(fastResult, {
|
|
2084
|
-
fastThinkingLevel,
|
|
2085
|
-
verifyThinkingLevel,
|
|
2086
|
-
verified: false,
|
|
2087
|
-
decisionSource: "fast",
|
|
2088
|
-
fastResult
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
const verifyArgs = {
|
|
2093
|
-
...args,
|
|
2094
|
-
config: withForcedLlmThinkingLevel(config, verifyThinkingLevel, {
|
|
2095
|
-
pass: "verify"
|
|
2096
|
-
}),
|
|
2097
|
-
requireReviewDecision: false
|
|
2098
|
-
};
|
|
2099
|
-
try {
|
|
2100
|
-
const verifyResult = await callSinglePassScreeningLlm(verifyArgs);
|
|
2101
|
-
return attachFastFirstScreeningMetadata(verifyResult, {
|
|
2102
|
-
fastThinkingLevel,
|
|
2103
|
-
verifyThinkingLevel,
|
|
2104
|
-
verified: true,
|
|
2105
|
-
verificationReason: verifyReason,
|
|
2106
|
-
decisionSource: "verify",
|
|
2107
|
-
fastResult,
|
|
2108
|
-
verifyResult
|
|
2109
|
-
});
|
|
2110
|
-
} catch (error) {
|
|
2111
|
-
if (isFatalLlmProviderError(error)) throw error;
|
|
2112
|
-
const failedVerifyResult = createFailedLlmScreeningResult(error);
|
|
2113
|
-
return attachFastFirstScreeningMetadata(failedVerifyResult, {
|
|
2114
|
-
fastThinkingLevel,
|
|
2115
|
-
verifyThinkingLevel,
|
|
2116
|
-
verified: true,
|
|
2117
|
-
verificationReason: verifyReason,
|
|
2118
|
-
decisionSource: "verify_error",
|
|
2119
|
-
fastResult,
|
|
2120
|
-
verifyResult: failedVerifyResult
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
export async function callScreeningLlm(args = {}) {
|
|
2126
|
-
const strategy = normalizeLlmScreeningStrategy(
|
|
2127
|
-
args.config?.llmScreeningStrategy
|
|
2128
|
-
?? args.config?.screeningStrategy
|
|
2129
|
-
?? args.config?.screening_strategy
|
|
2130
|
-
);
|
|
2131
|
-
if (strategy === "fast_first_verified") {
|
|
2132
|
-
return callFastFirstVerifiedScreeningLlm(args);
|
|
2133
|
-
}
|
|
2134
|
-
return callSinglePassScreeningLlm(args);
|
|
2135
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_DOMAINS = new Set(["recommend", "recruit", "chat"]);
|
|
5
|
+
|
|
6
|
+
const DEGREE_RANK = {
|
|
7
|
+
"初中及以下": 1,
|
|
8
|
+
"中专/中技": 2,
|
|
9
|
+
"高中": 3,
|
|
10
|
+
"大专": 4,
|
|
11
|
+
"本科": 5,
|
|
12
|
+
"硕士": 6,
|
|
13
|
+
"博士": 7
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEGREE_PATTERNS = [
|
|
17
|
+
{ value: "博士", regex: /博士|phd|doctor/i },
|
|
18
|
+
{ value: "硕士", regex: /硕士|研究生|master/i },
|
|
19
|
+
{ value: "本科", regex: /本科|学士|bachelor/i },
|
|
20
|
+
{ value: "大专", regex: /大专|专科|college/i },
|
|
21
|
+
{ value: "高中", regex: /高中/i },
|
|
22
|
+
{ value: "中专/中技", regex: /中专|中技/i },
|
|
23
|
+
{ value: "初中及以下", regex: /初中及以下|初中以下/i }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const ENTITY_MAP = {
|
|
27
|
+
amp: "&",
|
|
28
|
+
lt: "<",
|
|
29
|
+
gt: ">",
|
|
30
|
+
quot: "\"",
|
|
31
|
+
apos: "'",
|
|
32
|
+
nbsp: " "
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const GENDER_CODE_MAP = {
|
|
36
|
+
1: "男",
|
|
37
|
+
2: "女"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const LLM_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "auto", "current"]);
|
|
41
|
+
const LLM_SCREENING_STRATEGIES = new Set(["single_pass", "fast_first_verified"]);
|
|
42
|
+
const FAST_FIRST_DEFAULT_FAST_MAX_TOKENS = 384;
|
|
43
|
+
const FATAL_LLM_PROVIDER_MAX_RETRIES = 2;
|
|
44
|
+
|
|
45
|
+
function nowIso() {
|
|
46
|
+
return new Date().toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeLlmThinkingLevel(value) {
|
|
50
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
51
|
+
return LLM_THINKING_LEVELS.has(normalized) ? normalized : "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeLlmScreeningStrategy(value) {
|
|
55
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
56
|
+
return LLM_SCREENING_STRATEGIES.has(normalized) ? normalized : "single_pass";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeBaseUrl(baseUrl) {
|
|
60
|
+
return String(baseUrl || "").replace(/\/+$/, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildChatCompletionsUrl(baseUrl) {
|
|
64
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
65
|
+
if (/\/chat\/completions$/i.test(normalized)) return normalized;
|
|
66
|
+
return `${normalized}/chat/completions`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function redactBaseUrl(baseUrl) {
|
|
70
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
71
|
+
return normalized ? normalized.replace(/\/\/[^/]+/, "//[redacted-host]") : "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstConfiguredValue(...values) {
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
if (value === undefined || value === null) continue;
|
|
77
|
+
if (typeof value === "string" && !value.trim()) continue;
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeLlmProviderEntry(rawEntry, inherited = {}, index = 0) {
|
|
84
|
+
const entry = typeof rawEntry === "string"
|
|
85
|
+
? { model: rawEntry }
|
|
86
|
+
: (rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {});
|
|
87
|
+
const providerName = firstConfiguredValue(
|
|
88
|
+
entry.name,
|
|
89
|
+
entry.label,
|
|
90
|
+
entry.id,
|
|
91
|
+
entry.providerName,
|
|
92
|
+
entry.provider,
|
|
93
|
+
""
|
|
94
|
+
);
|
|
95
|
+
const next = {
|
|
96
|
+
...inherited,
|
|
97
|
+
...entry,
|
|
98
|
+
baseUrl: firstConfiguredValue(entry.baseUrl, entry.base_url, inherited.baseUrl, inherited.base_url),
|
|
99
|
+
apiKey: firstConfiguredValue(entry.apiKey, entry.api_key, inherited.apiKey, inherited.api_key),
|
|
100
|
+
model: firstConfiguredValue(entry.model, entry.modelName, entry.model_name, typeof rawEntry === "string" ? rawEntry : "", inherited.model),
|
|
101
|
+
openaiOrganization: firstConfiguredValue(entry.openaiOrganization, entry.organization, inherited.openaiOrganization, inherited.organization),
|
|
102
|
+
openaiProject: firstConfiguredValue(entry.openaiProject, entry.project, inherited.openaiProject, inherited.project),
|
|
103
|
+
topP: firstConfiguredValue(entry.topP, entry.top_p, inherited.topP, inherited.top_p),
|
|
104
|
+
llmProviderName: normalizeText(providerName),
|
|
105
|
+
llmProviderIndex: index
|
|
106
|
+
};
|
|
107
|
+
delete next.llmModels;
|
|
108
|
+
delete next.models;
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeLlmProviderConfigs(config = {}) {
|
|
113
|
+
if (Array.isArray(config)) {
|
|
114
|
+
return config.map((entry, index) => normalizeLlmProviderEntry(entry, {}, index));
|
|
115
|
+
}
|
|
116
|
+
const inherited = config && typeof config === "object" && !Array.isArray(config) ? { ...config } : {};
|
|
117
|
+
const rawProviders = Array.isArray(inherited.llmModels) && inherited.llmModels.length > 0
|
|
118
|
+
? inherited.llmModels
|
|
119
|
+
: (Array.isArray(inherited.models) && inherited.models.length > 0 ? inherited.models : [inherited]);
|
|
120
|
+
delete inherited.llmModels;
|
|
121
|
+
delete inherited.models;
|
|
122
|
+
return rawProviders.map((entry, index) => normalizeLlmProviderEntry(entry, inherited, index));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function compactLlmProviderFailure(error, providerConfig = {}, providerIndex = 0) {
|
|
126
|
+
return {
|
|
127
|
+
index: providerIndex + 1,
|
|
128
|
+
name: normalizeText(providerConfig.llmProviderName || providerConfig.name || providerConfig.label || providerConfig.id) || null,
|
|
129
|
+
baseUrl: redactBaseUrl(providerConfig.baseUrl),
|
|
130
|
+
model: normalizeText(providerConfig.model) || null,
|
|
131
|
+
status: Number.isFinite(Number(error?.status)) ? Number(error.status) : null,
|
|
132
|
+
attempts: Number(error?.llm_attempt_count) || 0,
|
|
133
|
+
message: String(error?.message || error || "").slice(0, 500)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function classifyFatalLlmProviderError(error) {
|
|
138
|
+
if (!error) return null;
|
|
139
|
+
if (error.llm_fatal_provider_error) {
|
|
140
|
+
return {
|
|
141
|
+
code: error.code || "LLM_FATAL_PROVIDER_ERROR",
|
|
142
|
+
reason: error.llm_fatal_reason || "fatal_provider_error"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const status = Number(error?.status);
|
|
146
|
+
const message = String(error?.message || error || "");
|
|
147
|
+
const providerCode = normalizeText(error?.provider_error_code).toLowerCase();
|
|
148
|
+
const providerType = normalizeText(error?.provider_error_type).toLowerCase();
|
|
149
|
+
const searchable = `${providerCode} ${providerType} ${message}`.toLowerCase();
|
|
150
|
+
if (/(?:budget[_\s-]*exceeded|budget has been exceeded|max budget|spend cap|hard limit|credit limit)/i.test(searchable)) {
|
|
151
|
+
return { code: "LLM_BUDGET_EXCEEDED", reason: "budget_exceeded" };
|
|
152
|
+
}
|
|
153
|
+
if (/(?:insufficient[_\s-]*quota|quota[_\s-]*(?:exceeded|exhausted)|(?:exceeded|exhausted).*quota|out of quota|usage quota|token quota|monthly quota|rate limit quota)/i.test(searchable)) {
|
|
154
|
+
return { code: "LLM_QUOTA_EXCEEDED", reason: "quota_exceeded" };
|
|
155
|
+
}
|
|
156
|
+
if (/(?:insufficient[_\s-]*(?:balance|credit|credits)|no credits|credit balance|billing|payment required|unpaid|account balance)/i.test(searchable)) {
|
|
157
|
+
return { code: "LLM_BILLING_REQUIRED", reason: "billing_required" };
|
|
158
|
+
}
|
|
159
|
+
if (status === 401 || /(?:unauthorized|unauthorised|invalid api key|incorrect api key|authentication failed|invalid authentication|api key invalid|no api key)/i.test(searchable)) {
|
|
160
|
+
return { code: "LLM_AUTH_FAILED", reason: "auth_failed" };
|
|
161
|
+
}
|
|
162
|
+
if (status === 403 || /(?:forbidden|permission denied|access denied|not authorized|not authorised|model access denied)/i.test(searchable)) {
|
|
163
|
+
return { code: "LLM_PERMISSION_DENIED", reason: "permission_denied" };
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function isFatalLlmProviderError(error) {
|
|
169
|
+
return Boolean(classifyFatalLlmProviderError(error));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createFatalLlmRunError(error, { domain = "", candidate = null } = {}) {
|
|
173
|
+
const classification = classifyFatalLlmProviderError(error) || {
|
|
174
|
+
code: "LLM_FATAL_PROVIDER_ERROR",
|
|
175
|
+
reason: "fatal_provider_error"
|
|
176
|
+
};
|
|
177
|
+
const attempts = Number(error?.llm_attempt_count) || 0;
|
|
178
|
+
const suffix = attempts ? ` after ${attempts} attempts` : "";
|
|
179
|
+
const fatal = new Error(`Fatal LLM provider error${suffix}: ${error?.message || String(error || "unknown")}`);
|
|
180
|
+
fatal.name = "FatalLlmProviderError";
|
|
181
|
+
fatal.code = classification.code;
|
|
182
|
+
fatal.llm_fatal_provider_error = true;
|
|
183
|
+
fatal.llm_fatal_reason = classification.reason;
|
|
184
|
+
fatal.llm_attempt_count = attempts;
|
|
185
|
+
fatal.status = Number.isFinite(Number(error?.status)) ? Number(error.status) : null;
|
|
186
|
+
fatal.provider_error_code = error?.provider_error_code || null;
|
|
187
|
+
fatal.provider_error_type = error?.provider_error_type || null;
|
|
188
|
+
fatal.provider_error_message = error?.provider_error_message || null;
|
|
189
|
+
fatal.domain = domain || null;
|
|
190
|
+
fatal.candidate_id = candidate?.id || null;
|
|
191
|
+
fatal.candidate_name = candidate?.identity?.name || null;
|
|
192
|
+
fatal.cause = error || null;
|
|
193
|
+
return fatal;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isVolcengineModel(baseUrl, model) {
|
|
197
|
+
return /volces|volcengine|ark\.cn|doubao|seed/i.test(`${baseUrl || ""} ${model || ""}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isNativeVolcengineBaseUrl(baseUrl) {
|
|
201
|
+
return /volces|volcengine|ark\.cn/i.test(String(baseUrl || ""));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isOpenAiCompatibleV1BaseUrl(baseUrl) {
|
|
205
|
+
return /(?:^|\/)v1(?:\/)?$/i.test(normalizeBaseUrl(baseUrl));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
|
|
209
|
+
const level = normalizeLlmThinkingLevel(thinkingLevel);
|
|
210
|
+
if (!level || level === "current" || level === "auto") return payload;
|
|
211
|
+
if (isVolcengineModel(baseUrl, model)) {
|
|
212
|
+
if (level === "off" || level === "minimal") {
|
|
213
|
+
payload.thinking = { type: "disabled" };
|
|
214
|
+
} else {
|
|
215
|
+
payload.thinking = { type: "enabled" };
|
|
216
|
+
}
|
|
217
|
+
if (!isNativeVolcengineBaseUrl(baseUrl) && isOpenAiCompatibleV1BaseUrl(baseUrl)) {
|
|
218
|
+
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
219
|
+
}
|
|
220
|
+
return payload;
|
|
221
|
+
}
|
|
222
|
+
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
223
|
+
return payload;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parsePositiveNumber(value, fallback = null) {
|
|
227
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
228
|
+
const parsed = Number(value);
|
|
229
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseFiniteNumber(value, fallback = null) {
|
|
233
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
234
|
+
const parsed = Number(value);
|
|
235
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveLlmOutputTokenBudget(config = {}, thinkingLevel = "", options = {}) {
|
|
239
|
+
const explicit = parsePositiveNumber(
|
|
240
|
+
config.llmMaxCompletionTokens
|
|
241
|
+
?? config.maxCompletionTokens
|
|
242
|
+
?? config.llmMaxTokens
|
|
243
|
+
?? config.maxTokens,
|
|
244
|
+
null
|
|
245
|
+
);
|
|
246
|
+
if (explicit) return Math.max(1, Math.floor(explicit));
|
|
247
|
+
if (options.requireReviewDecision) return 384;
|
|
248
|
+
const normalizedThinking = normalizeLlmThinkingLevel(thinkingLevel || "low") || "low";
|
|
249
|
+
return normalizedThinking === "off" || normalizedThinking === "minimal" ? 64 : 512;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parsePositiveInteger(value, fallback = null) {
|
|
253
|
+
const parsed = parsePositiveNumber(value, fallback);
|
|
254
|
+
return parsed ? Math.max(1, Math.floor(parsed)) : fallback;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function normalizeText(input) {
|
|
258
|
+
return String(input || "").replace(/\s+/g, " ").trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeBlockText(input) {
|
|
262
|
+
return String(input ?? "").trim();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeReasoningKey(input) {
|
|
266
|
+
return normalizeBlockText(input).replace(/\s+/g, " ");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function collapseRepeatedReasoningText(input) {
|
|
270
|
+
const text = normalizeBlockText(input);
|
|
271
|
+
if (!text) return "";
|
|
272
|
+
const chunks = text.split(/\n{2,}/).map(normalizeBlockText).filter(Boolean);
|
|
273
|
+
if (chunks.length >= 2 && chunks.length % 2 === 0) {
|
|
274
|
+
const midpoint = chunks.length / 2;
|
|
275
|
+
const first = chunks.slice(0, midpoint);
|
|
276
|
+
const second = chunks.slice(midpoint);
|
|
277
|
+
if (normalizeReasoningKey(first.join("\n\n")) === normalizeReasoningKey(second.join("\n\n"))) {
|
|
278
|
+
return first.join("\n\n");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const compacted = normalizeReasoningKey(text);
|
|
283
|
+
if (compacted.length < 160) return text;
|
|
284
|
+
const midpoint = Math.floor(compacted.length / 2);
|
|
285
|
+
for (let offset = -32; offset <= 32; offset += 1) {
|
|
286
|
+
const split = midpoint + offset;
|
|
287
|
+
if (split <= 80 || split >= compacted.length - 80) continue;
|
|
288
|
+
const first = compacted.slice(0, split).trim();
|
|
289
|
+
const second = compacted.slice(split).trim();
|
|
290
|
+
if (first && first === second) return first;
|
|
291
|
+
}
|
|
292
|
+
return text;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function firstReasoningText(lines) {
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const cleaned = collapseRepeatedReasoningText(line);
|
|
298
|
+
if (cleaned) return cleaned;
|
|
299
|
+
}
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function compact(input) {
|
|
304
|
+
return normalizeText(input).toLowerCase();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function decodeHtmlEntities(input) {
|
|
308
|
+
return String(input || "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
|
|
309
|
+
const key = String(entity).toLowerCase();
|
|
310
|
+
if (key.startsWith("#x")) {
|
|
311
|
+
const value = Number.parseInt(key.slice(2), 16);
|
|
312
|
+
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
313
|
+
}
|
|
314
|
+
if (key.startsWith("#")) {
|
|
315
|
+
const value = Number.parseInt(key.slice(1), 10);
|
|
316
|
+
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
317
|
+
}
|
|
318
|
+
return ENTITY_MAP[key] || match;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function htmlToText(html) {
|
|
323
|
+
const withoutScripts = String(html || "")
|
|
324
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
325
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
|
|
326
|
+
const withBreaks = withoutScripts
|
|
327
|
+
.replace(/<\/(?:div|li|p|section|article|header|footer|h[1-6]|tr)>/gi, "\n")
|
|
328
|
+
.replace(/<br\s*\/?>/gi, "\n");
|
|
329
|
+
return decodeHtmlEntities(withBreaks.replace(/<[^>]+>/g, " "))
|
|
330
|
+
.split(/\r?\n/)
|
|
331
|
+
.map((line) => normalizeText(line))
|
|
332
|
+
.filter(Boolean)
|
|
333
|
+
.join("\n");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function parseHtmlAttributes(html) {
|
|
337
|
+
const attributes = {};
|
|
338
|
+
const openTag = String(html || "").match(/^<[^>]+>/s)?.[0] || "";
|
|
339
|
+
const regex = /([:@A-Za-z0-9_-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
340
|
+
let match;
|
|
341
|
+
while ((match = regex.exec(openTag))) {
|
|
342
|
+
const name = match[1];
|
|
343
|
+
if (!name || name.startsWith("<")) continue;
|
|
344
|
+
attributes[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
345
|
+
}
|
|
346
|
+
return attributes;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function unique(values) {
|
|
350
|
+
return Array.from(new Set(values.map(normalizeText).filter(Boolean)));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeDomain(domain) {
|
|
354
|
+
const normalized = compact(domain);
|
|
355
|
+
if (!SUPPORTED_DOMAINS.has(normalized)) {
|
|
356
|
+
throw new Error(`Unsupported screening domain: ${domain}`);
|
|
357
|
+
}
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function collectTextParts(candidate = {}) {
|
|
362
|
+
return unique([
|
|
363
|
+
candidate.text,
|
|
364
|
+
candidate.raw_text,
|
|
365
|
+
candidate.summary,
|
|
366
|
+
candidate.resume_text,
|
|
367
|
+
candidate.identity?.name,
|
|
368
|
+
candidate.identity?.title,
|
|
369
|
+
candidate.identity?.current_position,
|
|
370
|
+
candidate.identity?.current_company,
|
|
371
|
+
candidate.identity?.school,
|
|
372
|
+
candidate.identity?.major,
|
|
373
|
+
...(candidate.tags || [])
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseDegree(text) {
|
|
378
|
+
for (const item of DEGREE_PATTERNS) {
|
|
379
|
+
if (item.regex.test(text)) return item.value;
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function parseYearsExperience(text) {
|
|
385
|
+
const normalized = normalizeText(text);
|
|
386
|
+
const match = normalized.match(/(?<!\d)(\d{1,2})\s*(?:年以上?|年)\s*(?:经验|工作经验)/i)
|
|
387
|
+
|| normalized.match(/(?:经验|工作经验|工作)\s*(?<!\d)(\d{1,2})\s*(?:年以上?|年)/i)
|
|
388
|
+
|| normalized.match(/(?<!\d)(\d{1,2})\s*years?\s*(?:of\s*)?(?:experience|work)?/i);
|
|
389
|
+
if (!match) return null;
|
|
390
|
+
const value = Number.parseInt(match[1], 10);
|
|
391
|
+
return Number.isFinite(value) ? value : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseAge(text) {
|
|
395
|
+
const match = normalizeText(text).match(/(\d{2})\s*岁/);
|
|
396
|
+
if (!match) return null;
|
|
397
|
+
const value = Number.parseInt(match[1], 10);
|
|
398
|
+
return Number.isFinite(value) ? value : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function parseGender(text) {
|
|
402
|
+
const normalized = normalizeText(text);
|
|
403
|
+
if (/(?:^|[\s||,,])男(?:$|[\s||,,])/.test(normalized)) return "男";
|
|
404
|
+
if (/(?:^|[\s||,,])女(?:$|[\s||,,])/.test(normalized)) return "女";
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeGenderValue(value) {
|
|
409
|
+
if (value == null || value === "") return null;
|
|
410
|
+
if (GENDER_CODE_MAP[value]) return GENDER_CODE_MAP[value];
|
|
411
|
+
const normalized = normalizeText(value);
|
|
412
|
+
if (normalized === "男" || normalized === "女") return normalized;
|
|
413
|
+
return parseGender(normalized);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseDateLike(value) {
|
|
417
|
+
const normalized = normalizeText(value);
|
|
418
|
+
if (!normalized || normalized === "0") return "";
|
|
419
|
+
if (/^\d{6}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
420
|
+
if (/^\d{8}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
421
|
+
return normalized;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function isLikelySalaryLine(value = "") {
|
|
425
|
+
const normalized = normalizeText(value);
|
|
426
|
+
return Boolean(
|
|
427
|
+
/^(?:面议|薪资面议)$/i.test(normalized)
|
|
428
|
+
|| /^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?$/.test(normalized)
|
|
429
|
+
|| /^\d+\s*-\s*\d+\s*元\s*\/\s*天$/.test(normalized)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isLikelyStatusLine(value = "") {
|
|
434
|
+
const normalized = normalizeText(value);
|
|
435
|
+
return Boolean(
|
|
436
|
+
!normalized
|
|
437
|
+
|| /^沟通|^收藏|^查看|^不合适/.test(normalized)
|
|
438
|
+
|| /^(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/.test(normalized)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function stripLeadingSalaryToken(value = "") {
|
|
443
|
+
return normalizeText(value)
|
|
444
|
+
.replace(/^(?:面议|薪资面议)\s+/i, "")
|
|
445
|
+
.replace(/^\d+(?:\.\d+)?(?:\s*-\s*\d+(?:\.\d+)?)?\s*[kK](?:\s*[·xX*]\s*\d+\s*薪?)?\s+/, "")
|
|
446
|
+
.replace(/^\d+\s*-\s*\d+\s*元\s*\/\s*天\s+/, "")
|
|
447
|
+
.trim();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function stripTrailingStatusToken(value = "") {
|
|
451
|
+
return normalizeText(value)
|
|
452
|
+
.replace(/\s*(?:在线|刚刚活跃|今日活跃|本周活跃|本月活跃|继续沟通|打招呼)$/u, "")
|
|
453
|
+
.trim();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function cleanInferredNameLine(value = "") {
|
|
457
|
+
const withoutSalary = stripLeadingSalaryToken(value);
|
|
458
|
+
const withoutStatus = stripTrailingStatusToken(withoutSalary);
|
|
459
|
+
return withoutStatus && !isLikelyStatusLine(withoutStatus) && !isLikelySalaryLine(withoutStatus)
|
|
460
|
+
? withoutStatus
|
|
461
|
+
: "";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function firstUsefulLine(lines) {
|
|
465
|
+
for (const line of lines) {
|
|
466
|
+
const cleaned = cleanInferredNameLine(line);
|
|
467
|
+
if (cleaned) return cleaned;
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function parseNetworkBodyText(networkBody = {}) {
|
|
473
|
+
const bodyResult = networkBody.body || networkBody;
|
|
474
|
+
let body = String(bodyResult?.body || "");
|
|
475
|
+
if (bodyResult?.base64Encoded) {
|
|
476
|
+
body = Buffer.from(body, "base64").toString("utf8");
|
|
477
|
+
}
|
|
478
|
+
return body;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function tryParseJson(text) {
|
|
482
|
+
try {
|
|
483
|
+
return JSON.parse(text);
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function tryExtractJsonObject(text) {
|
|
490
|
+
const normalized = String(text || "").trim();
|
|
491
|
+
const direct = tryParseJson(normalized);
|
|
492
|
+
if (direct && typeof direct === "object") return direct;
|
|
493
|
+
const fenced = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
494
|
+
if (fenced) {
|
|
495
|
+
const parsed = tryParseJson(fenced[1].trim());
|
|
496
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
497
|
+
}
|
|
498
|
+
const start = normalized.indexOf("{");
|
|
499
|
+
const end = normalized.lastIndexOf("}");
|
|
500
|
+
if (start >= 0 && end > start) {
|
|
501
|
+
const parsed = tryParseJson(normalized.slice(start, end + 1));
|
|
502
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function extractBalancedJsonAt(text = "", startIndex = 0) {
|
|
508
|
+
const source = String(text || "");
|
|
509
|
+
const start = source.indexOf("{", Math.max(0, Number(startIndex) || 0));
|
|
510
|
+
if (start < 0) return "";
|
|
511
|
+
let depth = 0;
|
|
512
|
+
let inString = false;
|
|
513
|
+
let quote = "";
|
|
514
|
+
let escaped = false;
|
|
515
|
+
for (let index = start; index < source.length; index += 1) {
|
|
516
|
+
const char = source[index];
|
|
517
|
+
if (inString) {
|
|
518
|
+
if (escaped) {
|
|
519
|
+
escaped = false;
|
|
520
|
+
} else if (char === "\\") {
|
|
521
|
+
escaped = true;
|
|
522
|
+
} else if (char === quote) {
|
|
523
|
+
inString = false;
|
|
524
|
+
quote = "";
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (char === "\"" || char === "'") {
|
|
529
|
+
inString = true;
|
|
530
|
+
quote = char;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (char === "{") depth += 1;
|
|
534
|
+
if (char === "}") {
|
|
535
|
+
depth -= 1;
|
|
536
|
+
if (depth === 0) return source.slice(start, index + 1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return "";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function tryParseEmbeddedJsonObjects(text = "") {
|
|
543
|
+
const source = decodeHtmlEntities(String(text || ""));
|
|
544
|
+
const objects = [];
|
|
545
|
+
const anchors = [
|
|
546
|
+
"__INITIAL_STATE__",
|
|
547
|
+
"__NEXT_DATA__",
|
|
548
|
+
"geekDetailInfo",
|
|
549
|
+
"geekDetail",
|
|
550
|
+
"geekBaseInfo",
|
|
551
|
+
"geekEduExpList",
|
|
552
|
+
"geekWorkExpList",
|
|
553
|
+
"resume"
|
|
554
|
+
];
|
|
555
|
+
for (const anchor of anchors) {
|
|
556
|
+
let searchIndex = 0;
|
|
557
|
+
while (searchIndex >= 0 && searchIndex < source.length) {
|
|
558
|
+
const anchorIndex = source.indexOf(anchor, searchIndex);
|
|
559
|
+
if (anchorIndex < 0) break;
|
|
560
|
+
const windowStart = Math.max(0, anchorIndex - 4000);
|
|
561
|
+
const braceIndex = source.lastIndexOf("{", anchorIndex);
|
|
562
|
+
if (braceIndex >= windowStart) {
|
|
563
|
+
const jsonText = extractBalancedJsonAt(source, braceIndex);
|
|
564
|
+
const parsed = tryParseJson(jsonText);
|
|
565
|
+
if (parsed && typeof parsed === "object") objects.push(parsed);
|
|
566
|
+
}
|
|
567
|
+
searchIndex = anchorIndex + anchor.length;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return objects;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function flattenChatMessageContent(content) {
|
|
574
|
+
if (typeof content === "string") return content;
|
|
575
|
+
if (Array.isArray(content)) {
|
|
576
|
+
return content.map((item) => {
|
|
577
|
+
if (typeof item === "string") return item;
|
|
578
|
+
return item?.text || item?.content || item?.reasoning_content || "";
|
|
579
|
+
}).filter(Boolean).join("\n");
|
|
580
|
+
}
|
|
581
|
+
return "";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function collectLlmReasoningText(choice = {}) {
|
|
585
|
+
const message = choice?.message || {};
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
const unique = [];
|
|
588
|
+
for (const item of [
|
|
589
|
+
message.reasoning_content,
|
|
590
|
+
message.provider_specific_fields?.reasoning_content,
|
|
591
|
+
message.reasoning,
|
|
592
|
+
message.provider_specific_fields?.reasoning,
|
|
593
|
+
message.cot,
|
|
594
|
+
message.provider_specific_fields?.cot,
|
|
595
|
+
message.chain_of_thought,
|
|
596
|
+
message.provider_specific_fields?.chain_of_thought,
|
|
597
|
+
choice.reasoning_content,
|
|
598
|
+
choice.provider_specific_fields?.reasoning_content,
|
|
599
|
+
choice.reasoning,
|
|
600
|
+
choice.provider_specific_fields?.reasoning,
|
|
601
|
+
choice.cot,
|
|
602
|
+
choice.provider_specific_fields?.cot,
|
|
603
|
+
choice.chain_of_thought,
|
|
604
|
+
choice.provider_specific_fields?.chain_of_thought
|
|
605
|
+
]) {
|
|
606
|
+
const text = collapseRepeatedReasoningText(flattenChatMessageContent(item));
|
|
607
|
+
const key = normalizeReasoningKey(text);
|
|
608
|
+
if (!key || seen.has(key)) continue;
|
|
609
|
+
seen.add(key);
|
|
610
|
+
unique.push(text);
|
|
611
|
+
}
|
|
612
|
+
return collapseRepeatedReasoningText(unique.join("\n\n"));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function mimeTypeForImagePath(filePath) {
|
|
616
|
+
const extension = path.extname(String(filePath || "")).toLowerCase();
|
|
617
|
+
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
|
|
618
|
+
if (extension === ".webp") return "image/webp";
|
|
619
|
+
return "image/png";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function normalizeImagePaths({ imageEvidence = null, imagePaths = [] } = {}) {
|
|
623
|
+
const paths = [];
|
|
624
|
+
if (Array.isArray(imagePaths)) {
|
|
625
|
+
paths.push(...imagePaths);
|
|
626
|
+
}
|
|
627
|
+
const evidenceLlmPaths = Array.isArray(imageEvidence?.llm_file_paths)
|
|
628
|
+
? imageEvidence.llm_file_paths
|
|
629
|
+
: [];
|
|
630
|
+
if (evidenceLlmPaths.length) {
|
|
631
|
+
paths.push(...evidenceLlmPaths);
|
|
632
|
+
} else {
|
|
633
|
+
if (Array.isArray(imageEvidence?.file_paths)) {
|
|
634
|
+
paths.push(...imageEvidence.file_paths);
|
|
635
|
+
}
|
|
636
|
+
if (Array.isArray(imageEvidence?.screenshots)) {
|
|
637
|
+
paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return unique(paths.map((filePath) => String(filePath || "").trim()).filter(Boolean));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function imagePathToLlmInput(filePath, {
|
|
644
|
+
detail = "high"
|
|
645
|
+
} = {}) {
|
|
646
|
+
const resolved = path.resolve(filePath);
|
|
647
|
+
const buffer = fs.readFileSync(resolved);
|
|
648
|
+
const mimeType = mimeTypeForImagePath(resolved);
|
|
649
|
+
return {
|
|
650
|
+
type: "image_url",
|
|
651
|
+
image_url: {
|
|
652
|
+
url: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
653
|
+
detail
|
|
654
|
+
},
|
|
655
|
+
metadata: {
|
|
656
|
+
file_path: resolved,
|
|
657
|
+
mime_type: mimeType,
|
|
658
|
+
byte_length: buffer.length
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function buildScreeningLlmImageInputs({
|
|
664
|
+
imageEvidence = null,
|
|
665
|
+
imagePaths = [],
|
|
666
|
+
maxImages = 8,
|
|
667
|
+
detail = "high"
|
|
668
|
+
} = {}) {
|
|
669
|
+
const paths = normalizeImagePaths({ imageEvidence, imagePaths });
|
|
670
|
+
const limit = Math.max(1, Number(maxImages) || 8);
|
|
671
|
+
return paths.slice(0, limit).map((filePath) => imagePathToLlmInput(filePath, { detail }));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function summarizeLlmImageInputs(imageInputs = []) {
|
|
675
|
+
return imageInputs.map((input, index) => ({
|
|
676
|
+
index,
|
|
677
|
+
file_path: input.metadata?.file_path || null,
|
|
678
|
+
mime_type: input.metadata?.mime_type || null,
|
|
679
|
+
byte_length: input.metadata?.byte_length || 0
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function parsePassedDecision(value) {
|
|
684
|
+
if (typeof value === "boolean") return value;
|
|
685
|
+
if (typeof value === "number") return value !== 0;
|
|
686
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
687
|
+
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(normalized)) return true;
|
|
688
|
+
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(normalized)) return false;
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function pickFirst(...values) {
|
|
693
|
+
for (const value of values) {
|
|
694
|
+
const normalized = normalizeText(value);
|
|
695
|
+
if (normalized && normalized !== "0") return normalized;
|
|
696
|
+
}
|
|
697
|
+
return "";
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function isPlainObject(value) {
|
|
701
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function isBossGeekDetailShape(value) {
|
|
705
|
+
if (!isPlainObject(value)) return false;
|
|
706
|
+
return Boolean(
|
|
707
|
+
isPlainObject(value.geekBaseInfo)
|
|
708
|
+
|| value.geekName
|
|
709
|
+
|| value.geekAdvantage
|
|
710
|
+
|| Array.isArray(value.geekEduExpList)
|
|
711
|
+
|| Array.isArray(value.geekEducationList)
|
|
712
|
+
|| Array.isArray(value.geekWorkExpList)
|
|
713
|
+
|| Array.isArray(value.geekProjExpList)
|
|
714
|
+
|| Array.isArray(value.geekCertificationList)
|
|
715
|
+
|| Array.isArray(value.geekSkillList)
|
|
716
|
+
|| isPlainObject(value.highestEduExp)
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function isBossChatProfileShape(value) {
|
|
721
|
+
if (!isPlainObject(value)) return false;
|
|
722
|
+
return Boolean(
|
|
723
|
+
(value.name || value.encryptGeekId || value.uid)
|
|
724
|
+
&& (
|
|
725
|
+
Array.isArray(value.eduExpList)
|
|
726
|
+
|| Array.isArray(value.workExpList)
|
|
727
|
+
|| value.school
|
|
728
|
+
|| value.major
|
|
729
|
+
|| value.lastCompany
|
|
730
|
+
|| value.positionName
|
|
731
|
+
)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function collectObjects(root, {
|
|
736
|
+
maxObjects = 500,
|
|
737
|
+
maxDepth = 8
|
|
738
|
+
} = {}) {
|
|
739
|
+
if (!root || typeof root !== "object") return [];
|
|
740
|
+
const queue = [{ value: root, depth: 0 }];
|
|
741
|
+
const seen = new WeakSet();
|
|
742
|
+
const objects = [];
|
|
743
|
+
while (queue.length && objects.length < maxObjects) {
|
|
744
|
+
const { value, depth } = queue.shift();
|
|
745
|
+
if (!value || typeof value !== "object" || seen.has(value)) continue;
|
|
746
|
+
seen.add(value);
|
|
747
|
+
if (isPlainObject(value)) objects.push(value);
|
|
748
|
+
if (depth >= maxDepth) continue;
|
|
749
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
750
|
+
for (const child of children) {
|
|
751
|
+
if (child && typeof child === "object") {
|
|
752
|
+
queue.push({ value: child, depth: depth + 1 });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return objects;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function joinRange(start, end, fallback = "") {
|
|
760
|
+
const left = parseDateLike(start);
|
|
761
|
+
const right = parseDateLike(end);
|
|
762
|
+
if (left && right) return `${left}-${right}`;
|
|
763
|
+
if (left) return `${left}-至今`;
|
|
764
|
+
if (right) return right;
|
|
765
|
+
return normalizeText(fallback);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function normalizeList(value) {
|
|
769
|
+
if (!value) return [];
|
|
770
|
+
if (Array.isArray(value)) return value;
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function normalizeTagList(value) {
|
|
775
|
+
if (!Array.isArray(value)) return [];
|
|
776
|
+
return value.map((item) => {
|
|
777
|
+
if (typeof item === "string") return item;
|
|
778
|
+
return pickFirst(item?.name, item?.label, item?.tagName, item?.text, item?.value);
|
|
779
|
+
}).filter(Boolean);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function formatNamedSection(title, lines = []) {
|
|
783
|
+
const normalized = lines.map(normalizeText).filter(Boolean);
|
|
784
|
+
if (!normalized.length) return "";
|
|
785
|
+
return [`【${title}】`, ...normalized].join("\n");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function formatWorkExperience(item = {}, index = 0) {
|
|
789
|
+
const company = pickFirst(item.formattedCompany, item.company);
|
|
790
|
+
const position = pickFirst(item.positionName, item.positionTitle, item.position);
|
|
791
|
+
const period = joinRange(item.startYearMonStr || item.startDate, item.endYearMonStr || item.endDate, item.workYearDesc);
|
|
792
|
+
const emphasis = [
|
|
793
|
+
...normalizeTagList(item.workEmphasisList),
|
|
794
|
+
...normalizeTagList(item.respHighlightList),
|
|
795
|
+
...normalizeTagList(item.workPerfHighlightList)
|
|
796
|
+
];
|
|
797
|
+
return [
|
|
798
|
+
`${index + 1}. ${[company, position, period].filter(Boolean).join(" / ")}`,
|
|
799
|
+
item.department ? `部门:${normalizeText(item.department)}` : "",
|
|
800
|
+
item.responsibility ? `职责:${normalizeText(item.responsibility)}` : "",
|
|
801
|
+
item.workPerformance ? `业绩:${normalizeText(item.workPerformance)}` : "",
|
|
802
|
+
item.workEmphasis ? `重点:${normalizeText(item.workEmphasis)}` : "",
|
|
803
|
+
emphasis.length ? `亮点:${unique(emphasis).join("、")}` : ""
|
|
804
|
+
].filter(Boolean).join("\n");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function formatProjectExperience(item = {}, index = 0) {
|
|
808
|
+
const period = joinRange(item.startYearMonStr || item.startDateDesc || item.startDate, item.endYearMonStr || item.endDateDesc || item.endDate);
|
|
809
|
+
return [
|
|
810
|
+
`${index + 1}. ${[pickFirst(item.name), pickFirst(item.roleName), period].filter(Boolean).join(" / ")}`,
|
|
811
|
+
pickFirst(item.projectDescription, item.description) ? `项目描述:${pickFirst(item.projectDescription, item.description)}` : "",
|
|
812
|
+
item.performance ? `项目业绩:${normalizeText(item.performance)}` : ""
|
|
813
|
+
].filter(Boolean).join("\n");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function formatEducation(item = {}, index = 0) {
|
|
817
|
+
const period = joinRange(
|
|
818
|
+
item.startDateDesc || item.startDate || item.startYearStr,
|
|
819
|
+
item.endDateDesc || item.endDate || item.endYearStr
|
|
820
|
+
);
|
|
821
|
+
const tags = [
|
|
822
|
+
...normalizeTagList(item.tags),
|
|
823
|
+
...normalizeTagList(item.schoolTags),
|
|
824
|
+
...normalizeTagList(item.keySubjectList)
|
|
825
|
+
];
|
|
826
|
+
return [
|
|
827
|
+
`${index + 1}. ${[
|
|
828
|
+
pickFirst(item.school, item.schoolName),
|
|
829
|
+
pickFirst(item.major, item.majorName),
|
|
830
|
+
pickFirst(item.degreeName, item.degree),
|
|
831
|
+
period
|
|
832
|
+
].filter(Boolean).join(" / ")}`,
|
|
833
|
+
tags.length ? `标签:${unique(tags).join("、")}` : "",
|
|
834
|
+
item.courseDesc ? `课程:${normalizeText(item.courseDesc)}` : "",
|
|
835
|
+
item.eduDescription ? `教育描述:${normalizeText(item.eduDescription)}` : "",
|
|
836
|
+
item.thesisTitle ? `论文:${normalizeText(item.thesisTitle)}` : "",
|
|
837
|
+
item.thesisDesc ? `论文描述:${normalizeText(item.thesisDesc)}` : ""
|
|
838
|
+
].filter(Boolean).join("\n");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function formatExpectation(item = {}, index = 0) {
|
|
842
|
+
return `${index + 1}. ${[
|
|
843
|
+
pickFirst(item.positionName, item.position),
|
|
844
|
+
pickFirst(item.locationName, item.location),
|
|
845
|
+
pickFirst(item.salaryDesc),
|
|
846
|
+
pickFirst(item.industryDesc)
|
|
847
|
+
].filter(Boolean).join(" / ")}`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function formatChatWorkExperience(item = {}, index = 0) {
|
|
851
|
+
return [
|
|
852
|
+
`${index + 1}. ${[
|
|
853
|
+
pickFirst(item.company, item.brandName),
|
|
854
|
+
pickFirst(item.positionName, item.position),
|
|
855
|
+
pickFirst(item.workYear, item.workYearDesc, item.dateRange)
|
|
856
|
+
].filter(Boolean).join(" / ")}`,
|
|
857
|
+
pickFirst(item.description, item.performance, item.content) ? `描述:${pickFirst(item.description, item.performance, item.content)}` : ""
|
|
858
|
+
].filter(Boolean).join("\n");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function formatChatEducation(item = {}, index = 0) {
|
|
862
|
+
return `${index + 1}. ${[
|
|
863
|
+
pickFirst(item.school),
|
|
864
|
+
pickFirst(item.major),
|
|
865
|
+
pickFirst(item.degree, item.degreeName),
|
|
866
|
+
pickFirst(item.year, item.dateRange)
|
|
867
|
+
].filter(Boolean).join(" / ")}`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function resolveBossGeekDetail(payload = {}) {
|
|
871
|
+
const candidates = [
|
|
872
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.geekDetailInfo },
|
|
873
|
+
{ sourceKey: "geekDetail", detail: payload?.zpData?.geekDetail },
|
|
874
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.geekDetailInfo },
|
|
875
|
+
{ sourceKey: "geekDetail", detail: payload?.zpData?.data?.geekDetail },
|
|
876
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.detailInfo },
|
|
877
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data?.resumeDetail },
|
|
878
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.data },
|
|
879
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.data?.geekDetailInfo },
|
|
880
|
+
{ sourceKey: "geekDetail", detail: payload?.data?.geekDetail },
|
|
881
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.data?.detailInfo },
|
|
882
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.data?.resumeDetail },
|
|
883
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.data },
|
|
884
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.geekDetailInfo },
|
|
885
|
+
{ sourceKey: "geekDetail", detail: payload?.geekDetail },
|
|
886
|
+
{ sourceKey: "geekDetailInfo", detail: payload }
|
|
887
|
+
];
|
|
888
|
+
const found = candidates.find((item) => isBossGeekDetailShape(item.detail));
|
|
889
|
+
return found || { sourceKey: "", detail: null };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function extractBossChatGeekInfo(payload = {}) {
|
|
893
|
+
const data = payload?.zpData?.data || payload?.data || payload?.zpData?.geekInfo || payload?.geekInfo;
|
|
894
|
+
if (!data || typeof data !== "object") return null;
|
|
895
|
+
if (!isBossChatProfileShape(data)) return null;
|
|
896
|
+
const educationList = normalizeList(data.eduExpList);
|
|
897
|
+
const workList = normalizeList(data.workExpList);
|
|
898
|
+
const firstEducation = educationList[0] || {};
|
|
899
|
+
const firstWork = workList[0] || {};
|
|
900
|
+
const tags = unique([
|
|
901
|
+
...normalizeTagList(data.highLightGeekResumeWords),
|
|
902
|
+
...normalizeTagList(data.highLightWords),
|
|
903
|
+
...normalizeTagList(data.skillTags),
|
|
904
|
+
...normalizeTagList(data.labels),
|
|
905
|
+
pickFirst(data.positionCategory),
|
|
906
|
+
pickFirst(data.positionName, data.position, data.toPosition)
|
|
907
|
+
]);
|
|
908
|
+
const salary = data.salaryDesc || (
|
|
909
|
+
data.lowSalary && data.highSalary ? `${data.lowSalary}-${data.highSalary}K` : ""
|
|
910
|
+
);
|
|
911
|
+
const sections = {
|
|
912
|
+
base: [
|
|
913
|
+
pickFirst(data.name) ? `姓名:${pickFirst(data.name)}` : "",
|
|
914
|
+
pickFirst(data.gender) ? `性别:${pickFirst(data.gender)}` : "",
|
|
915
|
+
pickFirst(data.age) ? `年龄:${pickFirst(data.age)}` : "",
|
|
916
|
+
pickFirst(data.year, data.workYear, data.workYearDesc) ? `工作年限:${pickFirst(data.year, data.workYear, data.workYearDesc)}` : "",
|
|
917
|
+
pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) ? `最高学历:${pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName)}` : "",
|
|
918
|
+
pickFirst(data.positionStatus, data.positionStatusDesc) ? `求职状态:${pickFirst(data.positionStatus, data.positionStatusDesc)}` : ""
|
|
919
|
+
].filter(Boolean),
|
|
920
|
+
expectation: [
|
|
921
|
+
[pickFirst(data.toPosition, data.positionName, data.position), salary].filter(Boolean).join(" / ")
|
|
922
|
+
].filter(Boolean),
|
|
923
|
+
current: [
|
|
924
|
+
[pickFirst(data.lastCompany, data.lastCompany2), pickFirst(data.lastPosition, data.lastPosition2)].filter(Boolean).join(" / ")
|
|
925
|
+
].filter(Boolean),
|
|
926
|
+
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
927
|
+
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
928
|
+
highlights: tags
|
|
929
|
+
};
|
|
930
|
+
const text = [
|
|
931
|
+
formatNamedSection("基础信息", sections.base),
|
|
932
|
+
formatNamedSection("求职期望", sections.expectation),
|
|
933
|
+
formatNamedSection("最近经历", sections.current),
|
|
934
|
+
formatNamedSection("工作经历", sections.work),
|
|
935
|
+
formatNamedSection("教育经历", sections.education),
|
|
936
|
+
formatNamedSection("亮点标签", sections.highlights)
|
|
937
|
+
].filter(Boolean).join("\n\n");
|
|
938
|
+
return {
|
|
939
|
+
text,
|
|
940
|
+
identity: {
|
|
941
|
+
name: pickFirst(data.name) || null,
|
|
942
|
+
title: pickFirst(data.positionName, data.position, data.toPosition) || null,
|
|
943
|
+
current_position: pickFirst(data.lastPosition, data.lastPosition2, firstWork.positionName, firstWork.position) || null,
|
|
944
|
+
current_company: pickFirst(data.lastCompany, data.lastCompany2, firstWork.company, firstWork.brandName) || null,
|
|
945
|
+
school: pickFirst(data.school, firstEducation.school) || null,
|
|
946
|
+
major: pickFirst(data.major, firstEducation.major) || null,
|
|
947
|
+
degree: pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) || parseDegree(text),
|
|
948
|
+
years_experience: parseYearsExperience(pickFirst(data.year, data.workYear, data.workYearDesc)) ?? null,
|
|
949
|
+
age: parseAge(String(data.age || "")) ?? null,
|
|
950
|
+
gender: normalizeGenderValue(data.gender)
|
|
951
|
+
},
|
|
952
|
+
tags,
|
|
953
|
+
source_keys: {
|
|
954
|
+
chat_geek_info: true,
|
|
955
|
+
geek_detail_info: false,
|
|
956
|
+
geek_detail: false,
|
|
957
|
+
education_count: educationList.length,
|
|
958
|
+
work_count: workList.length
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function extractBossChatHistoryResume(payload = {}) {
|
|
964
|
+
const messages = normalizeList(payload?.zpData?.messages).length
|
|
965
|
+
? normalizeList(payload?.zpData?.messages)
|
|
966
|
+
: normalizeList(payload?.messages).length
|
|
967
|
+
? normalizeList(payload?.messages)
|
|
968
|
+
: normalizeList(payload?.data?.messages).length
|
|
969
|
+
? normalizeList(payload?.data?.messages)
|
|
970
|
+
: normalizeList(payload?.zpData?.data?.messages);
|
|
971
|
+
const resumes = messages
|
|
972
|
+
.map((message) => message?.body?.resume)
|
|
973
|
+
.filter((resume) => resume && typeof resume === "object");
|
|
974
|
+
const resume = resumes[0];
|
|
975
|
+
if (!resume) return null;
|
|
976
|
+
const user = resume.user || {};
|
|
977
|
+
const educationList = normalizeList(resume.education);
|
|
978
|
+
const workList = normalizeList(resume.experiences);
|
|
979
|
+
const firstEducation = educationList[0] || {};
|
|
980
|
+
const firstWork = workList[0] || {};
|
|
981
|
+
const tags = unique([
|
|
982
|
+
pickFirst(resume.position),
|
|
983
|
+
pickFirst(resume.positionCategory),
|
|
984
|
+
...normalizeTagList(resume.skills),
|
|
985
|
+
...normalizeTagList(resume.tags)
|
|
986
|
+
]);
|
|
987
|
+
const sections = {
|
|
988
|
+
base: [
|
|
989
|
+
pickFirst(user.name) ? `姓名:${pickFirst(user.name)}` : "",
|
|
990
|
+
pickFirst(resume.workYear) ? `工作年限:${pickFirst(resume.workYear)}` : "",
|
|
991
|
+
pickFirst(firstEducation.degree, resume.degree) ? `最高学历:${pickFirst(firstEducation.degree, resume.degree)}` : ""
|
|
992
|
+
].filter(Boolean),
|
|
993
|
+
expectation: [
|
|
994
|
+
[pickFirst(resume.position), pickFirst(resume.positionCategory)].filter(Boolean).join(" / ")
|
|
995
|
+
].filter(Boolean),
|
|
996
|
+
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
997
|
+
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
998
|
+
highlights: tags
|
|
999
|
+
};
|
|
1000
|
+
const text = [
|
|
1001
|
+
formatNamedSection("基础信息", sections.base),
|
|
1002
|
+
formatNamedSection("求职期望", sections.expectation),
|
|
1003
|
+
formatNamedSection("工作经历", sections.work),
|
|
1004
|
+
formatNamedSection("教育经历", sections.education),
|
|
1005
|
+
formatNamedSection("亮点标签", sections.highlights)
|
|
1006
|
+
].filter(Boolean).join("\n\n");
|
|
1007
|
+
return {
|
|
1008
|
+
text,
|
|
1009
|
+
identity: {
|
|
1010
|
+
name: pickFirst(user.name) || null,
|
|
1011
|
+
title: pickFirst(resume.position) || null,
|
|
1012
|
+
current_position: pickFirst(firstWork.positionName, firstWork.position) || null,
|
|
1013
|
+
current_company: pickFirst(firstWork.company, firstWork.brandName, user.company) || null,
|
|
1014
|
+
school: pickFirst(firstEducation.school) || null,
|
|
1015
|
+
major: pickFirst(firstEducation.major) || null,
|
|
1016
|
+
degree: pickFirst(firstEducation.degree, firstEducation.degreeName, resume.degree) || parseDegree(text),
|
|
1017
|
+
years_experience: parseYearsExperience(pickFirst(resume.workYear)) ?? null,
|
|
1018
|
+
age: null,
|
|
1019
|
+
gender: null
|
|
1020
|
+
},
|
|
1021
|
+
tags,
|
|
1022
|
+
source_keys: {
|
|
1023
|
+
chat_history_resume: true,
|
|
1024
|
+
geek_detail_info: false,
|
|
1025
|
+
geek_detail: false,
|
|
1026
|
+
education_count: educationList.length,
|
|
1027
|
+
work_count: workList.length
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function extractBossProfileRecursively(payload = {}) {
|
|
1033
|
+
for (const object of collectObjects(payload)) {
|
|
1034
|
+
if (isBossGeekDetailShape(object)) {
|
|
1035
|
+
const profile = extractBossGeekDetailInfo({ geekDetailInfo: object });
|
|
1036
|
+
if (profile?.text || profile?.identity?.name) {
|
|
1037
|
+
return {
|
|
1038
|
+
...profile,
|
|
1039
|
+
source_keys: {
|
|
1040
|
+
...(profile.source_keys || {}),
|
|
1041
|
+
recursive_profile_match: true
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (isBossChatProfileShape(object)) {
|
|
1047
|
+
const profile = extractBossChatGeekInfo({ zpData: { data: object } });
|
|
1048
|
+
if (profile?.text || profile?.identity?.name) {
|
|
1049
|
+
return {
|
|
1050
|
+
...profile,
|
|
1051
|
+
source_keys: {
|
|
1052
|
+
...(profile.source_keys || {}),
|
|
1053
|
+
recursive_profile_match: true
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (isPlainObject(object.resume)) {
|
|
1059
|
+
const profile = extractBossChatHistoryResume({ zpData: { messages: [{ body: { resume: object.resume } }] } });
|
|
1060
|
+
if (profile?.text || profile?.identity?.name) {
|
|
1061
|
+
return {
|
|
1062
|
+
...profile,
|
|
1063
|
+
source_keys: {
|
|
1064
|
+
...(profile.source_keys || {}),
|
|
1065
|
+
recursive_profile_match: true
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function extractBossGeekDetailInfo(payload = {}) {
|
|
1075
|
+
const { sourceKey, detail } = resolveBossGeekDetail(payload);
|
|
1076
|
+
if (!detail || typeof detail !== "object") return null;
|
|
1077
|
+
|
|
1078
|
+
const base = detail.geekBaseInfo || detail.baseInfo || detail.base || {};
|
|
1079
|
+
const educationList = normalizeList(detail.geekEduExpList).length
|
|
1080
|
+
? normalizeList(detail.geekEduExpList)
|
|
1081
|
+
: normalizeList(detail.geekEducationList);
|
|
1082
|
+
const firstEducation = educationList[0] || detail.highestEduExp || {};
|
|
1083
|
+
const workList = normalizeList(detail.geekWorkExpList);
|
|
1084
|
+
const firstWork = workList[0] || {};
|
|
1085
|
+
const projectList = normalizeList(detail.geekProjExpList);
|
|
1086
|
+
const expectationList = normalizeList(detail.geekExpPosList).length
|
|
1087
|
+
? normalizeList(detail.geekExpPosList)
|
|
1088
|
+
: normalizeList(detail.geekExpectList);
|
|
1089
|
+
const expectationFallback = detail.showExpectPosition && typeof detail.showExpectPosition === "object"
|
|
1090
|
+
? [detail.showExpectPosition]
|
|
1091
|
+
: [];
|
|
1092
|
+
const normalizedExpectationList = expectationList.length ? expectationList : expectationFallback;
|
|
1093
|
+
const certifications = normalizeList(detail.geekCertificationList);
|
|
1094
|
+
const skillTags = [
|
|
1095
|
+
...normalizeTagList(detail.geekSkillList),
|
|
1096
|
+
...normalizeTagList(detail.skillList),
|
|
1097
|
+
...normalizeTagList(detail.blueGeekSkills),
|
|
1098
|
+
...normalizeTagList(base.userHighlightList),
|
|
1099
|
+
...normalizeTagList(base.userDescHighlightList),
|
|
1100
|
+
...normalizeTagList(base.userDescHighLightList),
|
|
1101
|
+
...normalizeTagList(detail.geekPersonalLabelList),
|
|
1102
|
+
...normalizeTagList(detail.professionalSkill)
|
|
1103
|
+
];
|
|
1104
|
+
const summaryParts = [
|
|
1105
|
+
pickFirst(detail.geekAdvantage),
|
|
1106
|
+
pickFirst(base.userDescription),
|
|
1107
|
+
pickFirst(base.userDesc),
|
|
1108
|
+
pickFirst(base.workEduDesc),
|
|
1109
|
+
pickFirst(detail.resumeSummary?.content, detail.resumeSummary?.text, detail.resumeSummary?.summary)
|
|
1110
|
+
].filter(Boolean);
|
|
1111
|
+
const sections = {
|
|
1112
|
+
base: [
|
|
1113
|
+
pickFirst(base.name, detail.geekName, detail.name) ? `姓名:${pickFirst(base.name, detail.geekName, detail.name)}` : "",
|
|
1114
|
+
normalizeGenderValue(base.gender) ? `性别:${normalizeGenderValue(base.gender)}` : "",
|
|
1115
|
+
pickFirst(base.ageDesc, base.age) ? `年龄:${pickFirst(base.ageDesc, base.age)}` : "",
|
|
1116
|
+
pickFirst(base.degreeCategory) ? `最高学历:${pickFirst(base.degreeCategory)}` : "",
|
|
1117
|
+
pickFirst(base.workYearDesc, base.workYearsDesc) ? `工作年限:${pickFirst(base.workYearDesc, base.workYearsDesc)}` : "",
|
|
1118
|
+
pickFirst(base.activeTimeDesc) ? `活跃状态:${pickFirst(base.activeTimeDesc)}` : "",
|
|
1119
|
+
pickFirst(base.applyStatusDesc, base.applyStatusContent) ? `求职状态:${pickFirst(base.applyStatusDesc, base.applyStatusContent)}` : ""
|
|
1120
|
+
].filter(Boolean),
|
|
1121
|
+
summary: summaryParts,
|
|
1122
|
+
expectations: normalizedExpectationList.map(formatExpectation).filter(Boolean),
|
|
1123
|
+
work_experience: workList.map(formatWorkExperience).filter(Boolean),
|
|
1124
|
+
project_experience: projectList.map(formatProjectExperience).filter(Boolean),
|
|
1125
|
+
education: educationList.map(formatEducation).filter(Boolean),
|
|
1126
|
+
certifications: certifications.map((item, index) => `${index + 1}. ${pickFirst(item.certName, item.name)}`).filter(Boolean),
|
|
1127
|
+
skills: unique(skillTags)
|
|
1128
|
+
};
|
|
1129
|
+
const text = [
|
|
1130
|
+
formatNamedSection("基础信息", sections.base),
|
|
1131
|
+
formatNamedSection("个人总结", sections.summary),
|
|
1132
|
+
formatNamedSection("求职期望", sections.expectations),
|
|
1133
|
+
formatNamedSection("工作经历", sections.work_experience),
|
|
1134
|
+
formatNamedSection("项目经历", sections.project_experience),
|
|
1135
|
+
formatNamedSection("教育经历", sections.education),
|
|
1136
|
+
formatNamedSection("证书", sections.certifications),
|
|
1137
|
+
formatNamedSection("技能/亮点", sections.skills)
|
|
1138
|
+
].filter(Boolean).join("\n\n");
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
identity: {
|
|
1142
|
+
name: pickFirst(base.name, detail.geekName, detail.name),
|
|
1143
|
+
current_position: pickFirst(firstWork.positionName, firstWork.positionTitle, firstWork.position),
|
|
1144
|
+
current_company: pickFirst(firstWork.formattedCompany, firstWork.company, firstWork.brandName),
|
|
1145
|
+
school: pickFirst(firstEducation.school, firstEducation.schoolName),
|
|
1146
|
+
major: pickFirst(firstEducation.major, firstEducation.majorName),
|
|
1147
|
+
degree: pickFirst(base.degreeCategory, firstEducation.degreeName, firstEducation.degree),
|
|
1148
|
+
years_experience: parseYearsExperience(pickFirst(base.workYearDesc, base.workYearsDesc)) ?? null,
|
|
1149
|
+
age: parseAge(pickFirst(base.ageDesc, base.age)) ?? null,
|
|
1150
|
+
gender: normalizeGenderValue(base.gender)
|
|
1151
|
+
},
|
|
1152
|
+
tags: unique([
|
|
1153
|
+
...sections.skills,
|
|
1154
|
+
...educationList.flatMap((item) => [
|
|
1155
|
+
...normalizeTagList(item.tags),
|
|
1156
|
+
...normalizeTagList(item.schoolTags)
|
|
1157
|
+
])
|
|
1158
|
+
]),
|
|
1159
|
+
sections,
|
|
1160
|
+
text,
|
|
1161
|
+
source_keys: {
|
|
1162
|
+
source_key: sourceKey,
|
|
1163
|
+
geek_detail_info: sourceKey === "geekDetailInfo",
|
|
1164
|
+
geek_detail: sourceKey === "geekDetail",
|
|
1165
|
+
work_count: workList.length,
|
|
1166
|
+
project_count: projectList.length,
|
|
1167
|
+
education_count: educationList.length,
|
|
1168
|
+
expectation_count: normalizedExpectationList.length,
|
|
1169
|
+
certification_count: certifications.length
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export function extractBossProfileFromNetworkBody(networkBody = {}) {
|
|
1175
|
+
const text = parseNetworkBodyText(networkBody);
|
|
1176
|
+
const parsedObjects = [
|
|
1177
|
+
tryParseJson(text),
|
|
1178
|
+
...tryParseEmbeddedJsonObjects(text)
|
|
1179
|
+
].filter((item) => item && typeof item === "object");
|
|
1180
|
+
if (!parsedObjects.length) {
|
|
1181
|
+
const htmlText = /<html|<body|<div|<section|<script/i.test(text) ? htmlToText(text) : "";
|
|
1182
|
+
if (htmlText && htmlText.length > 80) {
|
|
1183
|
+
const candidate = normalizeCandidateProfile({
|
|
1184
|
+
domain: "recommend",
|
|
1185
|
+
source: "network-html-fallback",
|
|
1186
|
+
text: htmlText
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
ok: true,
|
|
1190
|
+
url: networkBody.url || null,
|
|
1191
|
+
status: networkBody.status ?? null,
|
|
1192
|
+
mimeType: networkBody.mimeType || null,
|
|
1193
|
+
text_length: text.length,
|
|
1194
|
+
profile: {
|
|
1195
|
+
identity: candidate.identity,
|
|
1196
|
+
tags: candidate.tags,
|
|
1197
|
+
sections: { html_text: [htmlText] },
|
|
1198
|
+
text: htmlText,
|
|
1199
|
+
source_keys: {
|
|
1200
|
+
network_html_text: true,
|
|
1201
|
+
html_text_length: htmlText.length
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
error: "NETWORK_BODY_NOT_JSON",
|
|
1209
|
+
text_length: text.length
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
let profile = null;
|
|
1213
|
+
let parsed = parsedObjects[0];
|
|
1214
|
+
for (const candidateObject of parsedObjects) {
|
|
1215
|
+
profile = extractBossGeekDetailInfo(candidateObject)
|
|
1216
|
+
|| extractBossChatGeekInfo(candidateObject)
|
|
1217
|
+
|| extractBossChatHistoryResume(candidateObject)
|
|
1218
|
+
|| extractBossProfileRecursively(candidateObject);
|
|
1219
|
+
if (profile) {
|
|
1220
|
+
parsed = candidateObject;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (!profile) {
|
|
1225
|
+
const encryptedPayload = parsedObjects.find((item) => (
|
|
1226
|
+
normalizeText(item?.zpData?.encryptGeekDetailInfo || item?.encryptGeekDetailInfo || "")
|
|
1227
|
+
));
|
|
1228
|
+
return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
error: encryptedPayload ? "BOSS_GEEK_DETAIL_INFO_ENCRYPTED" : "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
|
|
1231
|
+
text_length: text.length,
|
|
1232
|
+
parsed_object_count: parsedObjects.length,
|
|
1233
|
+
top_level_keys: Object.keys(parsed || {}).slice(0, 30),
|
|
1234
|
+
zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50),
|
|
1235
|
+
data_keys: Object.keys(parsed?.data || parsed?.zpData?.data || {}).slice(0, 50),
|
|
1236
|
+
encrypted_resume: Boolean(encryptedPayload),
|
|
1237
|
+
encrypted_resume_length: normalizeText(encryptedPayload?.zpData?.encryptGeekDetailInfo || encryptedPayload?.encryptGeekDetailInfo || "").length
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
ok: true,
|
|
1242
|
+
url: networkBody.url || null,
|
|
1243
|
+
status: networkBody.status ?? null,
|
|
1244
|
+
mimeType: networkBody.mimeType || null,
|
|
1245
|
+
text_length: text.length,
|
|
1246
|
+
profile
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function mergeCandidateProfiles(...profiles) {
|
|
1251
|
+
const base = {};
|
|
1252
|
+
for (const profile of profiles) {
|
|
1253
|
+
if (!profile) continue;
|
|
1254
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
1255
|
+
if (value == null || value === "") continue;
|
|
1256
|
+
if (base[key] == null || base[key] === "") {
|
|
1257
|
+
base[key] = value;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return base;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export function buildScreeningCandidateFromDetail({
|
|
1265
|
+
cardCandidate,
|
|
1266
|
+
detailText = "",
|
|
1267
|
+
networkBodies = [],
|
|
1268
|
+
domain = "recommend",
|
|
1269
|
+
source = "live-cdp-detail",
|
|
1270
|
+
metadata = {}
|
|
1271
|
+
} = {}) {
|
|
1272
|
+
const parsedNetworkProfiles = networkBodies.map(extractBossProfileFromNetworkBody);
|
|
1273
|
+
const successfulProfiles = parsedNetworkProfiles.filter((item) => item.ok).map((item) => item.profile);
|
|
1274
|
+
const networkText = successfulProfiles.map((profile) => profile.text).filter(Boolean).join("\n\n");
|
|
1275
|
+
const networkIdentity = mergeCandidateProfiles(
|
|
1276
|
+
...successfulProfiles.map((profile) => profile.identity)
|
|
1277
|
+
);
|
|
1278
|
+
const networkTags = unique(successfulProfiles.flatMap((profile) => profile.tags || []));
|
|
1279
|
+
const combinedIdentity = mergeCandidateProfiles(
|
|
1280
|
+
networkIdentity,
|
|
1281
|
+
cardCandidate?.identity
|
|
1282
|
+
);
|
|
1283
|
+
const candidate = normalizeCandidateProfile({
|
|
1284
|
+
domain,
|
|
1285
|
+
source,
|
|
1286
|
+
id: cardCandidate?.id,
|
|
1287
|
+
href: cardCandidate?.links?.href,
|
|
1288
|
+
text: [
|
|
1289
|
+
networkText,
|
|
1290
|
+
detailText,
|
|
1291
|
+
cardCandidate?.text?.raw
|
|
1292
|
+
].filter(Boolean).join("\n\n"),
|
|
1293
|
+
attributes: cardCandidate?.metadata?.attributes || {},
|
|
1294
|
+
identity: combinedIdentity,
|
|
1295
|
+
tags: unique([
|
|
1296
|
+
...(cardCandidate?.tags || []),
|
|
1297
|
+
...networkTags
|
|
1298
|
+
]),
|
|
1299
|
+
metadata: {
|
|
1300
|
+
...metadata,
|
|
1301
|
+
card_candidate_source: cardCandidate?.source || null,
|
|
1302
|
+
network_profile_count: successfulProfiles.length,
|
|
1303
|
+
network_profiles: parsedNetworkProfiles.map((item) => ({
|
|
1304
|
+
ok: item.ok,
|
|
1305
|
+
url: item.url,
|
|
1306
|
+
status: item.status,
|
|
1307
|
+
error: item.error,
|
|
1308
|
+
text_length: item.text_length,
|
|
1309
|
+
source_keys: item.profile?.source_keys || null
|
|
1310
|
+
}))
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
return {
|
|
1314
|
+
candidate,
|
|
1315
|
+
parsed_network_profiles: parsedNetworkProfiles
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
export function normalizeCandidateProfile(input = {}) {
|
|
1320
|
+
const domain = normalizeDomain(input.domain || "recommend");
|
|
1321
|
+
const rawText = String(input.text || input.raw_text || input.resume_text || "")
|
|
1322
|
+
.split(/\r?\n/)
|
|
1323
|
+
.map((line) => normalizeText(line))
|
|
1324
|
+
.filter(Boolean)
|
|
1325
|
+
.join("\n");
|
|
1326
|
+
const lines = rawText.split(/\r?\n/).map(normalizeText).filter(Boolean);
|
|
1327
|
+
const attrs = {
|
|
1328
|
+
...(input.attributes || {}),
|
|
1329
|
+
...(input.metadata?.attributes || {})
|
|
1330
|
+
};
|
|
1331
|
+
const sourceId = normalizeText(
|
|
1332
|
+
input.id
|
|
1333
|
+
|| attrs["data-geek"]
|
|
1334
|
+
|| attrs["data-geekid"]
|
|
1335
|
+
|| attrs["data-expect"]
|
|
1336
|
+
|| attrs["data-uid"]
|
|
1337
|
+
|| attrs["data-securityid"]
|
|
1338
|
+
|| attrs.encryptgeekid
|
|
1339
|
+
|| attrs["data-lid"]
|
|
1340
|
+
|| attrs["data-jid"]
|
|
1341
|
+
|| attrs["data-itemid"]
|
|
1342
|
+
|| attrs.geekid
|
|
1343
|
+
|| attrs.expect
|
|
1344
|
+
|| attrs.uid
|
|
1345
|
+
|| attrs.securityid
|
|
1346
|
+
|| attrs.jid
|
|
1347
|
+
|| attrs.lid
|
|
1348
|
+
|| attrs.href
|
|
1349
|
+
|| ""
|
|
1350
|
+
) || null;
|
|
1351
|
+
const explicitName = cleanInferredNameLine(input.identity?.name || input.name || "");
|
|
1352
|
+
const inferredName = explicitName || firstUsefulLine(lines) || null;
|
|
1353
|
+
const fullText = collectTextParts({
|
|
1354
|
+
...input,
|
|
1355
|
+
text: rawText,
|
|
1356
|
+
raw_text: rawText,
|
|
1357
|
+
identity: {
|
|
1358
|
+
...(input.identity || {}),
|
|
1359
|
+
name: inferredName
|
|
1360
|
+
}
|
|
1361
|
+
}).join("\n");
|
|
1362
|
+
const degree = input.identity?.degree || input.degree || parseDegree(fullText);
|
|
1363
|
+
|
|
1364
|
+
return {
|
|
1365
|
+
schema_version: 1,
|
|
1366
|
+
domain,
|
|
1367
|
+
source: normalizeText(input.source || "unknown") || "unknown",
|
|
1368
|
+
id: sourceId,
|
|
1369
|
+
identity: {
|
|
1370
|
+
name: inferredName,
|
|
1371
|
+
title: normalizeText(input.identity?.title || input.title || "") || null,
|
|
1372
|
+
current_position: normalizeText(input.identity?.current_position || input.current_position || "") || null,
|
|
1373
|
+
current_company: normalizeText(input.identity?.current_company || input.current_company || "") || null,
|
|
1374
|
+
school: normalizeText(input.identity?.school || input.school || "") || null,
|
|
1375
|
+
major: normalizeText(input.identity?.major || input.major || "") || null,
|
|
1376
|
+
degree,
|
|
1377
|
+
years_experience: input.identity?.years_experience ?? input.years_experience ?? parseYearsExperience(fullText),
|
|
1378
|
+
age: input.identity?.age ?? input.age ?? parseAge(fullText),
|
|
1379
|
+
gender: input.identity?.gender || input.gender || parseGender(fullText)
|
|
1380
|
+
},
|
|
1381
|
+
tags: unique(input.tags || []),
|
|
1382
|
+
text: {
|
|
1383
|
+
summary: lines.slice(0, 8).join("\n"),
|
|
1384
|
+
raw: rawText
|
|
1385
|
+
},
|
|
1386
|
+
links: {
|
|
1387
|
+
href: normalizeText(input.href || attrs.href || "") || null
|
|
1388
|
+
},
|
|
1389
|
+
metadata: {
|
|
1390
|
+
...(input.metadata || {}),
|
|
1391
|
+
attributes: attrs,
|
|
1392
|
+
normalized_at: input.normalized_at || nowIso()
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
export function normalizeCandidateFromHtml({
|
|
1398
|
+
domain = "recommend",
|
|
1399
|
+
source = "dom",
|
|
1400
|
+
html,
|
|
1401
|
+
attributes = {},
|
|
1402
|
+
metadata = {}
|
|
1403
|
+
} = {}) {
|
|
1404
|
+
const parsedAttributes = parseHtmlAttributes(html);
|
|
1405
|
+
return normalizeCandidateProfile({
|
|
1406
|
+
domain,
|
|
1407
|
+
source,
|
|
1408
|
+
text: htmlToText(html),
|
|
1409
|
+
attributes: {
|
|
1410
|
+
...parsedAttributes,
|
|
1411
|
+
...attributes
|
|
1412
|
+
},
|
|
1413
|
+
metadata: {
|
|
1414
|
+
...metadata,
|
|
1415
|
+
html_length: String(html || "").length
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function normalizeKeywordList(value) {
|
|
1421
|
+
if (!value) return [];
|
|
1422
|
+
if (Array.isArray(value)) return unique(value);
|
|
1423
|
+
return unique(String(value).split(/[,\n,、|/]/));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function keywordMatches(text, keywords) {
|
|
1427
|
+
const haystack = compact(text);
|
|
1428
|
+
return keywords.filter((keyword) => haystack.includes(compact(keyword)));
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function degreeAtLeast(candidateDegree, minimumDegree) {
|
|
1432
|
+
if (!minimumDegree) return true;
|
|
1433
|
+
const candidateRank = DEGREE_RANK[candidateDegree] || 0;
|
|
1434
|
+
const minimumRank = DEGREE_RANK[minimumDegree] || 0;
|
|
1435
|
+
return candidateRank >= minimumRank;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
export function screenCandidate(candidateInput, criteria = {}) {
|
|
1439
|
+
const candidate = candidateInput?.schema_version
|
|
1440
|
+
? candidateInput
|
|
1441
|
+
: normalizeCandidateProfile(candidateInput);
|
|
1442
|
+
const text = [
|
|
1443
|
+
candidate.text?.raw,
|
|
1444
|
+
candidate.text?.summary,
|
|
1445
|
+
...Object.values(candidate.identity || {}).map((value) => value == null ? "" : String(value)),
|
|
1446
|
+
...(candidate.tags || [])
|
|
1447
|
+
].join("\n");
|
|
1448
|
+
const requiredKeywords = normalizeKeywordList(criteria.required_keywords || criteria.requiredKeywords);
|
|
1449
|
+
const preferredKeywords = normalizeKeywordList(criteria.preferred_keywords || criteria.preferredKeywords || criteria.criteria);
|
|
1450
|
+
const excludedKeywords = normalizeKeywordList(criteria.excluded_keywords || criteria.excludedKeywords);
|
|
1451
|
+
const matchedRequired = keywordMatches(text, requiredKeywords);
|
|
1452
|
+
const matchedPreferred = keywordMatches(text, preferredKeywords);
|
|
1453
|
+
const matchedExcluded = keywordMatches(text, excludedKeywords);
|
|
1454
|
+
const reasons = [];
|
|
1455
|
+
let score = 0;
|
|
1456
|
+
|
|
1457
|
+
if (requiredKeywords.length > 0) {
|
|
1458
|
+
if (matchedRequired.length === requiredKeywords.length) {
|
|
1459
|
+
score += 60;
|
|
1460
|
+
reasons.push(`Matched all required keywords: ${matchedRequired.join(", ")}`);
|
|
1461
|
+
} else {
|
|
1462
|
+
const missing = requiredKeywords.filter((keyword) => !matchedRequired.includes(keyword));
|
|
1463
|
+
reasons.push(`Missing required keywords: ${missing.join(", ")}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (preferredKeywords.length > 0) {
|
|
1468
|
+
score += Math.round((matchedPreferred.length / preferredKeywords.length) * 30);
|
|
1469
|
+
if (matchedPreferred.length) {
|
|
1470
|
+
reasons.push(`Matched preferred keywords: ${matchedPreferred.join(", ")}`);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (matchedExcluded.length > 0) {
|
|
1475
|
+
score -= 80;
|
|
1476
|
+
reasons.push(`Matched excluded keywords: ${matchedExcluded.join(", ")}`);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const minimumDegree = criteria.minimum_degree || criteria.minimumDegree || null;
|
|
1480
|
+
const degreeOk = degreeAtLeast(candidate.identity?.degree, minimumDegree);
|
|
1481
|
+
if (minimumDegree) {
|
|
1482
|
+
if (degreeOk) {
|
|
1483
|
+
score += 10;
|
|
1484
|
+
reasons.push(`Degree meets minimum: ${candidate.identity?.degree || "unknown"} >= ${minimumDegree}`);
|
|
1485
|
+
} else {
|
|
1486
|
+
reasons.push(`Degree below or unknown for minimum: ${minimumDegree}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const hasCriteria = (
|
|
1491
|
+
requiredKeywords.length > 0
|
|
1492
|
+
|| preferredKeywords.length > 0
|
|
1493
|
+
|| excludedKeywords.length > 0
|
|
1494
|
+
|| Boolean(minimumDegree)
|
|
1495
|
+
);
|
|
1496
|
+
const hasRequired = requiredKeywords.length === 0 || matchedRequired.length === requiredKeywords.length;
|
|
1497
|
+
const passed = hasCriteria && hasRequired && degreeOk && matchedExcluded.length === 0;
|
|
1498
|
+
const boundedScore = Math.max(0, Math.min(100, hasCriteria ? score : 0));
|
|
1499
|
+
|
|
1500
|
+
return {
|
|
1501
|
+
schema_version: 1,
|
|
1502
|
+
status: passed ? "pass" : "review",
|
|
1503
|
+
passed,
|
|
1504
|
+
score: boundedScore,
|
|
1505
|
+
reasons: reasons.length ? reasons : ["No explicit screening criteria supplied; candidate normalized for review."],
|
|
1506
|
+
matched: {
|
|
1507
|
+
required_keywords: matchedRequired,
|
|
1508
|
+
preferred_keywords: matchedPreferred,
|
|
1509
|
+
excluded_keywords: matchedExcluded
|
|
1510
|
+
},
|
|
1511
|
+
candidate: {
|
|
1512
|
+
domain: candidate.domain,
|
|
1513
|
+
source: candidate.source,
|
|
1514
|
+
id: candidate.id,
|
|
1515
|
+
identity: candidate.identity
|
|
1516
|
+
},
|
|
1517
|
+
screened_at: nowIso()
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export function compactScreeningLlmResult(llmResult) {
|
|
1522
|
+
if (!llmResult) return null;
|
|
1523
|
+
return {
|
|
1524
|
+
ok: Boolean(llmResult.ok),
|
|
1525
|
+
provider: llmResult.provider || null,
|
|
1526
|
+
passed: llmResult.passed,
|
|
1527
|
+
review_required: typeof llmResult.review_required === "boolean" ? llmResult.review_required : null,
|
|
1528
|
+
cot: llmResult.cot || llmResult.decision_cot || "",
|
|
1529
|
+
reasoning_content: llmResult.reasoning_content || "",
|
|
1530
|
+
raw_model_output: llmResult.raw_model_output || "",
|
|
1531
|
+
evidence_count: Array.isArray(llmResult.evidence) ? llmResult.evidence.length : 0,
|
|
1532
|
+
usage: llmResult.usage || null,
|
|
1533
|
+
finish_reason: llmResult.finish_reason || null,
|
|
1534
|
+
image_input_count: llmResult.image_input_count || 0,
|
|
1535
|
+
attempt_count: llmResult.attempt_count || 0,
|
|
1536
|
+
fallback_count: llmResult.fallback_count || 0,
|
|
1537
|
+
llm_model_failures: Array.isArray(llmResult.llm_model_failures) ? llmResult.llm_model_failures : [],
|
|
1538
|
+
screening_strategy: llmResult.screening_strategy || "",
|
|
1539
|
+
fast_thinking_level: llmResult.fast_thinking_level || "",
|
|
1540
|
+
verify_thinking_level: llmResult.verify_thinking_level || "",
|
|
1541
|
+
verified: typeof llmResult.verified === "boolean" ? llmResult.verified : null,
|
|
1542
|
+
verification_reason: llmResult.verification_reason || "",
|
|
1543
|
+
decision_source: llmResult.decision_source || "",
|
|
1544
|
+
fast_result: llmResult.fast_result || null,
|
|
1545
|
+
verify_result: llmResult.verify_result || null,
|
|
1546
|
+
error_code: llmResult.error_code || null,
|
|
1547
|
+
fatal: Boolean(llmResult.fatal),
|
|
1548
|
+
fatal_reason: llmResult.fatal_reason || "",
|
|
1549
|
+
error: llmResult.error || null,
|
|
1550
|
+
screened_at: llmResult.screened_at || null
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
export function llmResultToScreening(llmResult, candidate) {
|
|
1555
|
+
return {
|
|
1556
|
+
status: llmResult?.passed ? "pass" : "fail",
|
|
1557
|
+
passed: Boolean(llmResult?.passed),
|
|
1558
|
+
score: llmResult?.passed ? 100 : 0,
|
|
1559
|
+
reasons: llmResult?.error ? ["llm_invalid_response"] : [],
|
|
1560
|
+
candidate
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
export function isRecoverableLlmScreeningError(error) {
|
|
1565
|
+
return /(?:LLM response missing boolean passed decision|LLM response missing brief summary|LLM response missing boolean review_required decision|LLM response was not valid JSON)/i
|
|
1566
|
+
.test(String(error?.message || error || ""));
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
export function createFailedLlmScreeningResult(error) {
|
|
1570
|
+
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1571
|
+
return {
|
|
1572
|
+
ok: false,
|
|
1573
|
+
passed: false,
|
|
1574
|
+
reason: "",
|
|
1575
|
+
evidence: [],
|
|
1576
|
+
cot: "",
|
|
1577
|
+
decision_cot: "",
|
|
1578
|
+
reasoning_content: "",
|
|
1579
|
+
raw_model_output: "",
|
|
1580
|
+
image_input_count: Number(error?.image_input_count) || 0,
|
|
1581
|
+
image_inputs: Array.isArray(error?.image_inputs) ? error.image_inputs : [],
|
|
1582
|
+
attempt_count: Number(error?.llm_attempt_count) || 0,
|
|
1583
|
+
fallback_count: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures.length : 0,
|
|
1584
|
+
llm_model_failures: Array.isArray(error?.llm_model_failures) ? error.llm_model_failures : [],
|
|
1585
|
+
error_code: fatalClassification?.code || error?.code || null,
|
|
1586
|
+
fatal: Boolean(fatalClassification),
|
|
1587
|
+
fatal_reason: fatalClassification?.reason || "",
|
|
1588
|
+
error: error?.message || String(error || "unknown"),
|
|
1589
|
+
screened_at: nowIso()
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
export function buildScreeningLlmMessages({
|
|
1594
|
+
candidate,
|
|
1595
|
+
criteria,
|
|
1596
|
+
thinkingLevel = "low",
|
|
1597
|
+
requireReviewDecision = false,
|
|
1598
|
+
imageEvidence = null,
|
|
1599
|
+
imagePaths = [],
|
|
1600
|
+
imageInputs = null,
|
|
1601
|
+
maxImages = 8,
|
|
1602
|
+
imageDetail = "high"
|
|
1603
|
+
}) {
|
|
1604
|
+
const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
|
|
1605
|
+
const safeText = String(candidate?.text?.raw || candidate?.text || "");
|
|
1606
|
+
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1607
|
+
const requestReviewDecision = Boolean(requireReviewDecision);
|
|
1608
|
+
const requestSummary = requestReviewDecision || normalizedThinkingLevel === "current";
|
|
1609
|
+
const images = Array.isArray(imageInputs)
|
|
1610
|
+
? imageInputs
|
|
1611
|
+
: buildScreeningLlmImageInputs({
|
|
1612
|
+
imageEvidence,
|
|
1613
|
+
imagePaths,
|
|
1614
|
+
maxImages,
|
|
1615
|
+
detail: imageDetail
|
|
1616
|
+
});
|
|
1617
|
+
const outputShape = requestReviewDecision
|
|
1618
|
+
? "最后只返回 JSON,格式为:"
|
|
1619
|
+
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\", \"review_required\": true/false}"
|
|
1620
|
+
: requestSummary
|
|
1621
|
+
? "7) 只返回 JSON,格式为:"
|
|
1622
|
+
+ "{\"passed\": true/false, \"summary\": \"少于100个中文词的筛选总结\"}"
|
|
1623
|
+
: "7) 只返回 JSON,格式为:"
|
|
1624
|
+
+ "{\"passed\": true/false}";
|
|
1625
|
+
const failFastInstructions = [
|
|
1626
|
+
"3) 按筛选标准原文顺序拆解并检查硬性淘汰项;一旦某项可确定不满足,必须立即返回 passed=false,不要继续评估后续条件。",
|
|
1627
|
+
"4) 若筛选标准规定证据不足、业务线无法判断、任期无法判断、学校无法判断等情况不通过,这类缺失证据就是可确定不满足。",
|
|
1628
|
+
"5) 只有当前淘汰项本身存在截图不清、信息冲突、或可能被后续可见简历内容澄清时,才继续阅读以核实该项。"
|
|
1629
|
+
].join("\n");
|
|
1630
|
+
const fastReviewInstructions = requestReviewDecision
|
|
1631
|
+
? [
|
|
1632
|
+
"7) review_required 必须是布尔值;当证据缺失、证据冲突、截图/文本不完整或不清晰、结论接近规则边界、或你依赖假设时设为 true;若结论明确且无需更深推理核验,设为 false。",
|
|
1633
|
+
"8) 只有当某个硬性不通过条件有直接、明确、无可见反向证据的简历证据时,才可返回 review_required=false。",
|
|
1634
|
+
"9) 如果简历中存在任何可能满足当前被否定条件的反向证据、边界证据或可计入经历,即使你倾向 passed=false,也必须 review_required=true。",
|
|
1635
|
+
"10) 反向证据包括但不限于:可能合格的学校/学历、可能合格的产品或行业、可能合格的职责或指标、可能合格的海外/语言证据、或需要精确计算的任期/年限证据。",
|
|
1636
|
+
"11) 不要因为候选人其他经历不匹配,就忽略某一段可能匹配的经历;只要任一可见经历可能满足被否定条件,就必须交给复核。",
|
|
1637
|
+
"12) 当 review_required=false 时,summary 必须点明确切的决定性硬性失败,并说明没有可见反向证据或边界证据。"
|
|
1638
|
+
].join("\n")
|
|
1639
|
+
: "";
|
|
1640
|
+
const prompt =
|
|
1641
|
+
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1642
|
+
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
1643
|
+
+ (images.length
|
|
1644
|
+
? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。若截图是拼接长图,请按图内从上到下顺序阅读;若已出现明确硬性淘汰项,可停止后续评估。\n\n`
|
|
1645
|
+
: "")
|
|
1646
|
+
+ "要求:\n"
|
|
1647
|
+
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
1648
|
+
+ "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
|
|
1649
|
+
+ failFastInstructions + "\n"
|
|
1650
|
+
+ (requestSummary
|
|
1651
|
+
? "6) summary 必须为少于100个中文词的简短筛选总结,可包含核心依据和主要风险;不要输出推理过程。\n"
|
|
1652
|
+
: "6) 不要输出评估原因、证据列表、解释或额外文字。\n")
|
|
1653
|
+
+ (fastReviewInstructions ? `${fastReviewInstructions}\n` : "")
|
|
1654
|
+
+ outputShape;
|
|
1655
|
+
const userContent = images.length
|
|
1656
|
+
? [
|
|
1657
|
+
{ type: "text", text: prompt },
|
|
1658
|
+
...images.map((image) => ({
|
|
1659
|
+
type: "image_url",
|
|
1660
|
+
image_url: image.image_url
|
|
1661
|
+
}))
|
|
1662
|
+
]
|
|
1663
|
+
: prompt;
|
|
1664
|
+
return [
|
|
1665
|
+
{
|
|
1666
|
+
role: "system",
|
|
1667
|
+
content:
|
|
1668
|
+
"你是一位严谨的招聘筛选助手。必须按筛选标准顺序严格阅读和判断,严禁编造不存在的候选人经历;一旦确定命中硬性淘汰项,可立即给出最终不通过结论。"
|
|
1669
|
+
+ (requestReviewDecision
|
|
1670
|
+
? "只能返回严格 JSON。必须包含 passed、summary 和 review_required;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程;review_required 只能是 true 或 false。只有在硬性失败直接明确且无可见反向证据时,review_required 才能为 false。"
|
|
1671
|
+
: requestSummary
|
|
1672
|
+
? "只能返回严格 JSON。必须包含 passed 和 summary;summary 用中文,少于100个词,只概括筛选结论、核心依据和主要风险,不要输出推理过程。"
|
|
1673
|
+
: "只能返回严格 JSON,不要输出原因、证据或额外文字。")
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
role: "user",
|
|
1677
|
+
content: userContent
|
|
1678
|
+
}
|
|
1679
|
+
];
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function normalizeLlmMaxRetries(value) {
|
|
1683
|
+
if (value == null || value === "") return 1;
|
|
1684
|
+
const parsed = Number(value);
|
|
1685
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 1;
|
|
1686
|
+
return Math.min(3, Math.floor(parsed));
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function isRetryableLlmRequestError(error) {
|
|
1690
|
+
const status = Number(error?.status);
|
|
1691
|
+
if ([408, 409, 425, 429].includes(status) || status >= 500) return true;
|
|
1692
|
+
return /(?:aborted|abort|timeout|timed out|fetch failed|socket|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN)/i
|
|
1693
|
+
.test(String(error?.message || error || ""));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function sleepMs(ms) {
|
|
1697
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async function callScreeningLlmWithProvider({
|
|
1701
|
+
candidate,
|
|
1702
|
+
criteria,
|
|
1703
|
+
config = {},
|
|
1704
|
+
timeoutMs = 60000,
|
|
1705
|
+
requireReviewDecision = false,
|
|
1706
|
+
imageEvidence = null,
|
|
1707
|
+
imagePaths = [],
|
|
1708
|
+
maxImages = 8,
|
|
1709
|
+
imageDetail = "high"
|
|
1710
|
+
} = {}) {
|
|
1711
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
1712
|
+
const apiKey = normalizeText(config.apiKey);
|
|
1713
|
+
const model = normalizeText(config.model);
|
|
1714
|
+
if (!baseUrl || !apiKey || !model) {
|
|
1715
|
+
throw new Error("Missing LLM config fields: baseUrl/apiKey/model");
|
|
1716
|
+
}
|
|
1717
|
+
const imageInputs = buildScreeningLlmImageInputs({
|
|
1718
|
+
imageEvidence,
|
|
1719
|
+
imagePaths,
|
|
1720
|
+
maxImages: config.llmImageLimit || config.imageLimit || maxImages,
|
|
1721
|
+
detail: config.llmImageDetail || config.imageDetail || imageDetail
|
|
1722
|
+
});
|
|
1723
|
+
if (!candidate?.text?.raw && !candidate?.text && !imageInputs.length) {
|
|
1724
|
+
throw new Error("Candidate text and image evidence are empty");
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
const thinkingLevel = config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort || "low";
|
|
1728
|
+
const outputTokenBudget = resolveLlmOutputTokenBudget(config, thinkingLevel, { requireReviewDecision });
|
|
1729
|
+
const payload = {
|
|
1730
|
+
model,
|
|
1731
|
+
temperature: parseFiniteNumber(config.temperature, 0.1),
|
|
1732
|
+
max_tokens: outputTokenBudget,
|
|
1733
|
+
messages: buildScreeningLlmMessages({
|
|
1734
|
+
candidate,
|
|
1735
|
+
criteria,
|
|
1736
|
+
thinkingLevel,
|
|
1737
|
+
requireReviewDecision,
|
|
1738
|
+
imageInputs
|
|
1739
|
+
})
|
|
1740
|
+
};
|
|
1741
|
+
const topP = parseFiniteNumber(config.topP ?? config.top_p, null);
|
|
1742
|
+
if (topP !== null) payload.top_p = topP;
|
|
1743
|
+
const maxCompletionTokens = parsePositiveNumber(
|
|
1744
|
+
config.llmMaxCompletionTokens ?? config.maxCompletionTokens,
|
|
1745
|
+
null
|
|
1746
|
+
);
|
|
1747
|
+
if (maxCompletionTokens !== null) {
|
|
1748
|
+
payload.max_completion_tokens = Math.max(1, Math.floor(maxCompletionTokens));
|
|
1749
|
+
}
|
|
1750
|
+
applyChatCompletionThinking(payload, {
|
|
1751
|
+
baseUrl,
|
|
1752
|
+
model,
|
|
1753
|
+
thinkingLevel
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
const effectiveTimeoutMs = parsePositiveNumber(config.llmTimeoutMs ?? config.timeoutMs, timeoutMs) || timeoutMs;
|
|
1757
|
+
const maxRetries = normalizeLlmMaxRetries(config.llmMaxRetries ?? config.maxRetries);
|
|
1758
|
+
const maxAttempts = maxRetries + 1;
|
|
1759
|
+
let lastError = null;
|
|
1760
|
+
let attempt = 0;
|
|
1761
|
+
let fatalProviderAttempts = 0;
|
|
1762
|
+
while (true) {
|
|
1763
|
+
attempt += 1;
|
|
1764
|
+
const controller = new AbortController();
|
|
1765
|
+
const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs);
|
|
1766
|
+
try {
|
|
1767
|
+
const headers = {
|
|
1768
|
+
"Content-Type": "application/json",
|
|
1769
|
+
Authorization: `Bearer ${apiKey}`
|
|
1770
|
+
};
|
|
1771
|
+
if (config.openaiOrganization) headers["OpenAI-Organization"] = config.openaiOrganization;
|
|
1772
|
+
if (config.openaiProject) headers["OpenAI-Project"] = config.openaiProject;
|
|
1773
|
+
|
|
1774
|
+
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
1775
|
+
method: "POST",
|
|
1776
|
+
headers,
|
|
1777
|
+
body: JSON.stringify(payload),
|
|
1778
|
+
signal: controller.signal
|
|
1779
|
+
});
|
|
1780
|
+
const responseText = await response.text();
|
|
1781
|
+
if (!response.ok) {
|
|
1782
|
+
const error = new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1783
|
+
error.status = response.status;
|
|
1784
|
+
const providerError = tryParseJson(responseText)?.error || {};
|
|
1785
|
+
error.provider_error_code = providerError.code || null;
|
|
1786
|
+
error.provider_error_type = providerError.type || null;
|
|
1787
|
+
error.provider_error_message = providerError.message || null;
|
|
1788
|
+
throw error;
|
|
1789
|
+
}
|
|
1790
|
+
const json = tryParseJson(responseText);
|
|
1791
|
+
if (!json) {
|
|
1792
|
+
throw new Error("LLM response was not valid JSON");
|
|
1793
|
+
}
|
|
1794
|
+
const choice = json?.choices?.[0] || {};
|
|
1795
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
1796
|
+
const reasoningContent = collectLlmReasoningText(choice);
|
|
1797
|
+
const parsed = tryExtractJsonObject(content) || tryExtractJsonObject(reasoningContent);
|
|
1798
|
+
const passed = parsePassedDecision(parsed?.passed);
|
|
1799
|
+
if (passed === null) {
|
|
1800
|
+
throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
|
|
1801
|
+
}
|
|
1802
|
+
const normalizedThinkingLevel = normalizeLlmThinkingLevel(thinkingLevel) || "low";
|
|
1803
|
+
const summary = normalizeBlockText(parsed?.summary || parsed?.screen_summary || parsed?.brief_summary);
|
|
1804
|
+
if ((normalizedThinkingLevel === "current" || requireReviewDecision) && !summary) {
|
|
1805
|
+
throw new Error(`LLM response missing brief summary for current thinking level or fast-first strategy: ${content.slice(0, 240)}`);
|
|
1806
|
+
}
|
|
1807
|
+
const reviewRequired = requireReviewDecision
|
|
1808
|
+
? parsePassedDecision(parsed?.review_required ?? parsed?.reviewRequired ?? parsed?.needs_review ?? parsed?.needsReview)
|
|
1809
|
+
: null;
|
|
1810
|
+
if (requireReviewDecision && reviewRequired === null) {
|
|
1811
|
+
throw new Error(`LLM response missing boolean review_required decision: ${content.slice(0, 240)}`);
|
|
1812
|
+
}
|
|
1813
|
+
const evidence = Array.isArray(parsed?.evidence)
|
|
1814
|
+
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1815
|
+
: [];
|
|
1816
|
+
const decisionCot = (normalizedThinkingLevel === "current" || requireReviewDecision)
|
|
1817
|
+
? summary
|
|
1818
|
+
: (firstReasoningText([
|
|
1819
|
+
parsed?.cot,
|
|
1820
|
+
parsed?.decision_cot,
|
|
1821
|
+
parsed?.reasoning,
|
|
1822
|
+
parsed?.chain_of_thought,
|
|
1823
|
+
reasoningContent
|
|
1824
|
+
].map(normalizeBlockText).filter(Boolean)) || reasoningContent);
|
|
1825
|
+
const providerName = normalizeText(config.llmProviderName || config.name || config.label || config.id);
|
|
1826
|
+
const providerIndex = Number.isFinite(Number(config.llmProviderIndex)) ? Number(config.llmProviderIndex) : 0;
|
|
1827
|
+
const providerCount = Number.isFinite(Number(config.llmProviderCount)) ? Number(config.llmProviderCount) : 1;
|
|
1828
|
+
const result = {
|
|
1829
|
+
ok: true,
|
|
1830
|
+
provider: {
|
|
1831
|
+
baseUrl: redactBaseUrl(baseUrl),
|
|
1832
|
+
model,
|
|
1833
|
+
name: providerName || null,
|
|
1834
|
+
index: providerIndex + 1,
|
|
1835
|
+
total: providerCount,
|
|
1836
|
+
thinking_level: normalizeLlmThinkingLevel(thinkingLevel) || "low",
|
|
1837
|
+
thinking: payload.thinking || null,
|
|
1838
|
+
reasoning_effort: payload.reasoning_effort || null,
|
|
1839
|
+
max_tokens: payload.max_tokens,
|
|
1840
|
+
max_completion_tokens: payload.max_completion_tokens || null
|
|
1841
|
+
},
|
|
1842
|
+
passed,
|
|
1843
|
+
review_required: reviewRequired,
|
|
1844
|
+
reason: "",
|
|
1845
|
+
evidence,
|
|
1846
|
+
cot: decisionCot,
|
|
1847
|
+
decision_cot: decisionCot,
|
|
1848
|
+
reasoning_content: reasoningContent,
|
|
1849
|
+
raw_model_output: content,
|
|
1850
|
+
usage: json.usage || null,
|
|
1851
|
+
finish_reason: choice.finish_reason || null,
|
|
1852
|
+
raw_content_length: content.length,
|
|
1853
|
+
image_input_count: imageInputs.length,
|
|
1854
|
+
image_inputs: summarizeLlmImageInputs(imageInputs),
|
|
1855
|
+
attempt_count: attempt,
|
|
1856
|
+
provider_attempt_count: attempt,
|
|
1857
|
+
screened_at: nowIso()
|
|
1858
|
+
};
|
|
1859
|
+
return result;
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
lastError = error;
|
|
1862
|
+
const fatalClassification = classifyFatalLlmProviderError(error);
|
|
1863
|
+
if (fatalClassification) {
|
|
1864
|
+
fatalProviderAttempts += 1;
|
|
1865
|
+
error.code = fatalClassification.code;
|
|
1866
|
+
error.llm_fatal_provider_error = true;
|
|
1867
|
+
error.llm_fatal_reason = fatalClassification.reason;
|
|
1868
|
+
if (fatalProviderAttempts <= FATAL_LLM_PROVIDER_MAX_RETRIES) {
|
|
1869
|
+
await sleepMs(Math.min(2500, 500 * fatalProviderAttempts));
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
error.image_input_count = imageInputs.length;
|
|
1873
|
+
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1874
|
+
error.llm_attempt_count = attempt;
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
const retryable = isRetryableLlmRequestError(error) || isRecoverableLlmScreeningError(error);
|
|
1878
|
+
if (attempt >= maxAttempts || !retryable) {
|
|
1879
|
+
error.image_input_count = imageInputs.length;
|
|
1880
|
+
error.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1881
|
+
error.llm_attempt_count = attempt;
|
|
1882
|
+
throw error;
|
|
1883
|
+
}
|
|
1884
|
+
await sleepMs(Math.min(2500, 500 * attempt));
|
|
1885
|
+
} finally {
|
|
1886
|
+
clearTimeout(timer);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
lastError = lastError || new Error("LLM request failed without response");
|
|
1890
|
+
lastError.image_input_count = imageInputs.length;
|
|
1891
|
+
lastError.image_inputs = summarizeLlmImageInputs(imageInputs);
|
|
1892
|
+
lastError.llm_attempt_count = maxAttempts;
|
|
1893
|
+
throw lastError;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function compactStrategyLlmResult(result) {
|
|
1897
|
+
if (!result) return null;
|
|
1898
|
+
const compact = compactScreeningLlmResult(result);
|
|
1899
|
+
if (compact) {
|
|
1900
|
+
compact.fast_result = null;
|
|
1901
|
+
compact.verify_result = null;
|
|
1902
|
+
}
|
|
1903
|
+
return compact;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function attachFastFirstScreeningMetadata(result, {
|
|
1907
|
+
fastThinkingLevel = "current",
|
|
1908
|
+
verifyThinkingLevel = "low",
|
|
1909
|
+
verified = false,
|
|
1910
|
+
verificationReason = "",
|
|
1911
|
+
decisionSource = "fast",
|
|
1912
|
+
fastResult = null,
|
|
1913
|
+
verifyResult = null
|
|
1914
|
+
} = {}) {
|
|
1915
|
+
return {
|
|
1916
|
+
...result,
|
|
1917
|
+
screening_strategy: "fast_first_verified",
|
|
1918
|
+
fast_thinking_level: fastThinkingLevel,
|
|
1919
|
+
verify_thinking_level: verifyThinkingLevel,
|
|
1920
|
+
verified: Boolean(verified),
|
|
1921
|
+
verification_reason: verificationReason,
|
|
1922
|
+
decision_source: decisionSource,
|
|
1923
|
+
fast_result: compactStrategyLlmResult(fastResult),
|
|
1924
|
+
verify_result: compactStrategyLlmResult(verifyResult)
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function normalizeStrategyThinkingLevel(value, fallback) {
|
|
1929
|
+
return normalizeLlmThinkingLevel(value) || normalizeLlmThinkingLevel(fallback) || fallback;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function resolveStrategyPassMaxTokens(entry = {}, inherited = {}, pass = "fast", fallback = null) {
|
|
1933
|
+
const value = pass === "verify"
|
|
1934
|
+
? firstConfiguredValue(
|
|
1935
|
+
entry.llmVerifyMaxTokens,
|
|
1936
|
+
entry.verifyMaxTokens,
|
|
1937
|
+
entry.verify_max_tokens,
|
|
1938
|
+
inherited.llmVerifyMaxTokens,
|
|
1939
|
+
inherited.verifyMaxTokens,
|
|
1940
|
+
inherited.verify_max_tokens
|
|
1941
|
+
)
|
|
1942
|
+
: firstConfiguredValue(
|
|
1943
|
+
entry.llmFastMaxTokens,
|
|
1944
|
+
entry.fastMaxTokens,
|
|
1945
|
+
entry.fast_max_tokens,
|
|
1946
|
+
inherited.llmFastMaxTokens,
|
|
1947
|
+
inherited.fastMaxTokens,
|
|
1948
|
+
inherited.fast_max_tokens
|
|
1949
|
+
);
|
|
1950
|
+
return parsePositiveInteger(value, fallback);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function applyForcedOutputTokenBudget(config = {}, outputTokenBudget = null) {
|
|
1954
|
+
const budget = parsePositiveInteger(outputTokenBudget, null);
|
|
1955
|
+
if (!budget) return config;
|
|
1956
|
+
const next = {
|
|
1957
|
+
...config,
|
|
1958
|
+
llmMaxTokens: budget,
|
|
1959
|
+
maxTokens: budget
|
|
1960
|
+
};
|
|
1961
|
+
delete next.llmMaxCompletionTokens;
|
|
1962
|
+
delete next.maxCompletionTokens;
|
|
1963
|
+
return next;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function withForcedLlmThinkingLevel(config = {}, thinkingLevel = "low", options = {}) {
|
|
1967
|
+
const normalizedThinkingLevel = normalizeStrategyThinkingLevel(thinkingLevel, "low");
|
|
1968
|
+
const forceEntry = (entry) => {
|
|
1969
|
+
const objectEntry = typeof entry === "string" ? { model: entry } : { ...(entry || {}) };
|
|
1970
|
+
const forced = {
|
|
1971
|
+
...objectEntry,
|
|
1972
|
+
llmThinkingLevel: normalizedThinkingLevel,
|
|
1973
|
+
thinkingLevel: normalizedThinkingLevel,
|
|
1974
|
+
reasoningEffort: normalizedThinkingLevel
|
|
1975
|
+
};
|
|
1976
|
+
const entryBudget = options.pass
|
|
1977
|
+
? resolveStrategyPassMaxTokens(objectEntry, config, options.pass, options.defaultMaxTokens)
|
|
1978
|
+
: options.outputTokenBudget;
|
|
1979
|
+
return applyForcedOutputTokenBudget(forced, entryBudget);
|
|
1980
|
+
};
|
|
1981
|
+
const baseBudget = options.pass
|
|
1982
|
+
? resolveStrategyPassMaxTokens(config, {}, options.pass, options.defaultMaxTokens)
|
|
1983
|
+
: options.outputTokenBudget;
|
|
1984
|
+
const next = applyForcedOutputTokenBudget({
|
|
1985
|
+
...(config || {}),
|
|
1986
|
+
llmThinkingLevel: normalizedThinkingLevel,
|
|
1987
|
+
thinkingLevel: normalizedThinkingLevel,
|
|
1988
|
+
reasoningEffort: normalizedThinkingLevel
|
|
1989
|
+
}, baseBudget);
|
|
1990
|
+
if (Array.isArray(config?.llmModels)) {
|
|
1991
|
+
next.llmModels = config.llmModels.map(forceEntry);
|
|
1992
|
+
}
|
|
1993
|
+
if (Array.isArray(config?.models)) {
|
|
1994
|
+
next.models = config.models.map(forceEntry);
|
|
1995
|
+
}
|
|
1996
|
+
return next;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function callSinglePassScreeningLlm(args = {}) {
|
|
2000
|
+
const providers = normalizeLlmProviderConfigs(args.config || {});
|
|
2001
|
+
if (providers.length <= 1) {
|
|
2002
|
+
return callScreeningLlmWithProvider({
|
|
2003
|
+
...args,
|
|
2004
|
+
config: {
|
|
2005
|
+
...(providers[0] || args.config || {}),
|
|
2006
|
+
llmProviderCount: 1
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const providerFailures = [];
|
|
2012
|
+
let lastError = null;
|
|
2013
|
+
for (let index = 0; index < providers.length; index += 1) {
|
|
2014
|
+
const providerConfig = {
|
|
2015
|
+
...providers[index],
|
|
2016
|
+
llmProviderIndex: index,
|
|
2017
|
+
llmProviderCount: providers.length
|
|
2018
|
+
};
|
|
2019
|
+
try {
|
|
2020
|
+
const previousAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
2021
|
+
const result = await callScreeningLlmWithProvider({
|
|
2022
|
+
...args,
|
|
2023
|
+
config: providerConfig
|
|
2024
|
+
});
|
|
2025
|
+
const providerAttempts = Number(result.provider_attempt_count ?? result.attempt_count) || 0;
|
|
2026
|
+
return {
|
|
2027
|
+
...result,
|
|
2028
|
+
attempt_count: previousAttempts + providerAttempts,
|
|
2029
|
+
llm_model_failures: providerFailures,
|
|
2030
|
+
fallback_count: providerFailures.length
|
|
2031
|
+
};
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
2034
|
+
lastError = error;
|
|
2035
|
+
providerFailures.push(compactLlmProviderFailure(error, providerConfig, index));
|
|
2036
|
+
if (index < providers.length - 1) {
|
|
2037
|
+
await sleepMs(Math.min(1500, 250 * (index + 1)));
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const totalAttempts = providerFailures.reduce((sum, item) => sum + (Number(item.attempts) || 0), 0);
|
|
2043
|
+
const finalError = new Error(
|
|
2044
|
+
`All configured LLM models failed (${providers.length}); last error: ${lastError?.message || "unknown error"}`
|
|
2045
|
+
);
|
|
2046
|
+
finalError.cause = lastError || null;
|
|
2047
|
+
finalError.llm_provider_failures = providerFailures;
|
|
2048
|
+
finalError.llm_model_failures = providerFailures;
|
|
2049
|
+
finalError.llm_attempt_count = totalAttempts;
|
|
2050
|
+
finalError.image_input_count = lastError?.image_input_count || 0;
|
|
2051
|
+
finalError.image_inputs = lastError?.image_inputs || [];
|
|
2052
|
+
throw finalError;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
async function callFastFirstVerifiedScreeningLlm(args = {}) {
|
|
2056
|
+
const config = args.config || {};
|
|
2057
|
+
const fastThinkingLevel = normalizeStrategyThinkingLevel(config.llmFastThinkingLevel ?? config.fastThinkingLevel, "current");
|
|
2058
|
+
const verifyThinkingLevel = normalizeStrategyThinkingLevel(config.llmVerifyThinkingLevel ?? config.verifyThinkingLevel, "low");
|
|
2059
|
+
const fastArgs = {
|
|
2060
|
+
...args,
|
|
2061
|
+
config: withForcedLlmThinkingLevel(config, fastThinkingLevel, {
|
|
2062
|
+
pass: "fast",
|
|
2063
|
+
defaultMaxTokens: FAST_FIRST_DEFAULT_FAST_MAX_TOKENS
|
|
2064
|
+
}),
|
|
2065
|
+
requireReviewDecision: true
|
|
2066
|
+
};
|
|
2067
|
+
let fastResult = null;
|
|
2068
|
+
let verifyReason = "";
|
|
2069
|
+
try {
|
|
2070
|
+
fastResult = await callSinglePassScreeningLlm(fastArgs);
|
|
2071
|
+
if (fastResult.passed === true) {
|
|
2072
|
+
verifyReason = fastResult.review_required === true ? "fast_passed_review_required" : "fast_passed";
|
|
2073
|
+
} else if (fastResult.review_required === true) {
|
|
2074
|
+
verifyReason = "fast_review_required";
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
2078
|
+
fastResult = createFailedLlmScreeningResult(error);
|
|
2079
|
+
verifyReason = "fast_invalid_response";
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (!verifyReason) {
|
|
2083
|
+
return attachFastFirstScreeningMetadata(fastResult, {
|
|
2084
|
+
fastThinkingLevel,
|
|
2085
|
+
verifyThinkingLevel,
|
|
2086
|
+
verified: false,
|
|
2087
|
+
decisionSource: "fast",
|
|
2088
|
+
fastResult
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const verifyArgs = {
|
|
2093
|
+
...args,
|
|
2094
|
+
config: withForcedLlmThinkingLevel(config, verifyThinkingLevel, {
|
|
2095
|
+
pass: "verify"
|
|
2096
|
+
}),
|
|
2097
|
+
requireReviewDecision: false
|
|
2098
|
+
};
|
|
2099
|
+
try {
|
|
2100
|
+
const verifyResult = await callSinglePassScreeningLlm(verifyArgs);
|
|
2101
|
+
return attachFastFirstScreeningMetadata(verifyResult, {
|
|
2102
|
+
fastThinkingLevel,
|
|
2103
|
+
verifyThinkingLevel,
|
|
2104
|
+
verified: true,
|
|
2105
|
+
verificationReason: verifyReason,
|
|
2106
|
+
decisionSource: "verify",
|
|
2107
|
+
fastResult,
|
|
2108
|
+
verifyResult
|
|
2109
|
+
});
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
if (isFatalLlmProviderError(error)) throw error;
|
|
2112
|
+
const failedVerifyResult = createFailedLlmScreeningResult(error);
|
|
2113
|
+
return attachFastFirstScreeningMetadata(failedVerifyResult, {
|
|
2114
|
+
fastThinkingLevel,
|
|
2115
|
+
verifyThinkingLevel,
|
|
2116
|
+
verified: true,
|
|
2117
|
+
verificationReason: verifyReason,
|
|
2118
|
+
decisionSource: "verify_error",
|
|
2119
|
+
fastResult,
|
|
2120
|
+
verifyResult: failedVerifyResult
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
export async function callScreeningLlm(args = {}) {
|
|
2126
|
+
const strategy = normalizeLlmScreeningStrategy(
|
|
2127
|
+
args.config?.llmScreeningStrategy
|
|
2128
|
+
?? args.config?.screeningStrategy
|
|
2129
|
+
?? args.config?.screening_strategy
|
|
2130
|
+
);
|
|
2131
|
+
if (strategy === "fast_first_verified") {
|
|
2132
|
+
return callFastFirstVerifiedScreeningLlm(args);
|
|
2133
|
+
}
|
|
2134
|
+
return callSinglePassScreeningLlm(args);
|
|
2135
|
+
}
|