@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -0,0 +1,1166 @@
|
|
|
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", "low", "medium", "high", "current"]);
|
|
41
|
+
|
|
42
|
+
function nowIso() {
|
|
43
|
+
return new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeLlmThinkingLevel(value) {
|
|
47
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
48
|
+
return LLM_THINKING_LEVELS.has(normalized) ? normalized : "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeBaseUrl(baseUrl) {
|
|
52
|
+
return String(baseUrl || "").replace(/\/+$/, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildChatCompletionsUrl(baseUrl) {
|
|
56
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
57
|
+
if (/\/chat\/completions$/i.test(normalized)) return normalized;
|
|
58
|
+
return `${normalized}/chat/completions`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isVolcengineModel(baseUrl, model) {
|
|
62
|
+
return /volces|volcengine|ark\.cn|doubao|seed/i.test(`${baseUrl || ""} ${model || ""}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function applyChatCompletionThinking(payload, { baseUrl = "", model = "", thinkingLevel = "" } = {}) {
|
|
66
|
+
const level = normalizeLlmThinkingLevel(thinkingLevel);
|
|
67
|
+
if (!level || level === "current") return payload;
|
|
68
|
+
if (isVolcengineModel(baseUrl, model)) {
|
|
69
|
+
if (level === "off") {
|
|
70
|
+
payload.thinking = { type: "disabled" };
|
|
71
|
+
} else {
|
|
72
|
+
payload.thinking = { type: "enabled" };
|
|
73
|
+
}
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
payload.reasoning_effort = level === "off" ? "minimal" : level;
|
|
77
|
+
return payload;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function normalizeText(input) {
|
|
81
|
+
return String(input || "").replace(/\s+/g, " ").trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeBlockText(input) {
|
|
85
|
+
return String(input ?? "").trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function compact(input) {
|
|
89
|
+
return normalizeText(input).toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function decodeHtmlEntities(input) {
|
|
93
|
+
return String(input || "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
|
|
94
|
+
const key = String(entity).toLowerCase();
|
|
95
|
+
if (key.startsWith("#x")) {
|
|
96
|
+
const value = Number.parseInt(key.slice(2), 16);
|
|
97
|
+
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
98
|
+
}
|
|
99
|
+
if (key.startsWith("#")) {
|
|
100
|
+
const value = Number.parseInt(key.slice(1), 10);
|
|
101
|
+
return Number.isFinite(value) ? String.fromCodePoint(value) : match;
|
|
102
|
+
}
|
|
103
|
+
return ENTITY_MAP[key] || match;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function htmlToText(html) {
|
|
108
|
+
const withoutScripts = String(html || "")
|
|
109
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
110
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ");
|
|
111
|
+
const withBreaks = withoutScripts
|
|
112
|
+
.replace(/<\/(?:div|li|p|section|article|header|footer|h[1-6]|tr)>/gi, "\n")
|
|
113
|
+
.replace(/<br\s*\/?>/gi, "\n");
|
|
114
|
+
return decodeHtmlEntities(withBreaks.replace(/<[^>]+>/g, " "))
|
|
115
|
+
.split(/\r?\n/)
|
|
116
|
+
.map((line) => normalizeText(line))
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseHtmlAttributes(html) {
|
|
122
|
+
const attributes = {};
|
|
123
|
+
const openTag = String(html || "").match(/^<[^>]+>/s)?.[0] || "";
|
|
124
|
+
const regex = /([:@A-Za-z0-9_-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = regex.exec(openTag))) {
|
|
127
|
+
const name = match[1];
|
|
128
|
+
if (!name || name.startsWith("<")) continue;
|
|
129
|
+
attributes[name] = decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
130
|
+
}
|
|
131
|
+
return attributes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function unique(values) {
|
|
135
|
+
return Array.from(new Set(values.map(normalizeText).filter(Boolean)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeDomain(domain) {
|
|
139
|
+
const normalized = compact(domain);
|
|
140
|
+
if (!SUPPORTED_DOMAINS.has(normalized)) {
|
|
141
|
+
throw new Error(`Unsupported screening domain: ${domain}`);
|
|
142
|
+
}
|
|
143
|
+
return normalized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function collectTextParts(candidate = {}) {
|
|
147
|
+
return unique([
|
|
148
|
+
candidate.text,
|
|
149
|
+
candidate.raw_text,
|
|
150
|
+
candidate.summary,
|
|
151
|
+
candidate.resume_text,
|
|
152
|
+
candidate.identity?.name,
|
|
153
|
+
candidate.identity?.title,
|
|
154
|
+
candidate.identity?.current_position,
|
|
155
|
+
candidate.identity?.current_company,
|
|
156
|
+
candidate.identity?.school,
|
|
157
|
+
candidate.identity?.major,
|
|
158
|
+
...(candidate.tags || [])
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseDegree(text) {
|
|
163
|
+
for (const item of DEGREE_PATTERNS) {
|
|
164
|
+
if (item.regex.test(text)) return item.value;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseYearsExperience(text) {
|
|
170
|
+
const normalized = normalizeText(text);
|
|
171
|
+
const match = normalized.match(/(?<!\d)(\d{1,2})\s*(?:年以上?|年)\s*(?:经验|工作经验)/i)
|
|
172
|
+
|| normalized.match(/(?:经验|工作经验|工作)\s*(?<!\d)(\d{1,2})\s*(?:年以上?|年)/i)
|
|
173
|
+
|| normalized.match(/(?<!\d)(\d{1,2})\s*years?\s*(?:of\s*)?(?:experience|work)?/i);
|
|
174
|
+
if (!match) return null;
|
|
175
|
+
const value = Number.parseInt(match[1], 10);
|
|
176
|
+
return Number.isFinite(value) ? value : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseAge(text) {
|
|
180
|
+
const match = normalizeText(text).match(/(\d{2})\s*岁/);
|
|
181
|
+
if (!match) return null;
|
|
182
|
+
const value = Number.parseInt(match[1], 10);
|
|
183
|
+
return Number.isFinite(value) ? value : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseGender(text) {
|
|
187
|
+
const normalized = normalizeText(text);
|
|
188
|
+
if (/(?:^|[\s||,,])男(?:$|[\s||,,])/.test(normalized)) return "男";
|
|
189
|
+
if (/(?:^|[\s||,,])女(?:$|[\s||,,])/.test(normalized)) return "女";
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeGenderValue(value) {
|
|
194
|
+
if (value == null || value === "") return null;
|
|
195
|
+
if (GENDER_CODE_MAP[value]) return GENDER_CODE_MAP[value];
|
|
196
|
+
const normalized = normalizeText(value);
|
|
197
|
+
if (normalized === "男" || normalized === "女") return normalized;
|
|
198
|
+
return parseGender(normalized);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseDateLike(value) {
|
|
202
|
+
const normalized = normalizeText(value);
|
|
203
|
+
if (!normalized || normalized === "0") return "";
|
|
204
|
+
if (/^\d{6}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
205
|
+
if (/^\d{8}$/.test(normalized)) return `${normalized.slice(0, 4)}.${normalized.slice(4, 6)}`;
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function firstUsefulLine(lines) {
|
|
210
|
+
return lines.find((line) => {
|
|
211
|
+
const normalized = normalizeText(line);
|
|
212
|
+
return normalized && !/^沟通|^收藏|^查看|^不合适/.test(normalized);
|
|
213
|
+
}) || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseNetworkBodyText(networkBody = {}) {
|
|
217
|
+
const bodyResult = networkBody.body || networkBody;
|
|
218
|
+
let body = String(bodyResult?.body || "");
|
|
219
|
+
if (bodyResult?.base64Encoded) {
|
|
220
|
+
body = Buffer.from(body, "base64").toString("utf8");
|
|
221
|
+
}
|
|
222
|
+
return body;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function tryParseJson(text) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(text);
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function tryExtractJsonObject(text) {
|
|
234
|
+
const normalized = String(text || "").trim();
|
|
235
|
+
const direct = tryParseJson(normalized);
|
|
236
|
+
if (direct && typeof direct === "object") return direct;
|
|
237
|
+
const fenced = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
238
|
+
if (fenced) {
|
|
239
|
+
const parsed = tryParseJson(fenced[1].trim());
|
|
240
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
241
|
+
}
|
|
242
|
+
const start = normalized.indexOf("{");
|
|
243
|
+
const end = normalized.lastIndexOf("}");
|
|
244
|
+
if (start >= 0 && end > start) {
|
|
245
|
+
const parsed = tryParseJson(normalized.slice(start, end + 1));
|
|
246
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function flattenChatMessageContent(content) {
|
|
252
|
+
if (typeof content === "string") return content;
|
|
253
|
+
if (Array.isArray(content)) {
|
|
254
|
+
return content.map((item) => {
|
|
255
|
+
if (typeof item === "string") return item;
|
|
256
|
+
return item?.text || item?.content || item?.reasoning_content || "";
|
|
257
|
+
}).filter(Boolean).join("\n");
|
|
258
|
+
}
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function collectLlmReasoningText(choice = {}) {
|
|
263
|
+
const message = choice?.message || {};
|
|
264
|
+
return [
|
|
265
|
+
message.reasoning_content,
|
|
266
|
+
message.reasoning,
|
|
267
|
+
message.cot,
|
|
268
|
+
message.chain_of_thought,
|
|
269
|
+
choice.reasoning_content,
|
|
270
|
+
choice.reasoning,
|
|
271
|
+
choice.cot,
|
|
272
|
+
choice.chain_of_thought
|
|
273
|
+
].map(flattenChatMessageContent).map(normalizeBlockText).filter(Boolean).join("\n\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mimeTypeForImagePath(filePath) {
|
|
277
|
+
const extension = path.extname(String(filePath || "")).toLowerCase();
|
|
278
|
+
if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
|
|
279
|
+
if (extension === ".webp") return "image/webp";
|
|
280
|
+
return "image/png";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeImagePaths({ imageEvidence = null, imagePaths = [] } = {}) {
|
|
284
|
+
const paths = [];
|
|
285
|
+
if (Array.isArray(imagePaths)) {
|
|
286
|
+
paths.push(...imagePaths);
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(imageEvidence?.file_paths)) {
|
|
289
|
+
paths.push(...imageEvidence.file_paths);
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(imageEvidence?.screenshots)) {
|
|
292
|
+
paths.push(...imageEvidence.screenshots.map((item) => item.file_path));
|
|
293
|
+
}
|
|
294
|
+
return unique(paths.map((filePath) => String(filePath || "").trim()).filter(Boolean));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function imagePathToLlmInput(filePath, {
|
|
298
|
+
detail = "high"
|
|
299
|
+
} = {}) {
|
|
300
|
+
const resolved = path.resolve(filePath);
|
|
301
|
+
const buffer = fs.readFileSync(resolved);
|
|
302
|
+
const mimeType = mimeTypeForImagePath(resolved);
|
|
303
|
+
return {
|
|
304
|
+
type: "image_url",
|
|
305
|
+
image_url: {
|
|
306
|
+
url: `data:${mimeType};base64,${buffer.toString("base64")}`,
|
|
307
|
+
detail
|
|
308
|
+
},
|
|
309
|
+
metadata: {
|
|
310
|
+
file_path: resolved,
|
|
311
|
+
mime_type: mimeType,
|
|
312
|
+
byte_length: buffer.length
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function buildScreeningLlmImageInputs({
|
|
318
|
+
imageEvidence = null,
|
|
319
|
+
imagePaths = [],
|
|
320
|
+
maxImages = 8,
|
|
321
|
+
detail = "high"
|
|
322
|
+
} = {}) {
|
|
323
|
+
const paths = normalizeImagePaths({ imageEvidence, imagePaths });
|
|
324
|
+
const limit = Math.max(1, Number(maxImages) || 8);
|
|
325
|
+
return paths.slice(0, limit).map((filePath) => imagePathToLlmInput(filePath, { detail }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function summarizeLlmImageInputs(imageInputs = []) {
|
|
329
|
+
return imageInputs.map((input, index) => ({
|
|
330
|
+
index,
|
|
331
|
+
file_path: input.metadata?.file_path || null,
|
|
332
|
+
mime_type: input.metadata?.mime_type || null,
|
|
333
|
+
byte_length: input.metadata?.byte_length || 0
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parsePassedDecision(value) {
|
|
338
|
+
if (typeof value === "boolean") return value;
|
|
339
|
+
if (typeof value === "number") return value !== 0;
|
|
340
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
341
|
+
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(normalized)) return true;
|
|
342
|
+
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(normalized)) return false;
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function pickFirst(...values) {
|
|
347
|
+
for (const value of values) {
|
|
348
|
+
const normalized = normalizeText(value);
|
|
349
|
+
if (normalized && normalized !== "0") return normalized;
|
|
350
|
+
}
|
|
351
|
+
return "";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function joinRange(start, end, fallback = "") {
|
|
355
|
+
const left = parseDateLike(start);
|
|
356
|
+
const right = parseDateLike(end);
|
|
357
|
+
if (left && right) return `${left}-${right}`;
|
|
358
|
+
if (left) return `${left}-至今`;
|
|
359
|
+
if (right) return right;
|
|
360
|
+
return normalizeText(fallback);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeList(value) {
|
|
364
|
+
if (!value) return [];
|
|
365
|
+
if (Array.isArray(value)) return value;
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeTagList(value) {
|
|
370
|
+
if (!Array.isArray(value)) return [];
|
|
371
|
+
return value.map((item) => {
|
|
372
|
+
if (typeof item === "string") return item;
|
|
373
|
+
return pickFirst(item?.name, item?.label, item?.tagName, item?.text, item?.value);
|
|
374
|
+
}).filter(Boolean);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function formatNamedSection(title, lines = []) {
|
|
378
|
+
const normalized = lines.map(normalizeText).filter(Boolean);
|
|
379
|
+
if (!normalized.length) return "";
|
|
380
|
+
return [`【${title}】`, ...normalized].join("\n");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function formatWorkExperience(item = {}, index = 0) {
|
|
384
|
+
const company = pickFirst(item.formattedCompany, item.company);
|
|
385
|
+
const position = pickFirst(item.positionName, item.positionTitle, item.position);
|
|
386
|
+
const period = joinRange(item.startYearMonStr || item.startDate, item.endYearMonStr || item.endDate, item.workYearDesc);
|
|
387
|
+
const emphasis = [
|
|
388
|
+
...normalizeTagList(item.workEmphasisList),
|
|
389
|
+
...normalizeTagList(item.respHighlightList),
|
|
390
|
+
...normalizeTagList(item.workPerfHighlightList)
|
|
391
|
+
];
|
|
392
|
+
return [
|
|
393
|
+
`${index + 1}. ${[company, position, period].filter(Boolean).join(" / ")}`,
|
|
394
|
+
item.department ? `部门:${normalizeText(item.department)}` : "",
|
|
395
|
+
item.responsibility ? `职责:${normalizeText(item.responsibility)}` : "",
|
|
396
|
+
item.workPerformance ? `业绩:${normalizeText(item.workPerformance)}` : "",
|
|
397
|
+
item.workEmphasis ? `重点:${normalizeText(item.workEmphasis)}` : "",
|
|
398
|
+
emphasis.length ? `亮点:${unique(emphasis).join("、")}` : ""
|
|
399
|
+
].filter(Boolean).join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function formatProjectExperience(item = {}, index = 0) {
|
|
403
|
+
const period = joinRange(item.startYearMonStr || item.startDateDesc || item.startDate, item.endYearMonStr || item.endDateDesc || item.endDate);
|
|
404
|
+
return [
|
|
405
|
+
`${index + 1}. ${[pickFirst(item.name), pickFirst(item.roleName), period].filter(Boolean).join(" / ")}`,
|
|
406
|
+
pickFirst(item.projectDescription, item.description) ? `项目描述:${pickFirst(item.projectDescription, item.description)}` : "",
|
|
407
|
+
item.performance ? `项目业绩:${normalizeText(item.performance)}` : ""
|
|
408
|
+
].filter(Boolean).join("\n");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function formatEducation(item = {}, index = 0) {
|
|
412
|
+
const period = joinRange(
|
|
413
|
+
item.startDateDesc || item.startDate || item.startYearStr,
|
|
414
|
+
item.endDateDesc || item.endDate || item.endYearStr
|
|
415
|
+
);
|
|
416
|
+
const tags = [
|
|
417
|
+
...normalizeTagList(item.tags),
|
|
418
|
+
...normalizeTagList(item.schoolTags),
|
|
419
|
+
...normalizeTagList(item.keySubjectList)
|
|
420
|
+
];
|
|
421
|
+
return [
|
|
422
|
+
`${index + 1}. ${[
|
|
423
|
+
pickFirst(item.school),
|
|
424
|
+
pickFirst(item.major),
|
|
425
|
+
pickFirst(item.degreeName, item.degree),
|
|
426
|
+
period
|
|
427
|
+
].filter(Boolean).join(" / ")}`,
|
|
428
|
+
tags.length ? `标签:${unique(tags).join("、")}` : "",
|
|
429
|
+
item.courseDesc ? `课程:${normalizeText(item.courseDesc)}` : "",
|
|
430
|
+
item.eduDescription ? `教育描述:${normalizeText(item.eduDescription)}` : "",
|
|
431
|
+
item.thesisTitle ? `论文:${normalizeText(item.thesisTitle)}` : "",
|
|
432
|
+
item.thesisDesc ? `论文描述:${normalizeText(item.thesisDesc)}` : ""
|
|
433
|
+
].filter(Boolean).join("\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function formatExpectation(item = {}, index = 0) {
|
|
437
|
+
return `${index + 1}. ${[
|
|
438
|
+
pickFirst(item.positionName, item.position),
|
|
439
|
+
pickFirst(item.locationName, item.location),
|
|
440
|
+
pickFirst(item.salaryDesc),
|
|
441
|
+
pickFirst(item.industryDesc)
|
|
442
|
+
].filter(Boolean).join(" / ")}`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function formatChatWorkExperience(item = {}, index = 0) {
|
|
446
|
+
return [
|
|
447
|
+
`${index + 1}. ${[
|
|
448
|
+
pickFirst(item.company, item.brandName),
|
|
449
|
+
pickFirst(item.positionName, item.position),
|
|
450
|
+
pickFirst(item.workYear, item.workYearDesc, item.dateRange)
|
|
451
|
+
].filter(Boolean).join(" / ")}`,
|
|
452
|
+
pickFirst(item.description, item.performance, item.content) ? `描述:${pickFirst(item.description, item.performance, item.content)}` : ""
|
|
453
|
+
].filter(Boolean).join("\n");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatChatEducation(item = {}, index = 0) {
|
|
457
|
+
return `${index + 1}. ${[
|
|
458
|
+
pickFirst(item.school),
|
|
459
|
+
pickFirst(item.major),
|
|
460
|
+
pickFirst(item.degree, item.degreeName),
|
|
461
|
+
pickFirst(item.year, item.dateRange)
|
|
462
|
+
].filter(Boolean).join(" / ")}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resolveBossGeekDetail(payload = {}) {
|
|
466
|
+
const candidates = [
|
|
467
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.zpData?.geekDetailInfo },
|
|
468
|
+
{ sourceKey: "geekDetail", detail: payload?.zpData?.geekDetail },
|
|
469
|
+
{ sourceKey: "geekDetailInfo", detail: payload?.geekDetailInfo },
|
|
470
|
+
{ sourceKey: "geekDetail", detail: payload?.geekDetail }
|
|
471
|
+
];
|
|
472
|
+
const found = candidates.find((item) => item.detail && typeof item.detail === "object");
|
|
473
|
+
return found || { sourceKey: "", detail: null };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function extractBossChatGeekInfo(payload = {}) {
|
|
477
|
+
const data = payload?.zpData?.data;
|
|
478
|
+
if (!data || typeof data !== "object") return null;
|
|
479
|
+
const educationList = normalizeList(data.eduExpList);
|
|
480
|
+
const workList = normalizeList(data.workExpList);
|
|
481
|
+
const firstEducation = educationList[0] || {};
|
|
482
|
+
const firstWork = workList[0] || {};
|
|
483
|
+
const tags = unique([
|
|
484
|
+
...normalizeTagList(data.highLightGeekResumeWords),
|
|
485
|
+
...normalizeTagList(data.highLightWords),
|
|
486
|
+
...normalizeTagList(data.skillTags),
|
|
487
|
+
...normalizeTagList(data.labels),
|
|
488
|
+
pickFirst(data.positionCategory),
|
|
489
|
+
pickFirst(data.positionName, data.position, data.toPosition)
|
|
490
|
+
]);
|
|
491
|
+
const salary = data.salaryDesc || (
|
|
492
|
+
data.lowSalary && data.highSalary ? `${data.lowSalary}-${data.highSalary}K` : ""
|
|
493
|
+
);
|
|
494
|
+
const sections = {
|
|
495
|
+
base: [
|
|
496
|
+
pickFirst(data.name) ? `姓名:${pickFirst(data.name)}` : "",
|
|
497
|
+
pickFirst(data.gender) ? `性别:${pickFirst(data.gender)}` : "",
|
|
498
|
+
pickFirst(data.age) ? `年龄:${pickFirst(data.age)}` : "",
|
|
499
|
+
pickFirst(data.year, data.workYear, data.workYearDesc) ? `工作年限:${pickFirst(data.year, data.workYear, data.workYearDesc)}` : "",
|
|
500
|
+
pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) ? `最高学历:${pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName)}` : "",
|
|
501
|
+
pickFirst(data.positionStatus, data.positionStatusDesc) ? `求职状态:${pickFirst(data.positionStatus, data.positionStatusDesc)}` : ""
|
|
502
|
+
].filter(Boolean),
|
|
503
|
+
expectation: [
|
|
504
|
+
[pickFirst(data.toPosition, data.positionName, data.position), salary].filter(Boolean).join(" / ")
|
|
505
|
+
].filter(Boolean),
|
|
506
|
+
current: [
|
|
507
|
+
[pickFirst(data.lastCompany, data.lastCompany2), pickFirst(data.lastPosition, data.lastPosition2)].filter(Boolean).join(" / ")
|
|
508
|
+
].filter(Boolean),
|
|
509
|
+
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
510
|
+
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
511
|
+
highlights: tags
|
|
512
|
+
};
|
|
513
|
+
const text = [
|
|
514
|
+
formatNamedSection("基础信息", sections.base),
|
|
515
|
+
formatNamedSection("求职期望", sections.expectation),
|
|
516
|
+
formatNamedSection("最近经历", sections.current),
|
|
517
|
+
formatNamedSection("工作经历", sections.work),
|
|
518
|
+
formatNamedSection("教育经历", sections.education),
|
|
519
|
+
formatNamedSection("亮点标签", sections.highlights)
|
|
520
|
+
].filter(Boolean).join("\n\n");
|
|
521
|
+
return {
|
|
522
|
+
text,
|
|
523
|
+
identity: {
|
|
524
|
+
name: pickFirst(data.name) || null,
|
|
525
|
+
title: pickFirst(data.positionName, data.position, data.toPosition) || null,
|
|
526
|
+
current_position: pickFirst(data.lastPosition, data.lastPosition2, firstWork.positionName, firstWork.position) || null,
|
|
527
|
+
current_company: pickFirst(data.lastCompany, data.lastCompany2, firstWork.company, firstWork.brandName) || null,
|
|
528
|
+
school: pickFirst(data.school, firstEducation.school) || null,
|
|
529
|
+
major: pickFirst(data.major, firstEducation.major) || null,
|
|
530
|
+
degree: pickFirst(data.degree, firstEducation.degree, firstEducation.degreeName) || parseDegree(text),
|
|
531
|
+
years_experience: parseYearsExperience(pickFirst(data.year, data.workYear, data.workYearDesc)) ?? null,
|
|
532
|
+
age: parseAge(String(data.age || "")) ?? null,
|
|
533
|
+
gender: normalizeGenderValue(data.gender)
|
|
534
|
+
},
|
|
535
|
+
tags,
|
|
536
|
+
source_keys: {
|
|
537
|
+
chat_geek_info: true,
|
|
538
|
+
geek_detail_info: false,
|
|
539
|
+
geek_detail: false,
|
|
540
|
+
education_count: educationList.length,
|
|
541
|
+
work_count: workList.length
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function extractBossChatHistoryResume(payload = {}) {
|
|
547
|
+
const messages = normalizeList(payload?.zpData?.messages);
|
|
548
|
+
const resumes = messages
|
|
549
|
+
.map((message) => message?.body?.resume)
|
|
550
|
+
.filter((resume) => resume && typeof resume === "object");
|
|
551
|
+
const resume = resumes[0];
|
|
552
|
+
if (!resume) return null;
|
|
553
|
+
const user = resume.user || {};
|
|
554
|
+
const educationList = normalizeList(resume.education);
|
|
555
|
+
const workList = normalizeList(resume.experiences);
|
|
556
|
+
const firstEducation = educationList[0] || {};
|
|
557
|
+
const firstWork = workList[0] || {};
|
|
558
|
+
const tags = unique([
|
|
559
|
+
pickFirst(resume.position),
|
|
560
|
+
pickFirst(resume.positionCategory),
|
|
561
|
+
...normalizeTagList(resume.skills),
|
|
562
|
+
...normalizeTagList(resume.tags)
|
|
563
|
+
]);
|
|
564
|
+
const sections = {
|
|
565
|
+
base: [
|
|
566
|
+
pickFirst(user.name) ? `姓名:${pickFirst(user.name)}` : "",
|
|
567
|
+
pickFirst(resume.workYear) ? `工作年限:${pickFirst(resume.workYear)}` : "",
|
|
568
|
+
pickFirst(firstEducation.degree, resume.degree) ? `最高学历:${pickFirst(firstEducation.degree, resume.degree)}` : ""
|
|
569
|
+
].filter(Boolean),
|
|
570
|
+
expectation: [
|
|
571
|
+
[pickFirst(resume.position), pickFirst(resume.positionCategory)].filter(Boolean).join(" / ")
|
|
572
|
+
].filter(Boolean),
|
|
573
|
+
education: educationList.map(formatChatEducation).filter(Boolean),
|
|
574
|
+
work: workList.map(formatChatWorkExperience).filter(Boolean),
|
|
575
|
+
highlights: tags
|
|
576
|
+
};
|
|
577
|
+
const text = [
|
|
578
|
+
formatNamedSection("基础信息", sections.base),
|
|
579
|
+
formatNamedSection("求职期望", sections.expectation),
|
|
580
|
+
formatNamedSection("工作经历", sections.work),
|
|
581
|
+
formatNamedSection("教育经历", sections.education),
|
|
582
|
+
formatNamedSection("亮点标签", sections.highlights)
|
|
583
|
+
].filter(Boolean).join("\n\n");
|
|
584
|
+
return {
|
|
585
|
+
text,
|
|
586
|
+
identity: {
|
|
587
|
+
name: pickFirst(user.name) || null,
|
|
588
|
+
title: pickFirst(resume.position) || null,
|
|
589
|
+
current_position: pickFirst(firstWork.positionName, firstWork.position) || null,
|
|
590
|
+
current_company: pickFirst(firstWork.company, firstWork.brandName, user.company) || null,
|
|
591
|
+
school: pickFirst(firstEducation.school) || null,
|
|
592
|
+
major: pickFirst(firstEducation.major) || null,
|
|
593
|
+
degree: pickFirst(firstEducation.degree, firstEducation.degreeName, resume.degree) || parseDegree(text),
|
|
594
|
+
years_experience: parseYearsExperience(pickFirst(resume.workYear)) ?? null,
|
|
595
|
+
age: null,
|
|
596
|
+
gender: null
|
|
597
|
+
},
|
|
598
|
+
tags,
|
|
599
|
+
source_keys: {
|
|
600
|
+
chat_history_resume: true,
|
|
601
|
+
geek_detail_info: false,
|
|
602
|
+
geek_detail: false,
|
|
603
|
+
education_count: educationList.length,
|
|
604
|
+
work_count: workList.length
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function extractBossGeekDetailInfo(payload = {}) {
|
|
610
|
+
const { sourceKey, detail } = resolveBossGeekDetail(payload);
|
|
611
|
+
if (!detail || typeof detail !== "object") return null;
|
|
612
|
+
|
|
613
|
+
const base = detail.geekBaseInfo || {};
|
|
614
|
+
const educationList = normalizeList(detail.geekEduExpList);
|
|
615
|
+
const firstEducation = educationList[0] || detail.highestEduExp || {};
|
|
616
|
+
const workList = normalizeList(detail.geekWorkExpList);
|
|
617
|
+
const firstWork = workList[0] || {};
|
|
618
|
+
const projectList = normalizeList(detail.geekProjExpList);
|
|
619
|
+
const expectationList = normalizeList(detail.geekExpPosList).length
|
|
620
|
+
? normalizeList(detail.geekExpPosList)
|
|
621
|
+
: normalizeList(detail.geekExpectList);
|
|
622
|
+
const expectationFallback = detail.showExpectPosition && typeof detail.showExpectPosition === "object"
|
|
623
|
+
? [detail.showExpectPosition]
|
|
624
|
+
: [];
|
|
625
|
+
const normalizedExpectationList = expectationList.length ? expectationList : expectationFallback;
|
|
626
|
+
const certifications = normalizeList(detail.geekCertificationList);
|
|
627
|
+
const skillTags = [
|
|
628
|
+
...normalizeTagList(detail.blueGeekSkills),
|
|
629
|
+
...normalizeTagList(base.userHighlightList),
|
|
630
|
+
...normalizeTagList(base.userDescHighlightList),
|
|
631
|
+
...normalizeTagList(base.userDescHighLightList),
|
|
632
|
+
...normalizeTagList(detail.geekPersonalLabelList),
|
|
633
|
+
...normalizeTagList(detail.professionalSkill)
|
|
634
|
+
];
|
|
635
|
+
const summaryParts = [
|
|
636
|
+
pickFirst(base.userDescription),
|
|
637
|
+
pickFirst(base.userDesc),
|
|
638
|
+
pickFirst(base.workEduDesc),
|
|
639
|
+
pickFirst(detail.resumeSummary?.content, detail.resumeSummary?.text, detail.resumeSummary?.summary)
|
|
640
|
+
].filter(Boolean);
|
|
641
|
+
const sections = {
|
|
642
|
+
base: [
|
|
643
|
+
base.name ? `姓名:${normalizeText(base.name)}` : "",
|
|
644
|
+
normalizeGenderValue(base.gender) ? `性别:${normalizeGenderValue(base.gender)}` : "",
|
|
645
|
+
pickFirst(base.ageDesc, base.age) ? `年龄:${pickFirst(base.ageDesc, base.age)}` : "",
|
|
646
|
+
pickFirst(base.degreeCategory) ? `最高学历:${pickFirst(base.degreeCategory)}` : "",
|
|
647
|
+
pickFirst(base.workYearDesc, base.workYearsDesc) ? `工作年限:${pickFirst(base.workYearDesc, base.workYearsDesc)}` : "",
|
|
648
|
+
pickFirst(base.activeTimeDesc) ? `活跃状态:${pickFirst(base.activeTimeDesc)}` : "",
|
|
649
|
+
pickFirst(base.applyStatusDesc, base.applyStatusContent) ? `求职状态:${pickFirst(base.applyStatusDesc, base.applyStatusContent)}` : ""
|
|
650
|
+
].filter(Boolean),
|
|
651
|
+
summary: summaryParts,
|
|
652
|
+
expectations: normalizedExpectationList.map(formatExpectation).filter(Boolean),
|
|
653
|
+
work_experience: workList.map(formatWorkExperience).filter(Boolean),
|
|
654
|
+
project_experience: projectList.map(formatProjectExperience).filter(Boolean),
|
|
655
|
+
education: educationList.map(formatEducation).filter(Boolean),
|
|
656
|
+
certifications: certifications.map((item, index) => `${index + 1}. ${pickFirst(item.certName, item.name)}`).filter(Boolean),
|
|
657
|
+
skills: unique(skillTags)
|
|
658
|
+
};
|
|
659
|
+
const text = [
|
|
660
|
+
formatNamedSection("基础信息", sections.base),
|
|
661
|
+
formatNamedSection("个人总结", sections.summary),
|
|
662
|
+
formatNamedSection("求职期望", sections.expectations),
|
|
663
|
+
formatNamedSection("工作经历", sections.work_experience),
|
|
664
|
+
formatNamedSection("项目经历", sections.project_experience),
|
|
665
|
+
formatNamedSection("教育经历", sections.education),
|
|
666
|
+
formatNamedSection("证书", sections.certifications),
|
|
667
|
+
formatNamedSection("技能/亮点", sections.skills)
|
|
668
|
+
].filter(Boolean).join("\n\n");
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
identity: {
|
|
672
|
+
name: pickFirst(base.name),
|
|
673
|
+
current_position: pickFirst(firstWork.positionName, firstWork.positionTitle, firstWork.position),
|
|
674
|
+
current_company: pickFirst(firstWork.formattedCompany, firstWork.company),
|
|
675
|
+
school: pickFirst(firstEducation.school),
|
|
676
|
+
major: pickFirst(firstEducation.major),
|
|
677
|
+
degree: pickFirst(base.degreeCategory, firstEducation.degreeName, firstEducation.degree),
|
|
678
|
+
years_experience: parseYearsExperience(pickFirst(base.workYearDesc, base.workYearsDesc)) ?? null,
|
|
679
|
+
age: parseAge(pickFirst(base.ageDesc, base.age)) ?? null,
|
|
680
|
+
gender: normalizeGenderValue(base.gender)
|
|
681
|
+
},
|
|
682
|
+
tags: unique([
|
|
683
|
+
...sections.skills,
|
|
684
|
+
...educationList.flatMap((item) => [
|
|
685
|
+
...normalizeTagList(item.tags),
|
|
686
|
+
...normalizeTagList(item.schoolTags)
|
|
687
|
+
])
|
|
688
|
+
]),
|
|
689
|
+
sections,
|
|
690
|
+
text,
|
|
691
|
+
source_keys: {
|
|
692
|
+
source_key: sourceKey,
|
|
693
|
+
geek_detail_info: sourceKey === "geekDetailInfo",
|
|
694
|
+
geek_detail: sourceKey === "geekDetail",
|
|
695
|
+
work_count: workList.length,
|
|
696
|
+
project_count: projectList.length,
|
|
697
|
+
education_count: educationList.length,
|
|
698
|
+
expectation_count: normalizedExpectationList.length,
|
|
699
|
+
certification_count: certifications.length
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function extractBossProfileFromNetworkBody(networkBody = {}) {
|
|
705
|
+
const text = parseNetworkBodyText(networkBody);
|
|
706
|
+
const parsed = tryParseJson(text);
|
|
707
|
+
if (!parsed) {
|
|
708
|
+
return {
|
|
709
|
+
ok: false,
|
|
710
|
+
error: "NETWORK_BODY_NOT_JSON",
|
|
711
|
+
text_length: text.length
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const profile = extractBossGeekDetailInfo(parsed)
|
|
715
|
+
|| extractBossChatGeekInfo(parsed)
|
|
716
|
+
|| extractBossChatHistoryResume(parsed);
|
|
717
|
+
if (!profile) {
|
|
718
|
+
return {
|
|
719
|
+
ok: false,
|
|
720
|
+
error: "BOSS_GEEK_DETAIL_INFO_NOT_FOUND",
|
|
721
|
+
text_length: text.length,
|
|
722
|
+
top_level_keys: Object.keys(parsed).slice(0, 30),
|
|
723
|
+
zpData_keys: Object.keys(parsed?.zpData || {}).slice(0, 50)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
ok: true,
|
|
728
|
+
url: networkBody.url || null,
|
|
729
|
+
status: networkBody.status ?? null,
|
|
730
|
+
mimeType: networkBody.mimeType || null,
|
|
731
|
+
text_length: text.length,
|
|
732
|
+
profile
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function mergeCandidateProfiles(...profiles) {
|
|
737
|
+
const base = {};
|
|
738
|
+
for (const profile of profiles) {
|
|
739
|
+
if (!profile) continue;
|
|
740
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
741
|
+
if (value == null || value === "") continue;
|
|
742
|
+
if (base[key] == null || base[key] === "") {
|
|
743
|
+
base[key] = value;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return base;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function buildScreeningCandidateFromDetail({
|
|
751
|
+
cardCandidate,
|
|
752
|
+
detailText = "",
|
|
753
|
+
networkBodies = [],
|
|
754
|
+
domain = "recommend",
|
|
755
|
+
source = "live-cdp-detail",
|
|
756
|
+
metadata = {}
|
|
757
|
+
} = {}) {
|
|
758
|
+
const parsedNetworkProfiles = networkBodies.map(extractBossProfileFromNetworkBody);
|
|
759
|
+
const successfulProfiles = parsedNetworkProfiles.filter((item) => item.ok).map((item) => item.profile);
|
|
760
|
+
const networkText = successfulProfiles.map((profile) => profile.text).filter(Boolean).join("\n\n");
|
|
761
|
+
const networkIdentity = mergeCandidateProfiles(
|
|
762
|
+
...successfulProfiles.map((profile) => profile.identity)
|
|
763
|
+
);
|
|
764
|
+
const networkTags = unique(successfulProfiles.flatMap((profile) => profile.tags || []));
|
|
765
|
+
const combinedIdentity = mergeCandidateProfiles(
|
|
766
|
+
networkIdentity,
|
|
767
|
+
cardCandidate?.identity
|
|
768
|
+
);
|
|
769
|
+
const candidate = normalizeCandidateProfile({
|
|
770
|
+
domain,
|
|
771
|
+
source,
|
|
772
|
+
id: cardCandidate?.id,
|
|
773
|
+
href: cardCandidate?.links?.href,
|
|
774
|
+
text: [
|
|
775
|
+
networkText,
|
|
776
|
+
detailText,
|
|
777
|
+
cardCandidate?.text?.raw
|
|
778
|
+
].filter(Boolean).join("\n\n"),
|
|
779
|
+
attributes: cardCandidate?.metadata?.attributes || {},
|
|
780
|
+
identity: combinedIdentity,
|
|
781
|
+
tags: unique([
|
|
782
|
+
...(cardCandidate?.tags || []),
|
|
783
|
+
...networkTags
|
|
784
|
+
]),
|
|
785
|
+
metadata: {
|
|
786
|
+
...metadata,
|
|
787
|
+
card_candidate_source: cardCandidate?.source || null,
|
|
788
|
+
network_profile_count: successfulProfiles.length,
|
|
789
|
+
network_profiles: parsedNetworkProfiles.map((item) => ({
|
|
790
|
+
ok: item.ok,
|
|
791
|
+
url: item.url,
|
|
792
|
+
status: item.status,
|
|
793
|
+
error: item.error,
|
|
794
|
+
text_length: item.text_length,
|
|
795
|
+
source_keys: item.profile?.source_keys || null
|
|
796
|
+
}))
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
candidate,
|
|
801
|
+
parsed_network_profiles: parsedNetworkProfiles
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function normalizeCandidateProfile(input = {}) {
|
|
806
|
+
const domain = normalizeDomain(input.domain || "recommend");
|
|
807
|
+
const rawText = String(input.text || input.raw_text || input.resume_text || "")
|
|
808
|
+
.split(/\r?\n/)
|
|
809
|
+
.map((line) => normalizeText(line))
|
|
810
|
+
.filter(Boolean)
|
|
811
|
+
.join("\n");
|
|
812
|
+
const lines = rawText.split(/\r?\n/).map(normalizeText).filter(Boolean);
|
|
813
|
+
const attrs = {
|
|
814
|
+
...(input.attributes || {}),
|
|
815
|
+
...(input.metadata?.attributes || {})
|
|
816
|
+
};
|
|
817
|
+
const sourceId = normalizeText(
|
|
818
|
+
input.id
|
|
819
|
+
|| attrs["data-geek"]
|
|
820
|
+
|| attrs["data-geekid"]
|
|
821
|
+
|| attrs["data-expect"]
|
|
822
|
+
|| attrs["data-uid"]
|
|
823
|
+
|| attrs["data-securityid"]
|
|
824
|
+
|| attrs.encryptgeekid
|
|
825
|
+
|| attrs["data-lid"]
|
|
826
|
+
|| attrs["data-jid"]
|
|
827
|
+
|| attrs["data-itemid"]
|
|
828
|
+
|| attrs.geekid
|
|
829
|
+
|| attrs.expect
|
|
830
|
+
|| attrs.uid
|
|
831
|
+
|| attrs.securityid
|
|
832
|
+
|| attrs.jid
|
|
833
|
+
|| attrs.lid
|
|
834
|
+
|| attrs.href
|
|
835
|
+
|| ""
|
|
836
|
+
) || null;
|
|
837
|
+
const inferredName = normalizeText(input.identity?.name || input.name || firstUsefulLine(lines) || "") || null;
|
|
838
|
+
const fullText = collectTextParts({
|
|
839
|
+
...input,
|
|
840
|
+
text: rawText,
|
|
841
|
+
raw_text: rawText,
|
|
842
|
+
identity: {
|
|
843
|
+
...(input.identity || {}),
|
|
844
|
+
name: inferredName
|
|
845
|
+
}
|
|
846
|
+
}).join("\n");
|
|
847
|
+
const degree = input.identity?.degree || input.degree || parseDegree(fullText);
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
schema_version: 1,
|
|
851
|
+
domain,
|
|
852
|
+
source: normalizeText(input.source || "unknown") || "unknown",
|
|
853
|
+
id: sourceId,
|
|
854
|
+
identity: {
|
|
855
|
+
name: inferredName,
|
|
856
|
+
title: normalizeText(input.identity?.title || input.title || "") || null,
|
|
857
|
+
current_position: normalizeText(input.identity?.current_position || input.current_position || "") || null,
|
|
858
|
+
current_company: normalizeText(input.identity?.current_company || input.current_company || "") || null,
|
|
859
|
+
school: normalizeText(input.identity?.school || input.school || "") || null,
|
|
860
|
+
major: normalizeText(input.identity?.major || input.major || "") || null,
|
|
861
|
+
degree,
|
|
862
|
+
years_experience: input.identity?.years_experience ?? input.years_experience ?? parseYearsExperience(fullText),
|
|
863
|
+
age: input.identity?.age ?? input.age ?? parseAge(fullText),
|
|
864
|
+
gender: input.identity?.gender || input.gender || parseGender(fullText)
|
|
865
|
+
},
|
|
866
|
+
tags: unique(input.tags || []),
|
|
867
|
+
text: {
|
|
868
|
+
summary: lines.slice(0, 8).join("\n"),
|
|
869
|
+
raw: rawText
|
|
870
|
+
},
|
|
871
|
+
links: {
|
|
872
|
+
href: normalizeText(input.href || attrs.href || "") || null
|
|
873
|
+
},
|
|
874
|
+
metadata: {
|
|
875
|
+
...(input.metadata || {}),
|
|
876
|
+
attributes: attrs,
|
|
877
|
+
normalized_at: input.normalized_at || nowIso()
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function normalizeCandidateFromHtml({
|
|
883
|
+
domain = "recommend",
|
|
884
|
+
source = "dom",
|
|
885
|
+
html,
|
|
886
|
+
attributes = {},
|
|
887
|
+
metadata = {}
|
|
888
|
+
} = {}) {
|
|
889
|
+
const parsedAttributes = parseHtmlAttributes(html);
|
|
890
|
+
return normalizeCandidateProfile({
|
|
891
|
+
domain,
|
|
892
|
+
source,
|
|
893
|
+
text: htmlToText(html),
|
|
894
|
+
attributes: {
|
|
895
|
+
...parsedAttributes,
|
|
896
|
+
...attributes
|
|
897
|
+
},
|
|
898
|
+
metadata: {
|
|
899
|
+
...metadata,
|
|
900
|
+
html_length: String(html || "").length
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function normalizeKeywordList(value) {
|
|
906
|
+
if (!value) return [];
|
|
907
|
+
if (Array.isArray(value)) return unique(value);
|
|
908
|
+
return unique(String(value).split(/[,\n,、|/]/));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function keywordMatches(text, keywords) {
|
|
912
|
+
const haystack = compact(text);
|
|
913
|
+
return keywords.filter((keyword) => haystack.includes(compact(keyword)));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function degreeAtLeast(candidateDegree, minimumDegree) {
|
|
917
|
+
if (!minimumDegree) return true;
|
|
918
|
+
const candidateRank = DEGREE_RANK[candidateDegree] || 0;
|
|
919
|
+
const minimumRank = DEGREE_RANK[minimumDegree] || 0;
|
|
920
|
+
return candidateRank >= minimumRank;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export function screenCandidate(candidateInput, criteria = {}) {
|
|
924
|
+
const candidate = candidateInput?.schema_version
|
|
925
|
+
? candidateInput
|
|
926
|
+
: normalizeCandidateProfile(candidateInput);
|
|
927
|
+
const text = [
|
|
928
|
+
candidate.text?.raw,
|
|
929
|
+
candidate.text?.summary,
|
|
930
|
+
...Object.values(candidate.identity || {}).map((value) => value == null ? "" : String(value)),
|
|
931
|
+
...(candidate.tags || [])
|
|
932
|
+
].join("\n");
|
|
933
|
+
const requiredKeywords = normalizeKeywordList(criteria.required_keywords || criteria.requiredKeywords);
|
|
934
|
+
const preferredKeywords = normalizeKeywordList(criteria.preferred_keywords || criteria.preferredKeywords || criteria.criteria);
|
|
935
|
+
const excludedKeywords = normalizeKeywordList(criteria.excluded_keywords || criteria.excludedKeywords);
|
|
936
|
+
const matchedRequired = keywordMatches(text, requiredKeywords);
|
|
937
|
+
const matchedPreferred = keywordMatches(text, preferredKeywords);
|
|
938
|
+
const matchedExcluded = keywordMatches(text, excludedKeywords);
|
|
939
|
+
const reasons = [];
|
|
940
|
+
let score = 0;
|
|
941
|
+
|
|
942
|
+
if (requiredKeywords.length > 0) {
|
|
943
|
+
if (matchedRequired.length === requiredKeywords.length) {
|
|
944
|
+
score += 60;
|
|
945
|
+
reasons.push(`Matched all required keywords: ${matchedRequired.join(", ")}`);
|
|
946
|
+
} else {
|
|
947
|
+
const missing = requiredKeywords.filter((keyword) => !matchedRequired.includes(keyword));
|
|
948
|
+
reasons.push(`Missing required keywords: ${missing.join(", ")}`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (preferredKeywords.length > 0) {
|
|
953
|
+
score += Math.round((matchedPreferred.length / preferredKeywords.length) * 30);
|
|
954
|
+
if (matchedPreferred.length) {
|
|
955
|
+
reasons.push(`Matched preferred keywords: ${matchedPreferred.join(", ")}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (matchedExcluded.length > 0) {
|
|
960
|
+
score -= 80;
|
|
961
|
+
reasons.push(`Matched excluded keywords: ${matchedExcluded.join(", ")}`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const minimumDegree = criteria.minimum_degree || criteria.minimumDegree || null;
|
|
965
|
+
const degreeOk = degreeAtLeast(candidate.identity?.degree, minimumDegree);
|
|
966
|
+
if (minimumDegree) {
|
|
967
|
+
if (degreeOk) {
|
|
968
|
+
score += 10;
|
|
969
|
+
reasons.push(`Degree meets minimum: ${candidate.identity?.degree || "unknown"} >= ${minimumDegree}`);
|
|
970
|
+
} else {
|
|
971
|
+
reasons.push(`Degree below or unknown for minimum: ${minimumDegree}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const hasCriteria = (
|
|
976
|
+
requiredKeywords.length > 0
|
|
977
|
+
|| preferredKeywords.length > 0
|
|
978
|
+
|| excludedKeywords.length > 0
|
|
979
|
+
|| Boolean(minimumDegree)
|
|
980
|
+
);
|
|
981
|
+
const hasRequired = requiredKeywords.length === 0 || matchedRequired.length === requiredKeywords.length;
|
|
982
|
+
const passed = hasCriteria && hasRequired && degreeOk && matchedExcluded.length === 0;
|
|
983
|
+
const boundedScore = Math.max(0, Math.min(100, hasCriteria ? score : 0));
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
schema_version: 1,
|
|
987
|
+
status: passed ? "pass" : "review",
|
|
988
|
+
passed,
|
|
989
|
+
score: boundedScore,
|
|
990
|
+
reasons: reasons.length ? reasons : ["No explicit screening criteria supplied; candidate normalized for review."],
|
|
991
|
+
matched: {
|
|
992
|
+
required_keywords: matchedRequired,
|
|
993
|
+
preferred_keywords: matchedPreferred,
|
|
994
|
+
excluded_keywords: matchedExcluded
|
|
995
|
+
},
|
|
996
|
+
candidate: {
|
|
997
|
+
domain: candidate.domain,
|
|
998
|
+
source: candidate.source,
|
|
999
|
+
id: candidate.id,
|
|
1000
|
+
identity: candidate.identity
|
|
1001
|
+
},
|
|
1002
|
+
screened_at: nowIso()
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export function buildScreeningLlmMessages({
|
|
1007
|
+
candidate,
|
|
1008
|
+
criteria,
|
|
1009
|
+
imageEvidence = null,
|
|
1010
|
+
imagePaths = [],
|
|
1011
|
+
imageInputs = null,
|
|
1012
|
+
maxImages = 8,
|
|
1013
|
+
imageDetail = "high"
|
|
1014
|
+
}) {
|
|
1015
|
+
const safeCriteria = normalizeText(criteria || "判断候选人是否符合本次招聘筛选标准");
|
|
1016
|
+
const safeText = String(candidate?.text?.raw || candidate?.text || "");
|
|
1017
|
+
const images = Array.isArray(imageInputs)
|
|
1018
|
+
? imageInputs
|
|
1019
|
+
: buildScreeningLlmImageInputs({
|
|
1020
|
+
imageEvidence,
|
|
1021
|
+
imagePaths,
|
|
1022
|
+
maxImages,
|
|
1023
|
+
detail: imageDetail
|
|
1024
|
+
});
|
|
1025
|
+
const prompt =
|
|
1026
|
+
`请根据以下标准判断候选人是否通过筛选。\n\n筛选标准:\n${safeCriteria}\n\n`
|
|
1027
|
+
+ `候选人信息:\n${safeText || "候选人的完整简历信息在后续截图中,请按截图顺序阅读。"}\n\n`
|
|
1028
|
+
+ (images.length
|
|
1029
|
+
? `候选人简历截图共 ${images.length} 张,按从上到下的滚动顺序排列。请完整阅读所有截图后再判断。\n\n`
|
|
1030
|
+
: "")
|
|
1031
|
+
+ "要求:\n"
|
|
1032
|
+
+ "1) 只能依据候选人信息或截图中真实出现的内容判断。\n"
|
|
1033
|
+
+ "2) 若证据不足或截图无法确认,必须返回 passed=false。\n"
|
|
1034
|
+
+ "3) 不要输出评估原因、证据列表、解释或额外文字。\n"
|
|
1035
|
+
+ "4) 只返回 JSON,格式为:"
|
|
1036
|
+
+ "{\"passed\": true/false}";
|
|
1037
|
+
const userContent = images.length
|
|
1038
|
+
? [
|
|
1039
|
+
{ type: "text", text: prompt },
|
|
1040
|
+
...images.map((image) => ({
|
|
1041
|
+
type: "image_url",
|
|
1042
|
+
image_url: image.image_url
|
|
1043
|
+
}))
|
|
1044
|
+
]
|
|
1045
|
+
: prompt;
|
|
1046
|
+
return [
|
|
1047
|
+
{
|
|
1048
|
+
role: "system",
|
|
1049
|
+
content:
|
|
1050
|
+
"你是一位严谨的招聘筛选助手。必须完整阅读输入内容,严禁编造不存在的候选人经历。"
|
|
1051
|
+
+ "只能返回严格 JSON,不要输出原因、证据或额外文字。"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
role: "user",
|
|
1055
|
+
content: userContent
|
|
1056
|
+
}
|
|
1057
|
+
];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export async function callScreeningLlm({
|
|
1061
|
+
candidate,
|
|
1062
|
+
criteria,
|
|
1063
|
+
config = {},
|
|
1064
|
+
timeoutMs = 60000,
|
|
1065
|
+
imageEvidence = null,
|
|
1066
|
+
imagePaths = [],
|
|
1067
|
+
maxImages = 8,
|
|
1068
|
+
imageDetail = "high"
|
|
1069
|
+
} = {}) {
|
|
1070
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
1071
|
+
const apiKey = normalizeText(config.apiKey);
|
|
1072
|
+
const model = normalizeText(config.model);
|
|
1073
|
+
if (!baseUrl || !apiKey || !model) {
|
|
1074
|
+
throw new Error("Missing LLM config fields: baseUrl/apiKey/model");
|
|
1075
|
+
}
|
|
1076
|
+
const imageInputs = buildScreeningLlmImageInputs({
|
|
1077
|
+
imageEvidence,
|
|
1078
|
+
imagePaths,
|
|
1079
|
+
maxImages: config.llmImageLimit || config.imageLimit || maxImages,
|
|
1080
|
+
detail: config.llmImageDetail || config.imageDetail || imageDetail
|
|
1081
|
+
});
|
|
1082
|
+
if (!candidate?.text?.raw && !candidate?.text && !imageInputs.length) {
|
|
1083
|
+
throw new Error("Candidate text and image evidence are empty");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const payload = {
|
|
1087
|
+
model,
|
|
1088
|
+
temperature: 0.1,
|
|
1089
|
+
messages: buildScreeningLlmMessages({
|
|
1090
|
+
candidate,
|
|
1091
|
+
criteria,
|
|
1092
|
+
imageInputs
|
|
1093
|
+
})
|
|
1094
|
+
};
|
|
1095
|
+
applyChatCompletionThinking(payload, {
|
|
1096
|
+
baseUrl,
|
|
1097
|
+
model,
|
|
1098
|
+
thinkingLevel: config.llmThinkingLevel || config.thinkingLevel || config.reasoningEffort
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
const controller = new AbortController();
|
|
1102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1103
|
+
try {
|
|
1104
|
+
const headers = {
|
|
1105
|
+
"Content-Type": "application/json",
|
|
1106
|
+
Authorization: `Bearer ${apiKey}`
|
|
1107
|
+
};
|
|
1108
|
+
if (config.openaiOrganization) headers["OpenAI-Organization"] = config.openaiOrganization;
|
|
1109
|
+
if (config.openaiProject) headers["OpenAI-Project"] = config.openaiProject;
|
|
1110
|
+
|
|
1111
|
+
const response = await fetch(buildChatCompletionsUrl(baseUrl), {
|
|
1112
|
+
method: "POST",
|
|
1113
|
+
headers,
|
|
1114
|
+
body: JSON.stringify(payload),
|
|
1115
|
+
signal: controller.signal
|
|
1116
|
+
});
|
|
1117
|
+
const responseText = await response.text();
|
|
1118
|
+
if (!response.ok) {
|
|
1119
|
+
throw new Error(`LLM request failed: ${response.status} ${responseText.slice(0, 400)}`);
|
|
1120
|
+
}
|
|
1121
|
+
const json = tryParseJson(responseText);
|
|
1122
|
+
if (!json) {
|
|
1123
|
+
throw new Error("LLM response was not valid JSON");
|
|
1124
|
+
}
|
|
1125
|
+
const choice = json?.choices?.[0] || {};
|
|
1126
|
+
const content = flattenChatMessageContent(choice?.message?.content);
|
|
1127
|
+
const reasoningContent = collectLlmReasoningText(choice);
|
|
1128
|
+
const parsed = tryExtractJsonObject(content) || tryExtractJsonObject(reasoningContent);
|
|
1129
|
+
const passed = parsePassedDecision(parsed?.passed);
|
|
1130
|
+
if (passed === null) {
|
|
1131
|
+
throw new Error(`LLM response missing boolean passed decision: ${content.slice(0, 240)}`);
|
|
1132
|
+
}
|
|
1133
|
+
const evidence = Array.isArray(parsed?.evidence)
|
|
1134
|
+
? parsed.evidence.map(normalizeText).filter(Boolean)
|
|
1135
|
+
: [];
|
|
1136
|
+
const decisionCot = firstUsefulLine([
|
|
1137
|
+
parsed?.cot,
|
|
1138
|
+
parsed?.decision_cot,
|
|
1139
|
+
parsed?.reasoning,
|
|
1140
|
+
parsed?.chain_of_thought,
|
|
1141
|
+
reasoningContent
|
|
1142
|
+
].map(normalizeBlockText).filter(Boolean)) || reasoningContent;
|
|
1143
|
+
return {
|
|
1144
|
+
ok: true,
|
|
1145
|
+
provider: {
|
|
1146
|
+
baseUrl: baseUrl.replace(/\/\/[^/]+/, "//[redacted-host]"),
|
|
1147
|
+
model
|
|
1148
|
+
},
|
|
1149
|
+
passed,
|
|
1150
|
+
reason: "",
|
|
1151
|
+
evidence,
|
|
1152
|
+
cot: decisionCot,
|
|
1153
|
+
decision_cot: decisionCot,
|
|
1154
|
+
reasoning_content: reasoningContent,
|
|
1155
|
+
raw_model_output: content,
|
|
1156
|
+
usage: json.usage || null,
|
|
1157
|
+
finish_reason: choice.finish_reason || null,
|
|
1158
|
+
raw_content_length: content.length,
|
|
1159
|
+
image_input_count: imageInputs.length,
|
|
1160
|
+
image_inputs: summarizeLlmImageInputs(imageInputs),
|
|
1161
|
+
screened_at: nowIso()
|
|
1162
|
+
};
|
|
1163
|
+
} finally {
|
|
1164
|
+
clearTimeout(timer);
|
|
1165
|
+
}
|
|
1166
|
+
}
|