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.
@@ -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
+
@@ -87,22 +87,14 @@ const chatContainer = document.getElementById('chat-container');
87
87
 
88
88
  function restoreWelcomeMessage() {
89
89
  chatContainer.innerHTML = '';
90
- if (currentFingerprintData) {
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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);
@@ -32,9 +32,7 @@
32
32
 
33
33
  let finalPrompt = text;
34
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}`;
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);