@reconcrap/boss-recommend-mcp 2.1.21 → 2.1.23

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