@reconcrap/boss-recommend-mcp 2.0.47 → 2.0.48
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/bin/boss-recommend-mcp.js +4 -4
- package/config/screening-config.example.json +27 -27
- package/package.json +1 -1
- package/scripts/postinstall.cjs +44 -44
- package/skills/boss-chat/README.md +39 -39
- package/skills/boss-chat/SKILL.md +93 -93
- package/skills/boss-recommend-pipeline/README.md +12 -12
- package/skills/boss-recommend-pipeline/SKILL.md +180 -180
- package/skills/boss-recruit-pipeline/README.md +17 -17
- package/skills/boss-recruit-pipeline/SKILL.md +58 -58
- package/src/chat-mcp.js +1780 -1780
- package/src/chat-runtime-config.js +749 -749
- package/src/cli.js +3054 -3054
- package/src/core/boss-cards/index.js +199 -199
- package/src/core/browser/index.js +1586 -1453
- 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 +54 -54
- package/src/core/infinite-list/index.js +1326 -1326
- package/src/core/reporting/legacy-csv.js +341 -341
- package/src/core/run/timing.js +33 -33
- package/src/core/self-heal/index.js +973 -973
- package/src/core/self-heal/viewport.js +564 -564
- package/src/domains/chat/cards.js +137 -137
- package/src/domains/chat/constants.js +221 -221
- package/src/domains/chat/detail.js +1668 -1668
- package/src/domains/chat/index.js +7 -7
- package/src/domains/chat/jobs.js +592 -592
- package/src/domains/chat/page-guard.js +98 -98
- package/src/domains/chat/roots.js +56 -56
- package/src/domains/chat/run-service.js +1977 -1977
- package/src/domains/recommend/actions.js +457 -457
- package/src/domains/recommend/cards.js +243 -243
- package/src/domains/recommend/constants.js +165 -165
- package/src/domains/recommend/filters.js +610 -610
- package/src/domains/recommend/index.js +10 -10
- package/src/domains/recommend/jobs.js +316 -316
- package/src/domains/recommend/refresh.js +472 -472
- package/src/domains/recommend/roots.js +80 -80
- 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 +167 -167
- package/src/domains/recruit/detail.js +461 -461
- package/src/domains/recruit/index.js +9 -9
- package/src/domains/recruit/instruction-parser.js +451 -451
- package/src/domains/recruit/refresh.js +44 -44
- package/src/domains/recruit/roots.js +68 -68
- package/src/domains/recruit/run-service.js +1207 -1207
- package/src/domains/recruit/search.js +1202 -1202
- package/src/recommend-mcp.js +22 -22
- package/src/recruit-mcp.js +1338 -1338
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export const LEGACY_INPUT_HEADER = ["运行输入字段", "运行输入值"];
|
|
5
|
-
|
|
6
|
-
export const LEGACY_RESULT_HEADER = [
|
|
7
|
-
"姓名",
|
|
8
|
-
"最高学历学校",
|
|
9
|
-
"最高学历专业",
|
|
10
|
-
"最近工作公司",
|
|
11
|
-
"最近工作职位",
|
|
12
|
-
"评估通过详细原因",
|
|
13
|
-
"处理结果",
|
|
14
|
-
"判断依据(CoT)",
|
|
15
|
-
"动作执行结果",
|
|
16
|
-
"简历来源",
|
|
17
|
-
"原始判定通过",
|
|
18
|
-
"最终判定通过",
|
|
19
|
-
"证据总数",
|
|
20
|
-
"证据命中数",
|
|
21
|
-
"证据门控降级",
|
|
22
|
-
"错误码",
|
|
23
|
-
"错误信息",
|
|
24
|
-
"候选人ID",
|
|
25
|
-
"总耗时ms",
|
|
26
|
-
"候选卡片读取ms",
|
|
27
|
-
"点击候选人ms",
|
|
28
|
-
"详情打开ms",
|
|
29
|
-
"network简历等待ms",
|
|
30
|
-
"文本模型ms",
|
|
31
|
-
"截图获取ms",
|
|
32
|
-
"视觉模型ms",
|
|
33
|
-
"late network retry ms",
|
|
34
|
-
"DOM fallback ms",
|
|
35
|
-
"通过后动作ms",
|
|
36
|
-
"关闭详情ms",
|
|
37
|
-
"休息ms",
|
|
38
|
-
"checkpoint保存ms"
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const SEARCH_PARAM_ORDER = [
|
|
42
|
-
"school_tag",
|
|
43
|
-
"degree",
|
|
44
|
-
"degrees",
|
|
45
|
-
"gender",
|
|
46
|
-
"recent_not_view",
|
|
47
|
-
"city",
|
|
48
|
-
"schools",
|
|
49
|
-
"keyword",
|
|
50
|
-
"filter_recent_viewed",
|
|
51
|
-
"job",
|
|
52
|
-
"start_from",
|
|
53
|
-
"target_count",
|
|
54
|
-
"detail_source"
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const SCREEN_PARAM_ORDER = [
|
|
58
|
-
"criteria",
|
|
59
|
-
"target_count",
|
|
60
|
-
"post_action",
|
|
61
|
-
"max_greet_count"
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
function normalizeText(value) {
|
|
65
|
-
return String(value || "").replace(/\s+/g, " ").trim();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeBlockText(value) {
|
|
69
|
-
return String(value ?? "").trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function csvCell(value) {
|
|
73
|
-
const text = String(value ?? "");
|
|
74
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function ensureDirectory(dirPath) {
|
|
78
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function cloneJson(value, fallback = null) {
|
|
82
|
-
try {
|
|
83
|
-
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
84
|
-
} catch {
|
|
85
|
-
return fallback;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function formatInputValue(value) {
|
|
90
|
-
if (value === undefined) return "";
|
|
91
|
-
if (value === null) return "null";
|
|
92
|
-
if (typeof value === "string") return value;
|
|
93
|
-
return JSON.stringify(value);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function appendInputRow(rows, field, value) {
|
|
97
|
-
if (!field || value === undefined) return;
|
|
98
|
-
rows.push({
|
|
99
|
-
field,
|
|
100
|
-
value: formatInputValue(value)
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function appendPrefixedRows(rows, prefix, values = {}, order = []) {
|
|
105
|
-
const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
|
106
|
-
const emitted = new Set();
|
|
107
|
-
for (const key of order) {
|
|
108
|
-
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
109
|
-
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
110
|
-
emitted.add(key);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const key of Object.keys(source).sort()) {
|
|
114
|
-
if (emitted.has(key)) continue;
|
|
115
|
-
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function buildLegacyScreenInputRows({
|
|
120
|
-
instruction = "",
|
|
121
|
-
selectedPage = "",
|
|
122
|
-
selectedJob = null,
|
|
123
|
-
userSearchParams = {},
|
|
124
|
-
effectiveSearchParams = {},
|
|
125
|
-
screenParams = {},
|
|
126
|
-
followUp = null,
|
|
127
|
-
extraRows = []
|
|
128
|
-
} = {}) {
|
|
129
|
-
const rows = [];
|
|
130
|
-
appendInputRow(rows, "instruction", instruction);
|
|
131
|
-
appendInputRow(rows, "selected_page", selectedPage);
|
|
132
|
-
|
|
133
|
-
if (selectedJob && typeof selectedJob === "object") {
|
|
134
|
-
appendInputRow(rows, "selected_job.value", selectedJob.value);
|
|
135
|
-
appendInputRow(rows, "selected_job.title", selectedJob.title);
|
|
136
|
-
appendInputRow(rows, "selected_job.label", selectedJob.label);
|
|
137
|
-
} else if (selectedJob) {
|
|
138
|
-
appendInputRow(rows, "selected_job.label", selectedJob);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
appendPrefixedRows(rows, "user_search_params", userSearchParams, SEARCH_PARAM_ORDER);
|
|
142
|
-
appendPrefixedRows(rows, "effective_search_params", effectiveSearchParams, SEARCH_PARAM_ORDER);
|
|
143
|
-
appendPrefixedRows(rows, "screen_params", screenParams, SCREEN_PARAM_ORDER);
|
|
144
|
-
appendInputRow(rows, "follow_up", followUp);
|
|
145
|
-
|
|
146
|
-
for (const row of extraRows || []) {
|
|
147
|
-
if (Array.isArray(row)) appendInputRow(rows, row[0], row[1]);
|
|
148
|
-
else appendInputRow(rows, row?.field, row?.value);
|
|
149
|
-
}
|
|
150
|
-
return rows;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function defaultLegacyCsvPathForReport(reportPath) {
|
|
154
|
-
const resolved = path.resolve(reportPath);
|
|
155
|
-
const parsed = path.parse(resolved);
|
|
156
|
-
return path.join(parsed.dir, `${parsed.name}.csv`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function firstDefined(...values) {
|
|
160
|
-
for (const value of values) {
|
|
161
|
-
if (value !== undefined && value !== null) return value;
|
|
162
|
-
}
|
|
163
|
-
return "";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function firstText(...values) {
|
|
167
|
-
for (const value of values) {
|
|
168
|
-
const text = normalizeBlockText(value);
|
|
169
|
-
if (text) return text;
|
|
170
|
-
}
|
|
171
|
-
return "";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function firstBoolean(...values) {
|
|
175
|
-
for (const value of values) {
|
|
176
|
-
if (typeof value === "boolean") return value;
|
|
177
|
-
if (typeof value === "number") return value !== 0;
|
|
178
|
-
const text = normalizeText(value).toLowerCase();
|
|
179
|
-
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(text)) return true;
|
|
180
|
-
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(text)) return false;
|
|
181
|
-
}
|
|
182
|
-
return "";
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function evidenceCount(llm = {}) {
|
|
186
|
-
if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
|
|
187
|
-
if (Array.isArray(llm.evidence)) return llm.evidence.length;
|
|
188
|
-
return "";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function actionResultText(row = {}) {
|
|
192
|
-
const action = row.post_action || row.action || {};
|
|
193
|
-
if (action.requested === true && !action.skipped) {
|
|
194
|
-
return firstText(action.reason, action.kind, action.type, "requested");
|
|
195
|
-
}
|
|
196
|
-
if (action.skipped) {
|
|
197
|
-
return firstText(action.reason, action.kind, action.type, "skipped");
|
|
198
|
-
}
|
|
199
|
-
if (action.action_clicked || action.clicked) {
|
|
200
|
-
return firstText(action.effective, action.requested, action.kind, action.type, "clicked");
|
|
201
|
-
}
|
|
202
|
-
if (action.action_attempted || action.attempted) return "failed";
|
|
203
|
-
if (action.requested && action.requested !== "none") return "not_attempted";
|
|
204
|
-
return "";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function pickLlm(row = {}) {
|
|
208
|
-
return row.llm
|
|
209
|
-
|| row.llm_screening
|
|
210
|
-
|| row.detail?.llm_screening
|
|
211
|
-
|| row.screening?.llm
|
|
212
|
-
|| {};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function pickCandidate(row = {}) {
|
|
216
|
-
const screeningCandidate = row.screening?.candidate || {};
|
|
217
|
-
const candidate = row.candidate || row.card_candidate || {};
|
|
218
|
-
return {
|
|
219
|
-
...screeningCandidate,
|
|
220
|
-
...candidate,
|
|
221
|
-
identity: {
|
|
222
|
-
...(screeningCandidate.identity || {}),
|
|
223
|
-
...(candidate.identity || {})
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function timingValue(row = {}, ...keys) {
|
|
229
|
-
const timings = row.timings || row.timing || {};
|
|
230
|
-
const detail = row.detail || {};
|
|
231
|
-
const acquisition = detail.cv_acquisition || {};
|
|
232
|
-
const fallbackByKey = {
|
|
233
|
-
network_cv_wait_ms: acquisition.network_wait?.elapsed_ms,
|
|
234
|
-
screenshot_capture_ms: acquisition.image_evidence?.elapsed_ms || detail.image_evidence?.elapsed_ms,
|
|
235
|
-
dom_fallback_ms: acquisition.content_wait?.elapsed_ms,
|
|
236
|
-
close_detail_ms: detail.close_result?.elapsed_ms,
|
|
237
|
-
post_action_ms: row.post_action?.elapsed_ms
|
|
238
|
-
};
|
|
239
|
-
for (const key of keys) {
|
|
240
|
-
const value = firstDefined(row[key], timings[key], fallbackByKey[key]);
|
|
241
|
-
if (value !== "") return value;
|
|
242
|
-
}
|
|
243
|
-
return "";
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function legacyScreenResultRow(row = {}) {
|
|
247
|
-
const candidate = pickCandidate(row);
|
|
248
|
-
const identity = candidate.identity || {};
|
|
249
|
-
const detail = row.detail || {};
|
|
250
|
-
const screening = row.screening || {};
|
|
251
|
-
const llm = pickLlm(row);
|
|
252
|
-
const rawPassed = firstBoolean(llm.passed, screening.passed, row.raw_passed, row.passed);
|
|
253
|
-
const finalPassed = firstBoolean(row.final_passed, row.finalPassed, rawPassed);
|
|
254
|
-
const hasError = Boolean(row.error || row.error_code || row.error_message);
|
|
255
|
-
const processResult = hasError
|
|
256
|
-
? "error"
|
|
257
|
-
: finalPassed === true
|
|
258
|
-
? "passed"
|
|
259
|
-
: "skipped";
|
|
260
|
-
const cot = firstText(
|
|
261
|
-
llm.reasoning_content,
|
|
262
|
-
llm.raw_reasoning_content,
|
|
263
|
-
llm.decision_cot,
|
|
264
|
-
llm.cot,
|
|
265
|
-
llm.raw_model_output,
|
|
266
|
-
llm.raw_content,
|
|
267
|
-
row.decision_cot,
|
|
268
|
-
row.cot,
|
|
269
|
-
screening.decision_cot,
|
|
270
|
-
screening.cot
|
|
271
|
-
);
|
|
272
|
-
const error = row.error || {};
|
|
273
|
-
const cvSource = firstText(
|
|
274
|
-
detail.cv_acquisition?.source,
|
|
275
|
-
row.cv_source,
|
|
276
|
-
candidate.source,
|
|
277
|
-
screening.candidate?.source
|
|
278
|
-
);
|
|
279
|
-
const totalEvidence = evidenceCount(llm);
|
|
280
|
-
return [
|
|
281
|
-
identity.name,
|
|
282
|
-
identity.school,
|
|
283
|
-
identity.major,
|
|
284
|
-
identity.current_company,
|
|
285
|
-
identity.current_position,
|
|
286
|
-
"",
|
|
287
|
-
processResult,
|
|
288
|
-
cot,
|
|
289
|
-
actionResultText(row),
|
|
290
|
-
cvSource,
|
|
291
|
-
rawPassed,
|
|
292
|
-
finalPassed,
|
|
293
|
-
totalEvidence,
|
|
294
|
-
totalEvidence,
|
|
295
|
-
"",
|
|
296
|
-
row.error_code || error.code || error.name || (llm.error ? "LLM_SCREENING_ERROR" : ""),
|
|
297
|
-
row.error_message || error.message || llm.error || "",
|
|
298
|
-
candidate.id || row.candidate_id || "",
|
|
299
|
-
timingValue(row, "total_ms"),
|
|
300
|
-
timingValue(row, "card_read_ms"),
|
|
301
|
-
timingValue(row, "candidate_click_ms"),
|
|
302
|
-
timingValue(row, "detail_open_ms"),
|
|
303
|
-
timingValue(row, "network_cv_wait_ms"),
|
|
304
|
-
timingValue(row, "text_model_ms"),
|
|
305
|
-
timingValue(row, "screenshot_capture_ms"),
|
|
306
|
-
timingValue(row, "vision_model_ms"),
|
|
307
|
-
timingValue(row, "late_network_retry_ms"),
|
|
308
|
-
timingValue(row, "dom_fallback_ms"),
|
|
309
|
-
timingValue(row, "post_action_ms"),
|
|
310
|
-
timingValue(row, "close_detail_ms"),
|
|
311
|
-
timingValue(row, "sleep_ms"),
|
|
312
|
-
timingValue(row, "checkpoint_save_ms")
|
|
313
|
-
];
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function writeLegacyScreenCsv(filePath, {
|
|
317
|
-
inputRows = [],
|
|
318
|
-
results = []
|
|
319
|
-
} = {}) {
|
|
320
|
-
const resolved = path.resolve(filePath);
|
|
321
|
-
ensureDirectory(path.dirname(resolved));
|
|
322
|
-
const normalizedInputRows = (inputRows || []).map((row) => ({
|
|
323
|
-
field: row?.field ?? row?.[0] ?? "",
|
|
324
|
-
value: row?.value ?? row?.[1] ?? ""
|
|
325
|
-
}));
|
|
326
|
-
const lines = [
|
|
327
|
-
LEGACY_INPUT_HEADER.map(csvCell).join(","),
|
|
328
|
-
...normalizedInputRows.map((row) => [row.field, row.value].map(csvCell).join(",")),
|
|
329
|
-
"",
|
|
330
|
-
LEGACY_RESULT_HEADER.map(csvCell).join(","),
|
|
331
|
-
...(results || []).map((row) => legacyScreenResultRow(row).map(csvCell).join(","))
|
|
332
|
-
];
|
|
333
|
-
const tempPath = `${resolved}.tmp`;
|
|
334
|
-
fs.writeFileSync(tempPath, `\uFEFF${lines.join("\n")}\n`, "utf8");
|
|
335
|
-
fs.renameSync(tempPath, resolved);
|
|
336
|
-
return resolved;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function cloneReportInput(value, fallback = {}) {
|
|
340
|
-
return cloneJson(value, fallback);
|
|
341
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const LEGACY_INPUT_HEADER = ["运行输入字段", "运行输入值"];
|
|
5
|
+
|
|
6
|
+
export const LEGACY_RESULT_HEADER = [
|
|
7
|
+
"姓名",
|
|
8
|
+
"最高学历学校",
|
|
9
|
+
"最高学历专业",
|
|
10
|
+
"最近工作公司",
|
|
11
|
+
"最近工作职位",
|
|
12
|
+
"评估通过详细原因",
|
|
13
|
+
"处理结果",
|
|
14
|
+
"判断依据(CoT)",
|
|
15
|
+
"动作执行结果",
|
|
16
|
+
"简历来源",
|
|
17
|
+
"原始判定通过",
|
|
18
|
+
"最终判定通过",
|
|
19
|
+
"证据总数",
|
|
20
|
+
"证据命中数",
|
|
21
|
+
"证据门控降级",
|
|
22
|
+
"错误码",
|
|
23
|
+
"错误信息",
|
|
24
|
+
"候选人ID",
|
|
25
|
+
"总耗时ms",
|
|
26
|
+
"候选卡片读取ms",
|
|
27
|
+
"点击候选人ms",
|
|
28
|
+
"详情打开ms",
|
|
29
|
+
"network简历等待ms",
|
|
30
|
+
"文本模型ms",
|
|
31
|
+
"截图获取ms",
|
|
32
|
+
"视觉模型ms",
|
|
33
|
+
"late network retry ms",
|
|
34
|
+
"DOM fallback ms",
|
|
35
|
+
"通过后动作ms",
|
|
36
|
+
"关闭详情ms",
|
|
37
|
+
"休息ms",
|
|
38
|
+
"checkpoint保存ms"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const SEARCH_PARAM_ORDER = [
|
|
42
|
+
"school_tag",
|
|
43
|
+
"degree",
|
|
44
|
+
"degrees",
|
|
45
|
+
"gender",
|
|
46
|
+
"recent_not_view",
|
|
47
|
+
"city",
|
|
48
|
+
"schools",
|
|
49
|
+
"keyword",
|
|
50
|
+
"filter_recent_viewed",
|
|
51
|
+
"job",
|
|
52
|
+
"start_from",
|
|
53
|
+
"target_count",
|
|
54
|
+
"detail_source"
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SCREEN_PARAM_ORDER = [
|
|
58
|
+
"criteria",
|
|
59
|
+
"target_count",
|
|
60
|
+
"post_action",
|
|
61
|
+
"max_greet_count"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function normalizeText(value) {
|
|
65
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeBlockText(value) {
|
|
69
|
+
return String(value ?? "").trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function csvCell(value) {
|
|
73
|
+
const text = String(value ?? "");
|
|
74
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureDirectory(dirPath) {
|
|
78
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cloneJson(value, fallback = null) {
|
|
82
|
+
try {
|
|
83
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
84
|
+
} catch {
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatInputValue(value) {
|
|
90
|
+
if (value === undefined) return "";
|
|
91
|
+
if (value === null) return "null";
|
|
92
|
+
if (typeof value === "string") return value;
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function appendInputRow(rows, field, value) {
|
|
97
|
+
if (!field || value === undefined) return;
|
|
98
|
+
rows.push({
|
|
99
|
+
field,
|
|
100
|
+
value: formatInputValue(value)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function appendPrefixedRows(rows, prefix, values = {}, order = []) {
|
|
105
|
+
const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
|
106
|
+
const emitted = new Set();
|
|
107
|
+
for (const key of order) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
109
|
+
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
110
|
+
emitted.add(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const key of Object.keys(source).sort()) {
|
|
114
|
+
if (emitted.has(key)) continue;
|
|
115
|
+
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildLegacyScreenInputRows({
|
|
120
|
+
instruction = "",
|
|
121
|
+
selectedPage = "",
|
|
122
|
+
selectedJob = null,
|
|
123
|
+
userSearchParams = {},
|
|
124
|
+
effectiveSearchParams = {},
|
|
125
|
+
screenParams = {},
|
|
126
|
+
followUp = null,
|
|
127
|
+
extraRows = []
|
|
128
|
+
} = {}) {
|
|
129
|
+
const rows = [];
|
|
130
|
+
appendInputRow(rows, "instruction", instruction);
|
|
131
|
+
appendInputRow(rows, "selected_page", selectedPage);
|
|
132
|
+
|
|
133
|
+
if (selectedJob && typeof selectedJob === "object") {
|
|
134
|
+
appendInputRow(rows, "selected_job.value", selectedJob.value);
|
|
135
|
+
appendInputRow(rows, "selected_job.title", selectedJob.title);
|
|
136
|
+
appendInputRow(rows, "selected_job.label", selectedJob.label);
|
|
137
|
+
} else if (selectedJob) {
|
|
138
|
+
appendInputRow(rows, "selected_job.label", selectedJob);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
appendPrefixedRows(rows, "user_search_params", userSearchParams, SEARCH_PARAM_ORDER);
|
|
142
|
+
appendPrefixedRows(rows, "effective_search_params", effectiveSearchParams, SEARCH_PARAM_ORDER);
|
|
143
|
+
appendPrefixedRows(rows, "screen_params", screenParams, SCREEN_PARAM_ORDER);
|
|
144
|
+
appendInputRow(rows, "follow_up", followUp);
|
|
145
|
+
|
|
146
|
+
for (const row of extraRows || []) {
|
|
147
|
+
if (Array.isArray(row)) appendInputRow(rows, row[0], row[1]);
|
|
148
|
+
else appendInputRow(rows, row?.field, row?.value);
|
|
149
|
+
}
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function defaultLegacyCsvPathForReport(reportPath) {
|
|
154
|
+
const resolved = path.resolve(reportPath);
|
|
155
|
+
const parsed = path.parse(resolved);
|
|
156
|
+
return path.join(parsed.dir, `${parsed.name}.csv`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function firstDefined(...values) {
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
if (value !== undefined && value !== null) return value;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function firstText(...values) {
|
|
167
|
+
for (const value of values) {
|
|
168
|
+
const text = normalizeBlockText(value);
|
|
169
|
+
if (text) return text;
|
|
170
|
+
}
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function firstBoolean(...values) {
|
|
175
|
+
for (const value of values) {
|
|
176
|
+
if (typeof value === "boolean") return value;
|
|
177
|
+
if (typeof value === "number") return value !== 0;
|
|
178
|
+
const text = normalizeText(value).toLowerCase();
|
|
179
|
+
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(text)) return true;
|
|
180
|
+
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(text)) return false;
|
|
181
|
+
}
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function evidenceCount(llm = {}) {
|
|
186
|
+
if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
|
|
187
|
+
if (Array.isArray(llm.evidence)) return llm.evidence.length;
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function actionResultText(row = {}) {
|
|
192
|
+
const action = row.post_action || row.action || {};
|
|
193
|
+
if (action.requested === true && !action.skipped) {
|
|
194
|
+
return firstText(action.reason, action.kind, action.type, "requested");
|
|
195
|
+
}
|
|
196
|
+
if (action.skipped) {
|
|
197
|
+
return firstText(action.reason, action.kind, action.type, "skipped");
|
|
198
|
+
}
|
|
199
|
+
if (action.action_clicked || action.clicked) {
|
|
200
|
+
return firstText(action.effective, action.requested, action.kind, action.type, "clicked");
|
|
201
|
+
}
|
|
202
|
+
if (action.action_attempted || action.attempted) return "failed";
|
|
203
|
+
if (action.requested && action.requested !== "none") return "not_attempted";
|
|
204
|
+
return "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pickLlm(row = {}) {
|
|
208
|
+
return row.llm
|
|
209
|
+
|| row.llm_screening
|
|
210
|
+
|| row.detail?.llm_screening
|
|
211
|
+
|| row.screening?.llm
|
|
212
|
+
|| {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pickCandidate(row = {}) {
|
|
216
|
+
const screeningCandidate = row.screening?.candidate || {};
|
|
217
|
+
const candidate = row.candidate || row.card_candidate || {};
|
|
218
|
+
return {
|
|
219
|
+
...screeningCandidate,
|
|
220
|
+
...candidate,
|
|
221
|
+
identity: {
|
|
222
|
+
...(screeningCandidate.identity || {}),
|
|
223
|
+
...(candidate.identity || {})
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function timingValue(row = {}, ...keys) {
|
|
229
|
+
const timings = row.timings || row.timing || {};
|
|
230
|
+
const detail = row.detail || {};
|
|
231
|
+
const acquisition = detail.cv_acquisition || {};
|
|
232
|
+
const fallbackByKey = {
|
|
233
|
+
network_cv_wait_ms: acquisition.network_wait?.elapsed_ms,
|
|
234
|
+
screenshot_capture_ms: acquisition.image_evidence?.elapsed_ms || detail.image_evidence?.elapsed_ms,
|
|
235
|
+
dom_fallback_ms: acquisition.content_wait?.elapsed_ms,
|
|
236
|
+
close_detail_ms: detail.close_result?.elapsed_ms,
|
|
237
|
+
post_action_ms: row.post_action?.elapsed_ms
|
|
238
|
+
};
|
|
239
|
+
for (const key of keys) {
|
|
240
|
+
const value = firstDefined(row[key], timings[key], fallbackByKey[key]);
|
|
241
|
+
if (value !== "") return value;
|
|
242
|
+
}
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function legacyScreenResultRow(row = {}) {
|
|
247
|
+
const candidate = pickCandidate(row);
|
|
248
|
+
const identity = candidate.identity || {};
|
|
249
|
+
const detail = row.detail || {};
|
|
250
|
+
const screening = row.screening || {};
|
|
251
|
+
const llm = pickLlm(row);
|
|
252
|
+
const rawPassed = firstBoolean(llm.passed, screening.passed, row.raw_passed, row.passed);
|
|
253
|
+
const finalPassed = firstBoolean(row.final_passed, row.finalPassed, rawPassed);
|
|
254
|
+
const hasError = Boolean(row.error || row.error_code || row.error_message);
|
|
255
|
+
const processResult = hasError
|
|
256
|
+
? "error"
|
|
257
|
+
: finalPassed === true
|
|
258
|
+
? "passed"
|
|
259
|
+
: "skipped";
|
|
260
|
+
const cot = firstText(
|
|
261
|
+
llm.reasoning_content,
|
|
262
|
+
llm.raw_reasoning_content,
|
|
263
|
+
llm.decision_cot,
|
|
264
|
+
llm.cot,
|
|
265
|
+
llm.raw_model_output,
|
|
266
|
+
llm.raw_content,
|
|
267
|
+
row.decision_cot,
|
|
268
|
+
row.cot,
|
|
269
|
+
screening.decision_cot,
|
|
270
|
+
screening.cot
|
|
271
|
+
);
|
|
272
|
+
const error = row.error || {};
|
|
273
|
+
const cvSource = firstText(
|
|
274
|
+
detail.cv_acquisition?.source,
|
|
275
|
+
row.cv_source,
|
|
276
|
+
candidate.source,
|
|
277
|
+
screening.candidate?.source
|
|
278
|
+
);
|
|
279
|
+
const totalEvidence = evidenceCount(llm);
|
|
280
|
+
return [
|
|
281
|
+
identity.name,
|
|
282
|
+
identity.school,
|
|
283
|
+
identity.major,
|
|
284
|
+
identity.current_company,
|
|
285
|
+
identity.current_position,
|
|
286
|
+
"",
|
|
287
|
+
processResult,
|
|
288
|
+
cot,
|
|
289
|
+
actionResultText(row),
|
|
290
|
+
cvSource,
|
|
291
|
+
rawPassed,
|
|
292
|
+
finalPassed,
|
|
293
|
+
totalEvidence,
|
|
294
|
+
totalEvidence,
|
|
295
|
+
"",
|
|
296
|
+
row.error_code || error.code || error.name || (llm.error ? "LLM_SCREENING_ERROR" : ""),
|
|
297
|
+
row.error_message || error.message || llm.error || "",
|
|
298
|
+
candidate.id || row.candidate_id || "",
|
|
299
|
+
timingValue(row, "total_ms"),
|
|
300
|
+
timingValue(row, "card_read_ms"),
|
|
301
|
+
timingValue(row, "candidate_click_ms"),
|
|
302
|
+
timingValue(row, "detail_open_ms"),
|
|
303
|
+
timingValue(row, "network_cv_wait_ms"),
|
|
304
|
+
timingValue(row, "text_model_ms"),
|
|
305
|
+
timingValue(row, "screenshot_capture_ms"),
|
|
306
|
+
timingValue(row, "vision_model_ms"),
|
|
307
|
+
timingValue(row, "late_network_retry_ms"),
|
|
308
|
+
timingValue(row, "dom_fallback_ms"),
|
|
309
|
+
timingValue(row, "post_action_ms"),
|
|
310
|
+
timingValue(row, "close_detail_ms"),
|
|
311
|
+
timingValue(row, "sleep_ms"),
|
|
312
|
+
timingValue(row, "checkpoint_save_ms")
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function writeLegacyScreenCsv(filePath, {
|
|
317
|
+
inputRows = [],
|
|
318
|
+
results = []
|
|
319
|
+
} = {}) {
|
|
320
|
+
const resolved = path.resolve(filePath);
|
|
321
|
+
ensureDirectory(path.dirname(resolved));
|
|
322
|
+
const normalizedInputRows = (inputRows || []).map((row) => ({
|
|
323
|
+
field: row?.field ?? row?.[0] ?? "",
|
|
324
|
+
value: row?.value ?? row?.[1] ?? ""
|
|
325
|
+
}));
|
|
326
|
+
const lines = [
|
|
327
|
+
LEGACY_INPUT_HEADER.map(csvCell).join(","),
|
|
328
|
+
...normalizedInputRows.map((row) => [row.field, row.value].map(csvCell).join(",")),
|
|
329
|
+
"",
|
|
330
|
+
LEGACY_RESULT_HEADER.map(csvCell).join(","),
|
|
331
|
+
...(results || []).map((row) => legacyScreenResultRow(row).map(csvCell).join(","))
|
|
332
|
+
];
|
|
333
|
+
const tempPath = `${resolved}.tmp`;
|
|
334
|
+
fs.writeFileSync(tempPath, `\uFEFF${lines.join("\n")}\n`, "utf8");
|
|
335
|
+
fs.renameSync(tempPath, resolved);
|
|
336
|
+
return resolved;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function cloneReportInput(value, fallback = {}) {
|
|
340
|
+
return cloneJson(value, fallback);
|
|
341
|
+
}
|