backtrace-console 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,451 @@
1
+ const statusText = document.getElementById('statusText');
2
+ const directoryTree = document.getElementById('directoryTree');
3
+ const selectedDirectoryTitle = document.getElementById('selectedDirectoryTitle');
4
+ const selectedDirectoryPath = document.getElementById('selectedDirectoryPath');
5
+ const reportStatus = document.getElementById('reportStatus');
6
+ const logDirectories = document.getElementById('logDirectories');
7
+ const logFiles = document.getElementById('logFiles');
8
+ const reportFiles = document.getElementById('reportFiles');
9
+ const viewerTitle = document.getElementById('viewerTitle');
10
+ const viewerKind = document.getElementById('viewerKind');
11
+ const viewerPath = document.getElementById('viewerPath');
12
+ const viewerContent = document.getElementById('viewerContent');
13
+ const fixPlanContent = document.getElementById('fixPlanContent');
14
+ const collectBtn = document.getElementById('collectBtn');
15
+ const collectFingerprintInput = document.getElementById('collectFingerprintInput');
16
+ const refreshBtn = document.getElementById('refreshBtn');
17
+ const downloadBtn = document.getElementById('downloadBtn');
18
+ const generateFixBtn = document.getElementById('generateFixBtn');
19
+ const applyFixBtn = document.getElementById('applyFixBtn');
20
+ const rejectFixBtn = document.getElementById('rejectFixBtn');
21
+ const logsTabBtn = document.getElementById('logsTabBtn');
22
+ const reportTabBtn = document.getElementById('reportTabBtn');
23
+ const analyzeCurrentBtn = document.getElementById('analyzeCurrentBtn');
24
+ const logsTab = document.getElementById('logsTab');
25
+ const reportTab = document.getElementById('reportTab');
26
+
27
+ let currentDirectory = '';
28
+ let currentLogDir = '';
29
+ let currentFile = null;
30
+ let currentFixPlan = '';
31
+ let currentDirectoryCanAnalyze = false;
32
+
33
+ function setStatus(text, mode) {
34
+ statusText.textContent = text;
35
+ statusText.dataset.mode = mode || 'idle';
36
+ }
37
+
38
+ function logClient(scope, message, details) {
39
+ if (details !== undefined) {
40
+ console.log('[backtrace-ui][' + scope + ']', message, details);
41
+ return;
42
+ }
43
+ console.log('[backtrace-ui][' + scope + ']', message);
44
+ }
45
+
46
+ function escapeHtml(value) {
47
+ return String(value)
48
+ .replace(/&/g, '&')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/'/g, '&#39;');
53
+ }
54
+
55
+ function shortenFingerprint(value) {
56
+ return String(value || '');
57
+ }
58
+
59
+ function formatFetchedTime(value) {
60
+ if (!value) return '获取时间未知';
61
+ const date = new Date(value);
62
+ if (Number.isNaN(date.getTime())) return '获取时间未知';
63
+ return '获取时间 ' + date.toLocaleString('zh-CN', { hour12: false });
64
+ }
65
+
66
+ function getSelectedReportPath() {
67
+ if (currentFile && currentFile.kind === 'report' && currentFile.path) {
68
+ return currentFile.path;
69
+ }
70
+ const activeReportButton = reportFiles.querySelector('.file-row.is-active');
71
+ if (activeReportButton && activeReportButton.dataset && activeReportButton.dataset.path) {
72
+ return activeReportButton.dataset.path;
73
+ }
74
+ return '';
75
+ }
76
+
77
+ function updateDownloadButton() {
78
+ const reportPath = getSelectedReportPath();
79
+ downloadBtn.disabled = !reportPath;
80
+ generateFixBtn.disabled = !reportPath;
81
+ }
82
+
83
+ function updateAnalyzeCurrentButton() {
84
+ analyzeCurrentBtn.disabled = !(currentDirectory && currentDirectoryCanAnalyze);
85
+ }
86
+
87
+ function updateFixButtons() {
88
+ const enabled = !!(currentFile && currentFile.kind === 'report' && currentFixPlan);
89
+ applyFixBtn.disabled = !enabled;
90
+ rejectFixBtn.disabled = !enabled;
91
+ }
92
+
93
+ function resetFixPlan(text) {
94
+ currentFixPlan = '';
95
+ fixPlanContent.textContent = text || '请选择报告文件后点击“生成修复方案”。';
96
+ updateFixButtons();
97
+ }
98
+
99
+ function switchTab(tabName) {
100
+ const isLogs = tabName === 'logs';
101
+ logsTabBtn.classList.toggle('is-active', isLogs);
102
+ reportTabBtn.classList.toggle('is-active', !isLogs);
103
+ logsTab.classList.toggle('is-active', isLogs);
104
+ reportTab.classList.toggle('is-active', !isLogs);
105
+ }
106
+
107
+ function setCollectButtonsDisabled(disabled) {
108
+ collectBtn.disabled = disabled;
109
+ collectFingerprintInput.disabled = disabled;
110
+ }
111
+
112
+ function renderTopDirectories(items) {
113
+ if (!Array.isArray(items) || items.length === 0) {
114
+ return '<div class="empty-tree">当前没有目录</div>';
115
+ }
116
+ return '<div class="dir-items">' + items.map(function(item) {
117
+ const statusLabel = item.status || (item.hasReports ? '未修复状态' : '未生成报告');
118
+ const statusClass = statusLabel === '已完成'
119
+ ? 'status-tag done'
120
+ : statusLabel === '未修复状态'
121
+ ? 'status-tag pending'
122
+ : 'status-tag empty';
123
+ return [
124
+ '<button type="button" class="tree-directory-button" data-path="' + escapeHtml(item.path) + '" data-status="' + escapeHtml(statusLabel) + '">',
125
+ '<span class="node-badge dir">DIR</span>',
126
+ '<span class="file-row-main">',
127
+ '<strong>' + escapeHtml(shortenFingerprint(item.name)) + '</strong>',
128
+ '<small>' + escapeHtml(formatFetchedTime(item.fetchedAt)) + '</small>',
129
+ '<span class="' + statusClass + '">' + escapeHtml(statusLabel) + '</span>',
130
+ '</span>',
131
+ '</button>'
132
+ ].join('');
133
+ }).join('') + '</div>';
134
+ }
135
+ function renderButtonList(items, className, emptyText, kind) {
136
+ if (!Array.isArray(items) || items.length === 0) {
137
+ return '<div class="empty-tree">' + emptyText + '</div>';
138
+ }
139
+ return '<div class="file-items">' + items.map(function(item) {
140
+ return [
141
+ '<button type="button" class="' + className + '" data-kind="' + escapeHtml(kind || '') + '" data-path="' + escapeHtml(item.path) + '" data-name="' + escapeHtml(item.name) + '">',
142
+ '<span class="node-badge ' + (className === 'log-directory-button' ? 'dir' : 'file') + '">' + (className === 'log-directory-button' ? 'DIR' : 'FILE') + '</span>',
143
+ '<span class="file-row-main">',
144
+ '<strong>' + escapeHtml(item.name) + '</strong>',
145
+ '<small>' + escapeHtml(item.path) + '</small>',
146
+ '</span>',
147
+ '</button>'
148
+ ].join('');
149
+ }).join('') + '</div>';
150
+ }
151
+
152
+ async function loadDirectoryIndex() {
153
+ setStatus('加载目录索引中', 'loading');
154
+ directoryTree.innerHTML = '<div class="empty-tree">加载中...</div>';
155
+ try {
156
+ const response = await fetch('/api/backtrace/files/index');
157
+ const data = await response.json();
158
+ if (!response.ok || !data.ok) {
159
+ throw new Error(data.error || '加载目录失败');
160
+ }
161
+ directoryTree.innerHTML = renderTopDirectories(data.directories || []);
162
+ bindTopDirectoryButtons();
163
+ setStatus('目录索引已更新', 'success');
164
+ } catch (error) {
165
+ directoryTree.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
166
+ setStatus('目录索引加载失败', 'error');
167
+ }
168
+ }
169
+
170
+ async function loadDirectoryContent(relativePath, preferredLogDir, statusLabel) {
171
+ currentDirectory = relativePath;
172
+ currentDirectoryCanAnalyze = false;
173
+ currentFile = null;
174
+ updateDownloadButton();
175
+ updateAnalyzeCurrentButton();
176
+ resetFixPlan();
177
+ const displayName = relativePath ? shortenFingerprint(relativePath.split(/[\\/]/).pop()) : '未选择目录';
178
+ selectedDirectoryTitle.textContent = displayName;
179
+ selectedDirectoryPath.textContent = relativePath ? displayName : '请从左侧选择目录';
180
+ reportStatus.textContent = statusLabel || '未生成报告';
181
+ viewerTitle.textContent = '文件内容';
182
+ viewerKind.textContent = '未选择';
183
+ viewerPath.textContent = '请选择右侧文件';
184
+ viewerContent.textContent = '请先在右侧列表中选择文件。';
185
+ logDirectories.innerHTML = '加载中...';
186
+ logFiles.innerHTML = '加载中...';
187
+ reportFiles.innerHTML = '加载中...';
188
+ setStatus('加载目录内容中', 'loading');
189
+
190
+ try {
191
+ const params = new URLSearchParams();
192
+ params.set('path', relativePath || '');
193
+ if (preferredLogDir) {
194
+ params.set('logDir', preferredLogDir);
195
+ }
196
+ const response = await fetch('/api/backtrace/files/list?' + params.toString());
197
+ const data = await response.json();
198
+ if (!response.ok || !data.ok) {
199
+ throw new Error(data.error || '加载目录内容失败');
200
+ }
201
+
202
+ currentLogDir = data.selectedLogDir || '';
203
+ currentDirectoryCanAnalyze = Array.isArray(data.logDirectories) && data.logDirectories.length > 0;
204
+ reportStatus.textContent = data.reportStatus || '未生成报告';
205
+ logDirectories.innerHTML = renderButtonList(data.logDirectories || [], 'log-directory-button', '当前目录下没有日志子目录', 'logs-dir');
206
+ logFiles.innerHTML = renderButtonList(data.logFiles || [], 'file-row', '当前日志目录下没有日志文件', 'logs');
207
+ reportFiles.innerHTML = renderButtonList(data.reportFiles || [], 'file-row', '当前目录下没有报告文件', 'report');
208
+ updateAnalyzeCurrentButton();
209
+ bindLogDirectoryButtons();
210
+ bindFileButtons();
211
+ setStatus('目录内容已加载', 'success');
212
+ } catch (error) {
213
+ currentDirectoryCanAnalyze = false;
214
+ updateAnalyzeCurrentButton();
215
+ logDirectories.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
216
+ logFiles.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
217
+ reportFiles.innerHTML = '<div class="empty-tree">' + escapeHtml(error.message) + '</div>';
218
+ setStatus('目录内容加载失败', 'error');
219
+ }
220
+ }
221
+
222
+ async function openFile(kind, relativePath, button) {
223
+ document.querySelectorAll('.file-row.is-active').forEach(function(node) {
224
+ node.classList.remove('is-active');
225
+ });
226
+ if (button) button.classList.add('is-active');
227
+
228
+ currentFile = { kind: kind, path: relativePath };
229
+ updateDownloadButton();
230
+ resetFixPlan(kind === 'report' ? '当前已选中报告文件,可点击“生成修复方案”。' : '修复方案仅支持基于报告文件生成。');
231
+ viewerTitle.textContent = '文件内容';
232
+ viewerKind.textContent = kind;
233
+ viewerPath.textContent = relativePath;
234
+ viewerContent.textContent = '读取文件中...';
235
+ setStatus('读取文件中', 'loading');
236
+
237
+ try {
238
+ const url = '/api/backtrace/files/content?kind=' + encodeURIComponent(kind) + '&path=' + encodeURIComponent(relativePath);
239
+ const response = await fetch(url);
240
+ const data = await response.json();
241
+ if (!response.ok || !data.ok) {
242
+ throw new Error(data.error || '读取文件失败');
243
+ }
244
+ currentFile = { kind: kind, path: data.file.path };
245
+ updateDownloadButton();
246
+ viewerTitle.textContent = data.file.path.split(/[\\/]/).pop();
247
+ viewerKind.textContent = kind;
248
+ viewerPath.textContent = data.file.path;
249
+ viewerContent.textContent = data.file.content || '';
250
+ setStatus('文件已打开', 'success');
251
+ } catch (error) {
252
+ viewerContent.textContent = error.message;
253
+ setStatus('文件读取失败', 'error');
254
+ }
255
+ }
256
+
257
+ function bindTopDirectoryButtons() {
258
+ document.querySelectorAll('.tree-directory-button').forEach(function(button) {
259
+ button.classList.toggle('is-active', button.dataset.path === currentDirectory);
260
+ button.addEventListener('click', function() {
261
+ document.querySelectorAll('.tree-directory-button.is-active').forEach(function(node) {
262
+ node.classList.remove('is-active');
263
+ });
264
+ button.classList.add('is-active');
265
+ loadDirectoryContent(button.dataset.path, '', button.dataset.status || '未生成报告');
266
+ });
267
+ });
268
+ }
269
+
270
+ function bindLogDirectoryButtons() {
271
+ document.querySelectorAll('.log-directory-button').forEach(function(button) {
272
+ button.classList.toggle('is-active', button.dataset.name === currentLogDir);
273
+ button.addEventListener('click', function() {
274
+ loadDirectoryContent(currentDirectory, button.dataset.name, reportStatus.textContent);
275
+ });
276
+ });
277
+ }
278
+
279
+ function bindFileButtons() {
280
+ document.querySelectorAll('.file-row').forEach(function(button) {
281
+ button.addEventListener('click', function() {
282
+ openFile(button.dataset.kind, button.dataset.path, button);
283
+ });
284
+ });
285
+ }
286
+
287
+ async function runCollection() {
288
+ const now = Math.floor(Date.now() / 1000);
289
+ const payload = { command: 'collect-all' };
290
+ const fingerprint = String(collectFingerprintInput.value || '').trim();
291
+ payload.from = String(now - 86400);
292
+ payload.to = String(now);
293
+ if (fingerprint) {
294
+ payload.fingerprint = fingerprint;
295
+ }
296
+
297
+ setCollectButtonsDisabled(true);
298
+ setStatus(fingerprint ? '开始按 fingerprint 收集24小时日志...' : '开始收集24小时日志...', 'loading');
299
+ try {
300
+ const response = await fetch('/api/backtrace/run', {
301
+ method: 'POST',
302
+ headers: { 'content-type': 'application/json' },
303
+ body: JSON.stringify(payload)
304
+ });
305
+ const data = await response.json();
306
+ if (!response.ok || !data.ok) {
307
+ throw new Error(data.error || '收集失败');
308
+ }
309
+ setStatus(fingerprint ? '24小时定向收集完成,正在刷新索引' : '24小时收集完成,正在刷新索引', 'success');
310
+ await loadDirectoryIndex();
311
+ if (currentDirectory) {
312
+ await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
313
+ }
314
+ } catch (error) {
315
+ setStatus(error.message || '收集失败', 'error');
316
+ } finally {
317
+ setCollectButtonsDisabled(false);
318
+ }
319
+ }
320
+
321
+ logsTabBtn.addEventListener('click', function() { switchTab('logs'); });
322
+ reportTabBtn.addEventListener('click', function() { switchTab('report'); });
323
+
324
+ downloadBtn.addEventListener('click', function() {
325
+ const reportPath = getSelectedReportPath();
326
+ if (!reportPath) return;
327
+ const url = '/api/backtrace/files/download?kind=report&path=' + encodeURIComponent(reportPath);
328
+ window.open(url, '_blank');
329
+ });
330
+
331
+ generateFixBtn.addEventListener('click', async function() {
332
+ const reportPath = getSelectedReportPath();
333
+ if (!reportPath) {
334
+ setStatus('请先选择报告文件', 'error');
335
+ return;
336
+ }
337
+ logClient('fix-plan', 'generate requested', { reportPath: reportPath });
338
+ generateFixBtn.disabled = true;
339
+ fixPlanContent.textContent = '正在生成修复方案...\n\n阶段 1/3:已发起请求,等待服务端接收。';
340
+ setStatus('正在生成修复方案...', 'loading');
341
+ try {
342
+ logClient('fix-plan', 'sending request', { url: '/api/backtrace/fix-plan/generate' });
343
+ const response = await fetch('/api/backtrace/fix-plan/generate', {
344
+ method: 'POST',
345
+ headers: { 'content-type': 'application/json' },
346
+ body: JSON.stringify({ reportPath: reportPath })
347
+ });
348
+ fixPlanContent.textContent = '正在生成修复方案...\n\n阶段 2/3:服务端已响应,正在解析结果。';
349
+ logClient('fix-plan', 'response received', { status: response.status, ok: response.ok });
350
+ const data = await response.json();
351
+ logClient('fix-plan', 'response json parsed', data);
352
+ if (!response.ok || !data.ok) {
353
+ throw new Error(data.error || '生成修复方案失败');
354
+ }
355
+ currentFixPlan = data.plan || '';
356
+ fixPlanContent.textContent = currentFixPlan || 'Codex 未返回修复方案。';
357
+ updateFixButtons();
358
+ setStatus('修复方案已生成', 'success');
359
+ logClient('fix-plan', 'generate completed', { reportPath: reportPath, threadId: data.threadId });
360
+ } catch (error) {
361
+ currentFixPlan = '';
362
+ fixPlanContent.textContent = error.message;
363
+ updateFixButtons();
364
+ setStatus('生成修复方案失败', 'error');
365
+ logClient('fix-plan', 'generate failed', { reportPath: reportPath, message: error.message });
366
+ } finally {
367
+ updateDownloadButton();
368
+ }
369
+ });
370
+
371
+ applyFixBtn.addEventListener('click', async function() {
372
+ const reportPath = getSelectedReportPath();
373
+ if (!reportPath || !currentFixPlan) return;
374
+ applyFixBtn.disabled = true;
375
+ rejectFixBtn.disabled = true;
376
+ fixPlanContent.textContent = '正在应用修复方案...';
377
+ setStatus('正在应用修复方案...', 'loading');
378
+ try {
379
+ const response = await fetch('/api/backtrace/fix-plan/apply', {
380
+ method: 'POST',
381
+ headers: { 'content-type': 'application/json' },
382
+ body: JSON.stringify({ reportPath: reportPath, planText: currentFixPlan })
383
+ });
384
+ const data = await response.json();
385
+ if (!response.ok || !data.ok) {
386
+ throw new Error(data.error || '应用修复方案失败');
387
+ }
388
+ fixPlanContent.textContent = data.resultText || '修复应用完成。';
389
+ currentFixPlan = data.resultText || currentFixPlan;
390
+ updateFixButtons();
391
+ setStatus('修复方案已应用', 'success');
392
+ } catch (error) {
393
+ fixPlanContent.textContent = error.message;
394
+ updateFixButtons();
395
+ setStatus('应用修复方案失败', 'error');
396
+ }
397
+ });
398
+
399
+ rejectFixBtn.addEventListener('click', function() {
400
+ resetFixPlan('已拒绝当前修复方案。你可以重新生成新的修复方案。');
401
+ setStatus('已拒绝修复方案', 'success');
402
+ });
403
+
404
+ collectBtn.addEventListener('click', async function() {
405
+ await runCollection();
406
+ });
407
+
408
+ analyzeCurrentBtn.addEventListener('click', async function() {
409
+ if (!currentDirectory || !currentDirectoryCanAnalyze) return;
410
+ const fingerprint = currentDirectory.split(/[\\/]/).pop();
411
+ const originalText = analyzeCurrentBtn.textContent;
412
+ analyzeCurrentBtn.disabled = true;
413
+ analyzeCurrentBtn.textContent = '分析中...';
414
+ setStatus('正在分析 fingerprint: ' + shortenFingerprint(fingerprint), 'loading');
415
+ try {
416
+ const response = await fetch('/api/backtrace/run', {
417
+ method: 'POST',
418
+ headers: { 'content-type': 'application/json' },
419
+ body: JSON.stringify({
420
+ command: 'analyze-fingerprint',
421
+ fingerprint: fingerprint
422
+ })
423
+ });
424
+ const data = await response.json();
425
+ if (!response.ok || !data.ok) {
426
+ throw new Error(data.error || 'fingerprint 分析失败');
427
+ }
428
+ setStatus('fingerprint 分析完成,正在刷新索引', 'success');
429
+ await loadDirectoryIndex();
430
+ await loadDirectoryContent(currentDirectory, currentLogDir, '未修复状态');
431
+ } catch (error) {
432
+ setStatus(error.message || 'fingerprint 分析失败', 'error');
433
+ } finally {
434
+ analyzeCurrentBtn.textContent = originalText;
435
+ updateAnalyzeCurrentButton();
436
+ }
437
+ });
438
+
439
+ refreshBtn.addEventListener('click', async function() {
440
+ await loadDirectoryIndex();
441
+ if (currentDirectory) {
442
+ await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
443
+ }
444
+ });
445
+
446
+ updateDownloadButton();
447
+ updateAnalyzeCurrentButton();
448
+ resetFixPlan();
449
+ switchTab('logs');
450
+ loadDirectoryIndex();
451
+