backtrace-console 0.0.1

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.
@@ -0,0 +1,333 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+ const { createOptions } = require("./options");
4
+ const {
5
+ logStep,
6
+ normalizeFingerprint,
7
+ ensureDirectoryExists,
8
+ ensureDirectory,
9
+ resolveCanonicalFingerprint,
10
+ mergeFingerprintFamilyDirectories,
11
+ } = require("./utils");
12
+ const {
13
+ BacktraceSession,
14
+ queryFingerprintDetail,
15
+ queryAllItems,
16
+ downloadItemLogs,
17
+ } = require("./query");
18
+ const {
19
+ summarizeFingerprintErrorLines,
20
+ } = require("./analysis");
21
+
22
+ async function writeFingerprintMetadata(items, options) {
23
+ const written = [];
24
+
25
+ for (const item of items) {
26
+ /* eslint-disable no-await-in-loop */
27
+ const fingerprint = normalizeFingerprint(item.fingerprint || item.values.fingerprint);
28
+ const fingerprintDir = path.join(options.storageDir, fingerprint);
29
+ const metaPath = path.join(fingerprintDir, "meta.json");
30
+ const meta = {
31
+ fingerprint,
32
+ firstSeen: item.values["fingerprint;first_seen"] || "",
33
+ issueState: item.values["fingerprint;issues;state"] || "",
34
+ errorMessage: item.values["error.message"] || "",
35
+ classifiers: item.values.classifiers || "",
36
+ };
37
+
38
+ await ensureDirectory(fingerprintDir);
39
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), "utf8");
40
+ written.push({ fingerprint, metaPath });
41
+ /* eslint-enable no-await-in-loop */
42
+ }
43
+
44
+ logStep("fingerprint", "fingerprint metadata written", {
45
+ count: written.length,
46
+ storageDir: options.storageDir,
47
+ });
48
+ return written;
49
+ }
50
+
51
+ function getRequestedFingerprints(options) {
52
+ return String(options?.fingerprint || "")
53
+ .split(",")
54
+ .map((item) => String(item || "").trim())
55
+ .filter(Boolean);
56
+ }
57
+
58
+ async function getLocalFingerprints(storageDir) {
59
+ const entries = await fs.readdir(storageDir, { withFileTypes: true }).catch(() => []);
60
+ return entries
61
+ .filter((entry) => entry.isDirectory())
62
+ .map((entry) => entry.name.trim())
63
+ .filter(Boolean);
64
+ }
65
+
66
+ async function listLocalFingerprints(options) {
67
+ const fingerprints = await getLocalFingerprints(options.storageDir);
68
+ const items = [];
69
+
70
+ for (const fingerprint of fingerprints) {
71
+ /* eslint-disable no-await-in-loop */
72
+ const metaPath = path.join(options.storageDir, fingerprint, "meta.json");
73
+ const rawMeta = await fs.readFile(metaPath, "utf8").catch(() => "");
74
+ if (!rawMeta) {
75
+ continue;
76
+ }
77
+ let meta = null;
78
+ try {
79
+ meta = JSON.parse(rawMeta);
80
+ } catch (error) {
81
+ meta = null;
82
+ }
83
+ if (!meta) {
84
+ continue;
85
+ }
86
+ items.push({
87
+ index: items.length + 1,
88
+ fingerprint,
89
+ errorMessage: meta.errorMessage || "",
90
+ issueState: meta.issueState || "",
91
+ metaPath,
92
+ });
93
+ /* eslint-enable no-await-in-loop */
94
+ }
95
+
96
+ return {
97
+ command: "list",
98
+ options,
99
+ items,
100
+ };
101
+ }
102
+
103
+ function buildDownloadItemsFromDetail(detail) {
104
+ return detail.objects.map((objectItem, index) => ({
105
+ index: index + 1,
106
+ objectIdDecimal: objectItem.objectIdDecimal,
107
+ objectIdHex: objectItem.objectIdHex,
108
+ fingerprint: detail.fingerprint,
109
+ values: {
110
+ fingerprint: detail.fingerprint,
111
+ timestamp: objectItem.timestamp || "",
112
+ "error.message": detail.summary.values["error.message"] || "",
113
+ classifiers: detail.summary.values.classifiers || "",
114
+ "fingerprint;first_seen": detail.summary.values["fingerprint;first_seen"] || "",
115
+ "fingerprint;issues;state": detail.summary.values["fingerprint;issues;state"] || "",
116
+ _count: detail.summary.values._count || detail.objects.length,
117
+ },
118
+ }));
119
+ }
120
+
121
+ function pickRandomItems(items, maxCount) {
122
+ if (!Array.isArray(items) || items.length <= maxCount) {
123
+ return Array.isArray(items) ? items : [];
124
+ }
125
+
126
+ const shuffled = [...items];
127
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
128
+ const swapIndex = Math.floor(Math.random() * (index + 1));
129
+ [shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]];
130
+ }
131
+
132
+ return shuffled
133
+ .slice(0, maxCount)
134
+ .map((item, index) => ({ ...item, index: index + 1 }));
135
+ }
136
+
137
+ async function ensureFingerprintMetadata(detail, options) {
138
+ const fingerprint = normalizeFingerprint(detail.fingerprint || detail.summary?.fingerprint || detail.summary?.values?.fingerprint);
139
+ const fingerprintDir = path.join(options.storageDir, fingerprint);
140
+ const fingerprintDirStat = await fs.stat(fingerprintDir).catch(() => null);
141
+
142
+ if (!fingerprintDirStat || !fingerprintDirStat.isDirectory()) {
143
+ await writeFingerprintMetadata([detail.summary], options);
144
+ return;
145
+ }
146
+
147
+ const metaPath = path.join(fingerprintDir, "meta.json");
148
+ const metaStat = await fs.stat(metaPath).catch(() => null);
149
+ if (!metaStat || !metaStat.isFile()) {
150
+ await writeFingerprintMetadata([detail.summary], options);
151
+ }
152
+ }
153
+
154
+ // 高层编排入口,负责串起查询、下载、本地目录规范化和报告生成流程。
155
+ class BacktraceCodexTool {
156
+ constructor(defaultOverrides = {}) {
157
+ this.defaultOverrides = { ...defaultOverrides };
158
+ }
159
+
160
+ // 合并实例默认值和调用方覆盖值,并一次性标准化。
161
+ buildOptions(command, overrides = {}) {
162
+ return createOptions({ ...this.defaultOverrides, ...overrides, command });
163
+ }
164
+
165
+ // 执行一个完整的受支持命令流程。
166
+ async run(command, overrides = {}) {
167
+ const options = this.buildOptions(command, overrides);
168
+ // 除纯本地分析外,其余流程都依赖工作目录和存储目录先就绪。
169
+ await ensureDirectoryExists(options.workdir, "Working directory");
170
+ await ensureDirectory(options.storageDir);
171
+
172
+ if (command === "list") {
173
+ return listLocalFingerprints(options);
174
+ }
175
+
176
+ if (command === "summarize-fingerprint-errors") {
177
+ return summarizeFingerprintErrorLines(options);
178
+ }
179
+
180
+ const session = new BacktraceSession(options);
181
+ await session.login();
182
+
183
+ if (command === "fingerprint" && getRequestedFingerprints(options).length === 1) {
184
+ const detail = await queryFingerprintDetail(session, options);
185
+ const writtenMeta = await writeFingerprintMetadata([detail.summary], options);
186
+ return {
187
+ command,
188
+ mode: "detail",
189
+ options,
190
+ fingerprint: detail.fingerprint,
191
+ errorMessage: detail.summary.values["error.message"] || "",
192
+ classifiers: detail.summary.values.classifiers || "",
193
+ firstSeen: detail.summary.values["fingerprint;first_seen"] || "",
194
+ issueState: detail.summary.values["fingerprint;issues;state"] || "",
195
+ objectCount: detail.objects.length,
196
+ objects: detail.objects,
197
+ querySummary: detail.querySummary,
198
+ writtenMeta,
199
+ };
200
+ }
201
+
202
+ if (command === "collect-all") {
203
+ const requestedFingerprints = getRequestedFingerprints(options);
204
+ const sourceFingerprints = requestedFingerprints.length > 0
205
+ ? requestedFingerprints
206
+ : await getLocalFingerprints(options.storageDir);
207
+ const uniqueFingerprints = Array.from(new Set(sourceFingerprints.map((item) => String(item || "").trim()).filter(Boolean)));
208
+
209
+ if (uniqueFingerprints.length === 0) {
210
+ throw new Error("No local fingerprints found in storageDir");
211
+ }
212
+
213
+ const downloadedItems = [];
214
+ let totalRows = 0;
215
+ let pageLimit = 0;
216
+
217
+ for (const [fingerprintIndex, requestedFingerprint] of uniqueFingerprints.entries()) {
218
+ /* eslint-disable no-await-in-loop */
219
+ logStep("download", "processing fingerprint", {
220
+ current: fingerprintIndex + 1,
221
+ total: uniqueFingerprints.length,
222
+ fingerprint: requestedFingerprint,
223
+ });
224
+ const detail = await queryFingerprintDetail(session, { ...options, fingerprint: requestedFingerprint });
225
+ await ensureFingerprintMetadata(detail, options);
226
+ const rawItems = pickRandomItems(buildDownloadItemsFromDetail(detail), 10);
227
+ if (rawItems.length === 0) {
228
+ continue;
229
+ }
230
+
231
+ const fingerprint = await resolveCanonicalFingerprint(options.storageDir, requestedFingerprint, requestedFingerprint, options);
232
+ await mergeFingerprintFamilyDirectories(options.storageDir, fingerprint, [requestedFingerprint]);
233
+ const items = rawItems.map((item, index) => ({
234
+ ...item,
235
+ index: index + 1,
236
+ fingerprint,
237
+ values: { ...item.values, fingerprint },
238
+ }));
239
+
240
+ logStep("download", "starting fingerprint download phase", {
241
+ fingerprint,
242
+ objectCount: items.length,
243
+ });
244
+
245
+ const perFingerprintDownloaded = [];
246
+ for (const [itemIndex, item] of items.entries()) {
247
+ /* eslint-disable no-await-in-loop */
248
+ logStep("download", "downloading object", {
249
+ fingerprint,
250
+ current: itemIndex + 1,
251
+ total: items.length,
252
+ objectId: item.objectIdHex,
253
+ });
254
+ const downloadedItem = await downloadItemLogs(session, item, options);
255
+ perFingerprintDownloaded.push(downloadedItem);
256
+ /* eslint-enable no-await-in-loop */
257
+ }
258
+
259
+ downloadedItems.push(...perFingerprintDownloaded);
260
+ totalRows += detail.querySummary.totalRows;
261
+ pageLimit = Math.max(pageLimit, detail.querySummary.pageLimit || 0);
262
+ logStep("download", "fingerprint download phase completed", {
263
+ fingerprint,
264
+ objectCount: perFingerprintDownloaded.length,
265
+ });
266
+ /* eslint-enable no-await-in-loop */
267
+ }
268
+
269
+ return {
270
+ command,
271
+ options,
272
+ querySummary: {
273
+ totalRows,
274
+ fetchedRows: downloadedItems.length,
275
+ objectCount: downloadedItems.length,
276
+ pageLimit,
277
+ objectIds: downloadedItems.map((item) => item.objectIdHex),
278
+ selectedValues: downloadedItems.map((item) => item.values),
279
+ },
280
+ downloadedItems,
281
+ };
282
+ }
283
+ if (command === "fingerprint") {
284
+ const result = await queryAllItems(session, options);
285
+ const sharedQuerySummary = {
286
+ totalRows: result.totalRows,
287
+ fetchedRows: result.listedCount,
288
+ objectCount: result.items.length,
289
+ pageLimit: result.pageLimit,
290
+ objectIds: result.objectIds,
291
+ selectedValues: result.selectedValues,
292
+ };
293
+ const writtenMeta = await writeFingerprintMetadata(result.items, options);
294
+ return {
295
+ command,
296
+ mode: "list",
297
+ options,
298
+ querySummary: sharedQuerySummary,
299
+ items: result.items,
300
+ writtenMeta,
301
+ };
302
+ }
303
+
304
+ throw new Error(`Unsupported command: ${command}`);
305
+ }
306
+
307
+ // 这些便捷方法让库调用方的 API 更直观。
308
+ async fingerprint(overrides = {}) {
309
+ return this.run("fingerprint", overrides);
310
+ }
311
+
312
+ async list(overrides = {}) {
313
+ return this.run("list", overrides);
314
+ }
315
+
316
+ async collectAll(overrides = {}) {
317
+ return this.run("collect-all", overrides);
318
+ }
319
+
320
+ async summarizeFingerprintErrors(overrides = {}) {
321
+ return this.run("summarize-fingerprint-errors", overrides);
322
+ }
323
+ }
324
+
325
+ async function runBacktraceCommand(command, overrides = {}) {
326
+ const tool = new BacktraceCodexTool();
327
+ return tool.run(command, overrides);
328
+ }
329
+
330
+ module.exports = {
331
+ BacktraceCodexTool,
332
+ runBacktraceCommand,
333
+ };
@@ -0,0 +1,297 @@
1
+ const fs = require("node:fs/promises");
2
+ const path = require("node:path");
3
+
4
+ // 在查询、下载、分析、修复各阶段输出统一格式的进度日志。
5
+ function logStep(scope, message, details) {
6
+ const prefix = `[backtrace-tool][${scope}]`;
7
+ if (details !== undefined) {
8
+ console.log(prefix, message, details);
9
+ return;
10
+ }
11
+ console.log(prefix, message);
12
+ }
13
+
14
+ // 默认把查询时间范围设为最近 24 小时,使用 Unix 秒时间戳。
15
+ function nowRange() {
16
+ const now = Math.floor(Date.now() / 1000);
17
+ return {
18
+ from: "1",
19
+ to: String(now),
20
+ };
21
+ }
22
+
23
+ // 批次名使用时间戳,便于磁盘上的产物按时间自然排序。
24
+ function formatBatchName(date) {
25
+ const year = String(date.getFullYear());
26
+ const month = String(date.getMonth() + 1).padStart(2, "0");
27
+ const day = String(date.getDate()).padStart(2, "0");
28
+ const hour = String(date.getHours()).padStart(2, "0");
29
+ const minute = String(date.getMinutes()).padStart(2, "0");
30
+ const second = String(date.getSeconds()).padStart(2, "0");
31
+ return `${year}${month}${day}-${hour}${minute}${second}`;
32
+ }
33
+
34
+ // 在作为目录名或 Map 键之前,先统一规范 fingerprint 字符串。
35
+ function normalizeFingerprint(value) {
36
+ const fingerprint = String(value || "").trim().toLowerCase();
37
+ return fingerprint || "unknown-fingerprint";
38
+ }
39
+
40
+ // 获取一个 fingerprint 家族在存储目录下的根路径。
41
+ function getFingerprintRoot(storageDir, fingerprint) {
42
+ return path.join(storageDir, normalizeFingerprint(fingerprint));
43
+ }
44
+
45
+ // 原始下载附件统一存放到 logs 子目录。
46
+ function getFingerprintLogsRoot(storageDir, fingerprint) {
47
+ return path.join(getFingerprintRoot(storageDir, fingerprint), "logs");
48
+ }
49
+
50
+ // 生成的 markdown 和 json 报告统一存放到 reports 子目录。
51
+ function getFingerprintReportsRoot(storageDir, fingerprint) {
52
+ return path.join(getFingerprintRoot(storageDir, fingerprint), "reports");
53
+ }
54
+
55
+ // 把前缀匹配的 fingerprint 视为同一家族,便于把截断值合并到一起。
56
+ function areFingerprintsRelated(left, right) {
57
+ const normalizedLeft = normalizeFingerprint(left);
58
+ const normalizedRight = normalizeFingerprint(right);
59
+ if (normalizedLeft === "unknown-fingerprint" || normalizedRight === "unknown-fingerprint") {
60
+ return false;
61
+ }
62
+ return normalizedLeft === normalizedRight
63
+ || normalizedLeft.startsWith(normalizedRight)
64
+ || normalizedRight.startsWith(normalizedLeft);
65
+ }
66
+
67
+ // 读取本地所有 fingerprint 目录及其时间信息,供调用方选出规范目录。
68
+ async function listExistingFingerprintDirectories(storageDir) {
69
+ const directories = await fs.readdir(storageDir, { withFileTypes: true }).catch(() => []);
70
+ return Promise.all(
71
+ directories
72
+ .filter((entry) => entry.isDirectory())
73
+ .map(async (entry) => {
74
+ const absolutePath = path.join(storageDir, entry.name);
75
+ const stats = await fs.stat(absolutePath).catch(() => null);
76
+ return {
77
+ name: normalizeFingerprint(entry.name),
78
+ absolutePath,
79
+ birthtimeMs: stats?.birthtimeMs || 0,
80
+ ctimeMs: stats?.ctimeMs || 0,
81
+ };
82
+ }),
83
+ );
84
+ }
85
+
86
+ // 优先复用最早创建的相关本地 fingerprint 目录,
87
+ // 让重复下载和分析都落到同一个规范路径下。
88
+ async function resolveCanonicalFingerprint(storageDir, preferredFingerprint, fallbackFingerprint = "", runtimeState = null) {
89
+ const candidates = [preferredFingerprint, fallbackFingerprint]
90
+ .map((item) => normalizeFingerprint(item))
91
+ .filter((item) => item && item !== "unknown-fingerprint");
92
+
93
+ const cachedFingerprint = runtimeState && runtimeState.canonicalFingerprintFamily
94
+ ? normalizeFingerprint(runtimeState.canonicalFingerprintFamily)
95
+ : "";
96
+ if (cachedFingerprint) {
97
+ const canReuseCached = candidates.some((candidate) => areFingerprintsRelated(candidate, cachedFingerprint));
98
+ if (canReuseCached) {
99
+ return cachedFingerprint;
100
+ }
101
+ }
102
+
103
+ const existingDirectories = await listExistingFingerprintDirectories(storageDir);
104
+ const relatedDirectories = existingDirectories.filter((entry) => {
105
+ return candidates.some((candidate) => areFingerprintsRelated(entry.name, candidate));
106
+ });
107
+
108
+ if (relatedDirectories.length > 0) {
109
+ const canonical = relatedDirectories
110
+ .sort((a, b) => (a.birthtimeMs || a.ctimeMs || 0) - (b.birthtimeMs || b.ctimeMs || 0) || a.name.localeCompare(b.name))[0]
111
+ .name;
112
+ if (runtimeState) {
113
+ runtimeState.canonicalFingerprintFamily = canonical;
114
+ }
115
+ return canonical;
116
+ }
117
+
118
+ const resolved = normalizeFingerprint(preferredFingerprint || fallbackFingerprint);
119
+ if (runtimeState && resolved !== "unknown-fingerprint") {
120
+ runtimeState.canonicalFingerprintFamily = resolved;
121
+ }
122
+ return resolved;
123
+ }
124
+
125
+ // 递归把别名目录内容合并到规范 fingerprint 目录中。
126
+ async function mergeDirectoryContents(sourceDir, targetDir) {
127
+ const sourceStats = await fs.stat(sourceDir).catch(() => null);
128
+ if (!sourceStats || !sourceStats.isDirectory()) {
129
+ return;
130
+ }
131
+
132
+ await fs.mkdir(targetDir, { recursive: true });
133
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true }).catch(() => []);
134
+ for (const entry of entries) {
135
+ const sourcePath = path.join(sourceDir, entry.name);
136
+ const targetPath = path.join(targetDir, entry.name);
137
+ if (entry.isDirectory()) {
138
+ await mergeDirectoryContents(sourcePath, targetPath);
139
+ const remaining = await fs.readdir(sourcePath).catch(() => []);
140
+ if (remaining.length === 0) {
141
+ await fs.rmdir(sourcePath).catch(() => {});
142
+ }
143
+ continue;
144
+ }
145
+ const targetStats = await fs.stat(targetPath).catch(() => null);
146
+ if (!targetStats) {
147
+ await fs.rename(sourcePath, targetPath).catch(async () => {
148
+ const data = await fs.readFile(sourcePath);
149
+ await fs.writeFile(targetPath, data);
150
+ await fs.unlink(sourcePath).catch(() => {});
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // 把所有相关 fingerprint 别名目录收敛到同一个规范目录树下。
157
+ async function mergeFingerprintFamilyDirectories(storageDir, canonicalFingerprint, relatedFingerprints) {
158
+ const canonical = normalizeFingerprint(canonicalFingerprint);
159
+ if (!canonical || canonical === "unknown-fingerprint") {
160
+ return canonical;
161
+ }
162
+
163
+ const directoryEntries = await listExistingFingerprintDirectories(storageDir);
164
+ const aliases = new Set(
165
+ [canonical, ...(relatedFingerprints || [])]
166
+ .map((item) => normalizeFingerprint(item))
167
+ .filter((item) => item && item !== "unknown-fingerprint"),
168
+ );
169
+
170
+ directoryEntries.forEach((entry) => {
171
+ if ([...aliases].some((candidate) => areFingerprintsRelated(entry.name, candidate))) {
172
+ aliases.add(entry.name);
173
+ }
174
+ });
175
+
176
+ const canonicalRoot = getFingerprintRoot(storageDir, canonical);
177
+ await fs.mkdir(canonicalRoot, { recursive: true });
178
+
179
+ for (const alias of aliases) {
180
+ if (alias === canonical) {
181
+ continue;
182
+ }
183
+ const aliasRoot = getFingerprintRoot(storageDir, alias);
184
+ const aliasStats = await fs.stat(aliasRoot).catch(() => null);
185
+ if (!aliasStats || !aliasStats.isDirectory()) {
186
+ continue;
187
+ }
188
+ await mergeDirectoryContents(aliasRoot, canonicalRoot);
189
+ const remaining = await fs.readdir(aliasRoot).catch(() => []);
190
+ if (remaining.length === 0) {
191
+ await fs.rmdir(aliasRoot).catch(() => {});
192
+ }
193
+ logStep("download", "merged fingerprint alias directory", { alias, canonical });
194
+ }
195
+
196
+ return canonical;
197
+ }
198
+
199
+ // 当接口元数据不足时,基于文件名推断一个粗粒度 content type。
200
+ function inferContentType(fileName) {
201
+ const ext = path.extname(String(fileName || "")).toLowerCase();
202
+ if (ext === ".log" || ext === ".txt" || ext === ".xml" || ext === ".json" || ext === ".md") {
203
+ return "text/plain";
204
+ }
205
+ return "application/octet-stream";
206
+ }
207
+
208
+ // 重试流程里使用的简单休眠工具。
209
+ function sleep(ms) {
210
+ return new Promise((resolve) => {
211
+ setTimeout(resolve, ms);
212
+ });
213
+ }
214
+
215
+ // 为瞬时失败任务提供统一的重试、退避和日志输出。
216
+ async function retry(label, maxAttempts, task) {
217
+ let lastError = null;
218
+ let attempt = 1;
219
+
220
+ while (maxAttempts === 0 || attempt <= maxAttempts) {
221
+ try {
222
+ if (attempt > 1) {
223
+ logStep("retry", `${label} retry`, { attempt, maxAttempts });
224
+ }
225
+ return await task(attempt);
226
+ } catch (error) {
227
+ lastError = error;
228
+ logStep("retry", `${label} failed`, {
229
+ attempt,
230
+ maxAttempts,
231
+ message: error instanceof Error ? error.message : String(error),
232
+ });
233
+ const delayMs = Math.min(10000, 5000 + Math.floor(Math.random() * 5001));
234
+ logStep("retry", `${label} waiting before retry`, { attempt, delayMs });
235
+ await sleep(delayMs);
236
+ attempt += 1;
237
+ }
238
+ }
239
+
240
+ throw lastError;
241
+ }
242
+
243
+ // 对必须存在的目录做快速校验,避免后续流程在更深处失败。
244
+ async function ensureDirectoryExists(targetPath, label) {
245
+ const stats = await fs.stat(targetPath).catch(() => null);
246
+ if (!stats || !stats.isDirectory()) {
247
+ throw new Error(`${label} does not exist or is not a directory: ${targetPath}`);
248
+ }
249
+ }
250
+
251
+ // 按需创建下载和报告所需目录。
252
+ async function ensureDirectory(targetPath) {
253
+ await fs.mkdir(targetPath, { recursive: true });
254
+ }
255
+
256
+ // 在限制并发度的前提下执行异步任务,并保持结果顺序与输入一致。
257
+ async function mapWithConcurrency(items, concurrency, worker) {
258
+ const results = new Array(items.length);
259
+ let nextIndex = 0;
260
+
261
+ // 每个 worker 都从共享索引里领取下一个任务,直到队列耗尽。
262
+ async function runWorker() {
263
+ while (true) {
264
+ const currentIndex = nextIndex;
265
+ nextIndex += 1;
266
+ if (currentIndex >= items.length) {
267
+ return;
268
+ }
269
+ /* eslint-disable no-await-in-loop */
270
+ results[currentIndex] = await worker(items[currentIndex], currentIndex);
271
+ /* eslint-enable no-await-in-loop */
272
+ }
273
+ }
274
+
275
+ const workerCount = Math.min(concurrency, items.length);
276
+ await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
277
+ return results;
278
+ }
279
+
280
+ module.exports = {
281
+ logStep,
282
+ nowRange,
283
+ formatBatchName,
284
+ normalizeFingerprint,
285
+ getFingerprintRoot,
286
+ getFingerprintLogsRoot,
287
+ getFingerprintReportsRoot,
288
+ areFingerprintsRelated,
289
+ listExistingFingerprintDirectories,
290
+ resolveCanonicalFingerprint,
291
+ mergeFingerprintFamilyDirectories,
292
+ inferContentType,
293
+ retry,
294
+ ensureDirectoryExists,
295
+ ensureDirectory,
296
+ mapWithConcurrency,
297
+ };