backtrace-console 0.0.4 → 0.0.6
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/lib/feishu.js +5 -2
- package/lib/p4ops.js +72 -0
- package/lib/p4sync.js +35 -0
- package/lib/scheduler.js +101 -55
- package/package.json +2 -1
- package/public/chat-claude-core.js +650 -0
- package/public/chat-claude-send.js +241 -0
- package/public/chat-claude.html +84 -0
- package/public/chat-components.css +105 -0
- package/public/chat-core.js +27 -12
- package/public/chat-p4.js +113 -0
- package/public/chat-prompt.js +29 -0
- package/public/chat-send.js +3 -3
- package/public/chat.html +16 -1
- package/public/index-page.js +94 -67
- package/public/index.html +3 -0
- package/public/stylesheets/style.css +4 -3
- package/routes/backtrace-chat-claude.js +477 -0
- package/routes/backtrace-chat.js +88 -20
- package/routes/backtrace-fix-plan.js +65 -6
- package/routes/backtrace-p4.js +104 -0
- package/routes/backtrace-shared.js +32 -0
- package/routes/backtrace.js +2 -0
|
@@ -0,0 +1,241 @@
|
|
|
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 (!currentClaudeSessionId && currentFingerprintData) {
|
|
35
|
+
finalPrompt = buildCrashContextFirstTurnPrompt(text, currentFingerprintData);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setComposerStatus('正在处理中,请稍候…');
|
|
39
|
+
scrollToBottom();
|
|
40
|
+
|
|
41
|
+
const agentMsg = createMessageElement('', false);
|
|
42
|
+
const contentContainer = agentMsg.querySelector('.message-content');
|
|
43
|
+
const typingIndicatorId = `current-typing-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
44
|
+
contentContainer.innerHTML = `
|
|
45
|
+
<div class="typing-indicator" id="${typingIndicatorId}">
|
|
46
|
+
<div class="dot"></div>
|
|
47
|
+
<div class="dot"></div>
|
|
48
|
+
<div class="dot"></div>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
turnContainer.appendChild(agentMsg);
|
|
52
|
+
scrollToBottom();
|
|
53
|
+
|
|
54
|
+
const messageState = {
|
|
55
|
+
contentContainer,
|
|
56
|
+
scrollContainer: turnContainer,
|
|
57
|
+
queue: '',
|
|
58
|
+
fullText: '',
|
|
59
|
+
renderedText: '',
|
|
60
|
+
blocks: [],
|
|
61
|
+
currentBlockText: '',
|
|
62
|
+
toolPanelElement: null,
|
|
63
|
+
phase: 'pre-tool',
|
|
64
|
+
typingActive: false,
|
|
65
|
+
typewritingInterval: null
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
fetch('/api/backtrace/claude-chat', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
prompt: finalPrompt,
|
|
75
|
+
userMessage: text,
|
|
76
|
+
claudeSessionId: currentClaudeSessionId,
|
|
77
|
+
fingerprint: fingerprint,
|
|
78
|
+
sessionId: currentSessionId,
|
|
79
|
+
images: imagesToSend.map(function(img) { return { data: img.data, mimeType: img.mimeType }; })
|
|
80
|
+
})
|
|
81
|
+
}).then(async response => {
|
|
82
|
+
if (!response.ok || !response.body) {
|
|
83
|
+
let errorMessage = `请求失败: ${response.status}`;
|
|
84
|
+
try {
|
|
85
|
+
const errorPayload = await response.json();
|
|
86
|
+
if (errorPayload && (errorPayload.error || errorPayload.message)) {
|
|
87
|
+
errorMessage = errorPayload.error || errorPayload.message;
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {}
|
|
90
|
+
throw new Error(errorMessage);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reader = response.body.getReader();
|
|
94
|
+
const decoder = new TextDecoder('utf-8');
|
|
95
|
+
let chunkBuffer = '';
|
|
96
|
+
let typingIndicatorRemoved = false;
|
|
97
|
+
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done) break;
|
|
101
|
+
|
|
102
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
103
|
+
chunkBuffer += chunk;
|
|
104
|
+
const lines = chunkBuffer.split('\n');
|
|
105
|
+
chunkBuffer = lines.pop();
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.startsWith('data: ')) {
|
|
109
|
+
const dataStr = line.substring(6).trim();
|
|
110
|
+
if (!dataStr) continue;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(dataStr);
|
|
114
|
+
if (data.kind === 'agent_text' && data.text !== undefined) {
|
|
115
|
+
if (messageState.phase === 'tool') {
|
|
116
|
+
if (messageState.currentBlockText && messageState.currentBlockText.trim()) {
|
|
117
|
+
messageState.blocks.push(messageState.currentBlockText);
|
|
118
|
+
}
|
|
119
|
+
messageState.currentBlockText = '';
|
|
120
|
+
messageState.fullText = '';
|
|
121
|
+
messageState.phase = 'post-tool';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!typingIndicatorRemoved) {
|
|
125
|
+
const typingIndicator = document.getElementById(typingIndicatorId);
|
|
126
|
+
if (typingIndicator) typingIndicator.remove();
|
|
127
|
+
typingIndicatorRemoved = true;
|
|
128
|
+
}
|
|
129
|
+
if (!messageState.typingActive) {
|
|
130
|
+
startTypewriter(messageState);
|
|
131
|
+
}
|
|
132
|
+
setComposerStatus('正在整理最终答案…');
|
|
133
|
+
|
|
134
|
+
if (data.replace) {
|
|
135
|
+
const newText = data.text;
|
|
136
|
+
if (newText.startsWith(messageState.fullText)) {
|
|
137
|
+
const delta = newText.substring(messageState.fullText.length);
|
|
138
|
+
messageState.fullText = newText;
|
|
139
|
+
messageState.queue += delta;
|
|
140
|
+
} else {
|
|
141
|
+
if (messageState.currentBlockText && messageState.currentBlockText.trim()) {
|
|
142
|
+
messageState.blocks.push(messageState.currentBlockText);
|
|
143
|
+
}
|
|
144
|
+
messageState.currentBlockText = newText;
|
|
145
|
+
messageState.fullText = newText;
|
|
146
|
+
if (!messageState.typingActive) {
|
|
147
|
+
messageState.renderedText = messageState.blocks.length > 0
|
|
148
|
+
? messageState.blocks.join('\n\n') + '\n\n' + newText
|
|
149
|
+
: newText;
|
|
150
|
+
}
|
|
151
|
+
messageState.queue = '';
|
|
152
|
+
rerenderFullMessage(messageState);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
messageState.fullText += data.text;
|
|
156
|
+
messageState.queue += data.text;
|
|
157
|
+
}
|
|
158
|
+
} else if (data.kind && data.kind.startsWith('tool_')) {
|
|
159
|
+
messageState.phase = 'tool';
|
|
160
|
+
if (!typingIndicatorRemoved) {
|
|
161
|
+
const typingIndicator = document.getElementById(typingIndicatorId);
|
|
162
|
+
if (typingIndicator) typingIndicator.remove();
|
|
163
|
+
typingIndicatorRemoved = true;
|
|
164
|
+
stopTypewriter(messageState);
|
|
165
|
+
}
|
|
166
|
+
setComposerStatus(data.kind === 'tool_call_completed' ? '工具已完成,正在整理结果…' : '正在调用工具并等待结果…');
|
|
167
|
+
const normalizedToolName = data.toolName || data.itemType || data.text || 'tool';
|
|
168
|
+
const payload = {
|
|
169
|
+
...data,
|
|
170
|
+
toolName: normalizedToolName,
|
|
171
|
+
output: data.output || data.text || '',
|
|
172
|
+
result: data.result || data.text || ''
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (!messageState.toolPanelElement) {
|
|
176
|
+
messageState.toolPanelElement = createToolMessageElement(data.kind, payload);
|
|
177
|
+
turnContainer.appendChild(messageState.toolPanelElement);
|
|
178
|
+
} else {
|
|
179
|
+
updateToolMessageElement(messageState.toolPanelElement, data.kind, payload);
|
|
180
|
+
}
|
|
181
|
+
scrollToBottom();
|
|
182
|
+
} else if (data.kind === 'done') {
|
|
183
|
+
if (data.claudeSessionId) {
|
|
184
|
+
currentClaudeSessionId = data.claudeSessionId;
|
|
185
|
+
}
|
|
186
|
+
clearComposerStatus();
|
|
187
|
+
if (!typingIndicatorRemoved) {
|
|
188
|
+
const typingIndicator = document.getElementById(typingIndicatorId);
|
|
189
|
+
if (typingIndicator) typingIndicator.remove();
|
|
190
|
+
typingIndicatorRemoved = true;
|
|
191
|
+
}
|
|
192
|
+
if (messageState.queue.length === 0) {
|
|
193
|
+
stopTypewriter(messageState);
|
|
194
|
+
rerenderFullMessage(messageState);
|
|
195
|
+
}
|
|
196
|
+
if (data.p4Description && window.setP4Description) window.setP4Description(data.p4Description);
|
|
197
|
+
if (window.loadP4Pending) window.loadP4Pending();
|
|
198
|
+
} else if (data.kind === 'error' || data.error) {
|
|
199
|
+
const errMsg = data.error || '未知错误';
|
|
200
|
+
console.error('SSE 服务端错误:', errMsg);
|
|
201
|
+
clearComposerStatus();
|
|
202
|
+
stopTypewriter(messageState);
|
|
203
|
+
if (!typingIndicatorRemoved) {
|
|
204
|
+
const typingIndicator = document.getElementById(typingIndicatorId);
|
|
205
|
+
if (typingIndicator) typingIndicator.remove();
|
|
206
|
+
typingIndicatorRemoved = true;
|
|
207
|
+
}
|
|
208
|
+
setMessageContent(contentContainer, '请求发生错误:' + errMsg);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('SSE JSON 解析错误:', e, '数据:', dataStr);
|
|
212
|
+
}
|
|
213
|
+
} else if (line.startsWith('event: error')) {
|
|
214
|
+
console.error('SSE Error Event 头:', line, '(具体错误见后续 data 行)');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const waitInterval = setInterval(() => {
|
|
220
|
+
if (messageState.queue.length === 0) {
|
|
221
|
+
clearInterval(waitInterval);
|
|
222
|
+
stopTypewriter(messageState);
|
|
223
|
+
}
|
|
224
|
+
}, 50);
|
|
225
|
+
}).catch(err => {
|
|
226
|
+
clearComposerStatus();
|
|
227
|
+
const typingIndicator = document.getElementById(typingIndicatorId);
|
|
228
|
+
if (typingIndicator) {
|
|
229
|
+
typingIndicator.remove();
|
|
230
|
+
}
|
|
231
|
+
stopTypewriter(messageState);
|
|
232
|
+
setMessageContent(contentContainer, `请求发生错误: ${err.message}`);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function scrollToBottom() {
|
|
237
|
+
chatContainer.scrollTo({
|
|
238
|
+
top: chatContainer.scrollHeight,
|
|
239
|
+
behavior: 'smooth'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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>Claude 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
|
+
Claude 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>Claude 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">
|
|
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
|
+
|
|
64
|
+
<!-- P4 操作面板 -->
|
|
65
|
+
<div class="p4-panel">
|
|
66
|
+
<div class="p4-panel-header">Perforce 提交</div>
|
|
67
|
+
|
|
68
|
+
<div class="p4-action">
|
|
69
|
+
<div id="p4-pending-list" class="p4-pending" style="display:none"></div>
|
|
70
|
+
<textarea id="p4-submit-desc" class="p4-input" rows="2" placeholder="提交说明(AI 修复后自动填入)..."></textarea>
|
|
71
|
+
<button class="p4-btn" onclick="p4Submit()">↑ 提交 (submit)</button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<pre id="p4-output" class="p4-output" style="display:none"></pre>
|
|
75
|
+
</div>
|
|
76
|
+
</aside>
|
|
77
|
+
|
|
78
|
+
<script src="/chat-prompt.js"></script>
|
|
79
|
+
<script src="/chat-claude-core.js"></script>
|
|
80
|
+
<script src="/chat-render.js"></script>
|
|
81
|
+
<script src="/chat-claude-send.js"></script>
|
|
82
|
+
<script src="/chat-p4.js"></script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -412,6 +412,21 @@
|
|
|
412
412
|
flex-shrink: 0;
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
.modal-refresh {
|
|
416
|
+
background: green;
|
|
417
|
+
color: #fff;
|
|
418
|
+
border: none;
|
|
419
|
+
border-radius: 6px;
|
|
420
|
+
padding: 0.28rem 0.75rem;
|
|
421
|
+
cursor: pointer;
|
|
422
|
+
font-size: 0.82rem;
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
margin-left: auto;
|
|
425
|
+
margin-right: 0.6rem;
|
|
426
|
+
}
|
|
427
|
+
.modal-refresh:hover { background: var(--accent-hover); }
|
|
428
|
+
.modal-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
429
|
+
|
|
415
430
|
.modal-close {
|
|
416
431
|
background: none;
|
|
417
432
|
border: none;
|
|
@@ -567,3 +582,93 @@
|
|
|
567
582
|
|
|
568
583
|
.logs-btn:hover { background: var(--accent-hover); }
|
|
569
584
|
|
|
585
|
+
/* ── P4 面板 ── */
|
|
586
|
+
.p4-panel {
|
|
587
|
+
border-top: 1px solid var(--border-color);
|
|
588
|
+
padding: 1rem;
|
|
589
|
+
margin-top: 0.5rem;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.p4-panel-header {
|
|
593
|
+
font-size: 0.8rem;
|
|
594
|
+
font-weight: 600;
|
|
595
|
+
color: var(--accent-color);
|
|
596
|
+
text-transform: uppercase;
|
|
597
|
+
letter-spacing: 0.05em;
|
|
598
|
+
margin-bottom: 0.85rem;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.p4-action {
|
|
602
|
+
display: flex;
|
|
603
|
+
flex-direction: column;
|
|
604
|
+
gap: 0.4rem;
|
|
605
|
+
margin-bottom: 0.85rem;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.p4-btn {
|
|
609
|
+
width: 100%;
|
|
610
|
+
padding: 0.55rem 0.75rem;
|
|
611
|
+
border-radius: 8px;
|
|
612
|
+
background: var(--accent-color);
|
|
613
|
+
color: white;
|
|
614
|
+
border: none;
|
|
615
|
+
cursor: pointer;
|
|
616
|
+
font-size: 0.85rem;
|
|
617
|
+
font-weight: 500;
|
|
618
|
+
text-align: left;
|
|
619
|
+
transition: background 0.15s;
|
|
620
|
+
}
|
|
621
|
+
.p4-btn:hover { background: var(--accent-hover); }
|
|
622
|
+
.p4-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
623
|
+
|
|
624
|
+
.p4-input {
|
|
625
|
+
width: 100%;
|
|
626
|
+
box-sizing: border-box;
|
|
627
|
+
padding: 0.45rem 0.6rem;
|
|
628
|
+
border-radius: 6px;
|
|
629
|
+
border: 1px solid var(--border-color);
|
|
630
|
+
/*background: var(--input-bg, #1e1e2e);*/
|
|
631
|
+
color: var(--text-primary);
|
|
632
|
+
font-size: 0.82rem;
|
|
633
|
+
font-family: inherit;
|
|
634
|
+
resize: vertical;
|
|
635
|
+
outline: none;
|
|
636
|
+
}
|
|
637
|
+
.p4-input:focus { border-color: var(--accent-color); }
|
|
638
|
+
|
|
639
|
+
.p4-pending {
|
|
640
|
+
font-size: 0.78rem;
|
|
641
|
+
color: var(--text-muted);
|
|
642
|
+
background: rgba(255,255,255,0.04);
|
|
643
|
+
border-radius: 6px;
|
|
644
|
+
padding: 0.5rem 0.6rem;
|
|
645
|
+
max-height: 120px;
|
|
646
|
+
overflow-y: auto;
|
|
647
|
+
}
|
|
648
|
+
.p4-pending b { display: block; margin-bottom: 0.3rem; color: var(--text-secondary); }
|
|
649
|
+
.p4-file {
|
|
650
|
+
font-family: monospace;
|
|
651
|
+
font-size: 0.75rem;
|
|
652
|
+
word-break: break-all;
|
|
653
|
+
padding: 0.1rem 0;
|
|
654
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
655
|
+
}
|
|
656
|
+
.p4-file:last-child { border-bottom: none; }
|
|
657
|
+
|
|
658
|
+
.p4-output {
|
|
659
|
+
font-family: monospace;
|
|
660
|
+
font-size: 0.78rem;
|
|
661
|
+
white-space: pre-wrap;
|
|
662
|
+
word-break: break-all;
|
|
663
|
+
padding: 0.6rem 0.75rem;
|
|
664
|
+
border-radius: 6px;
|
|
665
|
+
background: #0d1117;
|
|
666
|
+
border: 1px solid var(--border-color);
|
|
667
|
+
max-height: 200px;
|
|
668
|
+
overflow-y: auto;
|
|
669
|
+
margin: 0;
|
|
670
|
+
color: var(--text-secondary);
|
|
671
|
+
}
|
|
672
|
+
.p4-output.ok { border-color: #238636; color: #3fb950; }
|
|
673
|
+
.p4-output.error { border-color: #da3633; color: #f85149; }
|
|
674
|
+
|
package/public/chat-core.js
CHANGED
|
@@ -87,22 +87,14 @@ const chatContainer = document.getElementById('chat-container');
|
|
|
87
87
|
|
|
88
88
|
function restoreWelcomeMessage() {
|
|
89
89
|
chatContainer.innerHTML = '';
|
|
90
|
-
|
|
91
|
-
chatContainer.appendChild(createSystemMessageElement(buildFingerprintContextMessage(currentFingerprintData)));
|
|
92
|
-
} else {
|
|
93
|
-
chatContainer.appendChild(createSystemMessageElement('你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\nError Message: -\nClassifiers: -\n\n请结合这些上下文,帮助用户定位问题并给出修复建议。'));
|
|
94
|
-
}
|
|
90
|
+
chatContainer.appendChild(createSystemMessageElement(buildCrashContextPrompt(currentFingerprintData || null)));
|
|
95
91
|
scrollToBottom();
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
function buildFingerprintContextMessage(meta) {
|
|
99
|
-
const errorMessage = meta && meta.errorMessage ? meta.errorMessage : '-';
|
|
100
|
-
const classifiers = meta && meta.classifiers ? meta.classifiers : '-';
|
|
101
|
-
return `你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\nError Message: ${errorMessage}\nClassifiers: ${classifiers}\n\n请结合这些上下文,帮助用户定位问题并给出修复建议。`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
94
|
function setActiveSession(sessionId) {
|
|
105
95
|
currentSessionId = sessionId || null;
|
|
96
|
+
window.currentChatSessionId = currentSessionId;
|
|
97
|
+
if (window.loadP4Pending) window.loadP4Pending();
|
|
106
98
|
const items = Array.from(sessionList.querySelectorAll('.session-item'));
|
|
107
99
|
items.forEach(item => {
|
|
108
100
|
const isActive = String(item.dataset.sessionId || '') === String(currentSessionId || '');
|
|
@@ -345,7 +337,7 @@ const chatContainer = document.getElementById('chat-container');
|
|
|
345
337
|
}
|
|
346
338
|
});
|
|
347
339
|
|
|
348
|
-
sendBtn.addEventListener('click', sendMessage);
|
|
340
|
+
sendBtn.addEventListener('click', function() { sendMessage(); });
|
|
349
341
|
|
|
350
342
|
function isFixIntent(text) {
|
|
351
343
|
const normalized = String(text || '').trim();
|
|
@@ -370,6 +362,7 @@ const chatContainer = document.getElementById('chat-container');
|
|
|
370
362
|
<div class="modal-box">
|
|
371
363
|
<div class="modal-header">
|
|
372
364
|
<span>日志列表</span>
|
|
365
|
+
<button class="modal-refresh" id="modal-refresh-btn">刷新</button>
|
|
373
366
|
<button class="modal-close" onclick="closeLogsModal()">×</button>
|
|
374
367
|
</div>
|
|
375
368
|
<div class="modal-status" id="modal-status">加载中...</div>
|
|
@@ -381,6 +374,28 @@ const chatContainer = document.getElementById('chat-container');
|
|
|
381
374
|
});
|
|
382
375
|
document.body.appendChild(overlay);
|
|
383
376
|
|
|
377
|
+
document.getElementById('modal-refresh-btn').addEventListener('click', async function() {
|
|
378
|
+
const btn = this;
|
|
379
|
+
const statusEl = document.getElementById('modal-status');
|
|
380
|
+
btn.disabled = true;
|
|
381
|
+
if (statusEl) statusEl.textContent = '正在从远端刷新...';
|
|
382
|
+
const now = Math.floor(Date.now() / 1000);
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch('/api/backtrace/run', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: { 'Content-Type': 'application/json' },
|
|
387
|
+
body: JSON.stringify({ command: 'fingerprint', fingerprint, from: '1', to: String(now) }),
|
|
388
|
+
}).then(r => r.json());
|
|
389
|
+
if (!res.ok) throw new Error(res.error || '刷新失败');
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (statusEl) statusEl.textContent = '刷新失败: ' + err.message;
|
|
392
|
+
btn.disabled = false;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
btn.disabled = false;
|
|
396
|
+
loadLogsModalData();
|
|
397
|
+
});
|
|
398
|
+
|
|
384
399
|
loadLogsModalData();
|
|
385
400
|
}
|
|
386
401
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
function getCurrentFingerprint() {
|
|
3
|
+
return new URLSearchParams(window.location.search).get('fingerprint') || '';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function getCurrentSessionId() {
|
|
7
|
+
return window.currentChatSessionId || '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function setOutput(text, ok) {
|
|
11
|
+
var el = document.getElementById('p4-output');
|
|
12
|
+
if (!el) return;
|
|
13
|
+
el.style.display = 'block';
|
|
14
|
+
el.textContent = text;
|
|
15
|
+
el.className = 'p4-output' + (ok === true ? ' ok' : ok === false ? ' error' : '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function p4Fetch(endpoint, body) {
|
|
19
|
+
var isGet = body === null;
|
|
20
|
+
if (!isGet) setOutput('执行中...', null);
|
|
21
|
+
var submitBtn = document.querySelector('.p4-btn');
|
|
22
|
+
if (submitBtn && !isGet) submitBtn.disabled = true;
|
|
23
|
+
try {
|
|
24
|
+
var res = await fetch('/api/backtrace/' + endpoint, {
|
|
25
|
+
method: isGet ? 'GET' : 'POST',
|
|
26
|
+
headers: isGet ? {} : { 'Content-Type': 'application/json' },
|
|
27
|
+
body: isGet ? undefined : JSON.stringify(body)
|
|
28
|
+
});
|
|
29
|
+
var data = await res.json();
|
|
30
|
+
if (!isGet) {
|
|
31
|
+
var text = data.ok
|
|
32
|
+
? (data.output ? data.output.slice(0, 2000) : '提交成功')
|
|
33
|
+
: ('错误: ' + (data.error || '未知错误'));
|
|
34
|
+
setOutput(text, data.ok);
|
|
35
|
+
}
|
|
36
|
+
return data;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (!isGet) setOutput('网络错误: ' + err.message, false);
|
|
39
|
+
return { ok: false };
|
|
40
|
+
} finally {
|
|
41
|
+
if (submitBtn && !isGet) submitBtn.disabled = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
window.p4Submit = async function() {
|
|
46
|
+
var descEl = document.getElementById('p4-submit-desc');
|
|
47
|
+
var desc = descEl ? descEl.value.trim() : '';
|
|
48
|
+
if (!desc) { setOutput('请输入提交说明', false); return; }
|
|
49
|
+
var fp = getCurrentFingerprint();
|
|
50
|
+
var sid = getCurrentSessionId();
|
|
51
|
+
var result = await p4Fetch('p4/submit', { description: desc, fingerprint: fp, sessionId: sid });
|
|
52
|
+
if (result.ok) loadP4Pending();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
async function loadP4Pending() {
|
|
56
|
+
var fp = getCurrentFingerprint();
|
|
57
|
+
var sid = getCurrentSessionId();
|
|
58
|
+
var qs = fp ? '?fingerprint=' + encodeURIComponent(fp) : '';
|
|
59
|
+
if (sid) qs += (qs ? '&' : '?') + 'sessionId=' + encodeURIComponent(sid);
|
|
60
|
+
var data = await p4Fetch('p4/pending' + qs, null).catch(function() { return { ok: false }; });
|
|
61
|
+
var listEl = document.getElementById('p4-pending-list');
|
|
62
|
+
var descEl = document.getElementById('p4-submit-desc');
|
|
63
|
+
if (!listEl) return;
|
|
64
|
+
var items = (data.ok && data.items) ? data.items : [];
|
|
65
|
+
var submitBtn = document.querySelector('.p4-btn');
|
|
66
|
+
if (!items.length) {
|
|
67
|
+
listEl.style.display = 'none';
|
|
68
|
+
if (descEl) descEl.value = '';
|
|
69
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
listEl.style.display = 'block';
|
|
73
|
+
var allFiles = [];
|
|
74
|
+
var descriptions = [];
|
|
75
|
+
var isSubmitted = items.every(function(item) { return !!item.submittedAt; });
|
|
76
|
+
items.forEach(function(item) {
|
|
77
|
+
if (item.files && item.files.length) allFiles = allFiles.concat(item.files);
|
|
78
|
+
if (item.description) descriptions.push(item.description);
|
|
79
|
+
});
|
|
80
|
+
if (submitBtn) submitBtn.disabled = isSubmitted;
|
|
81
|
+
var submittedItem = items.find(function(item) { return !!item.submittedAt; });
|
|
82
|
+
var changelistId = submittedItem ? (submittedItem.changelistId || null) : null;
|
|
83
|
+
var submittedAt = submittedItem ? submittedItem.submittedAt : null;
|
|
84
|
+
var statusLabel = '';
|
|
85
|
+
if (isSubmitted) {
|
|
86
|
+
var timeStr = submittedAt ? new Date(submittedAt).toLocaleString() : '';
|
|
87
|
+
var clStr = changelistId ? 'CL #' + escHtml(changelistId) : '';
|
|
88
|
+
statusLabel = '<div style="font-size:0.8em;color:#6ee7b7;margin:4px 0 6px">'
|
|
89
|
+
+ '✓ 已提交'
|
|
90
|
+
+ (clStr ? ' ' + clStr : '')
|
|
91
|
+
+ (timeStr ? ' ' + timeStr : '')
|
|
92
|
+
+ '</div>';
|
|
93
|
+
}
|
|
94
|
+
listEl.innerHTML = '<b>' + (isSubmitted ? '已提交' : '待提交') + '(' + allFiles.length + ' 个文件):</b>' + statusLabel +
|
|
95
|
+
allFiles.map(function(f) { return '<div class="p4-file">' + escHtml(f) + '</div>'; }).join('');
|
|
96
|
+
if (descEl && descriptions.length) {
|
|
97
|
+
if (!descEl.value.trim() || isSubmitted) descEl.value = descriptions[descriptions.length - 1];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function escHtml(str) {
|
|
102
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
window.setP4Description = function(desc) {
|
|
106
|
+
var descEl = document.getElementById('p4-submit-desc');
|
|
107
|
+
if (descEl && desc) descEl.value = desc;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
window.loadP4Pending = loadP4Pending;
|
|
111
|
+
|
|
112
|
+
document.addEventListener('DOMContentLoaded', loadP4Pending);
|
|
113
|
+
})();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// 崩溃上下文 prompt 前端共享模块
|
|
2
|
+
// 与 routes/backtrace-shared.js 中的 buildCrashContextPrompt / buildCrashContextFirstTurnPrompt
|
|
3
|
+
// 字符串内容必须保持一致,避免 UI 显示与实际发送给模型的 prompt 不一致。
|
|
4
|
+
|
|
5
|
+
(function (global) {
|
|
6
|
+
function buildCrashContextPrompt(meta) {
|
|
7
|
+
var errorMessage = meta && meta.errorMessage ? meta.errorMessage : '-';
|
|
8
|
+
var classifiers = meta && meta.classifiers ? meta.classifiers : '-';
|
|
9
|
+
return [
|
|
10
|
+
'你是一名资深 C++ 开发工程师与 Unreal Engine 引擎专家,正在协助分析虚幻引擎客户端的一次崩溃。',
|
|
11
|
+
'',
|
|
12
|
+
'## 崩溃上下文',
|
|
13
|
+
'- Error Message: ' + errorMessage,
|
|
14
|
+
'- Classifiers: ' + classifiers,
|
|
15
|
+
'',
|
|
16
|
+
'## 工作准则',
|
|
17
|
+
'- 优先从 Error Message 推断崩溃位置与触发条件,必要时使用工具读取项目代码与日志文件以定位根因',
|
|
18
|
+
'- 给出根因分析与最小化的修复建议;修改代码时保持改动可审查、影响范围明确',
|
|
19
|
+
'- 回答使用简体中文,代码块使用合适的语言标注',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildCrashContextFirstTurnPrompt(userText, meta) {
|
|
24
|
+
return buildCrashContextPrompt(meta) + '\n\n----\n我的问题:\n' + String(userText || '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
global.buildCrashContextPrompt = buildCrashContextPrompt;
|
|
28
|
+
global.buildCrashContextFirstTurnPrompt = buildCrashContextFirstTurnPrompt;
|
|
29
|
+
})(window);
|
package/public/chat-send.js
CHANGED
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
let finalPrompt = text;
|
|
34
34
|
if (!currentThreadId && currentFingerprintData) {
|
|
35
|
-
|
|
36
|
-
const classifiers = currentFingerprintData.classifiers || '-';
|
|
37
|
-
finalPrompt = `你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\nError Message: ${errorMessage}\nClassifiers: ${classifiers}\n\n请根据这些信息,回答我的问题:\n${text}`;
|
|
35
|
+
finalPrompt = buildCrashContextFirstTurnPrompt(text, currentFingerprintData);
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
setComposerStatus('正在处理中,请稍候…');
|
|
@@ -195,6 +193,8 @@
|
|
|
195
193
|
stopTypewriter(messageState);
|
|
196
194
|
rerenderFullMessage(messageState);
|
|
197
195
|
}
|
|
196
|
+
if (data.p4Description && window.setP4Description) window.setP4Description(data.p4Description);
|
|
197
|
+
if (window.loadP4Pending) window.loadP4Pending();
|
|
198
198
|
}
|
|
199
199
|
} catch (e) {
|
|
200
200
|
console.error('SSE JSON 解析错误:', e, '数据:', dataStr);
|