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
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# backtrace-console
|
|
2
|
+
|
|
3
|
+
CLI for querying Backtrace fingerprints, collecting artifacts, and generating repair plans.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g backtrace-console
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
backtrace list
|
|
15
|
+
backtrace fingerprint --fingerprint <value>
|
|
16
|
+
backtrace collect --fingerprint <value>
|
|
17
|
+
backtrace summarize-errors --fingerprint <value>
|
|
18
|
+
backtrace fix-plan --fingerprint <value>
|
|
19
|
+
backtrace apply-fix --fingerprint <value>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run `backtrace --help` for the full option list.
|
package/app.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var cookieParser = require('cookie-parser');
|
|
4
|
+
var logger = require('morgan');
|
|
5
|
+
|
|
6
|
+
var indexRouter = require('./routes/index');
|
|
7
|
+
var usersRouter = require('./routes/users');
|
|
8
|
+
var backtraceRouter = require('./routes/backtrace');
|
|
9
|
+
|
|
10
|
+
var app = express();
|
|
11
|
+
|
|
12
|
+
app.use(logger('dev'));
|
|
13
|
+
app.use(express.json());
|
|
14
|
+
app.use(express.urlencoded({ extended: false }));
|
|
15
|
+
app.use(cookieParser());
|
|
16
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
17
|
+
|
|
18
|
+
app.use('/', indexRouter);
|
|
19
|
+
app.use('/users', usersRouter);
|
|
20
|
+
app.use('/api/backtrace', backtraceRouter);
|
|
21
|
+
|
|
22
|
+
module.exports = app;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
require("dotenv").config();
|
|
4
|
+
|
|
5
|
+
const { parseCliArgs, printCliUsage } = require("../lib/cli/args");
|
|
6
|
+
const { runCliCommand } = require("../lib/cli/run");
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const parsed = parseCliArgs(process.argv.slice(2));
|
|
10
|
+
if (parsed.help) {
|
|
11
|
+
printCliUsage();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await runCliCommand(parsed.options);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch((error) => {
|
|
19
|
+
console.error("backtrace-cli failed:");
|
|
20
|
+
console.error(error instanceof Error ? error.stack || error.message : error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/bin/www
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Module dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var app = require('../app');
|
|
8
|
+
var debug = require('debug')('backtrace-console:server');
|
|
9
|
+
var http = require('http');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get port from environment and store in Express.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var port = normalizePort(process.env.PORT || '3000');
|
|
16
|
+
app.set('port', port);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create HTTP server.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
var server = http.createServer(app);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Listen on provided port, on all network interfaces.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
server.listen(port);
|
|
29
|
+
server.on('error', onError);
|
|
30
|
+
server.on('listening', onListening);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a port into a number, string, or false.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
function normalizePort(val) {
|
|
37
|
+
var port = parseInt(val, 10);
|
|
38
|
+
|
|
39
|
+
if (isNaN(port)) {
|
|
40
|
+
// named pipe
|
|
41
|
+
return val;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (port >= 0) {
|
|
45
|
+
// port number
|
|
46
|
+
return port;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Event listener for HTTP server "error" event.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
function onError(error) {
|
|
57
|
+
if (error.syscall !== 'listen') {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var bind = typeof port === 'string'
|
|
62
|
+
? 'Pipe ' + port
|
|
63
|
+
: 'Port ' + port;
|
|
64
|
+
|
|
65
|
+
// handle specific listen errors with friendly messages
|
|
66
|
+
switch (error.code) {
|
|
67
|
+
case 'EACCES':
|
|
68
|
+
console.error(bind + ' requires elevated privileges');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
break;
|
|
71
|
+
case 'EADDRINUSE':
|
|
72
|
+
console.error(bind + ' is already in use');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Event listener for HTTP server "listening" event.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
function onListening() {
|
|
85
|
+
var addr = server.address();
|
|
86
|
+
var bind = typeof addr === 'string'
|
|
87
|
+
? 'pipe ' + addr
|
|
88
|
+
: 'port ' + addr.port;
|
|
89
|
+
debug('Listening on ' + bind);
|
|
90
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// 保留历史导入入口,实际能力全部委托给
|
|
2
|
+
// lib/backtrace 和 lib/analysis 下的新模块实现。
|
|
3
|
+
const {
|
|
4
|
+
DEFAULT_QUERY_URL,
|
|
5
|
+
DEFAULT_WORKDIR,
|
|
6
|
+
DEFAULT_PROXY,
|
|
7
|
+
DEFAULT_SELECT,
|
|
8
|
+
DEFAULT_LIMIT,
|
|
9
|
+
DEFAULT_DOWNLOAD_CONCURRENCY,
|
|
10
|
+
DEFAULT_RETRIES,
|
|
11
|
+
DEFAULT_STORAGE_DIR,
|
|
12
|
+
} = require("./backtrace/constants");
|
|
13
|
+
const { printUsage, parseArgs, createOptions } = require("./backtrace/options");
|
|
14
|
+
const { printList } = require("./backtrace/analysis");
|
|
15
|
+
const { BacktraceCodexTool, runBacktraceCommand } = require("./backtrace/tool");
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
BacktraceCodexTool,
|
|
19
|
+
runBacktraceCommand,
|
|
20
|
+
createOptions,
|
|
21
|
+
parseArgs,
|
|
22
|
+
printUsage,
|
|
23
|
+
printList,
|
|
24
|
+
DEFAULT_QUERY_URL,
|
|
25
|
+
DEFAULT_WORKDIR,
|
|
26
|
+
DEFAULT_PROXY,
|
|
27
|
+
DEFAULT_SELECT,
|
|
28
|
+
DEFAULT_LIMIT,
|
|
29
|
+
DEFAULT_DOWNLOAD_CONCURRENCY,
|
|
30
|
+
DEFAULT_RETRIES,
|
|
31
|
+
DEFAULT_STORAGE_DIR,
|
|
32
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
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(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/&/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
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
};
|