backtrace-console 0.0.1 → 0.0.2

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,230 @@
1
+ function sendMessage() {
2
+ const text = chatInput.value.trim();
3
+ if ((!text && pendingImages.length === 0) || !currentSessionId) return;
4
+
5
+ const turnContainer = document.createElement('div');
6
+ turnContainer.className = 'turn-container';
7
+ const userMsg = createMessageElement(text, true);
8
+ // 在用户消息里追加图片预览
9
+ if (pendingImages.length > 0) {
10
+ const msgContent = userMsg.querySelector('.message-content');
11
+ if (msgContent) {
12
+ const imgBar = document.createElement('div');
13
+ imgBar.style.cssText = 'display:flex;flex-wrap:wrap;gap:0.4rem;margin-top:0.5rem;';
14
+ pendingImages.forEach(function(img) {
15
+ const imgEl = document.createElement('img');
16
+ imgEl.src = img.previewUrl;
17
+ imgEl.style.cssText = 'max-width:120px;max-height:120px;border-radius:8px;object-fit:cover;border:1px solid rgba(0,0,0,0.1);';
18
+ imgBar.appendChild(imgEl);
19
+ });
20
+ msgContent.appendChild(imgBar);
21
+ }
22
+ }
23
+ turnContainer.appendChild(userMsg);
24
+ chatContainer.appendChild(turnContainer);
25
+ chatInput.value = '';
26
+ chatInput.style.height = 'auto';
27
+ const imagesToSend = pendingImages.slice();
28
+ pendingImages = [];
29
+ renderImagePreviews();
30
+ sendBtn.disabled = true;
31
+ scrollToBottom();
32
+
33
+ let finalPrompt = text;
34
+ if (!currentThreadId && currentFingerprintData) {
35
+ const errorMessage = currentFingerprintData.errorMessage || '-';
36
+ const classifiers = currentFingerprintData.classifiers || '-';
37
+ finalPrompt = `你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\nError Message: ${errorMessage}\nClassifiers: ${classifiers}\n\n请根据这些信息,回答我的问题:\n${text}`;
38
+ }
39
+
40
+ setComposerStatus('正在处理中,请稍候…');
41
+ scrollToBottom();
42
+
43
+ const agentMsg = createMessageElement('', false);
44
+ const contentContainer = agentMsg.querySelector('.message-content');
45
+ const typingIndicatorId = `current-typing-${Date.now()}-${Math.random().toString(16).slice(2)}`;
46
+ contentContainer.innerHTML = `
47
+ <div class="typing-indicator" id="${typingIndicatorId}">
48
+ <div class="dot"></div>
49
+ <div class="dot"></div>
50
+ <div class="dot"></div>
51
+ </div>
52
+ `;
53
+ turnContainer.appendChild(agentMsg);
54
+ scrollToBottom();
55
+
56
+ const messageState = {
57
+ contentContainer,
58
+ scrollContainer: turnContainer,
59
+ queue: '',
60
+ fullText: '',
61
+ renderedText: '',
62
+ blocks: [],
63
+ currentBlockText: '',
64
+ toolPanelElement: null,
65
+ phase: 'pre-tool',
66
+ typingActive: false,
67
+ typewritingInterval: null
68
+ };
69
+
70
+ fetch('/api/backtrace/chat', {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json'
74
+ },
75
+ body: JSON.stringify({
76
+ prompt: finalPrompt,
77
+ userMessage: text,
78
+ threadId: currentThreadId,
79
+ fingerprint: fingerprint,
80
+ sessionId: currentSessionId,
81
+ images: imagesToSend.map(function(img) { return { data: img.data, mimeType: img.mimeType }; })
82
+ })
83
+ }).then(async response => {
84
+ if (!response.ok || !response.body) {
85
+ let errorMessage = `请求失败: ${response.status}`;
86
+ try {
87
+ const errorPayload = await response.json();
88
+ if (errorPayload && (errorPayload.error || errorPayload.message)) {
89
+ errorMessage = errorPayload.error || errorPayload.message;
90
+ }
91
+ } catch (e) {}
92
+ throw new Error(errorMessage);
93
+ }
94
+
95
+ const reader = response.body.getReader();
96
+ const decoder = new TextDecoder('utf-8');
97
+ let chunkBuffer = '';
98
+ let typingIndicatorRemoved = false;
99
+
100
+ while (true) {
101
+ const { done, value } = await reader.read();
102
+ if (done) break;
103
+
104
+ const chunk = decoder.decode(value, { stream: true });
105
+ chunkBuffer += chunk;
106
+ const lines = chunkBuffer.split('\n');
107
+ chunkBuffer = lines.pop();
108
+
109
+ for (const line of lines) {
110
+ if (line.startsWith('data: ')) {
111
+ const dataStr = line.substring(6).trim();
112
+ if (!dataStr) continue;
113
+
114
+ try {
115
+ const data = JSON.parse(dataStr);
116
+ if (data.kind === 'agent_text' && data.text !== undefined) {
117
+ if (messageState.phase === 'tool') {
118
+ if (messageState.currentBlockText && messageState.currentBlockText.trim()) {
119
+ messageState.blocks.push(messageState.currentBlockText);
120
+ }
121
+ messageState.currentBlockText = '';
122
+ messageState.fullText = '';
123
+ messageState.phase = 'post-tool';
124
+ }
125
+
126
+ if (!typingIndicatorRemoved) {
127
+ const typingIndicator = document.getElementById(typingIndicatorId);
128
+ if (typingIndicator) typingIndicator.remove();
129
+ typingIndicatorRemoved = true;
130
+ }
131
+ if (!messageState.typingActive) {
132
+ startTypewriter(messageState);
133
+ }
134
+ setComposerStatus('正在整理最终答案…');
135
+
136
+ if (data.replace) {
137
+ const newText = data.text;
138
+ if (newText.startsWith(messageState.fullText)) {
139
+ const delta = newText.substring(messageState.fullText.length);
140
+ messageState.fullText = newText;
141
+ messageState.queue += delta;
142
+ } else {
143
+ if (messageState.currentBlockText && messageState.currentBlockText.trim()) {
144
+ messageState.blocks.push(messageState.currentBlockText);
145
+ }
146
+ messageState.currentBlockText = newText;
147
+ messageState.fullText = newText;
148
+ if (!messageState.typingActive) {
149
+ messageState.renderedText = messageState.blocks.length > 0
150
+ ? messageState.blocks.join('\n\n') + '\n\n' + newText
151
+ : newText;
152
+ }
153
+ messageState.queue = '';
154
+ rerenderFullMessage(messageState);
155
+ }
156
+ } else {
157
+ messageState.fullText += data.text;
158
+ messageState.queue += data.text;
159
+ }
160
+ } else if (data.kind && data.kind.startsWith('tool_')) {
161
+ messageState.phase = 'tool';
162
+ if (!typingIndicatorRemoved) {
163
+ const typingIndicator = document.getElementById(typingIndicatorId);
164
+ if (typingIndicator) typingIndicator.remove();
165
+ typingIndicatorRemoved = true;
166
+ stopTypewriter(messageState);
167
+ }
168
+ setComposerStatus(data.kind === 'tool_call_completed' ? '工具已完成,正在整理结果…' : '正在调用工具并等待结果…');
169
+ const normalizedToolName = data.toolName || data.itemType || data.text || 'tool';
170
+ const payload = {
171
+ ...data,
172
+ toolName: normalizedToolName,
173
+ output: data.output || data.text || '',
174
+ result: data.result || data.text || ''
175
+ };
176
+
177
+ if (!messageState.toolPanelElement) {
178
+ messageState.toolPanelElement = createToolMessageElement(data.kind, payload);
179
+ turnContainer.appendChild(messageState.toolPanelElement);
180
+ } else {
181
+ updateToolMessageElement(messageState.toolPanelElement, data.kind, payload);
182
+ }
183
+ scrollToBottom();
184
+ } else if (data.kind === 'done') {
185
+ if (data.threadId) {
186
+ currentThreadId = data.threadId;
187
+ }
188
+ clearComposerStatus();
189
+ if (!typingIndicatorRemoved) {
190
+ const typingIndicator = document.getElementById(typingIndicatorId);
191
+ if (typingIndicator) typingIndicator.remove();
192
+ typingIndicatorRemoved = true;
193
+ }
194
+ if (messageState.queue.length === 0) {
195
+ stopTypewriter(messageState);
196
+ rerenderFullMessage(messageState);
197
+ }
198
+ }
199
+ } catch (e) {
200
+ console.error('SSE JSON 解析错误:', e, '数据:', dataStr);
201
+ }
202
+ } else if (line.startsWith('event: error')) {
203
+ console.error('SSE Error Event:', line);
204
+ }
205
+ }
206
+ }
207
+
208
+ const waitInterval = setInterval(() => {
209
+ if (messageState.queue.length === 0) {
210
+ clearInterval(waitInterval);
211
+ stopTypewriter(messageState);
212
+ }
213
+ }, 50);
214
+ }).catch(err => {
215
+ clearComposerStatus();
216
+ const typingIndicator = document.getElementById(typingIndicatorId);
217
+ if (typingIndicator) {
218
+ typingIndicator.remove();
219
+ }
220
+ stopTypewriter(messageState);
221
+ setMessageContent(contentContainer, `请求发生错误: ${err.message}`);
222
+ });
223
+ }
224
+
225
+ function scrollToBottom() {
226
+ chatContainer.scrollTo({
227
+ top: chatContainer.scrollHeight,
228
+ behavior: 'smooth'
229
+ });
230
+ }
@@ -0,0 +1,69 @@
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>Agent Chat Console</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="/chat-layout.css">
11
+ <link rel="stylesheet" href="/chat-components.css">
12
+ </head>
13
+ <body>
14
+
15
+ <!-- 左侧边栏 -->
16
+ <aside class="sidebar">
17
+ <div class="sidebar-header">
18
+ Agent Console
19
+ </div>
20
+ <button class="new-chat-btn" onclick="createNewSession()">
21
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
22
+ 新对话
23
+ </button>
24
+ <div id="session-list" class="session-list"></div>
25
+ </aside>
26
+
27
+ <!-- 右侧主区域 -->
28
+ <main class="main-content">
29
+ <header class="header">
30
+ <h2>AI 研发助手</h2>
31
+ <a href="/" class="back-link">← 返回控制台</a>
32
+ </header>
33
+
34
+ <!-- 聊天记录区 -->
35
+ <div class="chat-container" id="chat-container"></div>
36
+
37
+ <!-- 输入区 -->
38
+ <div class="input-container">
39
+ <div class="composer-status" id="composer-status"></div>
40
+ <div class="input-box">
41
+ <input type="file" id="image-input" accept="image/*" multiple style="display:none">
42
+ <button id="image-btn" class="image-btn" type="button" title="发送图片">
43
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
44
+ </button>
45
+ <div style="flex:1; display:flex; flex-direction:column; min-width:0;">
46
+ <div id="image-preview-bar" style="display:none; flex-wrap:wrap; gap:0.4rem; padding:0.4rem 0.5rem 0;"></div>
47
+ <textarea id="chat-input" placeholder="输入你想问的问题..." rows="1"></textarea>
48
+ </div>
49
+ <button id="send-btn" class="send-btn">
50
+ <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
51
+ </button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </main>
56
+
57
+ <!-- 最右侧信息面板 -->
58
+ <aside class="info-panel" id="info-panel" style="display: none;">
59
+ <div class="info-panel-header">Fingerprint 详情</div>
60
+ <div id="info-panel-content">
61
+ <div style="text-align: center; color: var(--text-muted); margin-top: 2rem;">加载中...</div>
62
+ </div>
63
+ </aside>
64
+
65
+ <script src="/chat-core.js"></script>
66
+ <script src="/chat-render.js"></script>
67
+ <script src="/chat-send.js"></script>
68
+ </body>
69
+ </html>
@@ -1,4 +1,5 @@
1
- const statusText = document.getElementById('statusText');
1
+
2
+ const statusText = document.getElementById('statusText');
2
3
  const directoryTree = document.getElementById('directoryTree');
3
4
  const selectedDirectoryTitle = document.getElementById('selectedDirectoryTitle');
4
5
  const selectedDirectoryPath = document.getElementById('selectedDirectoryPath');
@@ -20,16 +21,19 @@
20
21
  const rejectFixBtn = document.getElementById('rejectFixBtn');
21
22
  const logsTabBtn = document.getElementById('logsTabBtn');
22
23
  const reportTabBtn = document.getElementById('reportTabBtn');
24
+ const summarizeErrorsBtn = document.getElementById('summarizeErrorsBtn');
23
25
  const analyzeCurrentBtn = document.getElementById('analyzeCurrentBtn');
24
26
  const logsTab = document.getElementById('logsTab');
25
27
  const reportTab = document.getElementById('reportTab');
26
28
 
27
- let currentDirectory = '';
28
- let currentLogDir = '';
29
- let currentFile = null;
30
- let currentFixPlan = '';
31
- let currentDirectoryCanAnalyze = false;
32
-
29
+ let currentDirectory = '';
30
+ let currentLogDir = '';
31
+ let currentFile = null;
32
+ let currentFixPlan = '';
33
+ let currentRepairVersion = '';
34
+ let currentRepairPlanPath = '';
35
+ let currentDirectoryCanAnalyze = false;
36
+
33
37
  function setStatus(text, mode) {
34
38
  statusText.textContent = text;
35
39
  statusText.dataset.mode = mode || 'idle';
@@ -63,17 +67,21 @@
63
67
  return '获取时间 ' + date.toLocaleString('zh-CN', { hour12: false });
64
68
  }
65
69
 
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
-
70
+ function getSelectedReportPath() {
71
+ if (currentFile && currentFile.kind === 'report' && currentFile.path) {
72
+ return currentFile.path;
73
+ }
74
+ const activeReportButton = reportFiles.querySelector('.file-row.is-active');
75
+ if (activeReportButton && activeReportButton.dataset && activeReportButton.dataset.path) {
76
+ return activeReportButton.dataset.path;
77
+ }
78
+ const firstReportButton = reportFiles.querySelector('.file-row[data-kind="report"]');
79
+ if (firstReportButton && firstReportButton.dataset && firstReportButton.dataset.path) {
80
+ return firstReportButton.dataset.path;
81
+ }
82
+ return '';
83
+ }
84
+
77
85
  function updateDownloadButton() {
78
86
  const reportPath = getSelectedReportPath();
79
87
  downloadBtn.disabled = !reportPath;
@@ -81,7 +89,9 @@
81
89
  }
82
90
 
83
91
  function updateAnalyzeCurrentButton() {
84
- analyzeCurrentBtn.disabled = !(currentDirectory && currentDirectoryCanAnalyze);
92
+ const enabled = !!(currentDirectory && currentDirectoryCanAnalyze);
93
+ analyzeCurrentBtn.disabled = !enabled;
94
+ summarizeErrorsBtn.disabled = !enabled;
85
95
  }
86
96
 
87
97
  function updateFixButtons() {
@@ -90,12 +100,14 @@
90
100
  rejectFixBtn.disabled = !enabled;
91
101
  }
92
102
 
93
- function resetFixPlan(text) {
94
- currentFixPlan = '';
95
- fixPlanContent.textContent = text || '请选择报告文件后点击“生成修复方案”。';
96
- updateFixButtons();
97
- }
98
-
103
+ function resetFixPlan(text) {
104
+ currentFixPlan = '';
105
+ currentRepairVersion = '';
106
+ currentRepairPlanPath = '';
107
+ fixPlanContent.textContent = text || '请选择报告文件后点击“生成修复方案”。';
108
+ updateFixButtons();
109
+ }
110
+
99
111
  function switchTab(tabName) {
100
112
  const isLogs = tabName === 'logs';
101
113
  logsTabBtn.classList.toggle('is-active', isLogs);
@@ -124,7 +136,7 @@
124
136
  '<button type="button" class="tree-directory-button" data-path="' + escapeHtml(item.path) + '" data-status="' + escapeHtml(statusLabel) + '">',
125
137
  '<span class="node-badge dir">DIR</span>',
126
138
  '<span class="file-row-main">',
127
- '<strong>' + escapeHtml(shortenFingerprint(item.name)) + '</strong>',
139
+ '<strong>' + escapeHtml(shortenFingerprint(item.name)).substring(0,7) + '</strong>',
128
140
  '<small>' + escapeHtml(formatFetchedTime(item.fetchedAt)) + '</small>',
129
141
  '<span class="' + statusClass + '">' + escapeHtml(statusLabel) + '</span>',
130
142
  '</span>',
@@ -175,7 +187,7 @@
175
187
  updateAnalyzeCurrentButton();
176
188
  resetFixPlan();
177
189
  const displayName = relativePath ? shortenFingerprint(relativePath.split(/[\\/]/).pop()) : '未选择目录';
178
- selectedDirectoryTitle.textContent = displayName;
190
+ selectedDirectoryTitle.textContent = displayName.substring(0,7);
179
191
  selectedDirectoryPath.textContent = relativePath ? displayName : '请从左侧选择目录';
180
192
  reportStatus.textContent = statusLabel || '未生成报告';
181
193
  viewerTitle.textContent = '文件内容';
@@ -328,11 +340,17 @@
328
340
  window.open(url, '_blank');
329
341
  });
330
342
 
331
- generateFixBtn.addEventListener('click', async function() {
332
- const reportPath = getSelectedReportPath();
333
- if (!reportPath) {
334
- setStatus('请先选择报告文件', 'error');
335
- return;
343
+ generateFixBtn.addEventListener('click', async function() {
344
+ let reportPath = getSelectedReportPath();
345
+ if (!reportPath) {
346
+ const firstReportButton = reportFiles.querySelector('.file-row[data-kind="report"]');
347
+ if (firstReportButton && firstReportButton.dataset && firstReportButton.dataset.path) {
348
+ reportPath = firstReportButton.dataset.path;
349
+ }
350
+ }
351
+ if (!reportPath) {
352
+ setStatus('请先选择报告文件', 'error');
353
+ return;
336
354
  }
337
355
  logClient('fix-plan', 'generate requested', { reportPath: reportPath });
338
356
  generateFixBtn.disabled = true;
@@ -349,19 +367,23 @@
349
367
  logClient('fix-plan', 'response received', { status: response.status, ok: response.ok });
350
368
  const data = await response.json();
351
369
  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');
370
+ if (!response.ok || !data.ok) {
371
+ throw new Error(data.error || '生成修复方案失败');
372
+ }
373
+ currentFixPlan = data.plan || '';
374
+ currentRepairVersion = data.repairVersion || '';
375
+ currentRepairPlanPath = data.repairPlanPath || '';
376
+ fixPlanContent.textContent = currentFixPlan || 'Codex 未返回修复方案。';
377
+ updateFixButtons();
378
+ setStatus('修复方案已生成', 'success');
379
+ logClient('fix-plan', 'generate completed', { reportPath: reportPath, threadId: data.threadId, repairVersion: currentRepairVersion, repairPlanPath: currentRepairPlanPath });
380
+ } catch (error) {
381
+ currentFixPlan = '';
382
+ currentRepairVersion = '';
383
+ currentRepairPlanPath = '';
384
+ fixPlanContent.textContent = error.message;
385
+ updateFixButtons();
386
+ setStatus('生成修复方案失败', 'error');
365
387
  logClient('fix-plan', 'generate failed', { reportPath: reportPath, message: error.message });
366
388
  } finally {
367
389
  updateDownloadButton();
@@ -379,16 +401,17 @@
379
401
  const response = await fetch('/api/backtrace/fix-plan/apply', {
380
402
  method: 'POST',
381
403
  headers: { 'content-type': 'application/json' },
382
- body: JSON.stringify({ reportPath: reportPath, planText: currentFixPlan })
404
+ body: JSON.stringify({ reportPath: reportPath, planText: currentFixPlan, repairVersion: currentRepairVersion, repairPlanPath: currentRepairPlanPath })
383
405
  });
384
406
  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');
407
+ if (!response.ok || !data.ok) {
408
+ throw new Error(data.error || '应用修复方案失败');
409
+ }
410
+ fixPlanContent.textContent = data.resultText || '修复应用完成。';
411
+ currentFixPlan = data.resultText || currentFixPlan;
412
+ currentRepairVersion = data.repairVersion || currentRepairVersion;
413
+ updateFixButtons();
414
+ setStatus('修复方案已应用', 'success');
392
415
  } catch (error) {
393
416
  fixPlanContent.textContent = error.message;
394
417
  updateFixButtons();
@@ -405,6 +428,37 @@
405
428
  await runCollection();
406
429
  });
407
430
 
431
+ summarizeErrorsBtn.addEventListener('click', async function() {
432
+ if (!currentDirectory || !currentDirectoryCanAnalyze) return;
433
+ const fingerprint = currentDirectory.split(/[\/]/).pop();
434
+ const originalText = summarizeErrorsBtn.textContent;
435
+ summarizeErrorsBtn.disabled = true;
436
+ summarizeErrorsBtn.textContent = '汇总中...';
437
+ setStatus('正在汇总错误日志: ' + shortenFingerprint(fingerprint), 'loading');
438
+ try {
439
+ const response = await fetch('/api/backtrace/run', {
440
+ method: 'POST',
441
+ headers: { 'content-type': 'application/json' },
442
+ body: JSON.stringify({
443
+ command: 'summarize-fingerprint-errors',
444
+ fingerprint: fingerprint
445
+ })
446
+ });
447
+ const data = await response.json();
448
+ if (!response.ok || !data.ok) {
449
+ throw new Error(data.error || '错误日志汇总失败');
450
+ }
451
+ setStatus('错误日志汇总完成,正在刷新目录', 'success');
452
+ await loadDirectoryIndex();
453
+ await loadDirectoryContent(currentDirectory, currentLogDir, reportStatus.textContent);
454
+ } catch (error) {
455
+ setStatus(error.message || '错误日志汇总失败', 'error');
456
+ } finally {
457
+ summarizeErrorsBtn.textContent = originalText;
458
+ updateAnalyzeCurrentButton();
459
+ }
460
+ });
461
+
408
462
  analyzeCurrentBtn.addEventListener('click', async function() {
409
463
  if (!currentDirectory || !currentDirectoryCanAnalyze) return;
410
464
  const fingerprint = currentDirectory.split(/[\\/]/).pop();
@@ -448,4 +502,3 @@
448
502
  resetFixPlan();
449
503
  switchTab('logs');
450
504
  loadDirectoryIndex();
451
-