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.
- package/README.md +22 -0
- package/app.js +22 -0
- package/bin/backtrace-cli.js +22 -0
- package/bin/www +90 -0
- package/lib/BacktraceCodexTool.js +32 -0
- package/lib/backtrace/analysis.js +356 -0
- package/lib/backtrace/constants.js +23 -0
- package/lib/backtrace/options.js +278 -0
- package/lib/backtrace/query.js +940 -0
- package/lib/backtrace/repair-fingerprint.js +405 -0
- package/lib/backtrace/repair.js +495 -0
- package/lib/backtrace/tool.js +333 -0
- package/lib/backtrace/utils.js +297 -0
- package/lib/cli/args.js +177 -0
- package/lib/cli/run.js +191 -0
- package/package.json +29 -0
- package/public/__inline_check__.js +451 -0
- package/public/index.html +642 -0
- package/public/stylesheets/style.css +186 -0
- package/routes/backtrace.js +864 -0
- package/routes/index.js +9 -0
- package/routes/users.js +9 -0
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
};
|