backtrace-console 0.0.1 → 0.0.3

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.
@@ -1,356 +0,0 @@
1
- const fs = require("node:fs/promises");
2
- const path = require("node:path");
3
- const {
4
- logStep,
5
- normalizeFingerprint,
6
- getFingerprintLogsRoot,
7
- getFingerprintReportsRoot,
8
- inferContentType,
9
- ensureDirectory,
10
- } = require("./utils");
11
-
12
- // 分析辅助函数负责把下载后的崩溃附件整理成结构化提示词、
13
- // 汇总结果和 markdown 报告。
14
- function decodeXmlEntities(value) {
15
- return String(value || "")
16
- .replace(/"/g, "\"")
17
- .replace(/'/g, "'")
18
- .replace(/&lt;/g, "<")
19
- .replace(/&gt;/g, ">")
20
- .replace(/&amp;/g, "&");
21
- }
22
-
23
- // 使用一组候选 key 名,从 runtime XML 里提取一个值。
24
- function extractXmlValue(xmlText, keys) {
25
- for (const key of keys) {
26
- // 兼容 pair/item/entry 这种键值结构。
27
- const pairRegex = new RegExp(`<(?:pair|item|entry)[^>]*key="${key}"[^>]*>([\\s\\S]*?)<\\/(?:pair|item|entry)>`, "i");
28
- const pairMatch = xmlText.match(pairRegex);
29
- if (pairMatch?.[1]) {
30
- return decodeXmlEntities(pairMatch[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
31
- }
32
- // 也兼容直接以标签名承载值的 XML 结构。
33
- const directRegex = new RegExp(`<${key}[^>]*>([\\s\\S]*?)<\\/${key}>`, "i");
34
- const directMatch = xmlText.match(directRegex);
35
- if (directMatch?.[1]) {
36
- return decodeXmlEntities(directMatch[1].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
37
- }
38
- }
39
- return "";
40
- }
41
-
42
- // 提取 LLM 提示词和 markdown 报告真正会用到的运行时字段。
43
- function parseRuntimeXml(xmlText) {
44
- return {
45
- cpu: extractXmlValue(xmlText, ["CPU", "cpu", "processor", "Processor"]),
46
- gpu: extractXmlValue(xmlText, ["GPU", "gpu", "graphics_adapter", "GraphicsAdapter", "gfx"]),
47
- memory: extractXmlValue(xmlText, ["Memory", "memory", "ram", "RAM", "physical_memory"]),
48
- buildVersion: extractXmlValue(xmlText, ["BuildVersion", "build_version", "version", "GameVersion"]),
49
- systemModel: extractXmlValue(xmlText, ["SystemModel", "system_model", "device_model", "model"]),
50
- osVersion: extractXmlValue(xmlText, ["OSVersion", "os_version", "operating_system", "platform"]),
51
- };
52
- }
53
-
54
- // 对日志做粗粒度严重级分类,便于快速排查。
55
- function detectSeverity(logText) {
56
- if (!logText) return "普通严重级";
57
- if (/\berror\b/i.test(logText)) return "高严重级";
58
- if (/\bwarning\b/i.test(logText) || /\bwarn\b/i.test(logText)) return "中严重级";
59
- return "普通严重级";
60
- }
61
-
62
- // 限制日志摘录和 markdown 内容长度,避免报告过大。
63
- function truncateText(value, maxLength) {
64
- const text = String(value || "");
65
- if (text.length <= maxLength) return text;
66
- return `${text.slice(0, maxLength)}\n\n[truncated]`;
67
- }
68
-
69
- // 在去重和比对前先清洗噪声日志行。
70
- function normalizeLogLineForComparison(line) {
71
- return String(line || "")
72
- .replace(/\0/g, "")
73
- .replace(/\s+/g, " ")
74
- .trim();
75
- }
76
-
77
- // 识别更像真实错误的日志行,过滤掉泛化噪声。
78
- function isErrorLikeLogLine(line) {
79
- const text = normalizeLogLineForComparison(line);
80
- if (!text) {
81
- return false;
82
- }
83
- return /\b(error|fatal|exception|assert|ensure|crash|fail(?:ed|ure)?)\b/i.test(text);
84
- }
85
-
86
- // 对一个 fingerprint 家族下所有对象的疑似错误行做聚合和去重。
87
- function buildErrorLineDigest(downloadedItems, contexts) {
88
- const lineMap = new Map();
89
-
90
- contexts.forEach((context, index) => {
91
- const objectId = downloadedItems[index]?.objectIdHex || "";
92
- const lines = String(context?.logText || "").split(/\r?\n/);
93
- lines.forEach((rawLine, lineIndex) => {
94
- // 先过滤掉不具备错误特征的日志行,避免摘要被普通日志淹没。
95
- if (!isErrorLikeLogLine(rawLine)) {
96
- return;
97
- }
98
- const normalizedLine = normalizeLogLineForComparison(rawLine);
99
- if (!normalizedLine) {
100
- return;
101
- }
102
- if (!lineMap.has(normalizedLine)) {
103
- // 同一条归一化后的错误行只保留一份聚合记录。
104
- lineMap.set(normalizedLine, {
105
- line: normalizedLine,
106
- count: 0,
107
- objectIds: new Set(),
108
- samples: [],
109
- firstSeenAt: {
110
- objectId,
111
- lineNumber: lineIndex + 1,
112
- },
113
- });
114
- }
115
- const entry = lineMap.get(normalizedLine);
116
- entry.count += 1;
117
- if (objectId) {
118
- entry.objectIds.add(objectId);
119
- }
120
- // 样本行只保留少量原始文本,方便摘要里展示代表性原文。
121
- const sampleLine = String(rawLine || "").trim();
122
- if (sampleLine && entry.samples.length < 3 && !entry.samples.includes(sampleLine)) {
123
- entry.samples.push(sampleLine);
124
- }
125
- });
126
- });
127
-
128
- const entries = Array.from(lineMap.values())
129
- .map((entry) => ({
130
- line: entry.line,
131
- count: entry.count,
132
- objectCount: entry.objectIds.size,
133
- objectIds: Array.from(entry.objectIds).sort((a, b) => a.localeCompare(b)),
134
- samples: entry.samples,
135
- firstSeenAt: entry.firstSeenAt,
136
- }))
137
- // 先按出现次数排序,再按覆盖对象数排序,最后按文本稳定排序。
138
- .sort((left, right) => right.count - left.count || right.objectCount - left.objectCount || left.line.localeCompare(right.line));
139
-
140
- return {
141
- totalUniqueLines: entries.length,
142
- totalMatchedLines: entries.reduce((sum, entry) => sum + entry.count, 0),
143
- entries,
144
- };
145
- }
146
-
147
- // 把聚合后的错误行摘要渲染成 markdown 文件内容。
148
- function formatErrorLineDigestMarkdown(fingerprint, digest) {
149
- const lines = [
150
- `# Fingerprint Error Line Summary ${fingerprint}`,
151
- "",
152
- `- Generated At: ${new Date().toISOString()}`,
153
- `- Unique Error Lines: ${digest.totalUniqueLines}`,
154
- `- Total Matched Error Lines: ${digest.totalMatchedLines}`,
155
- "",
156
- "## Deduplicated Error Lines",
157
- "",
158
- ];
159
-
160
- if (digest.entries.length === 0) {
161
- lines.push("No error-like log lines found.");
162
- lines.push("");
163
- return lines.join("\n");
164
- }
165
-
166
- digest.entries.forEach((entry, index) => {
167
- lines.push(`${index + 1}. Count: ${entry.count} | Objects: ${entry.objectCount}`);
168
- lines.push(` Line: ${entry.line}`);
169
- if (entry.objectIds.length > 0) {
170
- lines.push(` Object IDs: ${entry.objectIds.join(", ")}`);
171
- }
172
- if (entry.firstSeenAt?.objectId) {
173
- lines.push(` First Seen: ${entry.firstSeenAt.objectId}:${entry.firstSeenAt.lineNumber}`);
174
- }
175
- lines.push("");
176
- });
177
-
178
- return lines.join("\n");
179
- }
180
-
181
-
182
- // 给 CLI 用户输出简洁列表视图,只展示匹配对象的关键信息。
183
- function printList(items, meta) {
184
- console.log(`Time range: ${meta.from} ~ ${meta.to}`);
185
- console.log(`Total rows: ${meta.totalRows}`);
186
- console.log(`Fetched rows: ${meta.totalListed}`);
187
- console.log(`Objects: ${meta.objectCount}`);
188
- console.log(`Optimized page limit: ${meta.pageLimit}`);
189
- console.log("");
190
-
191
- if (items.length === 0) {
192
- console.log("No results found.");
193
- return;
194
- }
195
-
196
- const rows = items.map((item, idx) => {
197
- const hasObjectId = Boolean(item.objectIdHex);
198
- if (!hasObjectId) {
199
- return {
200
- "#": idx + 1,
201
- fingerprint: item.values.fingerprint || "",
202
- firstSeen: item.values["fingerprint;first_seen"] || "",
203
- issueState: item.values["fingerprint;issues;state"] || "",
204
- errorMessage: item.values["error.message"] || "",
205
- classifiers: item.values.classifiers || "",
206
- };
207
- }
208
- return {
209
- "#": idx + 1,
210
- objectId: item.objectIdHex,
211
- fingerprint: item.values.fingerprint || "",
212
- count: item.values._count ?? "",
213
- };
214
- });
215
- console.table(rows);
216
- }
217
-
218
- // 读取分析所需的本地文件,并提取 runtime 和日志片段。
219
- async function collectAnalysisContext(downloadedItem) {
220
- // 运行时 XML 和主日志文件是后续分析最关键的两类输入。
221
- const runtimeXmlFile = downloadedItem.downloadedFiles.find((file) => file.name.toLowerCase() === "crashcontext.runtime-xml");
222
- const logFile = downloadedItem.downloadedFiles.find((file) => file.name.toLowerCase().endsWith(".log") || file.content_type === "text/plain" || file.contentType === "text/plain");
223
-
224
- const runtimeXmlText = runtimeXmlFile ? await fs.readFile(runtimeXmlFile.savedPath, "utf8") : "";
225
- const logText = logFile ? await fs.readFile(logFile.savedPath, "utf8") : "";
226
- const environmentInfo = runtimeXmlText ? parseRuntimeXml(runtimeXmlText) : {};
227
- const severity = detectSeverity(logText);
228
-
229
- logStep("analysis", "local context prepared", {
230
- objectId: downloadedItem.objectIdHex,
231
- runtimeXmlFile: runtimeXmlFile?.savedPath || null,
232
- logFile: logFile?.savedPath || null,
233
- severity,
234
- });
235
-
236
- return { runtimeXmlText, logText, environmentInfo, severity };
237
- }
238
-
239
- // 持久化 fingerprint 级错误摘要,供后续查看和批量提示词使用。
240
- async function writeErrorLineDigestReport(fingerprint, options, errorLineDigest) {
241
- const reportDir = getFingerprintReportsRoot(options.storageDir, fingerprint);
242
- await ensureDirectory(reportDir);
243
- const summaryPath = path.join(reportDir, "error-line-summary.md");
244
- const jsonPath = path.join(reportDir, "error-line-summary.json");
245
- await fs.writeFile(summaryPath, formatErrorLineDigestMarkdown(fingerprint, errorLineDigest), "utf8");
246
- await fs.writeFile(jsonPath, JSON.stringify(errorLineDigest, null, 2), "utf8");
247
- logStep("report", "error line digest written", { summaryPath, jsonPath });
248
- return { summaryPath, jsonPath };
249
- }
250
-
251
- // 枚举某个 fingerprint 家族下本地已下载的对象目录。
252
- async function listLocalDownloadedItems(storageDir, fingerprint) {
253
- const logsDir = getFingerprintLogsRoot(storageDir, fingerprint);
254
- const entries = await fs.readdir(logsDir, { withFileTypes: true }).catch(() => []);
255
- const directories = entries
256
- .filter((entry) => entry.isDirectory())
257
- .map((entry) => entry.name)
258
- .sort((a, b) => a.localeCompare(b));
259
-
260
- const items = [];
261
- for (const objectIdHex of directories) {
262
- /* eslint-disable no-await-in-loop */
263
- const targetDir = path.join(logsDir, objectIdHex);
264
- const files = await fs.readdir(targetDir, { withFileTypes: true }).catch(() => []);
265
- const downloadedFiles = [];
266
- for (const entry of files) {
267
- if (!entry.isFile()) {
268
- continue;
269
- }
270
- // 从本地磁盘重建与在线下载阶段相同的 downloadedFiles 结构。
271
- const savedPath = path.join(targetDir, entry.name);
272
- const stats = await fs.stat(savedPath).catch(() => null);
273
- const contentType = inferContentType(entry.name);
274
- downloadedFiles.push({
275
- name: entry.name,
276
- savedPath,
277
- size: stats?.size || 0,
278
- contentType,
279
- content_type: contentType,
280
- });
281
- }
282
- items.push({
283
- objectIdHex,
284
- fingerprint,
285
- values: { fingerprint },
286
- targetDir,
287
- downloadedFiles,
288
- });
289
- /* eslint-enable no-await-in-loop */
290
- }
291
-
292
- return items;
293
- }
294
-
295
- // 重新加载本地崩溃上下文,使分析可以脱离 Backtrace 远端查询单独运行。
296
- async function loadFingerprintContexts(options) {
297
- const fingerprint = normalizeFingerprint(options.fingerprint);
298
- const downloadedItems = await listLocalDownloadedItems(options.storageDir, fingerprint);
299
- if (downloadedItems.length === 0) {
300
- throw new Error(`No downloaded logs found for fingerprint: ${fingerprint}`);
301
- }
302
-
303
- const contexts = [];
304
- // 逐个恢复上下文,保证后续本地分析复用和远端流程同样的数据结构。
305
- for (const downloadedItem of downloadedItems) {
306
- /* eslint-disable no-await-in-loop */
307
- contexts.push(await collectAnalysisContext(downloadedItem));
308
- /* eslint-enable no-await-in-loop */
309
- }
310
-
311
- const sharedQuerySummary = {
312
- totalRows: downloadedItems.length,
313
- fetchedRows: downloadedItems.length,
314
- objectCount: downloadedItems.length,
315
- pageLimit: downloadedItems.length,
316
- objectIds: downloadedItems.map((item) => item.objectIdHex),
317
- selectedValues: downloadedItems.map((item) => item.values),
318
- };
319
-
320
- return { fingerprint, downloadedItems, contexts, sharedQuerySummary };
321
- }
322
-
323
- // 只基于本地存储重新计算 fingerprint 级错误行摘要。
324
- async function summarizeFingerprintErrorLines(options) {
325
- const loaded = await loadFingerprintContexts(options);
326
- const errorLineDigest = buildErrorLineDigest(loaded.downloadedItems, loaded.contexts);
327
- const errorLineDigestPaths = await writeErrorLineDigestReport(loaded.fingerprint, options, errorLineDigest);
328
- return {
329
- command: options.command,
330
- options,
331
- fingerprint: loaded.fingerprint,
332
- querySummary: loaded.sharedQuerySummary,
333
- downloadedItems: loaded.downloadedItems,
334
- contexts: loaded.contexts,
335
- errorLineDigest,
336
- errorLineDigestPaths,
337
- };
338
- }
339
-
340
- module.exports = {
341
- decodeXmlEntities,
342
- extractXmlValue,
343
- parseRuntimeXml,
344
- detectSeverity,
345
- truncateText,
346
- normalizeLogLineForComparison,
347
- isErrorLikeLogLine,
348
- buildErrorLineDigest,
349
- formatErrorLineDigestMarkdown,
350
- printList,
351
- collectAnalysisContext,
352
- writeErrorLineDigestReport,
353
- listLocalDownloadedItems,
354
- loadFingerprintContexts,
355
- summarizeFingerprintErrorLines,
356
- };
@@ -1,23 +0,0 @@
1
- const DEFAULT_QUERY_URL = process.env.BACKTRACE_QUERY_URL || process.env.BACKTRACE_BASE_URL || "https://apesquared.sp.backtrace.io";
2
- const DEFAULT_BACKTRACE_PROJECT = process.env.BACKTRACE_PROJECT || "AboveLand";
3
- const DEFAULT_BACKTRACE_FORMAT = process.env.BACKTRACE_FORMAT || "json";
4
- const DEFAULT_WORKDIR = process.env.BACKTRACE_WORKDIR || process.cwd();
5
- const DEFAULT_PROXY = process.env.BACKTRACE_PROXY || "";
6
- const DEFAULT_SELECT = ["fingerprint"];
7
- const DEFAULT_LIMIT = Number(process.env.BACKTRACE_LIMIT || 20);
8
- const DEFAULT_DOWNLOAD_CONCURRENCY = Number(process.env.BACKTRACE_DOWNLOAD_CONCURRENCY || 4);
9
- const DEFAULT_RETRIES = Number(process.env.BACKTRACE_RETRIES || 0);
10
- const DEFAULT_STORAGE_DIR = process.env.BACKTRACE_STORAGE_DIR || "fingerprints";
11
-
12
- module.exports = {
13
- DEFAULT_QUERY_URL,
14
- DEFAULT_BACKTRACE_PROJECT,
15
- DEFAULT_BACKTRACE_FORMAT,
16
- DEFAULT_WORKDIR,
17
- DEFAULT_PROXY,
18
- DEFAULT_SELECT,
19
- DEFAULT_LIMIT,
20
- DEFAULT_DOWNLOAD_CONCURRENCY,
21
- DEFAULT_RETRIES,
22
- DEFAULT_STORAGE_DIR,
23
- };
@@ -1,278 +0,0 @@
1
- const path = require("node:path");
2
- const {
3
- DEFAULT_QUERY_URL,
4
- DEFAULT_BACKTRACE_PROJECT,
5
- DEFAULT_BACKTRACE_FORMAT,
6
- DEFAULT_WORKDIR,
7
- DEFAULT_PROXY,
8
- DEFAULT_SELECT,
9
- DEFAULT_LIMIT,
10
- DEFAULT_DOWNLOAD_CONCURRENCY,
11
- DEFAULT_RETRIES,
12
- DEFAULT_STORAGE_DIR,
13
- } = require("./constants");
14
- const { nowRange, formatBatchName } = require("./utils");
15
-
16
- function normalizeBacktraceQueryUrl(value) {
17
- const rawValue = String(value || "").trim();
18
- const fallbackUrl = new URL(DEFAULT_QUERY_URL);
19
- const url = rawValue ? new URL(rawValue) : new URL(DEFAULT_QUERY_URL);
20
- if (!url.pathname || url.pathname === "/") {
21
- url.pathname = "/api/query";
22
- }
23
- if (!url.searchParams.get("project")) {
24
- url.searchParams.set("project", process.env.BACKTRACE_PROJECT || DEFAULT_BACKTRACE_PROJECT);
25
- }
26
- if (!url.searchParams.get("format")) {
27
- url.searchParams.set("format", process.env.BACKTRACE_FORMAT || DEFAULT_BACKTRACE_FORMAT);
28
- }
29
- if (!url.searchParams.get("token") && fallbackUrl.searchParams.get("token")) {
30
- url.searchParams.set("token", fallbackUrl.searchParams.get("token"));
31
- }
32
- return url.toString();
33
- }
34
-
35
- function normalizeOptionalInteger(value, fallback) {
36
- if (value === undefined || value === null || value === "") {
37
- return fallback;
38
- }
39
- return Number(value);
40
- }
41
-
42
- // 顶层工具只暴露固定命令集合,便于尽早做参数校验。
43
- const SUPPORTED_COMMANDS = ["list", "fingerprint", "collect-all", "summarize-fingerprint-errors"];
44
-
45
- // 输出旧入口 codex_demo.js 和库调用共用的帮助信息。
46
- function printUsage() {
47
- console.log(
48
- [
49
- "Usage:",
50
- " backtrace <command> [options]",
51
- "",
52
- "Commands:",
53
- " list list local fingerprint directories and meta information",
54
- " fingerprint query all grouped fingerprints and print them",
55
- " collect-all download all objects only, do not generate reports",
56
- " summarize-fingerprint-errors summarize and deduplicate error lines for one local fingerprint directory",
57
- " fix-plan generate a repair plan from the smallest AboveLand.LOG under one fingerprint",
58
- "",
59
- "Environment:",
60
- " BACKTRACE_USERNAME required Backtrace console login username",
61
- " BACKTRACE_PASSWORD required Backtrace console login password",
62
- ` BACKTRACE_PROJECT default: ${DEFAULT_BACKTRACE_PROJECT}`,
63
- ` BACKTRACE_FORMAT default: ${DEFAULT_BACKTRACE_FORMAT}`,
64
- "",
65
- "Options:",
66
- ` --query-url <url> default base url: ${DEFAULT_QUERY_URL}`,
67
- ` --workdir <dir> default: ${DEFAULT_WORKDIR}`,
68
- " --fingerprint <value[,value2,...]> optional query filter; collect supports multiple values",
69
- " --from <unixTs> default: 1",
70
- " --to <unixTs> default: now",
71
- ` --limit <n> default: ${DEFAULT_LIMIT}`,
72
- " --offset <n> default: 0",
73
- ` --select <a,b,c> default: ${DEFAULT_SELECT.join(",")}`,
74
- ` --proxy <url> default: ${DEFAULT_PROXY}`,
75
- ` --retries <n> default: ${DEFAULT_RETRIES} (0 means infinite retry)`,
76
- ` --download-concurrency <n> default: ${DEFAULT_DOWNLOAD_CONCURRENCY}`,
77
- " --storage-dir <dir> default: ./fingerprints",
78
- " --report-dir <dir> deprecated alias of --storage-dir",
79
- " --logs-dir <dir> deprecated alias of --storage-dir",
80
- " --help show this message",
81
- ].join("\n"),
82
- );
83
- }
84
-
85
- // 对标准化选项做一次集中校验,
86
- // 后续流程就可以默认命令前置条件和数值范围都已成立。
87
- function validateOptions(options) {
88
- if (!SUPPORTED_COMMANDS.includes(options.command)) {
89
- throw new Error(`Command must be one of: ${SUPPORTED_COMMANDS.join(", ")}`);
90
- }
91
- if (options.command === "summarize-fingerprint-errors" && !options.fingerprint) {
92
- throw new Error("--fingerprint is required for summarize-fingerprint-errors");
93
- }
94
- if (!["list", "summarize-fingerprint-errors"].includes(options.command) && (!options.username || !options.password)) {
95
- throw new Error("BACKTRACE_USERNAME and BACKTRACE_PASSWORD are required");
96
- }
97
- if (!Number.isInteger(options.limit) || options.limit <= 0) {
98
- throw new Error("--limit must be a positive integer");
99
- }
100
- if (!Number.isInteger(options.offset) || options.offset < 0) {
101
- throw new Error("--offset must be a non-negative integer");
102
- }
103
- if (!Number.isInteger(options.retries) || options.retries < 0) {
104
- throw new Error("--retries must be a non-negative integer; use 0 for infinite retry");
105
- }
106
- if (!Number.isInteger(options.downloadConcurrency) || options.downloadConcurrency <= 0) {
107
- throw new Error("--download-concurrency must be a positive integer");
108
- }
109
- if (options.select.length === 0) {
110
- throw new Error("--select must contain at least one field");
111
- }
112
- return options;
113
- }
114
-
115
- // 把旧版 Node 入口传入的 argv 解析为已校验的运行时选项。
116
- function parseArgs(argv) {
117
- const range = nowRange();
118
- const batchName = formatBatchName(new Date());
119
- // 这里先填满默认值,后续只覆盖用户显式传入的字段。
120
- const options = {
121
- command: "",
122
- queryUrl: normalizeBacktraceQueryUrl(DEFAULT_QUERY_URL),
123
- workdir: DEFAULT_WORKDIR,
124
- objectId: "",
125
- fingerprint: "",
126
- from: range.from,
127
- to: range.to,
128
- limit: DEFAULT_LIMIT,
129
- offset: 0,
130
- select: [...DEFAULT_SELECT],
131
- proxy: DEFAULT_PROXY,
132
- retries: DEFAULT_RETRIES,
133
- downloadConcurrency: DEFAULT_DOWNLOAD_CONCURRENCY,
134
- batchName,
135
- storageDir: path.resolve(DEFAULT_STORAGE_DIR),
136
- reportDir: path.resolve(DEFAULT_STORAGE_DIR),
137
- logsDir: path.resolve(DEFAULT_STORAGE_DIR),
138
- username: process.env.BACKTRACE_USERNAME || "",
139
- password: process.env.BACKTRACE_PASSWORD || "",
140
- };
141
-
142
- let index = 0;
143
- if (argv[0] && !argv[0].startsWith("-")) {
144
- // 第一个非 flag 参数视为主命令。
145
- options.command = argv[0];
146
- index = 1;
147
- }
148
-
149
- // 按顺序解析参数,因为这里采用的是简单的成对参数约定。
150
- for (; index < argv.length; index += 1) {
151
- const arg = argv[index];
152
-
153
- if (arg === "--query-url") {
154
- options.queryUrl = argv[index + 1] || "";
155
- index += 1;
156
- continue;
157
- }
158
- if (arg === "--workdir") {
159
- options.workdir = argv[index + 1] || "";
160
- index += 1;
161
- continue;
162
- }
163
- if (arg === "--object-id") {
164
- options.objectId = argv[index + 1] || "";
165
- index += 1;
166
- continue;
167
- }
168
- if (arg === "--fingerprint") {
169
- options.fingerprint = argv[index + 1] || "";
170
- index += 1;
171
- continue;
172
- }
173
- if (arg === "--from") {
174
- options.from = argv[index + 1] || options.from;
175
- index += 1;
176
- continue;
177
- }
178
- if (arg === "--to") {
179
- options.to = argv[index + 1] || options.to;
180
- index += 1;
181
- continue;
182
- }
183
- if (arg === "--limit") {
184
- options.limit = Number(argv[index + 1] || String(DEFAULT_LIMIT));
185
- index += 1;
186
- continue;
187
- }
188
- if (arg === "--offset") {
189
- options.offset = Number(argv[index + 1] || "0");
190
- index += 1;
191
- continue;
192
- }
193
- if (arg === "--select") {
194
- options.select = (argv[index + 1] || DEFAULT_SELECT.join(","))
195
- .split(",")
196
- .map((item) => item.trim())
197
- .filter(Boolean);
198
- index += 1;
199
- continue;
200
- }
201
- if (arg === "--proxy") {
202
- options.proxy = argv[index + 1] || "";
203
- index += 1;
204
- continue;
205
- }
206
- if (arg === "--retries") {
207
- options.retries = Number(argv[index + 1] || String(DEFAULT_RETRIES));
208
- index += 1;
209
- continue;
210
- }
211
- if (arg === "--download-concurrency") {
212
- options.downloadConcurrency = Number(argv[index + 1] || String(DEFAULT_DOWNLOAD_CONCURRENCY));
213
- index += 1;
214
- continue;
215
- }
216
- if (arg === "--storage-dir" || arg === "--report-dir" || arg === "--logs-dir") {
217
- options.storageDir = path.resolve(argv[index + 1] || DEFAULT_STORAGE_DIR);
218
- options.reportDir = options.storageDir;
219
- options.logsDir = options.storageDir;
220
- index += 1;
221
- continue;
222
- }
223
- if (arg === "--help" || arg === "-h") {
224
- printUsage();
225
- process.exit(0);
226
- }
227
- }
228
-
229
- options.queryUrl = normalizeBacktraceQueryUrl(options.queryUrl || DEFAULT_QUERY_URL);
230
- options.proxy = String(options.proxy || DEFAULT_PROXY || "").trim();
231
-
232
- return validateOptions(options);
233
- }
234
-
235
- // 给直接调用库的场景构造选项,跳过原始 argv 解析。
236
- function createOptions(overrides = {}) {
237
- const range = nowRange();
238
- const batchName = overrides.batchName || formatBatchName(new Date());
239
- // 支持调用方直接传数组,也支持传逗号分隔字符串。
240
- const selectValue = Array.isArray(overrides.select)
241
- ? overrides.select
242
- : String(overrides.select || DEFAULT_SELECT.join(","))
243
- .split(",")
244
- .map((item) => item.trim())
245
- .filter(Boolean);
246
-
247
- const options = {
248
- command: overrides.command || "",
249
- queryUrl: normalizeBacktraceQueryUrl(overrides.queryUrl || DEFAULT_QUERY_URL),
250
- workdir: overrides.workdir || DEFAULT_WORKDIR,
251
- objectId: overrides.objectId ? String(overrides.objectId) : "",
252
- fingerprint: overrides.fingerprint ? String(overrides.fingerprint) : "",
253
- from: String(overrides.from || range.from),
254
- to: String(overrides.to || range.to),
255
- limit: normalizeOptionalInteger(overrides.limit, DEFAULT_LIMIT),
256
- offset: normalizeOptionalInteger(overrides.offset, 0),
257
- select: selectValue.length > 0 ? selectValue : [...DEFAULT_SELECT],
258
- proxy: String(overrides.proxy || DEFAULT_PROXY || "").trim(),
259
- retries: normalizeOptionalInteger(overrides.retries, DEFAULT_RETRIES),
260
- downloadConcurrency: normalizeOptionalInteger(overrides.downloadConcurrency, DEFAULT_DOWNLOAD_CONCURRENCY),
261
- batchName,
262
- storageDir: path.resolve(overrides.storageDir || overrides.reportDir || overrides.logsDir || DEFAULT_STORAGE_DIR),
263
- reportDir: path.resolve(overrides.storageDir || overrides.reportDir || overrides.logsDir || DEFAULT_STORAGE_DIR),
264
- logsDir: path.resolve(overrides.storageDir || overrides.reportDir || overrides.logsDir || DEFAULT_STORAGE_DIR),
265
- username: overrides.username || process.env.BACKTRACE_USERNAME || "",
266
- password: overrides.password || process.env.BACKTRACE_PASSWORD || "",
267
- };
268
-
269
- return validateOptions(options);
270
- }
271
-
272
- module.exports = {
273
- SUPPORTED_COMMANDS,
274
- printUsage,
275
- parseArgs,
276
- createOptions,
277
- validateOptions,
278
- };