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,642 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Backtrace Directory Browser</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/stylesheets/style.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="app-shell">
|
|
14
|
+
<header class="topbar">
|
|
15
|
+
<div>
|
|
16
|
+
<p class="eyebrow">Crash Archive</p>
|
|
17
|
+
<h1>Directory Console</h1>
|
|
18
|
+
<p class="subcopy">左侧目录按时间从近到远排序。没有报告的目录会显示“未生成报告”。报告文件支持生成修复方案,并由你确认是否应用到项目。</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="toolbar">
|
|
21
|
+
<label class="toolbar-field" for="collectFingerprintInput">
|
|
22
|
+
<span>Fingerprint</span>
|
|
23
|
+
<input id="collectFingerprintInput" class="toolbar-input" type="text" placeholder="可选,按指纹收集">
|
|
24
|
+
</label>
|
|
25
|
+
<button id="collectBtn" class="action-button" type="button">收集</button>
|
|
26
|
+
<button id="refreshBtn" class="action-button secondary-action" type="button">刷新索引</button>
|
|
27
|
+
<div class="status-chip">
|
|
28
|
+
<span>状态</span>
|
|
29
|
+
<strong id="statusText">等待加载</strong>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<main class="browser-layout">
|
|
35
|
+
<aside class="browser-sidebar panel-surface">
|
|
36
|
+
<div class="panel-head sticky-head">
|
|
37
|
+
<div>
|
|
38
|
+
<p class="panel-kicker">Index</p>
|
|
39
|
+
<h2>目录索引</h2>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div id="directoryTree" class="tree-root tree-root-large">加载中...</div>
|
|
43
|
+
</aside>
|
|
44
|
+
|
|
45
|
+
<section class="browser-main panel-surface">
|
|
46
|
+
<div class="panel-head viewer-head">
|
|
47
|
+
<div>
|
|
48
|
+
<p class="panel-kicker">Selection</p>
|
|
49
|
+
<h2 id="selectedDirectoryTitle">未选择目录</h2>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="viewer-side-actions">
|
|
52
|
+
<div class="viewer-meta">
|
|
53
|
+
<span>当前目录</span>
|
|
54
|
+
<code id="selectedDirectoryPath">请从左侧选择目录</code>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="viewer-meta">
|
|
57
|
+
<span>报告状态</span>
|
|
58
|
+
<strong id="reportStatus">未生成报告</strong>
|
|
59
|
+
</div>
|
|
60
|
+
<button id="downloadBtn" class="action-button secondary-action" type="button" disabled>下载报告</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="tabs-shell">
|
|
65
|
+
<div class="tabs-header">
|
|
66
|
+
<div class="tabs-nav">
|
|
67
|
+
<button id="logsTabBtn" class="tab-button is-active" type="button">日志列表</button>
|
|
68
|
+
<button id="reportTabBtn" class="tab-button" type="button">报告</button>
|
|
69
|
+
</div>
|
|
70
|
+
<button id="summarizeErrorsBtn" class="action-button secondary-action" type="button" disabled>汇总错误日志</button>
|
|
71
|
+
<button id="analyzeCurrentBtn" class="action-button secondary-action" type="button" disabled>分析当前 fingerprint</button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div id="logsTab" class="tab-panel is-active">
|
|
75
|
+
<div class="logs-split">
|
|
76
|
+
<div class="subpanel">
|
|
77
|
+
<div class="subpanel-head">日志目录</div>
|
|
78
|
+
<div id="logDirectories" class="list-box">请选择左侧目录</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="subpanel">
|
|
81
|
+
<div class="subpanel-head">日志文件</div>
|
|
82
|
+
<div id="logFiles" class="list-box">请选择日志目录</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div id="reportTab" class="tab-panel">
|
|
88
|
+
<div class="subpanel report-panel">
|
|
89
|
+
<div class="subpanel-head">报告文件</div>
|
|
90
|
+
<div id="reportFiles" class="list-box">请选择左侧目录</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="viewer-body">
|
|
96
|
+
<div class="panel-head file-panel-head">
|
|
97
|
+
<div>
|
|
98
|
+
<p class="panel-kicker">Preview</p>
|
|
99
|
+
<h2 id="viewerTitle">文件内容</h2>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="viewer-side-actions">
|
|
102
|
+
<div class="viewer-meta">
|
|
103
|
+
<span id="viewerKind">未选择</span>
|
|
104
|
+
<code id="viewerPath">请选择右侧文件</code>
|
|
105
|
+
</div>
|
|
106
|
+
<button id="generateFixBtn" class="action-button secondary-action" type="button" disabled>生成修复方案</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<pre id="viewerContent" class="file-viewer">点击右侧文件后,这里会显示原始内容。</pre>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<section class="fix-panel panel-surface inner-panel">
|
|
113
|
+
<div class="panel-head file-panel-head">
|
|
114
|
+
<div>
|
|
115
|
+
<p class="panel-kicker">Fix Plan</p>
|
|
116
|
+
<h2>修复方案</h2>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="viewer-side-actions">
|
|
119
|
+
<button id="applyFixBtn" class="action-button" type="button" disabled>确认应用</button>
|
|
120
|
+
<button id="rejectFixBtn" class="action-button secondary-action" type="button" disabled>拒绝</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<pre id="fixPlanContent" class="file-viewer fix-viewer">请选择报告文件后点击“生成修复方案”。</pre>
|
|
124
|
+
</section>
|
|
125
|
+
</section>
|
|
126
|
+
</main>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<script>
|
|
130
|
+
const statusText = document.getElementById('statusText');
|
|
131
|
+
const directoryTree = document.getElementById('directoryTree');
|
|
132
|
+
const selectedDirectoryTitle = document.getElementById('selectedDirectoryTitle');
|
|
133
|
+
const selectedDirectoryPath = document.getElementById('selectedDirectoryPath');
|
|
134
|
+
const reportStatus = document.getElementById('reportStatus');
|
|
135
|
+
const logDirectories = document.getElementById('logDirectories');
|
|
136
|
+
const logFiles = document.getElementById('logFiles');
|
|
137
|
+
const reportFiles = document.getElementById('reportFiles');
|
|
138
|
+
const viewerTitle = document.getElementById('viewerTitle');
|
|
139
|
+
const viewerKind = document.getElementById('viewerKind');
|
|
140
|
+
const viewerPath = document.getElementById('viewerPath');
|
|
141
|
+
const viewerContent = document.getElementById('viewerContent');
|
|
142
|
+
const fixPlanContent = document.getElementById('fixPlanContent');
|
|
143
|
+
const collectBtn = document.getElementById('collectBtn');
|
|
144
|
+
const collectFingerprintInput = document.getElementById('collectFingerprintInput');
|
|
145
|
+
const refreshBtn = document.getElementById('refreshBtn');
|
|
146
|
+
const downloadBtn = document.getElementById('downloadBtn');
|
|
147
|
+
const generateFixBtn = document.getElementById('generateFixBtn');
|
|
148
|
+
const applyFixBtn = document.getElementById('applyFixBtn');
|
|
149
|
+
const rejectFixBtn = document.getElementById('rejectFixBtn');
|
|
150
|
+
const logsTabBtn = document.getElementById('logsTabBtn');
|
|
151
|
+
const reportTabBtn = document.getElementById('reportTabBtn');
|
|
152
|
+
const summarizeErrorsBtn = document.getElementById('summarizeErrorsBtn');
|
|
153
|
+
const analyzeCurrentBtn = document.getElementById('analyzeCurrentBtn');
|
|
154
|
+
const logsTab = document.getElementById('logsTab');
|
|
155
|
+
const reportTab = document.getElementById('reportTab');
|
|
156
|
+
|
|
157
|
+
let currentDirectory = '';
|
|
158
|
+
let currentLogDir = '';
|
|
159
|
+
let currentFile = null;
|
|
160
|
+
let currentFixPlan = '';
|
|
161
|
+
let currentRepairVersion = '';
|
|
162
|
+
let currentRepairPlanPath = '';
|
|
163
|
+
let currentDirectoryCanAnalyze = false;
|
|
164
|
+
|
|
165
|
+
function setStatus(text, mode) {
|
|
166
|
+
statusText.textContent = text;
|
|
167
|
+
statusText.dataset.mode = mode || 'idle';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function logClient(scope, message, details) {
|
|
171
|
+
if (details !== undefined) {
|
|
172
|
+
console.log('[backtrace-ui][' + scope + ']', message, details);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log('[backtrace-ui][' + scope + ']', message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function escapeHtml(value) {
|
|
179
|
+
return String(value)
|
|
180
|
+
.replace(/&/g, '&')
|
|
181
|
+
.replace(/</g, '<')
|
|
182
|
+
.replace(/>/g, '>')
|
|
183
|
+
.replace(/"/g, '"')
|
|
184
|
+
.replace(/'/g, ''');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function shortenFingerprint(value) {
|
|
188
|
+
return String(value || '');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatFetchedTime(value) {
|
|
192
|
+
if (!value) return '获取时间未知';
|
|
193
|
+
const date = new Date(value);
|
|
194
|
+
if (Number.isNaN(date.getTime())) return '获取时间未知';
|
|
195
|
+
return '获取时间 ' + date.toLocaleString('zh-CN', { hour12: false });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getSelectedReportPath() {
|
|
199
|
+
if (currentFile && currentFile.kind === 'report' && currentFile.path) {
|
|
200
|
+
return currentFile.path;
|
|
201
|
+
}
|
|
202
|
+
const activeReportButton = reportFiles.querySelector('.file-row.is-active');
|
|
203
|
+
if (activeReportButton && activeReportButton.dataset && activeReportButton.dataset.path) {
|
|
204
|
+
return activeReportButton.dataset.path;
|
|
205
|
+
}
|
|
206
|
+
const firstReportButton = reportFiles.querySelector('.file-row[data-kind="report"]');
|
|
207
|
+
if (firstReportButton && firstReportButton.dataset && firstReportButton.dataset.path) {
|
|
208
|
+
return firstReportButton.dataset.path;
|
|
209
|
+
}
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateDownloadButton() {
|
|
214
|
+
const reportPath = getSelectedReportPath();
|
|
215
|
+
downloadBtn.disabled = !reportPath;
|
|
216
|
+
generateFixBtn.disabled = !reportPath;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function updateAnalyzeCurrentButton() {
|
|
220
|
+
const enabled = !!(currentDirectory && currentDirectoryCanAnalyze);
|
|
221
|
+
analyzeCurrentBtn.disabled = !enabled;
|
|
222
|
+
summarizeErrorsBtn.disabled = !enabled;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function updateFixButtons() {
|
|
226
|
+
const enabled = !!(currentFile && currentFile.kind === 'report' && currentFixPlan);
|
|
227
|
+
applyFixBtn.disabled = !enabled;
|
|
228
|
+
rejectFixBtn.disabled = !enabled;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resetFixPlan(text) {
|
|
232
|
+
currentFixPlan = '';
|
|
233
|
+
currentRepairVersion = '';
|
|
234
|
+
currentRepairPlanPath = '';
|
|
235
|
+
fixPlanContent.textContent = text || '请选择报告文件后点击“生成修复方案”。';
|
|
236
|
+
updateFixButtons();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function switchTab(tabName) {
|
|
240
|
+
const isLogs = tabName === 'logs';
|
|
241
|
+
logsTabBtn.classList.toggle('is-active', isLogs);
|
|
242
|
+
reportTabBtn.classList.toggle('is-active', !isLogs);
|
|
243
|
+
logsTab.classList.toggle('is-active', isLogs);
|
|
244
|
+
reportTab.classList.toggle('is-active', !isLogs);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function setCollectButtonsDisabled(disabled) {
|
|
248
|
+
collectBtn.disabled = disabled;
|
|
249
|
+
collectFingerprintInput.disabled = disabled;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderTopDirectories(items) {
|
|
253
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
254
|
+
return '<div class="empty-tree">当前没有目录</div>';
|
|
255
|
+
}
|
|
256
|
+
return '<div class="dir-items">' + items.map(function(item) {
|
|
257
|
+
const statusLabel = item.status || (item.hasReports ? '未修复状态' : '未生成报告');
|
|
258
|
+
const statusClass = statusLabel === '已完成'
|
|
259
|
+
? 'status-tag done'
|
|
260
|
+
: statusLabel === '未修复状态'
|
|
261
|
+
? 'status-tag pending'
|
|
262
|
+
: 'status-tag empty';
|
|
263
|
+
return [
|
|
264
|
+
'<button type="button" class="tree-directory-button" data-path="' + escapeHtml(item.path) + '" data-status="' + escapeHtml(statusLabel) + '">',
|
|
265
|
+
'<span class="node-badge dir">DIR</span>',
|
|
266
|
+
'<span class="file-row-main">',
|
|
267
|
+
'<strong>' + escapeHtml(shortenFingerprint(item.name)).substring(0,7) + '</strong>',
|
|
268
|
+
'<small>' + escapeHtml(formatFetchedTime(item.fetchedAt)) + '</small>',
|
|
269
|
+
'<span class="' + statusClass + '">' + escapeHtml(statusLabel) + '</span>',
|
|
270
|
+
'</span>',
|
|
271
|
+
'</button>'
|
|
272
|
+
].join('');
|
|
273
|
+
}).join('') + '</div>';
|
|
274
|
+
}
|
|
275
|
+
function renderButtonList(items, className, emptyText, kind) {
|
|
276
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
277
|
+
return '<div class="empty-tree">' + emptyText + '</div>';
|
|
278
|
+
}
|
|
279
|
+
return '<div class="file-items">' + items.map(function(item) {
|
|
280
|
+
return [
|
|
281
|
+
'<button type="button" class="' + className + '" data-kind="' + escapeHtml(kind || '') + '" data-path="' + escapeHtml(item.path) + '" data-name="' + escapeHtml(item.name) + '">',
|
|
282
|
+
'<span class="node-badge ' + (className === 'log-directory-button' ? 'dir' : 'file') + '">' + (className === 'log-directory-button' ? 'DIR' : 'FILE') + '</span>',
|
|
283
|
+
'<span class="file-row-main">',
|
|
284
|
+
'<strong>' + escapeHtml(item.name) + '</strong>',
|
|
285
|
+
'<small>' + escapeHtml(item.path) + '</small>',
|
|
286
|
+
'</span>',
|
|
287
|
+
'</button>'
|
|
288
|
+
].join('');
|
|
289
|
+
}).join('') + '</div>';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function loadDirectoryIndex() {
|
|
293
|
+
setStatus('加载目录索引中', 'loading');
|
|
294
|
+
directoryTree.innerHTML = '<div class="empty-tree">加载中...</div>';
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetch('/api/backtrace/files/index');
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
if (!response.ok || !data.ok) {
|
|
299
|
+
throw new Error(data.error || '加载目录失败');
|
|
300
|
+
}
|
|
301
|
+
directoryTree.innerHTML = renderTopDirectories(data.directories || []);
|
|
302
|
+
bindTopDirectoryButtons();
|
|
303
|
+
setStatus('目录索引已更新', 'success');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
directoryTree.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
|
|
306
|
+
setStatus('目录索引加载失败', 'error');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function loadDirectoryContent(relativePath, preferredLogDir, statusLabel) {
|
|
311
|
+
currentDirectory = relativePath;
|
|
312
|
+
currentDirectoryCanAnalyze = false;
|
|
313
|
+
currentFile = null;
|
|
314
|
+
updateDownloadButton();
|
|
315
|
+
updateAnalyzeCurrentButton();
|
|
316
|
+
resetFixPlan();
|
|
317
|
+
const displayName = relativePath ? shortenFingerprint(relativePath.split(/[\\/]/).pop()) : '未选择目录';
|
|
318
|
+
selectedDirectoryTitle.textContent = displayName.substring(0,7);
|
|
319
|
+
selectedDirectoryPath.textContent = relativePath ? displayName : '请从左侧选择目录';
|
|
320
|
+
reportStatus.textContent = statusLabel || '未生成报告';
|
|
321
|
+
viewerTitle.textContent = '文件内容';
|
|
322
|
+
viewerKind.textContent = '未选择';
|
|
323
|
+
viewerPath.textContent = '请选择右侧文件';
|
|
324
|
+
viewerContent.textContent = '请先在右侧列表中选择文件。';
|
|
325
|
+
logDirectories.innerHTML = '加载中...';
|
|
326
|
+
logFiles.innerHTML = '加载中...';
|
|
327
|
+
reportFiles.innerHTML = '加载中...';
|
|
328
|
+
setStatus('加载目录内容中', 'loading');
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const params = new URLSearchParams();
|
|
332
|
+
params.set('path', relativePath || '');
|
|
333
|
+
if (preferredLogDir) {
|
|
334
|
+
params.set('logDir', preferredLogDir);
|
|
335
|
+
}
|
|
336
|
+
const response = await fetch('/api/backtrace/files/list?' + params.toString());
|
|
337
|
+
const data = await response.json();
|
|
338
|
+
if (!response.ok || !data.ok) {
|
|
339
|
+
throw new Error(data.error || '加载目录内容失败');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
currentLogDir = data.selectedLogDir || '';
|
|
343
|
+
currentDirectoryCanAnalyze = Array.isArray(data.logDirectories) && data.logDirectories.length > 0;
|
|
344
|
+
reportStatus.textContent = data.reportStatus || '未生成报告';
|
|
345
|
+
logDirectories.innerHTML = renderButtonList(data.logDirectories || [], 'log-directory-button', '当前目录下没有日志子目录', 'logs-dir');
|
|
346
|
+
logFiles.innerHTML = renderButtonList(data.logFiles || [], 'file-row', '当前日志目录下没有日志文件', 'logs');
|
|
347
|
+
reportFiles.innerHTML = renderButtonList(data.reportFiles || [], 'file-row', '当前目录下没有报告文件', 'report');
|
|
348
|
+
updateAnalyzeCurrentButton();
|
|
349
|
+
bindLogDirectoryButtons();
|
|
350
|
+
bindFileButtons();
|
|
351
|
+
setStatus('目录内容已加载', 'success');
|
|
352
|
+
} catch (error) {
|
|
353
|
+
currentDirectoryCanAnalyze = false;
|
|
354
|
+
updateAnalyzeCurrentButton();
|
|
355
|
+
logDirectories.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
|
|
356
|
+
logFiles.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
|
|
357
|
+
reportFiles.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
|
|
358
|
+
setStatus('目录内容加载失败', 'error');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function openFile(kind, relativePath, button) {
|
|
363
|
+
document.querySelectorAll('.file-row.is-active').forEach(function(node) {
|
|
364
|
+
node.classList.remove('is-active');
|
|
365
|
+
});
|
|
366
|
+
if (button) button.classList.add('is-active');
|
|
367
|
+
|
|
368
|
+
currentFile = { kind: kind, path: relativePath };
|
|
369
|
+
updateDownloadButton();
|
|
370
|
+
resetFixPlan(kind === 'report' ? '当前已选中报告文件,可点击“生成修复方案”。' : '修复方案仅支持基于报告文件生成。');
|
|
371
|
+
viewerTitle.textContent = '文件内容';
|
|
372
|
+
viewerKind.textContent = kind;
|
|
373
|
+
viewerPath.textContent = relativePath;
|
|
374
|
+
viewerContent.textContent = '读取文件中...';
|
|
375
|
+
setStatus('读取文件中', 'loading');
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const url = '/api/backtrace/files/content?kind=' + encodeURIComponent(kind) + '&path=' + encodeURIComponent(relativePath);
|
|
379
|
+
const response = await fetch(url);
|
|
380
|
+
const data = await response.json();
|
|
381
|
+
if (!response.ok || !data.ok) {
|
|
382
|
+
throw new Error(data.error || '读取文件失败');
|
|
383
|
+
}
|
|
384
|
+
currentFile = { kind: kind, path: data.file.path };
|
|
385
|
+
updateDownloadButton();
|
|
386
|
+
viewerTitle.textContent = data.file.path.split(/[\\/]/).pop();
|
|
387
|
+
viewerKind.textContent = kind;
|
|
388
|
+
viewerPath.textContent = data.file.path;
|
|
389
|
+
viewerContent.textContent = data.file.content || '';
|
|
390
|
+
setStatus('文件已打开', 'success');
|
|
391
|
+
} catch (error) {
|
|
392
|
+
viewerContent.textContent = error.message;
|
|
393
|
+
setStatus('文件读取失败', 'error');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function bindTopDirectoryButtons() {
|
|
398
|
+
document.querySelectorAll('.tree-directory-button').forEach(function(button) {
|
|
399
|
+
button.classList.toggle('is-active', button.dataset.path === currentDirectory);
|
|
400
|
+
button.addEventListener('click', function() {
|
|
401
|
+
document.querySelectorAll('.tree-directory-button.is-active').forEach(function(node) {
|
|
402
|
+
node.classList.remove('is-active');
|
|
403
|
+
});
|
|
404
|
+
button.classList.add('is-active');
|
|
405
|
+
loadDirectoryContent(button.dataset.path, '', button.dataset.status || '未生成报告');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function bindLogDirectoryButtons() {
|
|
411
|
+
document.querySelectorAll('.log-directory-button').forEach(function(button) {
|
|
412
|
+
button.classList.toggle('is-active', button.dataset.name === currentLogDir);
|
|
413
|
+
button.addEventListener('click', function() {
|
|
414
|
+
loadDirectoryContent(currentDirectory, button.dataset.name, reportStatus.textContent);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function bindFileButtons() {
|
|
420
|
+
document.querySelectorAll('.file-row').forEach(function(button) {
|
|
421
|
+
button.addEventListener('click', function() {
|
|
422
|
+
openFile(button.dataset.kind, button.dataset.path, button);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function runCollection() {
|
|
428
|
+
const now = Math.floor(Date.now() / 1000);
|
|
429
|
+
const payload = { command: 'collect-all' };
|
|
430
|
+
const fingerprint = String(collectFingerprintInput.value || '').trim();
|
|
431
|
+
payload.from = String(now - 86400);
|
|
432
|
+
payload.to = String(now);
|
|
433
|
+
if (fingerprint) {
|
|
434
|
+
payload.fingerprint = fingerprint;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
setCollectButtonsDisabled(true);
|
|
438
|
+
setStatus(fingerprint ? '开始按 fingerprint 收集24小时日志...' : '开始收集24小时日志...', 'loading');
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetch('/api/backtrace/run', {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: { 'content-type': 'application/json' },
|
|
443
|
+
body: JSON.stringify(payload)
|
|
444
|
+
});
|
|
445
|
+
const data = await response.json();
|
|
446
|
+
if (!response.ok || !data.ok) {
|
|
447
|
+
throw new Error(data.error || '收集失败');
|
|
448
|
+
}
|
|
449
|
+
setStatus(fingerprint ? '24小时定向收集完成,正在刷新索引' : '24小时收集完成,正在刷新索引', 'success');
|
|
450
|
+
await loadDirectoryIndex();
|
|
451
|
+
if (currentDirectory) {
|
|
452
|
+
await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
setStatus(error.message || '收集失败', 'error');
|
|
456
|
+
} finally {
|
|
457
|
+
setCollectButtonsDisabled(false);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
logsTabBtn.addEventListener('click', function() { switchTab('logs'); });
|
|
462
|
+
reportTabBtn.addEventListener('click', function() { switchTab('report'); });
|
|
463
|
+
|
|
464
|
+
downloadBtn.addEventListener('click', function() {
|
|
465
|
+
const reportPath = getSelectedReportPath();
|
|
466
|
+
if (!reportPath) return;
|
|
467
|
+
const url = '/api/backtrace/files/download?kind=report&path=' + encodeURIComponent(reportPath);
|
|
468
|
+
window.open(url, '_blank');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
generateFixBtn.addEventListener('click', async function() {
|
|
472
|
+
let reportPath = getSelectedReportPath();
|
|
473
|
+
if (!reportPath) {
|
|
474
|
+
const firstReportButton = reportFiles.querySelector('.file-row[data-kind="report"]');
|
|
475
|
+
if (firstReportButton && firstReportButton.dataset && firstReportButton.dataset.path) {
|
|
476
|
+
reportPath = firstReportButton.dataset.path;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!reportPath) {
|
|
480
|
+
setStatus('请先选择报告文件', 'error');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
logClient('fix-plan', 'generate requested', { reportPath: reportPath });
|
|
484
|
+
generateFixBtn.disabled = true;
|
|
485
|
+
fixPlanContent.textContent = '正在生成修复方案...\n\n阶段 1/3:已发起请求,等待服务端接收。';
|
|
486
|
+
setStatus('正在生成修复方案...', 'loading');
|
|
487
|
+
try {
|
|
488
|
+
logClient('fix-plan', 'sending request', { url: '/api/backtrace/fix-plan/generate' });
|
|
489
|
+
const response = await fetch('/api/backtrace/fix-plan/generate', {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
headers: { 'content-type': 'application/json' },
|
|
492
|
+
body: JSON.stringify({ reportPath: reportPath })
|
|
493
|
+
});
|
|
494
|
+
fixPlanContent.textContent = '正在生成修复方案...\n\n阶段 2/3:服务端已响应,正在解析结果。';
|
|
495
|
+
logClient('fix-plan', 'response received', { status: response.status, ok: response.ok });
|
|
496
|
+
const data = await response.json();
|
|
497
|
+
logClient('fix-plan', 'response json parsed', data);
|
|
498
|
+
if (!response.ok || !data.ok) {
|
|
499
|
+
throw new Error(data.error || '生成修复方案失败');
|
|
500
|
+
}
|
|
501
|
+
currentFixPlan = data.plan || '';
|
|
502
|
+
currentRepairVersion = data.repairVersion || '';
|
|
503
|
+
currentRepairPlanPath = data.repairPlanPath || '';
|
|
504
|
+
fixPlanContent.textContent = currentFixPlan || 'Codex 未返回修复方案。';
|
|
505
|
+
updateFixButtons();
|
|
506
|
+
setStatus('修复方案已生成', 'success');
|
|
507
|
+
logClient('fix-plan', 'generate completed', { reportPath: reportPath, threadId: data.threadId, repairVersion: currentRepairVersion, repairPlanPath: currentRepairPlanPath });
|
|
508
|
+
} catch (error) {
|
|
509
|
+
currentFixPlan = '';
|
|
510
|
+
currentRepairVersion = '';
|
|
511
|
+
currentRepairPlanPath = '';
|
|
512
|
+
fixPlanContent.textContent = error.message;
|
|
513
|
+
updateFixButtons();
|
|
514
|
+
setStatus('生成修复方案失败', 'error');
|
|
515
|
+
logClient('fix-plan', 'generate failed', { reportPath: reportPath, message: error.message });
|
|
516
|
+
} finally {
|
|
517
|
+
updateDownloadButton();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
applyFixBtn.addEventListener('click', async function() {
|
|
522
|
+
const reportPath = getSelectedReportPath();
|
|
523
|
+
if (!reportPath || !currentFixPlan) return;
|
|
524
|
+
applyFixBtn.disabled = true;
|
|
525
|
+
rejectFixBtn.disabled = true;
|
|
526
|
+
fixPlanContent.textContent = '正在应用修复方案...';
|
|
527
|
+
setStatus('正在应用修复方案...', 'loading');
|
|
528
|
+
try {
|
|
529
|
+
const response = await fetch('/api/backtrace/fix-plan/apply', {
|
|
530
|
+
method: 'POST',
|
|
531
|
+
headers: { 'content-type': 'application/json' },
|
|
532
|
+
body: JSON.stringify({ reportPath: reportPath, planText: currentFixPlan, repairVersion: currentRepairVersion, repairPlanPath: currentRepairPlanPath })
|
|
533
|
+
});
|
|
534
|
+
const data = await response.json();
|
|
535
|
+
if (!response.ok || !data.ok) {
|
|
536
|
+
throw new Error(data.error || '应用修复方案失败');
|
|
537
|
+
}
|
|
538
|
+
fixPlanContent.textContent = data.resultText || '修复应用完成。';
|
|
539
|
+
currentFixPlan = data.resultText || currentFixPlan;
|
|
540
|
+
currentRepairVersion = data.repairVersion || currentRepairVersion;
|
|
541
|
+
updateFixButtons();
|
|
542
|
+
setStatus('修复方案已应用', 'success');
|
|
543
|
+
} catch (error) {
|
|
544
|
+
fixPlanContent.textContent = error.message;
|
|
545
|
+
updateFixButtons();
|
|
546
|
+
setStatus('应用修复方案失败', 'error');
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
rejectFixBtn.addEventListener('click', function() {
|
|
551
|
+
resetFixPlan('已拒绝当前修复方案。你可以重新生成新的修复方案。');
|
|
552
|
+
setStatus('已拒绝修复方案', 'success');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
collectBtn.addEventListener('click', async function() {
|
|
556
|
+
await runCollection();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
summarizeErrorsBtn.addEventListener('click', async function() {
|
|
560
|
+
if (!currentDirectory || !currentDirectoryCanAnalyze) return;
|
|
561
|
+
const fingerprint = currentDirectory.split(/[\/]/).pop();
|
|
562
|
+
const originalText = summarizeErrorsBtn.textContent;
|
|
563
|
+
summarizeErrorsBtn.disabled = true;
|
|
564
|
+
summarizeErrorsBtn.textContent = '汇总中...';
|
|
565
|
+
setStatus('正在汇总错误日志: ' + shortenFingerprint(fingerprint), 'loading');
|
|
566
|
+
try {
|
|
567
|
+
const response = await fetch('/api/backtrace/run', {
|
|
568
|
+
method: 'POST',
|
|
569
|
+
headers: { 'content-type': 'application/json' },
|
|
570
|
+
body: JSON.stringify({
|
|
571
|
+
command: 'summarize-fingerprint-errors',
|
|
572
|
+
fingerprint: fingerprint
|
|
573
|
+
})
|
|
574
|
+
});
|
|
575
|
+
const data = await response.json();
|
|
576
|
+
if (!response.ok || !data.ok) {
|
|
577
|
+
throw new Error(data.error || '错误日志汇总失败');
|
|
578
|
+
}
|
|
579
|
+
setStatus('错误日志汇总完成,正在刷新目录', 'success');
|
|
580
|
+
await loadDirectoryIndex();
|
|
581
|
+
await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
setStatus(error.message || '错误日志汇总失败', 'error');
|
|
584
|
+
} finally {
|
|
585
|
+
summarizeErrorsBtn.textContent = originalText;
|
|
586
|
+
updateAnalyzeCurrentButton();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
analyzeCurrentBtn.addEventListener('click', async function() {
|
|
591
|
+
if (!currentDirectory || !currentDirectoryCanAnalyze) return;
|
|
592
|
+
const fingerprint = currentDirectory.split(/[\\/]/).pop();
|
|
593
|
+
const originalText = analyzeCurrentBtn.textContent;
|
|
594
|
+
analyzeCurrentBtn.disabled = true;
|
|
595
|
+
analyzeCurrentBtn.textContent = '分析中...';
|
|
596
|
+
setStatus('正在分析 fingerprint: ' + shortenFingerprint(fingerprint), 'loading');
|
|
597
|
+
try {
|
|
598
|
+
const response = await fetch('/api/backtrace/run', {
|
|
599
|
+
method: 'POST',
|
|
600
|
+
headers: { 'content-type': 'application/json' },
|
|
601
|
+
body: JSON.stringify({
|
|
602
|
+
command: 'analyze-fingerprint',
|
|
603
|
+
fingerprint: fingerprint
|
|
604
|
+
})
|
|
605
|
+
});
|
|
606
|
+
const data = await response.json();
|
|
607
|
+
if (!response.ok || !data.ok) {
|
|
608
|
+
throw new Error(data.error || 'fingerprint 分析失败');
|
|
609
|
+
}
|
|
610
|
+
setStatus('fingerprint 分析完成,正在刷新索引', 'success');
|
|
611
|
+
await loadDirectoryIndex();
|
|
612
|
+
await loadDirectoryContent(currentDirectory, currentLogDir, '未修复状态');
|
|
613
|
+
} catch (error) {
|
|
614
|
+
setStatus(error.message || 'fingerprint 分析失败', 'error');
|
|
615
|
+
} finally {
|
|
616
|
+
analyzeCurrentBtn.textContent = originalText;
|
|
617
|
+
updateAnalyzeCurrentButton();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
refreshBtn.addEventListener('click', async function() {
|
|
622
|
+
await loadDirectoryIndex();
|
|
623
|
+
if (currentDirectory) {
|
|
624
|
+
await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
updateDownloadButton();
|
|
629
|
+
updateAnalyzeCurrentButton();
|
|
630
|
+
resetFixPlan();
|
|
631
|
+
switchTab('logs');
|
|
632
|
+
loadDirectoryIndex();
|
|
633
|
+
</script>
|
|
634
|
+
</body>
|
|
635
|
+
</html>
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|