@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.
Files changed (53) hide show
  1. package/bin/boss-recommend-mcp.js +4 -4
  2. package/config/screening-config.example.json +27 -27
  3. package/package.json +1 -1
  4. package/scripts/postinstall.cjs +44 -44
  5. package/skills/boss-chat/README.md +39 -39
  6. package/skills/boss-chat/SKILL.md +93 -93
  7. package/skills/boss-recommend-pipeline/README.md +12 -12
  8. package/skills/boss-recommend-pipeline/SKILL.md +180 -180
  9. package/skills/boss-recruit-pipeline/README.md +17 -17
  10. package/skills/boss-recruit-pipeline/SKILL.md +58 -58
  11. package/src/chat-mcp.js +1780 -1780
  12. package/src/chat-runtime-config.js +749 -749
  13. package/src/cli.js +3054 -3054
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +1586 -1453
  16. package/src/core/capture/index.js +1201 -1201
  17. package/src/core/cv-acquisition/index.js +238 -238
  18. package/src/core/cv-capture-target/index.js +299 -299
  19. package/src/core/greet-quota/index.js +54 -54
  20. package/src/core/infinite-list/index.js +1326 -1326
  21. package/src/core/reporting/legacy-csv.js +341 -341
  22. package/src/core/run/timing.js +33 -33
  23. package/src/core/self-heal/index.js +973 -973
  24. package/src/core/self-heal/viewport.js +564 -564
  25. package/src/domains/chat/cards.js +137 -137
  26. package/src/domains/chat/constants.js +221 -221
  27. package/src/domains/chat/detail.js +1668 -1668
  28. package/src/domains/chat/index.js +7 -7
  29. package/src/domains/chat/jobs.js +592 -592
  30. package/src/domains/chat/page-guard.js +98 -98
  31. package/src/domains/chat/roots.js +56 -56
  32. package/src/domains/chat/run-service.js +1977 -1977
  33. package/src/domains/recommend/actions.js +457 -457
  34. package/src/domains/recommend/cards.js +243 -243
  35. package/src/domains/recommend/constants.js +165 -165
  36. package/src/domains/recommend/filters.js +610 -610
  37. package/src/domains/recommend/index.js +10 -10
  38. package/src/domains/recommend/jobs.js +316 -316
  39. package/src/domains/recommend/refresh.js +472 -472
  40. package/src/domains/recommend/roots.js +80 -80
  41. package/src/domains/recommend/scopes.js +246 -246
  42. package/src/domains/recruit/actions.js +277 -277
  43. package/src/domains/recruit/cards.js +74 -74
  44. package/src/domains/recruit/constants.js +167 -167
  45. package/src/domains/recruit/detail.js +461 -461
  46. package/src/domains/recruit/index.js +9 -9
  47. package/src/domains/recruit/instruction-parser.js +451 -451
  48. package/src/domains/recruit/refresh.js +44 -44
  49. package/src/domains/recruit/roots.js +68 -68
  50. package/src/domains/recruit/run-service.js +1207 -1207
  51. package/src/domains/recruit/search.js +1202 -1202
  52. package/src/recommend-mcp.js +22 -22
  53. 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
+ }