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.
- package/app.js +3 -2
- package/bin/backtrace-server.js +101 -0
- package/bin/www +3 -0
- package/lib/backtrace/constants.js +4 -0
- package/lib/backtrace/query-download.js +117 -0
- package/lib/backtrace/query-session.js +229 -0
- package/lib/backtrace/query.js +11 -445
- package/lib/backtrace/repair.js +35 -0
- package/lib/backtrace/tool.js +34 -3
- package/lib/feishu.js +66 -0
- package/lib/scheduler.js +126 -0
- package/package.json +10 -4
- package/public/chat-components.css +569 -0
- package/public/chat-core.js +635 -0
- package/public/chat-layout.css +290 -0
- package/public/chat-render.js +308 -0
- package/public/chat-send.js +230 -0
- package/public/chat.html +69 -0
- package/public/{__inline_check__.js → index-page.js} +107 -54
- package/public/index.html +1 -505
- package/routes/backtrace-chat.js +389 -0
- package/routes/backtrace-files.js +88 -0
- package/routes/backtrace-fix-plan.js +53 -0
- package/routes/backtrace-run.js +128 -0
- package/routes/backtrace-shared.js +202 -0
- package/routes/backtrace.js +7 -861
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var fs = require('node:fs/promises');
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var os = require('node:os');
|
|
5
|
+
var shared = require('./backtrace-shared');
|
|
6
|
+
|
|
7
|
+
var router = express.Router();
|
|
8
|
+
|
|
9
|
+
async function getChatSessionIds(relativeFingerprintPath) {
|
|
10
|
+
var entries = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
11
|
+
return entries.filter(function(entry) { return entry.ext === '.json'; })
|
|
12
|
+
.map(function(entry) { return path.basename(entry.name, '.json'); })
|
|
13
|
+
.sort(function(a, b) { return b.localeCompare(a); });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildContextMessage(meta) {
|
|
17
|
+
var errorMessage = meta && meta.errorMessage ? meta.errorMessage : '-';
|
|
18
|
+
var classifiers = meta && meta.classifiers ? meta.classifiers : '-';
|
|
19
|
+
return '你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\n'
|
|
20
|
+
+ 'Error Message: ' + errorMessage + '\n'
|
|
21
|
+
+ 'Classifiers: ' + classifiers + '\n\n'
|
|
22
|
+
+ '请结合这些上下文,帮助用户定位问题并给出修复建议。';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readChatSession(relativeFingerprintPath, sessionId) {
|
|
26
|
+
var relativePath = path.join(relativeFingerprintPath, 'messages', sessionId + '.json');
|
|
27
|
+
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, relativePath);
|
|
28
|
+
var raw = await fs.readFile(absolutePath, 'utf8').catch(function(error) {
|
|
29
|
+
if (error && error.code === 'ENOENT') return '';
|
|
30
|
+
throw error;
|
|
31
|
+
});
|
|
32
|
+
return raw ? JSON.parse(raw) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function writeChatSession(relativeFingerprintPath, session) {
|
|
36
|
+
var messagesDir = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
37
|
+
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages', session.sessionId + '.json'));
|
|
38
|
+
await fs.mkdir(messagesDir, { recursive: true });
|
|
39
|
+
await fs.writeFile(absolutePath, JSON.stringify(session, null, 2), 'utf8');
|
|
40
|
+
return absolutePath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function listChatSessions(relativeFingerprintPath) {
|
|
44
|
+
var sessionIds = await getChatSessionIds(relativeFingerprintPath);
|
|
45
|
+
var sessions = [];
|
|
46
|
+
for (var i = 0; i < sessionIds.length; i += 1) {
|
|
47
|
+
var session = await readChatSession(relativeFingerprintPath, sessionIds[i]);
|
|
48
|
+
if (!session) continue;
|
|
49
|
+
sessions.push({
|
|
50
|
+
sessionId: session.sessionId,
|
|
51
|
+
threadId: session.threadId || null,
|
|
52
|
+
title: session.title || session.sessionId,
|
|
53
|
+
createdAt: session.createdAt || null,
|
|
54
|
+
updatedAt: session.updatedAt || session.createdAt || null,
|
|
55
|
+
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return sessions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function createChatSession(relativeFingerprintPath) {
|
|
62
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
63
|
+
if (!meta) throw new Error('fingerprint not found');
|
|
64
|
+
var now = new Date();
|
|
65
|
+
var sessionId = shared.formatChatSessionId(now);
|
|
66
|
+
var session = {
|
|
67
|
+
sessionId: sessionId,
|
|
68
|
+
fingerprint: relativeFingerprintPath,
|
|
69
|
+
threadId: null,
|
|
70
|
+
createdAt: now.toISOString(),
|
|
71
|
+
updatedAt: now.toISOString(),
|
|
72
|
+
title: '新对话 ' + sessionId,
|
|
73
|
+
messages: [{ kind: 'context', role: 'system', text: buildContextMessage(meta), createdAt: now.toISOString() }],
|
|
74
|
+
};
|
|
75
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
76
|
+
return session;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function appendChatSessionEvent(relativeFingerprintPath, sessionId, event) {
|
|
80
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
81
|
+
if (!session) throw new Error('chat session not found');
|
|
82
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
83
|
+
session.messages.push(event);
|
|
84
|
+
session.updatedAt = new Date().toISOString();
|
|
85
|
+
if ((!session.title || session.title.indexOf('新对话') === 0) && event.role === 'user' && event.text) {
|
|
86
|
+
session.title = String(event.text).slice(0, 80);
|
|
87
|
+
}
|
|
88
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
89
|
+
return session;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function updateChatSessionThread(relativeFingerprintPath, sessionId, threadId) {
|
|
93
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
94
|
+
if (!session) throw new Error('chat session not found');
|
|
95
|
+
session.threadId = typeof threadId === 'string' ? threadId : null;
|
|
96
|
+
session.updatedAt = new Date().toISOString();
|
|
97
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text, extraMeta) {
|
|
102
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
103
|
+
if (!session) throw new Error('chat session not found');
|
|
104
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
105
|
+
var now = new Date().toISOString();
|
|
106
|
+
var draft = session.messages.find(function(message) {
|
|
107
|
+
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
108
|
+
});
|
|
109
|
+
if (!draft) {
|
|
110
|
+
draft = { kind: 'agent_draft', role: 'agent', text: '', createdAt: now, updatedAt: now, meta: { draftId: draftId, status: 'streaming' } };
|
|
111
|
+
session.messages.push(draft);
|
|
112
|
+
}
|
|
113
|
+
draft.text = String(text || '');
|
|
114
|
+
draft.updatedAt = now;
|
|
115
|
+
draft.meta = Object.assign({}, draft.meta || {}, extraMeta || {}, { draftId: draftId, status: 'streaming' });
|
|
116
|
+
session.updatedAt = now;
|
|
117
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text) {
|
|
122
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
123
|
+
if (!session) throw new Error('chat session not found');
|
|
124
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
125
|
+
var now = new Date().toISOString();
|
|
126
|
+
var draft = session.messages.find(function(message) {
|
|
127
|
+
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
128
|
+
});
|
|
129
|
+
if (!draft) {
|
|
130
|
+
draft = { createdAt: now, meta: { draftId: draftId } };
|
|
131
|
+
session.messages.push(draft);
|
|
132
|
+
}
|
|
133
|
+
draft.kind = 'message';
|
|
134
|
+
draft.role = 'agent';
|
|
135
|
+
draft.text = String(text || '');
|
|
136
|
+
draft.updatedAt = now;
|
|
137
|
+
draft.meta = Object.assign({}, draft.meta || {}, { draftId: draftId, status: 'completed' });
|
|
138
|
+
session.updatedAt = now;
|
|
139
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
140
|
+
return session;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildCodexEventMeta(event) {
|
|
144
|
+
var meta = {};
|
|
145
|
+
if (!event || typeof event !== 'object') return meta;
|
|
146
|
+
if (event.type) meta.type = event.type;
|
|
147
|
+
if (event.thread_id) meta.threadId = event.thread_id;
|
|
148
|
+
if (event.item && event.item.type) meta.itemType = event.item.type;
|
|
149
|
+
if (event.item && event.item.id) meta.itemId = event.item.id;
|
|
150
|
+
if (event.usage) meta.usage = event.usage;
|
|
151
|
+
if (event.error && event.error.message) meta.error = event.error.message;
|
|
152
|
+
return meta;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function extractCodexToolArgs(event) {
|
|
156
|
+
if (!event || !event.item) return null;
|
|
157
|
+
return event.item.arguments || event.item.input || event.item.args || null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractCodexToolResult(event) {
|
|
161
|
+
if (!event || !event.item) return '';
|
|
162
|
+
return event.item.result || event.item.output || event.item.text || '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function extractCodexEventText(event) {
|
|
166
|
+
if (!event || typeof event !== 'object') return '';
|
|
167
|
+
return event.text || event.delta || (event.item && (event.item.text || event.item.delta)) || event.message || '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatToolCompletedResult(toolName, args, result, eventMeta) {
|
|
171
|
+
var text = result;
|
|
172
|
+
if (text && typeof text === 'string' && text.trim() && text.trim() !== toolName) return text;
|
|
173
|
+
var parts = [];
|
|
174
|
+
if (args) parts.push('args: ' + (typeof args === 'string' ? args : JSON.stringify(args, null, 2)));
|
|
175
|
+
if (eventMeta && eventMeta.error) parts.push('error: ' + eventMeta.error);
|
|
176
|
+
return parts.length > 0 ? parts.join('\n\n') : (text || toolName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
router.get('/chat-sessions', async function(req, res) {
|
|
180
|
+
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
181
|
+
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
182
|
+
try {
|
|
183
|
+
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
184
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
185
|
+
if (!meta) return res.status(404).json({ ok: false, error: 'fingerprint not found' });
|
|
186
|
+
var sessions = await listChatSessions(relativeFingerprintPath);
|
|
187
|
+
var activeSessionId = meta.activeSessionId || null;
|
|
188
|
+
if (activeSessionId && !sessions.some(function(session) { return session.sessionId === activeSessionId; })) {
|
|
189
|
+
activeSessionId = null;
|
|
190
|
+
}
|
|
191
|
+
return res.json({ ok: true, fingerprint: fingerprint, activeSessionId: activeSessionId, sessions: sessions });
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
router.get('/chat-session', async function(req, res) {
|
|
198
|
+
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
199
|
+
var sessionId = String(req.query.sessionId || '').trim();
|
|
200
|
+
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
201
|
+
try {
|
|
202
|
+
var session = await readChatSession(shared.normalizeFingerprintPath(fingerprint), sessionId);
|
|
203
|
+
if (!session) return res.status(404).json({ ok: false, error: 'chat session not found' });
|
|
204
|
+
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
router.post('/chat-session/create', async function(req, res) {
|
|
211
|
+
var fingerprint = String((req.body || {}).fingerprint || '').trim();
|
|
212
|
+
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
213
|
+
try {
|
|
214
|
+
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
215
|
+
var session = await createChatSession(relativeFingerprintPath);
|
|
216
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
217
|
+
meta.activeSessionId = session.sessionId;
|
|
218
|
+
meta.threadId = null;
|
|
219
|
+
await shared.writeFingerprintMeta(relativeFingerprintPath, meta);
|
|
220
|
+
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
router.post('/chat', async function(req, res) {
|
|
227
|
+
var body = req.body || {};
|
|
228
|
+
var prompt = body.prompt;
|
|
229
|
+
var threadId = body.threadId;
|
|
230
|
+
var fingerprint = String(body.fingerprint || '').trim();
|
|
231
|
+
var sessionId = String(body.sessionId || '').trim();
|
|
232
|
+
var images = Array.isArray(body.images) ? body.images : [];
|
|
233
|
+
var relativeFingerprintPath = '';
|
|
234
|
+
var draftId = '';
|
|
235
|
+
var finalResponse = '';
|
|
236
|
+
var tempImagePaths = [];
|
|
237
|
+
if (!prompt) return res.status(400).json({ ok: false, error: 'prompt is required' });
|
|
238
|
+
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
239
|
+
try {
|
|
240
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
241
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
242
|
+
res.setHeader('Connection', 'keep-alive');
|
|
243
|
+
res.write('event: ping\ndata: \n\n');
|
|
244
|
+
|
|
245
|
+
relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
246
|
+
var existingMeta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
247
|
+
if (!existingMeta) throw new Error('fingerprint not found');
|
|
248
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
249
|
+
if (!session) throw new Error('chat session not found');
|
|
250
|
+
threadId = threadId || session.threadId || existingMeta.threadId;
|
|
251
|
+
|
|
252
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, {
|
|
253
|
+
kind: 'message',
|
|
254
|
+
role: 'user',
|
|
255
|
+
text: body.userMessage || prompt,
|
|
256
|
+
createdAt: new Date().toISOString(),
|
|
257
|
+
meta: images.length > 0 ? { imageCount: images.length } : null,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 处理图片:base64 解码写到临时文件
|
|
261
|
+
for (var i = 0; i < images.length; i++) {
|
|
262
|
+
var img = images[i];
|
|
263
|
+
if (!img.data || !img.mimeType) continue;
|
|
264
|
+
var ext = img.mimeType.split('/')[1] || 'png';
|
|
265
|
+
var tempPath = path.join(os.tmpdir(), 'codex-image-' + Date.now() + '-' + i + '.' + ext);
|
|
266
|
+
var buffer = Buffer.from(img.data, 'base64');
|
|
267
|
+
await fs.writeFile(tempPath, buffer);
|
|
268
|
+
tempImagePaths.push(tempPath);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var workdir = body.workdir || shared.DEFAULT_WORKDIR;
|
|
272
|
+
var proxy = shared.resolveCodexProxy(body.proxy);
|
|
273
|
+
var mod = await import('@openai/codex-sdk');
|
|
274
|
+
var codex = new mod.Codex({ env: Object.assign({}, process.env, { HTTP_PROXY: proxy, HTTPS_PROXY: proxy, ALL_PROXY: proxy }) });
|
|
275
|
+
var thread = threadId ? codex.resumeThread(threadId, {
|
|
276
|
+
workingDirectory: workdir,
|
|
277
|
+
skipGitRepoCheck: true,
|
|
278
|
+
sandboxMode: 'workspace-write',
|
|
279
|
+
approvalPolicy: 'never',
|
|
280
|
+
modelReasoningEffort: 'high',
|
|
281
|
+
}) : codex.startThread({
|
|
282
|
+
workingDirectory: workdir,
|
|
283
|
+
skipGitRepoCheck: true,
|
|
284
|
+
sandboxMode: 'workspace-write',
|
|
285
|
+
approvalPolicy: 'never',
|
|
286
|
+
modelReasoningEffort: 'high',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 构造 UserInput[]:文本 + 图片
|
|
290
|
+
var userInput = [{ type: 'text', text: shared.buildCodexChatPrompt(prompt, { workdir: workdir }) }];
|
|
291
|
+
for (var j = 0; j < tempImagePaths.length; j++) {
|
|
292
|
+
userInput.push({ type: 'local_image', path: tempImagePaths[j] });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
var stream = await thread.runStreamed(userInput);
|
|
296
|
+
finalResponse = '';
|
|
297
|
+
var lastSentText = '';
|
|
298
|
+
var toolBuffers = {};
|
|
299
|
+
draftId = 'draft-' + Date.now() + '-' + Math.random().toString(16).slice(2, 10);
|
|
300
|
+
var lastDraftWriteAt = 0;
|
|
301
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, '', { threadId: thread.id });
|
|
302
|
+
|
|
303
|
+
function writeSse(payload) {
|
|
304
|
+
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
|
305
|
+
}
|
|
306
|
+
async function persistDraft(force) {
|
|
307
|
+
var now = Date.now();
|
|
308
|
+
if (!force && now - lastDraftWriteAt < 600) return;
|
|
309
|
+
lastDraftWriteAt = now;
|
|
310
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, { threadId: thread.id });
|
|
311
|
+
}
|
|
312
|
+
function writeSseText(nextText, replace) {
|
|
313
|
+
if (typeof nextText !== 'string') return;
|
|
314
|
+
if (!replace && nextText.startsWith(lastSentText)) {
|
|
315
|
+
var deltaText = nextText.substring(lastSentText.length);
|
|
316
|
+
if (!deltaText) return;
|
|
317
|
+
lastSentText = nextText;
|
|
318
|
+
writeSse({ kind: 'agent_text', text: deltaText, replace: false });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
lastSentText = nextText;
|
|
322
|
+
writeSse({ kind: 'agent_text', text: nextText, replace: true });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for await (var event of stream.events) {
|
|
326
|
+
var eventMeta = buildCodexEventMeta(event);
|
|
327
|
+
if (event.type === 'item.stream.delta' || event.type === 'text.delta' || event.type === 'turn.stream.delta') {
|
|
328
|
+
var delta = event.delta || (event.item && event.item.delta);
|
|
329
|
+
if (typeof delta === 'string') {
|
|
330
|
+
finalResponse += delta;
|
|
331
|
+
writeSseText(finalResponse, false);
|
|
332
|
+
await persistDraft(false);
|
|
333
|
+
}
|
|
334
|
+
} else if (event.type === 'item.completed' && event.item && event.item.type === 'agent_message' && event.item.text) {
|
|
335
|
+
finalResponse = event.item.text;
|
|
336
|
+
writeSseText(finalResponse, false);
|
|
337
|
+
await persistDraft(true);
|
|
338
|
+
} else if (event.type === 'turn.failed') {
|
|
339
|
+
throw new Error(event.error && event.error.message ? event.error.message : 'Codex turn failed');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (event.item && event.item.type && event.item.type !== 'agent_message' && event.item.type !== 'user_message') {
|
|
343
|
+
var toolId = event.item.id || eventMeta.itemId || ('tool-' + Date.now());
|
|
344
|
+
var toolText = extractCodexEventText(event);
|
|
345
|
+
if (!toolBuffers[toolId]) {
|
|
346
|
+
toolBuffers[toolId] = { itemType: event.item.type, text: '', args: extractCodexToolArgs(event), result: extractCodexToolResult(event) };
|
|
347
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_started', role: 'tool', text: event.item.type, createdAt: new Date().toISOString(), meta: Object.assign({ args: toolBuffers[toolId].args }, eventMeta) });
|
|
348
|
+
writeSse({ kind: 'tool_call_started', toolId: toolId, toolName: event.item.type, args: toolBuffers[toolId].args, text: event.item.type, meta: eventMeta });
|
|
349
|
+
}
|
|
350
|
+
if (extractCodexToolResult(event)) toolBuffers[toolId].result = extractCodexToolResult(event);
|
|
351
|
+
if (toolText) {
|
|
352
|
+
toolBuffers[toolId].text += toolText;
|
|
353
|
+
if (!toolBuffers[toolId].result) toolBuffers[toolId].result = toolBuffers[toolId].text;
|
|
354
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_output', role: 'tool', text: toolText, createdAt: new Date().toISOString(), meta: Object.assign({ toolId: toolId }, eventMeta) });
|
|
355
|
+
writeSse({ kind: 'tool_call_output', toolId: toolId, toolName: event.item.type, output: toolBuffers[toolId].text, text: toolText, meta: eventMeta });
|
|
356
|
+
}
|
|
357
|
+
if (event.type === 'item.completed') {
|
|
358
|
+
var completedResult = formatToolCompletedResult(event.item.type, toolBuffers[toolId].args, toolBuffers[toolId].result || toolBuffers[toolId].text, eventMeta);
|
|
359
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_completed', role: 'tool', text: completedResult, createdAt: new Date().toISOString(), meta: Object.assign({ toolId: toolId }, eventMeta) });
|
|
360
|
+
writeSse({ kind: 'tool_call_completed', toolId: toolId, toolName: event.item.type, result: completedResult, text: completedResult, meta: eventMeta });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse);
|
|
366
|
+
await updateChatSessionThread(relativeFingerprintPath, sessionId, thread.id);
|
|
367
|
+
existingMeta.threadId = thread.id;
|
|
368
|
+
existingMeta.activeSessionId = sessionId;
|
|
369
|
+
await shared.writeFingerprintMeta(relativeFingerprintPath, existingMeta);
|
|
370
|
+
writeSse({ kind: 'done', threadId: thread.id, sessionId: sessionId });
|
|
371
|
+
res.end();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (relativeFingerprintPath && sessionId && draftId) {
|
|
374
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, {
|
|
375
|
+
status: 'interrupted',
|
|
376
|
+
error: error instanceof Error ? error.message : String(error),
|
|
377
|
+
}).catch(function() {});
|
|
378
|
+
}
|
|
379
|
+
res.write('event: error\ndata: ' + JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) + '\n\n');
|
|
380
|
+
res.end();
|
|
381
|
+
} finally {
|
|
382
|
+
// 清理临时图片文件
|
|
383
|
+
for (var k = 0; k < tempImagePaths.length; k++) {
|
|
384
|
+
fs.unlink(tempImagePaths[k]).catch(function() {});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
module.exports = router;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var path = require('node:path');
|
|
3
|
+
var shared = require('./backtrace-shared');
|
|
4
|
+
|
|
5
|
+
var router = express.Router();
|
|
6
|
+
|
|
7
|
+
router.get('/files/index', async function(req, res) {
|
|
8
|
+
try {
|
|
9
|
+
var directories = await shared.listTopLevelMergedDirectories();
|
|
10
|
+
return res.json({ ok: true, directories: directories });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
router.get('/files/list', async function(req, res) {
|
|
17
|
+
var topPath = String(req.query.path || '');
|
|
18
|
+
var logDir = String(req.query.logDir || '');
|
|
19
|
+
try {
|
|
20
|
+
var logDirectories = await shared.listImmediateDirectories(shared.FINGERPRINTS_ROOT, path.join(topPath, 'logs'));
|
|
21
|
+
var selectedLogDir = logDir || (logDirectories[0] ? logDirectories[0].name : '');
|
|
22
|
+
var logFiles = selectedLogDir ? await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(topPath, 'logs', selectedLogDir)) : [];
|
|
23
|
+
var reportFiles = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(topPath, 'reports'));
|
|
24
|
+
var repairStatus = await shared.readRepairStatus(topPath);
|
|
25
|
+
var hasCompletedRepair = !!(repairStatus && repairStatus.completed);
|
|
26
|
+
return res.json({
|
|
27
|
+
ok: true,
|
|
28
|
+
path: topPath,
|
|
29
|
+
selectedLogDir: selectedLogDir,
|
|
30
|
+
logDirectories: logDirectories,
|
|
31
|
+
logFiles: logFiles,
|
|
32
|
+
reportFiles: reportFiles,
|
|
33
|
+
reportStatus: shared.buildFingerprintStatus(reportFiles.length > 0, hasCompletedRepair),
|
|
34
|
+
hasCompletedRepair: hasCompletedRepair,
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
router.get('/files/content', async function(req, res) {
|
|
42
|
+
var kind = req.query.kind;
|
|
43
|
+
var relativePath = req.query.path;
|
|
44
|
+
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
45
|
+
if (!rootDir || !relativePath) {
|
|
46
|
+
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
50
|
+
var content = await require('node:fs/promises').readFile(targetPath, 'utf8');
|
|
51
|
+
return res.json({ ok: true, kind: kind, path: relativePath, content: content });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
router.get('/files/download', function(req, res) {
|
|
58
|
+
var kind = req.query.kind;
|
|
59
|
+
var relativePath = req.query.path;
|
|
60
|
+
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
61
|
+
if (!rootDir || !relativePath) {
|
|
62
|
+
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
66
|
+
return res.download(targetPath);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
router.get('/files/view', function(req, res) {
|
|
73
|
+
var kind = req.query.kind;
|
|
74
|
+
var relativePath = req.query.path;
|
|
75
|
+
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
76
|
+
if (!rootDir || !relativePath) {
|
|
77
|
+
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
81
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
82
|
+
return res.sendFile(targetPath);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
module.exports = router;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var RepairModule = require('../lib/backtrace/repair');
|
|
3
|
+
var shared = require('./backtrace-shared');
|
|
4
|
+
|
|
5
|
+
var router = express.Router();
|
|
6
|
+
|
|
7
|
+
router.post('/fix-plan/generate', async function(req, res) {
|
|
8
|
+
var body = req.body || {};
|
|
9
|
+
var reportPath = body.reportPath;
|
|
10
|
+
if (!reportPath) {
|
|
11
|
+
return res.status(400).json({ ok: false, error: 'reportPath is required' });
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
var result = await RepairModule.generateRepairPlan({
|
|
15
|
+
reportPath: reportPath,
|
|
16
|
+
}, {
|
|
17
|
+
rootDir: shared.ROOT_DIR,
|
|
18
|
+
fingerprintsRoot: shared.FINGERPRINTS_ROOT,
|
|
19
|
+
workdir: body.workdir || shared.DEFAULT_WORKDIR,
|
|
20
|
+
proxy: shared.resolveCodexProxy(body.proxy),
|
|
21
|
+
});
|
|
22
|
+
return res.json(result);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
router.post('/fix-plan/apply', async function(req, res) {
|
|
29
|
+
var body = req.body || {};
|
|
30
|
+
var reportPath = body.reportPath;
|
|
31
|
+
var planText = body.planText;
|
|
32
|
+
if (!reportPath || !planText) {
|
|
33
|
+
return res.status(400).json({ ok: false, error: 'reportPath and planText are required' });
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
var result = await RepairModule.applyRepairPlan({
|
|
37
|
+
reportPath: reportPath,
|
|
38
|
+
planText: planText,
|
|
39
|
+
repairVersion: body.repairVersion,
|
|
40
|
+
repairPlanPath: body.repairPlanPath,
|
|
41
|
+
}, {
|
|
42
|
+
rootDir: shared.ROOT_DIR,
|
|
43
|
+
fingerprintsRoot: shared.FINGERPRINTS_ROOT,
|
|
44
|
+
workdir: body.workdir || shared.DEFAULT_WORKDIR,
|
|
45
|
+
proxy: shared.resolveCodexProxy(body.proxy),
|
|
46
|
+
});
|
|
47
|
+
return res.json(result);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
module.exports = router;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var fs = require('node:fs/promises');
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var BacktraceCodexTool = require('../lib/BacktraceCodexTool').BacktraceCodexTool;
|
|
5
|
+
var { BacktraceSession, downloadItemLogs, fetchAttachmentList } = require('../lib/backtrace/query');
|
|
6
|
+
var { createOptions } = require('../lib/backtrace/options');
|
|
7
|
+
var shared = require('./backtrace-shared');
|
|
8
|
+
|
|
9
|
+
var router = express.Router();
|
|
10
|
+
|
|
11
|
+
router.post('/run', async function(req, res) {
|
|
12
|
+
var body = req.body || {};
|
|
13
|
+
var command = body.command || 'collect-all';
|
|
14
|
+
var tool = new BacktraceCodexTool();
|
|
15
|
+
try {
|
|
16
|
+
var result = await tool.run(command, body);
|
|
17
|
+
return res.json({ ok: true, command: command, result: result });
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return res.status(500).json({ ok: false, command: command, error: error instanceof Error ? error.message : String(error) });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
router.get('/fingerprint', async function(req, res) {
|
|
24
|
+
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
25
|
+
if (!fingerprint) {
|
|
26
|
+
return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
30
|
+
var result = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
31
|
+
if (!result) {
|
|
32
|
+
return res.status(404).json({ ok: false, error: 'fingerprint not found' });
|
|
33
|
+
}
|
|
34
|
+
var reports = await shared.listFingerprintReports(relativeFingerprintPath);
|
|
35
|
+
result.reportFiles = reports;
|
|
36
|
+
result.defaultReportPath = reports.length > 0 ? reports[0].path : '';
|
|
37
|
+
return res.json({ ok: true, command: 'fingerprint', result: result });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return res.status(500).json({ ok: false, command: 'fingerprint', error: error instanceof Error ? error.message : String(error) });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
router.get('/list', async function(req, res) {
|
|
44
|
+
var tool = new BacktraceCodexTool();
|
|
45
|
+
var overrides = {
|
|
46
|
+
from: req.query.from,
|
|
47
|
+
to: req.query.to,
|
|
48
|
+
limit: req.query.limit,
|
|
49
|
+
offset: req.query.offset,
|
|
50
|
+
select: req.query.select,
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
var result = await tool.list(overrides);
|
|
54
|
+
return res.json({ ok: true, command: 'list', result: result });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return res.status(500).json({ ok: false, command: 'list', error: error instanceof Error ? error.message : String(error) });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.post('/download-object', async function(req, res) {
|
|
61
|
+
var body = req.body || {};
|
|
62
|
+
var fp = String(body.fingerprint || '').trim();
|
|
63
|
+
var objectIdHex = String(body.objectIdHex || '').trim();
|
|
64
|
+
if (!fp || !objectIdHex) {
|
|
65
|
+
return res.status(400).json({ ok: false, error: 'fingerprint and objectIdHex are required' });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
var options = createOptions({ command: 'collect-all', fingerprint: fp });
|
|
69
|
+
var session = new BacktraceSession(options);
|
|
70
|
+
await session.login();
|
|
71
|
+
var item = { objectIdHex: objectIdHex, objectIdDecimal: '', fingerprint: fp, values: { fingerprint: fp } };
|
|
72
|
+
var result = await downloadItemLogs(session, item, options);
|
|
73
|
+
return res.json({ ok: true, fingerprint: fp, objectIdHex: objectIdHex, downloadedFiles: result.downloadedFiles, targetDir: result.targetDir });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
router.post('/query-object-attachments', async function(req, res) {
|
|
80
|
+
var body = req.body || {};
|
|
81
|
+
var fp = String(body.fingerprint || '').trim();
|
|
82
|
+
var objectIdHex = String(body.objectIdHex || '').trim();
|
|
83
|
+
if (!fp || !objectIdHex) {
|
|
84
|
+
return res.status(400).json({ ok: false, error: 'fingerprint and objectIdHex are required' });
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
var options = createOptions({ command: 'collect-all', fingerprint: fp });
|
|
88
|
+
var session = new BacktraceSession(options);
|
|
89
|
+
await session.login();
|
|
90
|
+
|
|
91
|
+
// 查询远端附件列表
|
|
92
|
+
var attachmentResult = await fetchAttachmentList(session, options.queryUrl, objectIdHex);
|
|
93
|
+
var remoteAttachments = (attachmentResult && attachmentResult.attachments) ? attachmentResult.attachments : [];
|
|
94
|
+
|
|
95
|
+
// 读取本地已下载文件
|
|
96
|
+
var objDir = path.join(shared.FINGERPRINTS_ROOT, fp, 'logs', objectIdHex);
|
|
97
|
+
var localFiles = await fs.readdir(objDir).catch(function() { return []; });
|
|
98
|
+
var localFileSet = new Set(localFiles);
|
|
99
|
+
|
|
100
|
+
// 合并:逐文件检查本地是否已下载
|
|
101
|
+
var attachments = remoteAttachments.map(function(att) {
|
|
102
|
+
return {
|
|
103
|
+
id: att.id || '',
|
|
104
|
+
name: att.name || '',
|
|
105
|
+
size: att.size || 0,
|
|
106
|
+
content_type: att.content_type || '',
|
|
107
|
+
downloaded: localFileSet.has(att.name),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 读取现有 object-meta.json,合并写回
|
|
112
|
+
var metaPath = path.join(objDir, 'object-meta.json');
|
|
113
|
+
var existingRaw = await fs.readFile(metaPath, 'utf8').catch(function() { return ''; });
|
|
114
|
+
var existing = {};
|
|
115
|
+
if (existingRaw) {
|
|
116
|
+
try { existing = JSON.parse(existingRaw); } catch (_) { existing = {}; }
|
|
117
|
+
}
|
|
118
|
+
var updated = Object.assign({}, existing, { attachments: attachments });
|
|
119
|
+
await fs.mkdir(objDir, { recursive: true });
|
|
120
|
+
await fs.writeFile(metaPath, JSON.stringify(updated, null, 2), 'utf8');
|
|
121
|
+
|
|
122
|
+
return res.json({ ok: true, fingerprint: fp, objectIdHex: objectIdHex, attachments: attachments });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
module.exports = router;
|