backtrace-console 0.0.5 → 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 +3 -1
- 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,477 @@
|
|
|
1
|
+
var express = require('express');
|
|
2
|
+
var fs = require('node:fs/promises');
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var shared = require('./backtrace-shared');
|
|
5
|
+
|
|
6
|
+
var router = express.Router();
|
|
7
|
+
|
|
8
|
+
// ── 会话文件管理 ─────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
async function getChatSessionIds(relativeFingerprintPath) {
|
|
11
|
+
var entries = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
12
|
+
return entries
|
|
13
|
+
.filter(function(entry) { return entry.ext === '.json' && entry.name.startsWith('claude-'); })
|
|
14
|
+
.map(function(entry) { return path.basename(entry.name, '.json'); })
|
|
15
|
+
.sort(function(a, b) { return b.localeCompare(a); });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 崩溃上下文 prompt 由 shared.buildCrashContextPrompt 统一提供
|
|
19
|
+
|
|
20
|
+
async function readChatSession(relativeFingerprintPath, sessionId) {
|
|
21
|
+
var relativePath = path.join(relativeFingerprintPath, 'messages', sessionId + '.json');
|
|
22
|
+
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, relativePath);
|
|
23
|
+
var raw = await fs.readFile(absolutePath, 'utf8').catch(function(error) {
|
|
24
|
+
if (error && error.code === 'ENOENT') return '';
|
|
25
|
+
throw error;
|
|
26
|
+
});
|
|
27
|
+
return raw ? JSON.parse(raw) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function writeChatSession(relativeFingerprintPath, session) {
|
|
31
|
+
var messagesDir = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
32
|
+
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages', session.sessionId + '.json'));
|
|
33
|
+
await fs.mkdir(messagesDir, { recursive: true });
|
|
34
|
+
await fs.writeFile(absolutePath, JSON.stringify(session, null, 2), 'utf8');
|
|
35
|
+
return absolutePath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function listChatSessions(relativeFingerprintPath) {
|
|
39
|
+
var sessionIds = await getChatSessionIds(relativeFingerprintPath);
|
|
40
|
+
var sessions = [];
|
|
41
|
+
for (var i = 0; i < sessionIds.length; i += 1) {
|
|
42
|
+
var session = await readChatSession(relativeFingerprintPath, sessionIds[i]);
|
|
43
|
+
if (!session) continue;
|
|
44
|
+
sessions.push({
|
|
45
|
+
sessionId: session.sessionId,
|
|
46
|
+
claudeSessionId: session.claudeSessionId || null,
|
|
47
|
+
title: session.title || session.sessionId,
|
|
48
|
+
createdAt: session.createdAt || null,
|
|
49
|
+
updatedAt: session.updatedAt || session.createdAt || null,
|
|
50
|
+
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return sessions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function createChatSession(relativeFingerprintPath) {
|
|
57
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
58
|
+
if (!meta) throw new Error('fingerprint not found');
|
|
59
|
+
var now = new Date();
|
|
60
|
+
var sessionId = 'claude-' + shared.formatChatSessionId(now);
|
|
61
|
+
var session = {
|
|
62
|
+
sessionId: sessionId,
|
|
63
|
+
fingerprint: relativeFingerprintPath,
|
|
64
|
+
claudeSessionId: null,
|
|
65
|
+
createdAt: now.toISOString(),
|
|
66
|
+
updatedAt: now.toISOString(),
|
|
67
|
+
title: '新对话 ' + sessionId,
|
|
68
|
+
messages: [{ kind: 'context', role: 'system', text: shared.buildCrashContextPrompt(meta), createdAt: now.toISOString() }],
|
|
69
|
+
};
|
|
70
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function appendChatSessionEvent(relativeFingerprintPath, sessionId, event) {
|
|
75
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
76
|
+
if (!session) throw new Error('chat session not found');
|
|
77
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
78
|
+
session.messages.push(event);
|
|
79
|
+
session.updatedAt = new Date().toISOString();
|
|
80
|
+
if ((!session.title || session.title.indexOf('新对话') === 0) && event.role === 'user' && event.text) {
|
|
81
|
+
session.title = String(event.text).slice(0, 80);
|
|
82
|
+
}
|
|
83
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
84
|
+
return session;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function updateChatSessionClaudeId(relativeFingerprintPath, sessionId, claudeSessionId) {
|
|
88
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
89
|
+
if (!session) throw new Error('chat session not found');
|
|
90
|
+
session.claudeSessionId = typeof claudeSessionId === 'string' ? claudeSessionId : null;
|
|
91
|
+
session.updatedAt = new Date().toISOString();
|
|
92
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
93
|
+
return session;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text, extraMeta) {
|
|
97
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
98
|
+
if (!session) throw new Error('chat session not found');
|
|
99
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
100
|
+
var now = new Date().toISOString();
|
|
101
|
+
var draft = session.messages.find(function(message) {
|
|
102
|
+
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
103
|
+
});
|
|
104
|
+
if (!draft) {
|
|
105
|
+
draft = { kind: 'agent_draft', role: 'agent', text: '', createdAt: now, updatedAt: now, meta: { draftId: draftId, status: 'streaming' } };
|
|
106
|
+
session.messages.push(draft);
|
|
107
|
+
}
|
|
108
|
+
draft.text = String(text || '');
|
|
109
|
+
draft.updatedAt = now;
|
|
110
|
+
draft.meta = Object.assign({}, draft.meta || {}, extraMeta || {}, { draftId: draftId, status: 'streaming' });
|
|
111
|
+
session.updatedAt = now;
|
|
112
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text) {
|
|
117
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
118
|
+
if (!session) throw new Error('chat session not found');
|
|
119
|
+
if (!Array.isArray(session.messages)) session.messages = [];
|
|
120
|
+
var now = new Date().toISOString();
|
|
121
|
+
var draft = session.messages.find(function(message) {
|
|
122
|
+
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
123
|
+
});
|
|
124
|
+
if (!draft) {
|
|
125
|
+
draft = { createdAt: now, meta: { draftId: draftId } };
|
|
126
|
+
session.messages.push(draft);
|
|
127
|
+
}
|
|
128
|
+
draft.kind = 'message';
|
|
129
|
+
draft.role = 'agent';
|
|
130
|
+
draft.text = String(text || '');
|
|
131
|
+
draft.updatedAt = now;
|
|
132
|
+
draft.meta = Object.assign({}, draft.meta || {}, { draftId: draftId, status: 'completed' });
|
|
133
|
+
session.updatedAt = now;
|
|
134
|
+
await writeChatSession(relativeFingerprintPath, session);
|
|
135
|
+
return session;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── P4 提交说明构建 ──────────────────────────────────────────────────────
|
|
139
|
+
// 与 backtrace-chat.js 中的 buildP4SubmitDescription 完全一致
|
|
140
|
+
function buildP4SubmitDescription(changes, workdir) {
|
|
141
|
+
if (!changes || !changes.length) return '';
|
|
142
|
+
var wd = String(workdir || '').replace(/\\/g, '/').replace(/\/$/, '') + '/';
|
|
143
|
+
|
|
144
|
+
var relPaths = changes.map(function(c) {
|
|
145
|
+
var p = String(c.path || '').replace(/\\/g, '/');
|
|
146
|
+
return p.startsWith(wd) ? p.slice(wd.length) : p;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
var segArrays = relPaths.map(function(p) {
|
|
150
|
+
return p.split('/').filter(Boolean).slice(0, -1);
|
|
151
|
+
});
|
|
152
|
+
var common = segArrays[0] ? segArrays[0].slice() : [];
|
|
153
|
+
for (var i = 1; i < segArrays.length; i++) {
|
|
154
|
+
var newCommon = [];
|
|
155
|
+
for (var j = 0; j < Math.min(common.length, segArrays[i].length); j++) {
|
|
156
|
+
if (common[j] === segArrays[i][j]) newCommon.push(common[j]);
|
|
157
|
+
else break;
|
|
158
|
+
}
|
|
159
|
+
common = newCommon;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var skip = new Set(['source', 'src', 'private', 'public', 'classes', 'aboveland', 'game']);
|
|
163
|
+
var module = 'Core';
|
|
164
|
+
for (var k = common.length - 1; k >= 0; k--) {
|
|
165
|
+
if (!skip.has(common[k].toLowerCase())) { module = common[k]; break; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
var kinds = changes.map(function(c) { return c.kind || 'update'; });
|
|
169
|
+
var op = kinds.every(function(x) { return x === 'add'; }) ? 'add'
|
|
170
|
+
: kinds.every(function(x) { return x === 'delete'; }) ? 'delete'
|
|
171
|
+
: 'update';
|
|
172
|
+
|
|
173
|
+
var names = changes.slice(0, 2).map(function(c) {
|
|
174
|
+
return String(c.path || '').split(/[/\\]/).pop().replace(/\.[^.]+$/, '');
|
|
175
|
+
}).join(', ');
|
|
176
|
+
if (changes.length > 2) names += ' +' + (changes.length - 2);
|
|
177
|
+
|
|
178
|
+
return '[' + module + '] ' + op + ' ' + names + ' <by claude code>';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 流式输入构建:支持图片 ───────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
async function* buildPromptStream(builtPromptText, images) {
|
|
184
|
+
var content = [{ type: 'text', text: builtPromptText }];
|
|
185
|
+
for (var i = 0; i < images.length; i++) {
|
|
186
|
+
var img = images[i];
|
|
187
|
+
if (!img || !img.data || !img.mimeType) continue;
|
|
188
|
+
content.push({
|
|
189
|
+
type: 'image',
|
|
190
|
+
source: { type: 'base64', media_type: img.mimeType, data: img.data }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
yield {
|
|
194
|
+
type: 'user',
|
|
195
|
+
message: { role: 'user', content: content }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── 路由:会话 CRUD ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
router.get('/claude-chat-sessions', async function(req, res) {
|
|
202
|
+
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
203
|
+
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
204
|
+
try {
|
|
205
|
+
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
206
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
207
|
+
if (!meta) return res.status(404).json({ ok: false, error: 'fingerprint not found' });
|
|
208
|
+
var sessions = await listChatSessions(relativeFingerprintPath);
|
|
209
|
+
var activeSessionId = meta.activeClaudeSessionId || null;
|
|
210
|
+
if (activeSessionId && !sessions.some(function(session) { return session.sessionId === activeSessionId; })) {
|
|
211
|
+
activeSessionId = null;
|
|
212
|
+
}
|
|
213
|
+
return res.json({ ok: true, fingerprint: fingerprint, activeSessionId: activeSessionId, sessions: sessions });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
router.get('/claude-chat-session', async function(req, res) {
|
|
220
|
+
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
221
|
+
var sessionId = String(req.query.sessionId || '').trim();
|
|
222
|
+
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
223
|
+
try {
|
|
224
|
+
var session = await readChatSession(shared.normalizeFingerprintPath(fingerprint), sessionId);
|
|
225
|
+
if (!session) return res.status(404).json({ ok: false, error: 'chat session not found' });
|
|
226
|
+
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
router.post('/claude-chat-session/create', async function(req, res) {
|
|
233
|
+
var fingerprint = String((req.body || {}).fingerprint || '').trim();
|
|
234
|
+
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
235
|
+
try {
|
|
236
|
+
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
237
|
+
var session = await createChatSession(relativeFingerprintPath);
|
|
238
|
+
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
239
|
+
meta.activeClaudeSessionId = session.sessionId;
|
|
240
|
+
meta.claudeSessionId = null;
|
|
241
|
+
await shared.writeFingerprintMeta(relativeFingerprintPath, meta);
|
|
242
|
+
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── 路由:主对话 SSE 流 ─────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
router.post('/claude-chat', async function(req, res) {
|
|
251
|
+
var body = req.body || {};
|
|
252
|
+
var prompt = body.prompt;
|
|
253
|
+
var claudeSessionId = body.claudeSessionId;
|
|
254
|
+
var fingerprint = String(body.fingerprint || '').trim();
|
|
255
|
+
var sessionId = String(body.sessionId || '').trim();
|
|
256
|
+
var images = Array.isArray(body.images) ? body.images : [];
|
|
257
|
+
var relativeFingerprintPath = '';
|
|
258
|
+
var draftId = '';
|
|
259
|
+
var finalResponse = '';
|
|
260
|
+
|
|
261
|
+
if (!prompt) return res.status(400).json({ ok: false, error: 'prompt is required' });
|
|
262
|
+
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
266
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
267
|
+
res.setHeader('Connection', 'keep-alive');
|
|
268
|
+
res.write('event: ping\ndata: \n\n');
|
|
269
|
+
|
|
270
|
+
relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
271
|
+
var existingMeta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
272
|
+
if (!existingMeta) throw new Error('fingerprint not found');
|
|
273
|
+
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
274
|
+
if (!session) throw new Error('chat session not found');
|
|
275
|
+
claudeSessionId = claudeSessionId || session.claudeSessionId || existingMeta.claudeSessionId;
|
|
276
|
+
|
|
277
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, {
|
|
278
|
+
kind: 'message',
|
|
279
|
+
role: 'user',
|
|
280
|
+
text: body.userMessage || prompt,
|
|
281
|
+
createdAt: new Date().toISOString(),
|
|
282
|
+
meta: images.length > 0 ? { imageCount: images.length } : null,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
var workdir = body.workdir || shared.DEFAULT_WORKDIR;
|
|
286
|
+
var proxy = shared.resolveCodexProxy(body.proxy);
|
|
287
|
+
var builtPrompt = shared.buildCodexChatPrompt(prompt, { workdir: workdir });
|
|
288
|
+
|
|
289
|
+
var childEnv = Object.assign({}, process.env);
|
|
290
|
+
if (proxy) {
|
|
291
|
+
childEnv.HTTP_PROXY = proxy;
|
|
292
|
+
childEnv.HTTPS_PROXY = proxy;
|
|
293
|
+
childEnv.http_proxy = proxy;
|
|
294
|
+
childEnv.https_proxy = proxy;
|
|
295
|
+
childEnv.ALL_PROXY = proxy;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var claudeAgentSdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
299
|
+
var queryOptions = {
|
|
300
|
+
prompt: buildPromptStream(builtPrompt, images),
|
|
301
|
+
options: {
|
|
302
|
+
allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'],
|
|
303
|
+
permissionMode: 'acceptEdits',
|
|
304
|
+
cwd: workdir,
|
|
305
|
+
model: shared.CLAUDE_MODEL,
|
|
306
|
+
maxTurns: 20,
|
|
307
|
+
includePartialMessages: true,
|
|
308
|
+
env: childEnv,
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
if (claudeSessionId) queryOptions.options.resume = claudeSessionId;
|
|
312
|
+
|
|
313
|
+
draftId = 'claude-draft-' + Date.now() + '-' + Math.random().toString(16).slice(2, 10);
|
|
314
|
+
var lastDraftWriteAt = 0;
|
|
315
|
+
var pendingTool = null; // { toolId, toolName, argsBuffer }
|
|
316
|
+
var fileChanges = [];
|
|
317
|
+
var resolvedClaudeSessionId = claudeSessionId;
|
|
318
|
+
|
|
319
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, '', { claudeSessionId: claudeSessionId });
|
|
320
|
+
|
|
321
|
+
function writeSse(payload) {
|
|
322
|
+
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
|
323
|
+
}
|
|
324
|
+
async function persistDraft(force) {
|
|
325
|
+
var now = Date.now();
|
|
326
|
+
if (!force && now - lastDraftWriteAt < 600) return;
|
|
327
|
+
lastDraftWriteAt = now;
|
|
328
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, { claudeSessionId: resolvedClaudeSessionId });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for await (var message of claudeAgentSdk.query(queryOptions)) {
|
|
332
|
+
// 1. 捕获 session_id
|
|
333
|
+
if (message.type === 'system' && message.subtype === 'init' && message.session_id) {
|
|
334
|
+
resolvedClaudeSessionId = message.session_id;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 2. 流式事件 → SSE
|
|
338
|
+
if (message.type === 'stream_event') {
|
|
339
|
+
var ev = message.event;
|
|
340
|
+
if (!ev || !ev.type) continue;
|
|
341
|
+
|
|
342
|
+
if (ev.type === 'content_block_start' && ev.content_block && ev.content_block.type === 'tool_use') {
|
|
343
|
+
pendingTool = {
|
|
344
|
+
toolId: ev.content_block.id || ('claude-tool-' + Date.now()),
|
|
345
|
+
toolName: ev.content_block.name || 'tool',
|
|
346
|
+
argsBuffer: ''
|
|
347
|
+
};
|
|
348
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, {
|
|
349
|
+
kind: 'tool_call_started',
|
|
350
|
+
role: 'tool',
|
|
351
|
+
text: pendingTool.toolName,
|
|
352
|
+
createdAt: new Date().toISOString(),
|
|
353
|
+
meta: { toolId: pendingTool.toolId }
|
|
354
|
+
});
|
|
355
|
+
writeSse({
|
|
356
|
+
kind: 'tool_call_started',
|
|
357
|
+
toolId: pendingTool.toolId,
|
|
358
|
+
toolName: pendingTool.toolName,
|
|
359
|
+
args: null,
|
|
360
|
+
text: pendingTool.toolName
|
|
361
|
+
});
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (ev.type === 'content_block_delta' && ev.delta) {
|
|
366
|
+
if (ev.delta.type === 'text_delta') {
|
|
367
|
+
var delta = ev.delta.text || '';
|
|
368
|
+
if (delta) {
|
|
369
|
+
finalResponse += delta;
|
|
370
|
+
writeSse({ kind: 'agent_text', text: delta, replace: false });
|
|
371
|
+
await persistDraft(false);
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (ev.delta.type === 'input_json_delta' && pendingTool) {
|
|
376
|
+
pendingTool.argsBuffer += ev.delta.partial_json || '';
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (ev.type === 'content_block_stop' && pendingTool) {
|
|
382
|
+
var parsedArgs = null;
|
|
383
|
+
try { parsedArgs = pendingTool.argsBuffer ? JSON.parse(pendingTool.argsBuffer) : null; } catch (_) {}
|
|
384
|
+
await appendChatSessionEvent(relativeFingerprintPath, sessionId, {
|
|
385
|
+
kind: 'tool_call_completed',
|
|
386
|
+
role: 'tool',
|
|
387
|
+
text: '',
|
|
388
|
+
createdAt: new Date().toISOString(),
|
|
389
|
+
meta: { toolId: pendingTool.toolId, args: parsedArgs }
|
|
390
|
+
});
|
|
391
|
+
writeSse({
|
|
392
|
+
kind: 'tool_call_completed',
|
|
393
|
+
toolId: pendingTool.toolId,
|
|
394
|
+
toolName: pendingTool.toolName,
|
|
395
|
+
args: parsedArgs,
|
|
396
|
+
result: '',
|
|
397
|
+
text: ''
|
|
398
|
+
});
|
|
399
|
+
pendingTool = null;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 3. assistant 完整消息 → 提取 tool_use 用于 P4 文件追踪
|
|
405
|
+
if (message.type === 'assistant' && message.message && Array.isArray(message.message.content)) {
|
|
406
|
+
for (var b = 0; b < message.message.content.length; b++) {
|
|
407
|
+
var block = message.message.content[b];
|
|
408
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
409
|
+
var input = block.input || {};
|
|
410
|
+
var fpath = input.file_path || input.path || '';
|
|
411
|
+
if (!fpath) continue;
|
|
412
|
+
if (block.name === 'Write') fileChanges.push({ path: fpath, kind: 'add' });
|
|
413
|
+
else if (block.name === 'Edit') fileChanges.push({ path: fpath, kind: 'update' });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 4. result 消息携带最终 session_id
|
|
418
|
+
if (message.type === 'result' && message.session_id) {
|
|
419
|
+
resolvedClaudeSessionId = message.session_id;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 收尾:持久化、写 P4 描述、发送 done
|
|
424
|
+
await finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse);
|
|
425
|
+
await updateChatSessionClaudeId(relativeFingerprintPath, sessionId, resolvedClaudeSessionId);
|
|
426
|
+
existingMeta.claudeSessionId = resolvedClaudeSessionId;
|
|
427
|
+
existingMeta.activeClaudeSessionId = sessionId;
|
|
428
|
+
await shared.writeFingerprintMeta(relativeFingerprintPath, existingMeta);
|
|
429
|
+
|
|
430
|
+
// P4 文件去重 + 描述构建(与 Codex 版相同的输出格式)
|
|
431
|
+
var p4Description = '';
|
|
432
|
+
if (fileChanges.length > 0) {
|
|
433
|
+
var dedup = {};
|
|
434
|
+
fileChanges.forEach(function(c) { dedup[c.path] = c; });
|
|
435
|
+
var changes = Object.keys(dedup).map(function(k) { return dedup[k]; });
|
|
436
|
+
try {
|
|
437
|
+
p4Description = buildP4SubmitDescription(changes, workdir);
|
|
438
|
+
var repairDir = path.join(shared.FINGERPRINTS_ROOT, relativeFingerprintPath, 'repair', 'chat-' + sessionId);
|
|
439
|
+
await fs.mkdir(repairDir, { recursive: true });
|
|
440
|
+
await fs.writeFile(path.join(repairDir, 'p4-changes.json'), JSON.stringify({
|
|
441
|
+
fingerprint: relativeFingerprintPath,
|
|
442
|
+
sessionId: sessionId,
|
|
443
|
+
description: p4Description,
|
|
444
|
+
files: changes.map(function(c) { return c.path; }),
|
|
445
|
+
recordedAt: new Date().toISOString(),
|
|
446
|
+
submittedAt: null
|
|
447
|
+
}, null, 2));
|
|
448
|
+
console.log('[claude-chat] p4-changes.json 已写入 ' + changes.length + ' 个文件,描述: ' + p4Description);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
console.error('[claude-chat] p4-changes.json 写入失败:', e.message);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
writeSse({
|
|
455
|
+
kind: 'done',
|
|
456
|
+
claudeSessionId: resolvedClaudeSessionId,
|
|
457
|
+
sessionId: sessionId,
|
|
458
|
+
p4Description: p4Description || undefined
|
|
459
|
+
});
|
|
460
|
+
res.end();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
var errMsg = error instanceof Error ? error.message : String(error);
|
|
463
|
+
var errStack = error instanceof Error && error.stack ? error.stack : '';
|
|
464
|
+
console.error('[claude-chat] 处理失败:', errMsg);
|
|
465
|
+
if (errStack) console.error(errStack);
|
|
466
|
+
if (relativeFingerprintPath && sessionId && draftId) {
|
|
467
|
+
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, {
|
|
468
|
+
status: 'interrupted',
|
|
469
|
+
error: errMsg,
|
|
470
|
+
}).catch(function() {});
|
|
471
|
+
}
|
|
472
|
+
res.write('event: error\ndata: ' + JSON.stringify({ kind: 'error', error: errMsg }) + '\n\n');
|
|
473
|
+
res.end();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
module.exports = router;
|
package/routes/backtrace-chat.js
CHANGED
|
@@ -3,24 +3,18 @@ var fs = require('node:fs/promises');
|
|
|
3
3
|
var path = require('node:path');
|
|
4
4
|
var os = require('node:os');
|
|
5
5
|
var shared = require('./backtrace-shared');
|
|
6
|
+
var p4ops = require('../lib/p4ops');
|
|
6
7
|
|
|
7
8
|
var router = express.Router();
|
|
8
9
|
|
|
9
10
|
async function getChatSessionIds(relativeFingerprintPath) {
|
|
10
11
|
var entries = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
11
|
-
return entries.filter(function(entry) { return entry.ext === '.json'; })
|
|
12
|
+
return entries.filter(function(entry) { return entry.ext === '.json' && !entry.name.startsWith('claude-'); })
|
|
12
13
|
.map(function(entry) { return path.basename(entry.name, '.json'); })
|
|
13
14
|
.sort(function(a, b) { return b.localeCompare(a); });
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
}
|
|
17
|
+
// 崩溃上下文 prompt 由 shared.buildCrashContextPrompt 统一提供
|
|
24
18
|
|
|
25
19
|
async function readChatSession(relativeFingerprintPath, sessionId) {
|
|
26
20
|
var relativePath = path.join(relativeFingerprintPath, 'messages', sessionId + '.json');
|
|
@@ -70,7 +64,7 @@ async function createChatSession(relativeFingerprintPath) {
|
|
|
70
64
|
createdAt: now.toISOString(),
|
|
71
65
|
updatedAt: now.toISOString(),
|
|
72
66
|
title: '新对话 ' + sessionId,
|
|
73
|
-
messages: [{ kind: 'context', role: 'system', text:
|
|
67
|
+
messages: [{ kind: 'context', role: 'system', text: shared.buildCrashContextPrompt(meta), createdAt: now.toISOString() }],
|
|
74
68
|
};
|
|
75
69
|
await writeChatSession(relativeFingerprintPath, session);
|
|
76
70
|
return session;
|
|
@@ -140,6 +134,53 @@ async function finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, dra
|
|
|
140
134
|
return session;
|
|
141
135
|
}
|
|
142
136
|
|
|
137
|
+
// 从 file_change 事件的 changes 数组构建 P4 提交说明
|
|
138
|
+
// 格式:[<module>] <op> <filenames> <by codex ai>
|
|
139
|
+
function buildP4SubmitDescription(changes, workdir) {
|
|
140
|
+
if (!changes || !changes.length) return '';
|
|
141
|
+
var wd = String(workdir || '').replace(/\\/g, '/').replace(/\/$/, '') + '/';
|
|
142
|
+
|
|
143
|
+
var relPaths = changes.map(function(c) {
|
|
144
|
+
var p = String(c.path || '').replace(/\\/g, '/');
|
|
145
|
+
return p.startsWith(wd) ? p.slice(wd.length) : p;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// 找各路径目录部分的最深公共段
|
|
149
|
+
var segArrays = relPaths.map(function(p) {
|
|
150
|
+
return p.split('/').filter(Boolean).slice(0, -1); // 去掉文件名
|
|
151
|
+
});
|
|
152
|
+
var common = segArrays[0] ? segArrays[0].slice() : [];
|
|
153
|
+
for (var i = 1; i < segArrays.length; i++) {
|
|
154
|
+
var newCommon = [];
|
|
155
|
+
for (var j = 0; j < Math.min(common.length, segArrays[i].length); j++) {
|
|
156
|
+
if (common[j] === segArrays[i][j]) newCommon.push(common[j]);
|
|
157
|
+
else break;
|
|
158
|
+
}
|
|
159
|
+
common = newCommon;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 取最深的非通用目录名作为 module
|
|
163
|
+
var skip = new Set(['source', 'src', 'private', 'public', 'classes', 'aboveland', 'game']);
|
|
164
|
+
var module = 'Core';
|
|
165
|
+
for (var k = common.length - 1; k >= 0; k--) {
|
|
166
|
+
if (!skip.has(common[k].toLowerCase())) { module = common[k]; break; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 确定操作类型
|
|
170
|
+
var kinds = changes.map(function(c) { return c.kind || 'update'; });
|
|
171
|
+
var op = kinds.every(function(x) { return x === 'add'; }) ? 'add'
|
|
172
|
+
: kinds.every(function(x) { return x === 'delete'; }) ? 'delete'
|
|
173
|
+
: 'update';
|
|
174
|
+
|
|
175
|
+
// 文件名(最多 2 个,不含扩展名)
|
|
176
|
+
var names = changes.slice(0, 2).map(function(c) {
|
|
177
|
+
return String(c.path || '').split(/[/\\]/).pop().replace(/\.[^.]+$/, '');
|
|
178
|
+
}).join(', ');
|
|
179
|
+
if (changes.length > 2) names += ' +' + (changes.length - 2);
|
|
180
|
+
|
|
181
|
+
return '[' + module + '] ' + op + ' ' + names + ' <by codex ai>';
|
|
182
|
+
}
|
|
183
|
+
|
|
143
184
|
function buildCodexEventMeta(event) {
|
|
144
185
|
var meta = {};
|
|
145
186
|
if (!event || typeof event !== 'object') return meta;
|
|
@@ -272,19 +313,15 @@ router.post('/chat', async function(req, res) {
|
|
|
272
313
|
var proxy = shared.resolveCodexProxy(body.proxy);
|
|
273
314
|
var mod = await import('@openai/codex-sdk');
|
|
274
315
|
var codex = new mod.Codex({ env: Object.assign({}, process.env, { HTTP_PROXY: proxy, HTTPS_PROXY: proxy, ALL_PROXY: proxy }) });
|
|
275
|
-
var
|
|
316
|
+
var threadOptions = {
|
|
276
317
|
workingDirectory: workdir,
|
|
277
318
|
skipGitRepoCheck: true,
|
|
278
319
|
sandboxMode: 'workspace-write',
|
|
279
320
|
approvalPolicy: 'never',
|
|
280
|
-
modelReasoningEffort:
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
sandboxMode: 'workspace-write',
|
|
285
|
-
approvalPolicy: 'never',
|
|
286
|
-
modelReasoningEffort: 'high',
|
|
287
|
-
});
|
|
321
|
+
modelReasoningEffort: shared.CODEX_REASONING_EFFORT,
|
|
322
|
+
};
|
|
323
|
+
if (shared.CODEX_MODEL) threadOptions.model = shared.CODEX_MODEL;
|
|
324
|
+
var thread = threadId ? codex.resumeThread(threadId, threadOptions) : codex.startThread(threadOptions);
|
|
288
325
|
|
|
289
326
|
// 构造 UserInput[]:文本 + 图片
|
|
290
327
|
var userInput = [{ type: 'text', text: shared.buildCodexChatPrompt(prompt, { workdir: workdir }) }];
|
|
@@ -296,6 +333,7 @@ router.post('/chat', async function(req, res) {
|
|
|
296
333
|
finalResponse = '';
|
|
297
334
|
var lastSentText = '';
|
|
298
335
|
var toolBuffers = {};
|
|
336
|
+
var fileChanges = []; // 收集 Codex file_change 事件的 {path, kind} 列表
|
|
299
337
|
draftId = 'draft-' + Date.now() + '-' + Math.random().toString(16).slice(2, 10);
|
|
300
338
|
var lastDraftWriteAt = 0;
|
|
301
339
|
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, '', { threadId: thread.id });
|
|
@@ -339,6 +377,14 @@ router.post('/chat', async function(req, res) {
|
|
|
339
377
|
throw new Error(event.error && event.error.message ? event.error.message : 'Codex turn failed');
|
|
340
378
|
}
|
|
341
379
|
|
|
380
|
+
// 直接从 file_change 事件提取 {path, kind}(FileChangeItem.changes[])
|
|
381
|
+
if (event.type === 'item.completed' && event.item && event.item.type === 'file_change' && event.item.status === 'completed') {
|
|
382
|
+
var changes = event.item.changes || [];
|
|
383
|
+
for (var ci = 0; ci < changes.length; ci++) {
|
|
384
|
+
if (changes[ci].path) fileChanges.push({ path: changes[ci].path, kind: changes[ci].kind || 'update' });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
342
388
|
if (event.item && event.item.type && event.item.type !== 'agent_message' && event.item.type !== 'user_message') {
|
|
343
389
|
var toolId = event.item.id || eventMeta.itemId || ('tool-' + Date.now());
|
|
344
390
|
var toolText = extractCodexEventText(event);
|
|
@@ -367,7 +413,29 @@ router.post('/chat', async function(req, res) {
|
|
|
367
413
|
existingMeta.threadId = thread.id;
|
|
368
414
|
existingMeta.activeSessionId = sessionId;
|
|
369
415
|
await shared.writeFingerprintMeta(relativeFingerprintPath, existingMeta);
|
|
370
|
-
|
|
416
|
+
|
|
417
|
+
// Codex 修改了文件时,写 p4-changes.json 并把描述附在 done 事件里
|
|
418
|
+
var p4Description = '';
|
|
419
|
+
if (fileChanges.length > 0) {
|
|
420
|
+
try {
|
|
421
|
+
p4Description = buildP4SubmitDescription(fileChanges, workdir);
|
|
422
|
+
var repairDir = path.join(shared.FINGERPRINTS_ROOT, relativeFingerprintPath, 'repair', 'chat-' + sessionId);
|
|
423
|
+
await fs.mkdir(repairDir, { recursive: true });
|
|
424
|
+
await fs.writeFile(path.join(repairDir, 'p4-changes.json'), JSON.stringify({
|
|
425
|
+
fingerprint: relativeFingerprintPath,
|
|
426
|
+
sessionId: sessionId,
|
|
427
|
+
description: p4Description,
|
|
428
|
+
files: fileChanges.map(function(c) { return c.path; }),
|
|
429
|
+
recordedAt: new Date().toISOString(),
|
|
430
|
+
submittedAt: null
|
|
431
|
+
}, null, 2));
|
|
432
|
+
console.log('[chat] p4-changes.json 已写入 ' + fileChanges.length + ' 个文件,描述: ' + p4Description);
|
|
433
|
+
} catch (e) {
|
|
434
|
+
console.error('[chat] p4-changes.json 写入失败:', e.message);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
writeSse({ kind: 'done', threadId: thread.id, sessionId: sessionId, p4Description: p4Description || undefined });
|
|
371
439
|
res.end();
|
|
372
440
|
} catch (error) {
|
|
373
441
|
if (relativeFingerprintPath && sessionId && draftId) {
|