@reconcrap/boss-recommend-mcp 1.3.30 → 1.3.32
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/config/screening-config.example.json +2 -0
- package/package.json +1 -1
- package/src/adapters.js +22 -0
- package/src/boss-chat.js +14 -1
- package/src/test-adapters-runtime.js +90 -0
- package/src/test-boss-chat.js +719 -58
- package/vendor/boss-chat-cli/src/app.js +411 -175
- package/vendor/boss-chat-cli/src/cli.js +20 -0
- package/vendor/boss-chat-cli/src/services/chrome-client.js +8 -2
- package/vendor/boss-chat-cli/src/services/llm.js +252 -84
- package/vendor/boss-chat-cli/src/services/profile-store.js +6 -0
- package/vendor/boss-chat-cli/src/services/report-store.js +301 -3
- package/vendor/boss-chat-cli/src/services/resume-capture.js +41 -126
- package/vendor/boss-chat-cli/src/services/resume-network.js +727 -0
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +44 -2
|
@@ -1,10 +1,289 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
const TIMING_BUCKETS = [
|
|
5
|
+
['initialNetworkWaitMs', '初始 network 等待'],
|
|
6
|
+
['networkRetryMs', 'network 重试'],
|
|
7
|
+
['imageCaptureMs', '简历截图'],
|
|
8
|
+
['imageModelMs', '图片模型'],
|
|
9
|
+
['lateNetworkRetryMs', '晚到 network 重试'],
|
|
10
|
+
['domFallbackMs', 'DOM 兜底'],
|
|
11
|
+
['textModelMs', '文本模型'],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const CSV_HEADER = [
|
|
15
|
+
'index',
|
|
16
|
+
'name',
|
|
17
|
+
'source_job',
|
|
18
|
+
'decision',
|
|
19
|
+
'passed',
|
|
20
|
+
'requested',
|
|
21
|
+
'resume_acquisition_mode',
|
|
22
|
+
'resume_acquisition_reason',
|
|
23
|
+
'evaluation_mode',
|
|
24
|
+
'evaluation_image_count',
|
|
25
|
+
'initial_network_wait_ms',
|
|
26
|
+
'network_retry_ms',
|
|
27
|
+
'image_capture_ms',
|
|
28
|
+
'image_model_ms',
|
|
29
|
+
'late_network_retry_ms',
|
|
30
|
+
'dom_fallback_ms',
|
|
31
|
+
'text_model_ms',
|
|
32
|
+
'timing_summary',
|
|
33
|
+
'reason',
|
|
34
|
+
'llm_summary',
|
|
35
|
+
'llm_cot',
|
|
36
|
+
'llm_evidence',
|
|
37
|
+
'llm_raw_reasoning',
|
|
38
|
+
'error_message',
|
|
39
|
+
'llm_raw_output',
|
|
40
|
+
'llm_raw_output_preview',
|
|
41
|
+
];
|
|
42
|
+
|
|
4
43
|
function timestampToken(date = new Date()) {
|
|
5
44
|
return date.toISOString().replace(/[:.]/g, '-');
|
|
6
45
|
}
|
|
7
46
|
|
|
47
|
+
function normalizeText(value) {
|
|
48
|
+
if (value === null || value === undefined) return '';
|
|
49
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function previewText(value, maxLength = 160) {
|
|
53
|
+
const normalized = normalizeText(value);
|
|
54
|
+
if (!normalized) return '';
|
|
55
|
+
if (normalized.length <= maxLength) return normalized;
|
|
56
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeMs(value) {
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
62
|
+
return Math.round(parsed);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatDurationMs(startedAt, finishedAt) {
|
|
66
|
+
const started = startedAt ? Date.parse(startedAt) : NaN;
|
|
67
|
+
const finished = finishedAt ? Date.parse(finishedAt) : NaN;
|
|
68
|
+
if (!Number.isFinite(started) || !Number.isFinite(finished) || finished < started) {
|
|
69
|
+
return '-';
|
|
70
|
+
}
|
|
71
|
+
const totalMs = Math.round(finished - started);
|
|
72
|
+
if (totalMs < 1000) return `${totalMs}ms`;
|
|
73
|
+
if (totalMs < 60_000) return `${(totalMs / 1000).toFixed(1)}s`;
|
|
74
|
+
return `${(totalMs / 60_000).toFixed(1)}m`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function csvEscape(value) {
|
|
78
|
+
return `"${String(value ?? '').replace(/"/g, '""')}"`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toResults(summary) {
|
|
82
|
+
return Array.isArray(summary?.results) ? summary.results : [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toOutcome(result) {
|
|
86
|
+
if (normalizeText(result?.decision)) return normalizeText(result.decision);
|
|
87
|
+
if (result?.passed) return 'passed';
|
|
88
|
+
if (normalizeText(result?.error)) return 'error';
|
|
89
|
+
return 'skipped';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getArtifacts(result) {
|
|
93
|
+
return result?.artifacts && typeof result.artifacts === 'object' ? result.artifacts : {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getAcquisitionMode(result) {
|
|
97
|
+
return normalizeText(getArtifacts(result).resumeAcquisitionMode);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getAcquisitionReason(result) {
|
|
101
|
+
return normalizeText(getArtifacts(result).resumeAcquisitionReason);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getTimingValue(result, key) {
|
|
105
|
+
return normalizeMs(getArtifacts(result)[key]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getLlmSummary(result) {
|
|
109
|
+
return normalizeText(getArtifacts(result).llmSummary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getLlmCot(result) {
|
|
113
|
+
return normalizeText(getArtifacts(result).llmCot);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getLlmEvidence(result) {
|
|
117
|
+
const evidence = getArtifacts(result).llmEvidence;
|
|
118
|
+
if (!Array.isArray(evidence)) return '';
|
|
119
|
+
return evidence.map((item) => normalizeText(item)).filter(Boolean).join(' | ');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getLlmRawReasoning(result) {
|
|
123
|
+
return normalizeText(getArtifacts(result).llmRawReasoning);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatTimingSummary(result) {
|
|
127
|
+
const parts = [];
|
|
128
|
+
for (const [key, label] of TIMING_BUCKETS) {
|
|
129
|
+
const value = getTimingValue(result, key);
|
|
130
|
+
if (value === null) continue;
|
|
131
|
+
parts.push(`${label} ${value}ms`);
|
|
132
|
+
}
|
|
133
|
+
return parts.length > 0 ? parts.join(' | ') : '-';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatResultNotes(result) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
const reason = previewText(result?.reason, 120);
|
|
139
|
+
const summary = previewText(getLlmSummary(result), 120);
|
|
140
|
+
const cot = previewText(getLlmCot(result), 180);
|
|
141
|
+
const errorMessage = previewText(result?.error, 120);
|
|
142
|
+
const llmRawReasoning = previewText(getLlmRawReasoning(result), 180);
|
|
143
|
+
const llmRawOutput = previewText(getArtifacts(result).llmRawOutput, 180);
|
|
144
|
+
if (reason) parts.push(`原因: ${reason}`);
|
|
145
|
+
if (summary) parts.push(`摘要: ${summary}`);
|
|
146
|
+
if (cot) parts.push(`CoT: ${cot}`);
|
|
147
|
+
if (errorMessage) parts.push(`错误: ${errorMessage}`);
|
|
148
|
+
if (llmRawReasoning) parts.push(`Reasoning: ${llmRawReasoning}`);
|
|
149
|
+
if (llmRawOutput) parts.push(`LLM: ${llmRawOutput}`);
|
|
150
|
+
return parts.length > 0 ? parts.join(' | ') : '-';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildAcquisitionSummaryRows(results) {
|
|
154
|
+
const counts = new Map();
|
|
155
|
+
for (const result of results) {
|
|
156
|
+
const mode = getAcquisitionMode(result) || 'unknown';
|
|
157
|
+
const reason = getAcquisitionReason(result) || 'unspecified';
|
|
158
|
+
const key = `${mode}__${reason}`;
|
|
159
|
+
const current = counts.get(key) || { mode, reason, count: 0 };
|
|
160
|
+
current.count += 1;
|
|
161
|
+
counts.set(key, current);
|
|
162
|
+
}
|
|
163
|
+
return [...counts.values()].sort((left, right) => right.count - left.count || left.mode.localeCompare(right.mode));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildTimingSummaryRows(results) {
|
|
167
|
+
return TIMING_BUCKETS.map(([key, label]) => {
|
|
168
|
+
let count = 0;
|
|
169
|
+
let total = 0;
|
|
170
|
+
for (const result of results) {
|
|
171
|
+
const value = getTimingValue(result, key);
|
|
172
|
+
if (value === null) continue;
|
|
173
|
+
count += 1;
|
|
174
|
+
total += value;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
key,
|
|
178
|
+
label,
|
|
179
|
+
count,
|
|
180
|
+
total,
|
|
181
|
+
average: count > 0 ? Math.round(total / count) : null,
|
|
182
|
+
};
|
|
183
|
+
}).filter((item) => item.count > 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildMarkdownSummary(summary) {
|
|
187
|
+
const results = toResults(summary);
|
|
188
|
+
const acquisitionRows = buildAcquisitionSummaryRows(results);
|
|
189
|
+
const timingRows = buildTimingSummaryRows(results);
|
|
190
|
+
const lines = [
|
|
191
|
+
'# Boss Chat 运行报告',
|
|
192
|
+
'',
|
|
193
|
+
'## 概览',
|
|
194
|
+
`- 开始时间: ${summary?.startedAt || '-'}`,
|
|
195
|
+
`- 结束时间: ${summary?.finishedAt || '-'}`,
|
|
196
|
+
`- 总耗时: ${formatDurationMs(summary?.startedAt, summary?.finishedAt)}`,
|
|
197
|
+
`- 处理进度: inspected=${Number(summary?.inspected || 0)} / target=${summary?.profile?.targetCount || '∞'}`,
|
|
198
|
+
`- 结果统计: passed=${Number(summary?.passed || 0)} | requested=${Number(summary?.requested || 0)} | skipped=${Number(summary?.skipped || 0)} | errors=${Number(summary?.errors || 0)}`,
|
|
199
|
+
`- 停止状态: ${summary?.stopped ? `stopped (${summary?.stopReason || 'unknown'})` : 'completed'}`,
|
|
200
|
+
`- 穷尽列表: ${summary?.exhausted === true ? 'yes' : 'no'}`,
|
|
201
|
+
`- 报告文件: JSON=${summary?.reportPath || '-'} | Markdown=${summary?.reportMarkdownPath || '-'} | CSV=${summary?.reportCsvPath || '-'}`,
|
|
202
|
+
'',
|
|
203
|
+
'## Resume Acquisition 汇总',
|
|
204
|
+
'',
|
|
205
|
+
'| mode | retry_reason | count |',
|
|
206
|
+
'| --- | --- | ---: |',
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
if (acquisitionRows.length === 0) {
|
|
210
|
+
lines.push('| - | - | 0 |');
|
|
211
|
+
} else {
|
|
212
|
+
for (const row of acquisitionRows) {
|
|
213
|
+
lines.push(`| ${row.mode} | ${row.reason} | ${row.count} |`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('## Timing 汇总');
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push('| bucket | hits | total | avg |');
|
|
221
|
+
lines.push('| --- | ---: | ---: | ---: |');
|
|
222
|
+
if (timingRows.length === 0) {
|
|
223
|
+
lines.push('| - | 0 | - | - |');
|
|
224
|
+
} else {
|
|
225
|
+
for (const row of timingRows) {
|
|
226
|
+
lines.push(`| ${row.label} | ${row.count} | ${row.total}ms | ${row.average === null ? '-' : `${row.average}ms`} |`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push('## 候选人明细');
|
|
232
|
+
lines.push('');
|
|
233
|
+
lines.push('| # | 姓名 | 结论 | acquisition | retry_reason | timing | notes |');
|
|
234
|
+
lines.push('| ---: | --- | --- | --- | --- | --- | --- |');
|
|
235
|
+
|
|
236
|
+
if (results.length === 0) {
|
|
237
|
+
lines.push('| 1 | - | - | - | - | - | - |');
|
|
238
|
+
} else {
|
|
239
|
+
results.forEach((result, index) => {
|
|
240
|
+
lines.push(
|
|
241
|
+
`| ${index + 1} | ${previewText(result?.name || '未知', 32) || '未知'} | ${toOutcome(result)} | ${getAcquisitionMode(result) || '-'} | ${getAcquisitionReason(result) || '-'} | ${formatTimingSummary(result)} | ${formatResultNotes(result)} |`,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push('');
|
|
247
|
+
return `${lines.join('\n')}\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildCsvSummary(summary) {
|
|
251
|
+
const results = toResults(summary);
|
|
252
|
+
const lines = [CSV_HEADER.join(',')];
|
|
253
|
+
results.forEach((result, index) => {
|
|
254
|
+
const artifacts = getArtifacts(result);
|
|
255
|
+
lines.push([
|
|
256
|
+
csvEscape(index + 1),
|
|
257
|
+
csvEscape(result?.name || ''),
|
|
258
|
+
csvEscape(result?.sourceJob || ''),
|
|
259
|
+
csvEscape(toOutcome(result)),
|
|
260
|
+
csvEscape(result?.passed === true ? 'true' : 'false'),
|
|
261
|
+
csvEscape(result?.requested === true ? 'true' : 'false'),
|
|
262
|
+
csvEscape(getAcquisitionMode(result)),
|
|
263
|
+
csvEscape(getAcquisitionReason(result)),
|
|
264
|
+
csvEscape(artifacts.evaluationMode || ''),
|
|
265
|
+
csvEscape(Number.isFinite(Number(artifacts.evaluationImageCount)) ? Number(artifacts.evaluationImageCount) : ''),
|
|
266
|
+
csvEscape(getTimingValue(result, 'initialNetworkWaitMs') ?? ''),
|
|
267
|
+
csvEscape(getTimingValue(result, 'networkRetryMs') ?? ''),
|
|
268
|
+
csvEscape(getTimingValue(result, 'imageCaptureMs') ?? ''),
|
|
269
|
+
csvEscape(getTimingValue(result, 'imageModelMs') ?? ''),
|
|
270
|
+
csvEscape(getTimingValue(result, 'lateNetworkRetryMs') ?? ''),
|
|
271
|
+
csvEscape(getTimingValue(result, 'domFallbackMs') ?? ''),
|
|
272
|
+
csvEscape(getTimingValue(result, 'textModelMs') ?? ''),
|
|
273
|
+
csvEscape(formatTimingSummary(result)),
|
|
274
|
+
csvEscape(result?.reason || ''),
|
|
275
|
+
csvEscape(getLlmSummary(result)),
|
|
276
|
+
csvEscape(getLlmCot(result)),
|
|
277
|
+
csvEscape(getLlmEvidence(result)),
|
|
278
|
+
csvEscape(getLlmRawReasoning(result)),
|
|
279
|
+
csvEscape(result?.error || ''),
|
|
280
|
+
csvEscape(artifacts.llmRawOutput || ''),
|
|
281
|
+
csvEscape(previewText(artifacts.llmRawOutput, 500)),
|
|
282
|
+
].join(','));
|
|
283
|
+
});
|
|
284
|
+
return `\uFEFF${lines.join('\n')}\n`;
|
|
285
|
+
}
|
|
286
|
+
|
|
8
287
|
export class ReportStore {
|
|
9
288
|
constructor(baseDir) {
|
|
10
289
|
this.reportsDir = path.join(baseDir, 'reports');
|
|
@@ -12,8 +291,27 @@ export class ReportStore {
|
|
|
12
291
|
|
|
13
292
|
async write(summary) {
|
|
14
293
|
await mkdir(this.reportsDir, { recursive: true });
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
294
|
+
const baseName = `run-${timestampToken()}`;
|
|
295
|
+
const jsonPath = path.join(this.reportsDir, `${baseName}.json`);
|
|
296
|
+
const markdownPath = path.join(this.reportsDir, `${baseName}.md`);
|
|
297
|
+
const csvPath = path.join(this.reportsDir, `${baseName}.csv`);
|
|
298
|
+
|
|
299
|
+
if (summary && typeof summary === 'object') {
|
|
300
|
+
summary.reportPath = jsonPath;
|
|
301
|
+
summary.reportMarkdownPath = markdownPath;
|
|
302
|
+
summary.reportCsvPath = csvPath;
|
|
303
|
+
summary.reportArtifacts = {
|
|
304
|
+
jsonPath,
|
|
305
|
+
markdownPath,
|
|
306
|
+
csvPath,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await Promise.all([
|
|
311
|
+
writeFile(jsonPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'),
|
|
312
|
+
writeFile(markdownPath, buildMarkdownSummary(summary), 'utf8'),
|
|
313
|
+
writeFile(csvPath, buildCsvSummary(summary), 'utf8'),
|
|
314
|
+
]);
|
|
315
|
+
return jsonPath;
|
|
18
316
|
}
|
|
19
317
|
}
|
|
@@ -293,133 +293,47 @@ function browserProbeResumeContext(options = {}) {
|
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
async function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
scrollTop: Number(chunk.scrollTop || 0),
|
|
302
|
-
clipHeightCss: Number(chunk.clipHeightCss || 0),
|
|
303
|
-
}))
|
|
304
|
-
.sort((left, right) => {
|
|
305
|
-
if (left.scrollTop !== right.scrollTop) return left.scrollTop - right.scrollTop;
|
|
306
|
-
return left.index - right.index;
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
const composites = [];
|
|
310
|
-
const used = [];
|
|
311
|
-
let outWidth = 1;
|
|
312
|
-
let outHeight = 0;
|
|
313
|
-
let prevChunk = null;
|
|
314
|
-
|
|
315
|
-
for (const chunk of sorted) {
|
|
316
|
-
const info = await sharp(chunk.file).metadata();
|
|
317
|
-
const width = Number(info?.width || 0);
|
|
318
|
-
const height = Number(info?.height || 0);
|
|
319
|
-
if (width <= 0 || height <= 0) {
|
|
320
|
-
throw new Error(`Invalid chunk image size: ${chunk.file}`);
|
|
321
|
-
}
|
|
296
|
+
async function detectLikelyBlankChunks(chunkFiles = []) {
|
|
297
|
+
const normalizedFiles = Array.isArray(chunkFiles) ? chunkFiles.filter(Boolean) : [];
|
|
298
|
+
if (normalizedFiles.length <= 0) {
|
|
299
|
+
return { likelyBlank: false, luma: 0, avgStd: 0, blankChunks: 0, totalChunks: 0 };
|
|
300
|
+
}
|
|
322
301
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const newPixels = clamp(Math.round(deltaCss * ratio), 1, height);
|
|
332
|
-
const cropTop = clamp(height - newPixels, 0, height - 1);
|
|
333
|
-
const segHeight = height - cropTop;
|
|
334
|
-
const segment = await sharp(chunk.file)
|
|
335
|
-
.removeAlpha()
|
|
336
|
-
.extract({
|
|
337
|
-
left: 0,
|
|
338
|
-
top: cropTop,
|
|
339
|
-
width,
|
|
340
|
-
height: segHeight,
|
|
341
|
-
})
|
|
342
|
-
.png()
|
|
343
|
-
.toBuffer();
|
|
344
|
-
composites.push({
|
|
345
|
-
input: segment,
|
|
346
|
-
top: outHeight,
|
|
347
|
-
left: 0,
|
|
348
|
-
});
|
|
349
|
-
used.push({
|
|
350
|
-
file: chunk.file,
|
|
351
|
-
scrollTop: chunk.scrollTop,
|
|
352
|
-
cropTopPx: cropTop,
|
|
353
|
-
keptHeightPx: segHeight,
|
|
354
|
-
});
|
|
355
|
-
outWidth = Math.max(outWidth, width);
|
|
356
|
-
outHeight += segHeight;
|
|
357
|
-
prevChunk = chunk;
|
|
302
|
+
let lumaTotal = 0;
|
|
303
|
+
let stdTotal = 0;
|
|
304
|
+
let blankChunks = 0;
|
|
305
|
+
|
|
306
|
+
for (const file of normalizedFiles) {
|
|
307
|
+
const stats = await sharp(file).stats();
|
|
308
|
+
const channels = stats?.channels || [];
|
|
309
|
+
if (channels.length < 3) {
|
|
358
310
|
continue;
|
|
359
311
|
}
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
outWidth = Math.max(outWidth, width);
|
|
374
|
-
outHeight += height;
|
|
375
|
-
prevChunk = chunk;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (composites.length === 0 || outHeight <= 0 || outWidth <= 0) {
|
|
379
|
-
throw new Error('No valid segments to stitch with sharp.');
|
|
312
|
+
const meanR = Number(channels[0]?.mean || 0);
|
|
313
|
+
const meanG = Number(channels[1]?.mean || 0);
|
|
314
|
+
const meanB = Number(channels[2]?.mean || 0);
|
|
315
|
+
const stdR = Number(channels[0]?.stdev || 0);
|
|
316
|
+
const stdG = Number(channels[1]?.stdev || 0);
|
|
317
|
+
const stdB = Number(channels[2]?.stdev || 0);
|
|
318
|
+
const luma = 0.299 * meanR + 0.587 * meanG + 0.114 * meanB;
|
|
319
|
+
const avgStd = (stdR + stdG + stdB) / 3;
|
|
320
|
+
lumaTotal += luma;
|
|
321
|
+
stdTotal += avgStd;
|
|
322
|
+
if (luma >= 244 && avgStd <= 9) {
|
|
323
|
+
blankChunks += 1;
|
|
324
|
+
}
|
|
380
325
|
}
|
|
381
326
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
channels: 3,
|
|
387
|
-
background: { r: 255, g: 255, b: 255 },
|
|
388
|
-
},
|
|
389
|
-
})
|
|
390
|
-
.composite(composites)
|
|
391
|
-
.png()
|
|
392
|
-
.toFile(stitchedImage);
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
segments: composites.length,
|
|
396
|
-
size: {
|
|
397
|
-
width: outWidth,
|
|
398
|
-
height: outHeight,
|
|
399
|
-
},
|
|
400
|
-
used,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
async function detectLikelyBlankImage(imagePath) {
|
|
405
|
-
const stats = await sharp(imagePath).stats();
|
|
406
|
-
const channels = stats?.channels || [];
|
|
407
|
-
if (channels.length < 3) {
|
|
408
|
-
return { likelyBlank: false, luma: 0, avgStd: 0 };
|
|
409
|
-
}
|
|
410
|
-
const meanR = Number(channels[0]?.mean || 0);
|
|
411
|
-
const meanG = Number(channels[1]?.mean || 0);
|
|
412
|
-
const meanB = Number(channels[2]?.mean || 0);
|
|
413
|
-
const stdR = Number(channels[0]?.stdev || 0);
|
|
414
|
-
const stdG = Number(channels[1]?.stdev || 0);
|
|
415
|
-
const stdB = Number(channels[2]?.stdev || 0);
|
|
416
|
-
const luma = 0.299 * meanR + 0.587 * meanG + 0.114 * meanB;
|
|
417
|
-
const avgStd = (stdR + stdG + stdB) / 3;
|
|
418
|
-
const likelyBlank = luma >= 244 && avgStd <= 9;
|
|
327
|
+
const totalChunks = normalizedFiles.length;
|
|
328
|
+
const avgLuma = totalChunks > 0 ? lumaTotal / totalChunks : 0;
|
|
329
|
+
const avgStd = totalChunks > 0 ? stdTotal / totalChunks : 0;
|
|
330
|
+
const likelyBlank = blankChunks === totalChunks;
|
|
419
331
|
return {
|
|
420
332
|
likelyBlank,
|
|
421
|
-
luma: Number(
|
|
333
|
+
luma: Number(avgLuma.toFixed(2)),
|
|
422
334
|
avgStd: Number(avgStd.toFixed(2)),
|
|
335
|
+
blankChunks,
|
|
336
|
+
totalChunks,
|
|
423
337
|
};
|
|
424
338
|
}
|
|
425
339
|
|
|
@@ -459,7 +373,6 @@ export class ResumeCaptureService {
|
|
|
459
373
|
const chunkDir = path.join(artifactDir, 'chunks');
|
|
460
374
|
await mkdir(chunkDir, { recursive: true });
|
|
461
375
|
const metadataFile = path.join(artifactDir, 'chunks.json');
|
|
462
|
-
const stitchedImage = path.join(artifactDir, 'resume.png');
|
|
463
376
|
|
|
464
377
|
const probe = await this.waitForProbe({ waitResumeMs });
|
|
465
378
|
const maxScroll = Math.max(0, Number(probe.maxScroll || 0));
|
|
@@ -535,19 +448,21 @@ export class ResumeCaptureService {
|
|
|
535
448
|
chunks,
|
|
536
449
|
};
|
|
537
450
|
await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
538
|
-
const
|
|
539
|
-
const blank = await
|
|
451
|
+
const chunkFiles = chunks.map((chunk) => path.resolve(chunk.file));
|
|
452
|
+
const blank = await detectLikelyBlankChunks(chunkFiles);
|
|
540
453
|
this.logger.log(
|
|
541
|
-
`简历截图完成: chunks=${chunks.length},
|
|
454
|
+
`简历截图完成: chunks=${chunks.length}, modelImages=${chunkFiles.length}, likelyBlank=${blank.likelyBlank}, blankChunks=${blank.blankChunks}/${blank.totalChunks}, luma=${blank.luma}, std=${blank.avgStd}`,
|
|
542
455
|
);
|
|
543
456
|
|
|
544
457
|
return {
|
|
545
|
-
stitchedImage,
|
|
546
458
|
metadataFile,
|
|
547
459
|
chunkDir,
|
|
548
460
|
chunkCount: chunks.length,
|
|
549
|
-
|
|
550
|
-
|
|
461
|
+
chunkFiles,
|
|
462
|
+
modelImagePaths: chunkFiles,
|
|
463
|
+
stitchedImage: '',
|
|
464
|
+
stitchEngine: 'skipped',
|
|
465
|
+
stitched: null,
|
|
551
466
|
quality: blank,
|
|
552
467
|
};
|
|
553
468
|
}
|