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,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;
@@ -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
- 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
- }
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: buildContextMessage(meta), createdAt: now.toISOString() }],
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 thread = threadId ? codex.resumeThread(threadId, {
316
+ var threadOptions = {
276
317
  workingDirectory: workdir,
277
318
  skipGitRepoCheck: true,
278
319
  sandboxMode: 'workspace-write',
279
320
  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
- });
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
- writeSse({ kind: 'done', threadId: thread.id, sessionId: sessionId });
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) {