backtrace-console 0.0.1

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,864 @@
1
+ var express = require('express');
2
+ var fs = require('node:fs/promises');
3
+ var path = require('node:path');
4
+ var router = express.Router();
5
+ var BacktraceCodexToolModule = require('../lib/BacktraceCodexTool');
6
+ var BacktraceCodexTool = BacktraceCodexToolModule.BacktraceCodexTool;
7
+ var DEFAULT_WORKDIR = BacktraceCodexToolModule.DEFAULT_WORKDIR;
8
+ var DEFAULT_PROXY = BacktraceCodexToolModule.DEFAULT_PROXY;
9
+ var DEFAULT_QUERY_URL = BacktraceCodexToolModule.DEFAULT_QUERY_URL;
10
+ var BacktraceRepairModule = require('../lib/backtrace/repair');
11
+ var generateRepairPlanShared = BacktraceRepairModule.generateRepairPlan;
12
+ var applyRepairPlanShared = BacktraceRepairModule.applyRepairPlan;
13
+ var resolveCodexProxyShared = BacktraceRepairModule.resolveCodexProxy;
14
+
15
+ var ROOT_DIR = process.cwd();
16
+ var FINGERPRINTS_ROOT = path.join(ROOT_DIR, 'fingerprints');
17
+ var BACKTRACE_USERNAME = process.env.BACKTRACE_USERNAME || '';
18
+ var BACKTRACE_PASSWORD = process.env.BACKTRACE_PASSWORD || '';
19
+ var REPAIR_STATUS_FILE = '.repair-status.json';
20
+
21
+ function formatRepairVersion(date) {
22
+ var year = String(date.getFullYear());
23
+ var month = String(date.getMonth() + 1).padStart(2, '0');
24
+ var day = String(date.getDate()).padStart(2, '0');
25
+ var hour = String(date.getHours()).padStart(2, '0');
26
+ var minute = String(date.getMinutes()).padStart(2, '0');
27
+ var second = String(date.getSeconds()).padStart(2, '0');
28
+ return year + month + day + '-' + hour + minute + second;
29
+ }
30
+
31
+ function getFingerprintPathFromReportPath(relativeReportPath) {
32
+ var normalized = String(relativeReportPath || '').replace(/[\\/]+/g, '/').trim();
33
+ if (!normalized) {
34
+ throw new Error('Invalid report path');
35
+ }
36
+
37
+ var parts = normalized.split('/').filter(Boolean);
38
+ var reportsIndex = parts.lastIndexOf('reports');
39
+ if (reportsIndex > 0) {
40
+ return parts.slice(0, reportsIndex).join(path.sep);
41
+ }
42
+
43
+ if (parts.length >= 2) {
44
+ return parts.slice(0, -1).join(path.sep);
45
+ }
46
+
47
+ return parts[0];
48
+ }
49
+
50
+ function getRepairVersionRelativeDir(relativeReportPath, repairVersion) {
51
+ return path.join(getFingerprintPathFromReportPath(relativeReportPath), 'repair', repairVersion);
52
+ }
53
+
54
+ function getRepairVersionFromRepairPlanPath(repairPlanPath) {
55
+ var normalized = String(repairPlanPath || '').replace(/[\\/]+/g, '/').trim();
56
+ if (!normalized) {
57
+ return '';
58
+ }
59
+ var parts = normalized.split('/').filter(Boolean);
60
+ var repairIndex = parts.lastIndexOf('repair');
61
+ if (repairIndex >= 0 && parts.length > repairIndex + 1) {
62
+ return parts[repairIndex + 1];
63
+ }
64
+ return '';
65
+ }
66
+
67
+ async function resolveNewRepairVersion(relativeReportPath) {
68
+ var baseVersion = formatRepairVersion(new Date());
69
+ var repairRootRelative = path.join(getFingerprintPathFromReportPath(relativeReportPath), 'repair');
70
+ var repairRootAbsolute = toSafeAbsolute(FINGERPRINTS_ROOT, repairRootRelative);
71
+ var suffix = 0;
72
+ while (true) {
73
+ var repairVersion = suffix === 0 ? baseVersion : (baseVersion + '-' + String(suffix).padStart(2, '0'));
74
+ var candidateDir = path.join(repairRootAbsolute, repairVersion);
75
+ if (!(await pathExists(candidateDir))) {
76
+ return repairVersion;
77
+ }
78
+ suffix += 1;
79
+ }
80
+ }
81
+
82
+ async function resolveRepairVersionForApply(relativeReportPath, repairVersion, repairPlanPath) {
83
+ var normalized = String(repairVersion || '').trim();
84
+ if (normalized) {
85
+ return normalized;
86
+ }
87
+ var planVersion = getRepairVersionFromRepairPlanPath(repairPlanPath);
88
+ if (planVersion) {
89
+ return planVersion;
90
+ }
91
+ var repairRootRelative = path.join(getFingerprintPathFromReportPath(relativeReportPath), 'repair');
92
+ var repairRootAbsolute = toSafeAbsolute(FINGERPRINTS_ROOT, repairRootRelative);
93
+ var entries = await fs.readdir(repairRootAbsolute, { withFileTypes: true }).catch(function() { return []; });
94
+ var versions = entries.filter(function(entry) { return entry.isDirectory(); }).map(function(entry) { return entry.name; }).sort().reverse();
95
+ if (versions.length > 0) {
96
+ return versions[0];
97
+ }
98
+ return formatRepairVersion(new Date());
99
+ }
100
+
101
+ async function ensureRepairVersionDir(relativeReportPath, repairVersion) {
102
+ var relativeDir = getRepairVersionRelativeDir(relativeReportPath, repairVersion);
103
+ var absoluteDir = toSafeAbsolute(FINGERPRINTS_ROOT, relativeDir);
104
+ await fs.mkdir(absoluteDir, { recursive: true });
105
+ return { relativeDir: relativeDir, absoluteDir: absoluteDir };
106
+ }
107
+
108
+ async function writeRepairArtifact(relativeReportPath, repairVersion, fileName, text) {
109
+ var versionDir = await ensureRepairVersionDir(relativeReportPath, repairVersion);
110
+ var relativePath = path.join(versionDir.relativeDir, fileName);
111
+ var absolutePath = path.join(versionDir.absoluteDir, fileName);
112
+ await fs.writeFile(absolutePath, String(text || ''), 'utf8');
113
+ return { repairVersion: repairVersion, relativeDir: versionDir.relativeDir, relativePath: relativePath, absolutePath: absolutePath };
114
+ }
115
+
116
+ function collectModifiedSourceCandidates(text) {
117
+ var matches = String(text || '').match(/(?:[A-Za-z]:)?[A-Za-z0-9_./\\-]+\.(?:js|jsx|ts|tsx|json|css|scss|less|html|vue|java|kt|go|py|rb|php|cs|cpp|c|h|hpp|m|mm|swift|xml|yml|yaml)/g) || [];
118
+ return Array.from(new Set(matches.map(function(item) {
119
+ return String(item || '').replace(/\\/g, '/').replace(/^\.\//, '').trim();
120
+ }).filter(Boolean)));
121
+ }
122
+
123
+ async function archiveModifiedSources(repairVersionRelativeDir, text) {
124
+ var candidates = collectModifiedSourceCandidates(text);
125
+ var archived = [];
126
+ for (var i = 0; i < candidates.length; i += 1) {
127
+ var candidate = candidates[i];
128
+ var workspaceAbsolute = path.resolve(ROOT_DIR, candidate);
129
+ if (workspaceAbsolute !== ROOT_DIR && !workspaceAbsolute.startsWith(ROOT_DIR + path.sep)) {
130
+ continue;
131
+ }
132
+ var stats = await fs.stat(workspaceAbsolute).catch(function() { return null; });
133
+ if (!stats || !stats.isFile()) {
134
+ continue;
135
+ }
136
+ var sourceRelativePath = path.relative(ROOT_DIR, workspaceAbsolute);
137
+ if (!sourceRelativePath || sourceRelativePath.startsWith('fingerprints' + path.sep)) {
138
+ continue;
139
+ }
140
+ var targetRelativePath = path.join(repairVersionRelativeDir, 'sources', sourceRelativePath);
141
+ var targetAbsolutePath = toSafeAbsolute(FINGERPRINTS_ROOT, targetRelativePath);
142
+ await fs.mkdir(path.dirname(targetAbsolutePath), { recursive: true });
143
+ var fileContent = await fs.readFile(workspaceAbsolute);
144
+ await fs.writeFile(targetAbsolutePath, fileContent);
145
+ archived.push(targetRelativePath);
146
+ }
147
+ return archived;
148
+ }
149
+
150
+ function buildFingerprintStatus(hasReports, hasCompletedRepair) {
151
+ if (!hasReports) {
152
+ return '未生成报告';
153
+ }
154
+ return hasCompletedRepair ? '已完成' : '未修复状态';
155
+ }
156
+
157
+ async function readRepairStatus(relativeFingerprintPath) {
158
+ var statusPath = toSafeAbsolute(FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'reports', REPAIR_STATUS_FILE));
159
+ var raw = await fs.readFile(statusPath, 'utf8').catch(function() { return ''; });
160
+ if (!raw) {
161
+ return null;
162
+ }
163
+ try {
164
+ return JSON.parse(raw);
165
+ } catch (error) {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ async function writeRepairStatus(relativeReportPath, payload) {
171
+ var reportsRelativeDir = path.dirname(relativeReportPath);
172
+ var statusPath = toSafeAbsolute(FINGERPRINTS_ROOT, path.join(reportsRelativeDir, REPAIR_STATUS_FILE));
173
+ await fs.writeFile(statusPath, JSON.stringify(payload, null, 2), 'utf8');
174
+ return path.relative(FINGERPRINTS_ROOT, statusPath);
175
+ }
176
+
177
+ function toSafeAbsolute(rootDir, relativePath) {
178
+ var safeRelative = String(relativePath || '').replace(/[\\/]+/g, path.sep);
179
+ var targetPath = path.resolve(rootDir, safeRelative);
180
+ if (targetPath !== rootDir && !targetPath.startsWith(rootDir + path.sep)) {
181
+ throw new Error('Invalid path');
182
+ }
183
+ return targetPath;
184
+ }
185
+
186
+ async function pathExists(targetPath) {
187
+ try {
188
+ await fs.access(targetPath);
189
+ return true;
190
+ } catch (error) {
191
+ return false;
192
+ }
193
+ }
194
+
195
+ function compareBatchNameDescending(a, b) {
196
+ return b.name.localeCompare(a.name);
197
+ }
198
+
199
+ function compareFingerprintByFetchedAtDescending(a, b) {
200
+ var timeDiff = Number(b.fetchedAtTs || 0) - Number(a.fetchedAtTs || 0);
201
+ if (timeDiff !== 0) {
202
+ return timeDiff;
203
+ }
204
+ return compareBatchNameDescending(a, b);
205
+ }
206
+
207
+ async function listImmediateDirectories(rootDir, relativeDir) {
208
+ var absoluteDir = toSafeAbsolute(rootDir, relativeDir || '');
209
+ if (!(await pathExists(absoluteDir))) {
210
+ return [];
211
+ }
212
+
213
+ var entries = await fs.readdir(absoluteDir, { withFileTypes: true });
214
+ return entries
215
+ .filter(function(entry) { return entry.isDirectory(); })
216
+ .map(function(entry) {
217
+ return {
218
+ name: entry.name,
219
+ path: relativeDir ? path.join(relativeDir, entry.name) : entry.name,
220
+ };
221
+ });
222
+ }
223
+
224
+ async function listImmediateFiles(rootDir, relativeDir) {
225
+ var absoluteDir = toSafeAbsolute(rootDir, relativeDir || '');
226
+ if (!(await pathExists(absoluteDir))) {
227
+ return [];
228
+ }
229
+
230
+ var entries = await fs.readdir(absoluteDir, { withFileTypes: true });
231
+ return entries
232
+ .filter(function(entry) { return entry.isFile(); })
233
+ .map(function(entry) {
234
+ var childRelative = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
235
+ return {
236
+ name: entry.name,
237
+ path: childRelative,
238
+ ext: path.extname(entry.name).toLowerCase(),
239
+ };
240
+ })
241
+ .sort(function(a, b) { return a.name.localeCompare(b.name); });
242
+ }
243
+
244
+ async function readFingerprintFetchedMeta(relativeFingerprintPath) {
245
+ var absolutePath = toSafeAbsolute(FINGERPRINTS_ROOT, relativeFingerprintPath);
246
+ var stats = await fs.stat(absolutePath);
247
+ var fetchedAtDate = stats.birthtimeMs ? new Date(stats.birthtimeMs) : (stats.ctimeMs ? new Date(stats.ctimeMs) : new Date(stats.mtimeMs));
248
+ return {
249
+ fetchedAt: fetchedAtDate.toISOString(),
250
+ fetchedAtTs: fetchedAtDate.getTime(),
251
+ };
252
+ }
253
+
254
+ async function listTopLevelMergedDirectories() {
255
+ var fingerprintDirs = await listImmediateDirectories(FINGERPRINTS_ROOT, '');
256
+ var items = await Promise.all(fingerprintDirs.map(async function(item) {
257
+ var logDirs = await listImmediateDirectories(FINGERPRINTS_ROOT, path.join(item.path, 'logs'));
258
+ var reportFiles = await listImmediateFiles(FINGERPRINTS_ROOT, path.join(item.path, 'reports'));
259
+ var repairStatus = await readRepairStatus(item.path);
260
+ var fetchedMeta = await readFingerprintFetchedMeta(item.path);
261
+ var hasReports = reportFiles.length > 0;
262
+ var hasCompletedRepair = !!(repairStatus && repairStatus.completed);
263
+ return {
264
+ name: item.name,
265
+ path: item.path,
266
+ hasLogs: logDirs.length > 0,
267
+ hasReports: hasReports,
268
+ hasCompletedRepair: hasCompletedRepair,
269
+ status: buildFingerprintStatus(hasReports, hasCompletedRepair),
270
+ fetchedAt: fetchedMeta.fetchedAt,
271
+ fetchedAtTs: fetchedMeta.fetchedAtTs,
272
+ };
273
+ }));
274
+
275
+ return items.sort(compareFingerprintByFetchedAtDescending);
276
+ }
277
+
278
+ function buildLoginUrl(queryUrl) {
279
+ var url = new URL(queryUrl || DEFAULT_QUERY_URL);
280
+ url.pathname = '/api/login';
281
+ url.search = '';
282
+ return url.toString();
283
+ }
284
+
285
+ async function loginBacktraceSession(queryUrl) {
286
+ if (!BACKTRACE_USERNAME || !BACKTRACE_PASSWORD) {
287
+ throw new Error('BACKTRACE_USERNAME and BACKTRACE_PASSWORD are required');
288
+ }
289
+
290
+ var body = new URLSearchParams();
291
+ body.set('username', BACKTRACE_USERNAME);
292
+ body.set('password', BACKTRACE_PASSWORD);
293
+
294
+ var response = await fetch(buildLoginUrl(queryUrl), {
295
+ method: 'POST',
296
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
297
+ body: body.toString(),
298
+ });
299
+
300
+ var payload = await response.json().catch(function() { return null; });
301
+ if (!response.ok || !payload || !payload.token) {
302
+ throw new Error('Backtrace login failed');
303
+ }
304
+
305
+ return payload.token;
306
+ }
307
+
308
+ async function requestBacktraceQuery(queryUrl, sessionToken, body) {
309
+ var response = await fetch(queryUrl, {
310
+ method: 'POST',
311
+ headers: {
312
+ 'content-type': 'application/json',
313
+ Cookie: 'token=' + sessionToken,
314
+ },
315
+ body: JSON.stringify(body),
316
+ });
317
+
318
+ var payload = await response.json().catch(function() { return null; });
319
+ if (!response.ok || !payload) {
320
+ throw new Error('Backtrace query request failed');
321
+ }
322
+
323
+ return payload;
324
+ }
325
+
326
+ function toHexObjectId(value) {
327
+ if (typeof value === 'string' && /^[0-9a-f]+$/i.test(value)) {
328
+ return value.toLowerCase();
329
+ }
330
+
331
+ var numeric = Number(value);
332
+ if (!Number.isFinite(numeric)) {
333
+ return String(value);
334
+ }
335
+
336
+ return numeric.toString(16);
337
+ }
338
+
339
+ function extractGroupFingerprints(payload) {
340
+ var values = payload && payload.response && Array.isArray(payload.response.values) ? payload.response.values : [];
341
+ return values.map(function(entry) {
342
+ return {
343
+ fingerprint: Array.isArray(entry) ? entry[0] : '',
344
+ count: Array.isArray(entry) ? entry[entry.length - 1] : 0,
345
+ };
346
+ }).filter(function(item) { return item.fingerprint; });
347
+ }
348
+
349
+ function extractObjectIds(objects) {
350
+ if (!Array.isArray(objects) || objects.length === 0) {
351
+ return [];
352
+ }
353
+
354
+ var firstGroup = objects[0];
355
+ var rows = Array.isArray(firstGroup) ? firstGroup[1] : [];
356
+ if (!Array.isArray(rows)) {
357
+ return [];
358
+ }
359
+
360
+ return rows
361
+ .map(function(row) { return Array.isArray(row) ? row[0] : null; })
362
+ .filter(function(value) { return value !== null && value !== undefined; });
363
+ }
364
+
365
+ async function readFilePreview(rootDir, relativePath) {
366
+ var targetPath = toSafeAbsolute(rootDir, relativePath);
367
+ var content = await fs.readFile(targetPath, 'utf8');
368
+ return {
369
+ path: relativePath,
370
+ absolutePath: targetPath,
371
+ content: content,
372
+ };
373
+ }
374
+
375
+ async function getCodex(proxyUrl) {
376
+ var mod = await import('@openai/codex-sdk');
377
+ var env = Object.assign({}, process.env, {
378
+ HTTP_PROXY: proxyUrl,
379
+ HTTPS_PROXY: proxyUrl,
380
+ ALL_PROXY: proxyUrl,
381
+ });
382
+ return new mod.Codex({ env: env });
383
+ }
384
+
385
+ function resolveCodexProxy(proxyValue) {
386
+ return String(proxyValue || process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.ALL_PROXY || DEFAULT_PROXY || '').trim();
387
+ }
388
+
389
+ async function withHeartbeat(scope, stage, context, task) {
390
+ var startedAt = Date.now();
391
+ console.log(scope, stage + ' started', context);
392
+ var timer = setInterval(function() {
393
+ console.log(scope, stage + ' waiting', Object.assign({}, context, { elapsedMs: Date.now() - startedAt }));
394
+ }, 5000);
395
+ if (typeof timer.unref === 'function') {
396
+ timer.unref();
397
+ }
398
+ try {
399
+ var result = await task();
400
+ console.log(scope, stage + ' completed', Object.assign({}, context, { elapsedMs: Date.now() - startedAt }));
401
+ return result;
402
+ } catch (error) {
403
+ console.error(scope, stage + ' failed', Object.assign({}, context, {
404
+ elapsedMs: Date.now() - startedAt,
405
+ error: error instanceof Error ? error.message : String(error),
406
+ }));
407
+ throw error;
408
+ } finally {
409
+ clearInterval(timer);
410
+ }
411
+ }
412
+
413
+ function truncateConsoleText(value, maxLength) {
414
+ var text = String(value || '').replace(/\r\n/g, '\n');
415
+ if (text.length <= maxLength) {
416
+ return text;
417
+ }
418
+ return text.slice(0, maxLength) + '\n...[truncated]';
419
+ }
420
+
421
+ function extractCodexEventText(event) {
422
+ if (!event || typeof event !== 'object') {
423
+ return '';
424
+ }
425
+ if (typeof event.text === 'string' && event.text.trim()) {
426
+ return event.text;
427
+ }
428
+ if (typeof event.delta === 'string' && event.delta.trim()) {
429
+ return event.delta;
430
+ }
431
+ if (event.item && typeof event.item.text === 'string' && event.item.text.trim()) {
432
+ return event.item.text;
433
+ }
434
+ if (event.item && typeof event.item.delta === 'string' && event.item.delta.trim()) {
435
+ return event.item.delta;
436
+ }
437
+ if (event.message && typeof event.message === 'string' && event.message.trim()) {
438
+ return event.message;
439
+ }
440
+ return '';
441
+ }
442
+
443
+ function buildCodexEventMeta(event) {
444
+ if (!event || typeof event !== 'object') {
445
+ return {};
446
+ }
447
+ var meta = {};
448
+ if (event.type) {
449
+ meta.type = event.type;
450
+ }
451
+ if (event.thread_id) {
452
+ meta.threadId = event.thread_id;
453
+ }
454
+ if (event.item && event.item.type) {
455
+ meta.itemType = event.item.type;
456
+ }
457
+ if (event.item && event.item.id) {
458
+ meta.itemId = event.item.id;
459
+ }
460
+ if (event.usage) {
461
+ meta.usage = event.usage;
462
+ }
463
+ if (event.error && event.error.message) {
464
+ meta.error = event.error.message;
465
+ }
466
+ return meta;
467
+ }
468
+
469
+ function logCodexEvent(scope, stage, context, event) {
470
+ var meta = Object.assign({}, context, buildCodexEventMeta(event));
471
+ console.log(scope, stage + ' event', meta);
472
+ var text = extractCodexEventText(event);
473
+ if (text) {
474
+ console.log(scope, stage + ' output:\n' + truncateConsoleText(text, 4000));
475
+ }
476
+ }
477
+
478
+ async function runCodexTurnWithStreaming(thread, prompt, scope, stage, context) {
479
+ var startedAt = Date.now();
480
+ var lastEventAt = startedAt;
481
+ var finalResponse = '';
482
+ var usage = null;
483
+ console.log(scope, stage + ' started', context);
484
+ var timer = setInterval(function() {
485
+ console.log(scope, stage + ' waiting', Object.assign({}, context, {
486
+ elapsedMs: Date.now() - startedAt,
487
+ idleMs: Date.now() - lastEventAt,
488
+ }));
489
+ }, 5000);
490
+ if (typeof timer.unref === 'function') {
491
+ timer.unref();
492
+ }
493
+ try {
494
+ var stream = await thread.runStreamed(prompt);
495
+ for await (var event of stream.events) {
496
+ lastEventAt = Date.now();
497
+ logCodexEvent(scope, stage, context, event);
498
+ if (event.type === 'item.completed' && event.item && event.item.type === 'agent_message') {
499
+ finalResponse = event.item.text || finalResponse;
500
+ } else if (event.type === 'turn.completed') {
501
+ usage = event.usage || null;
502
+ } else if (event.type === 'turn.failed') {
503
+ throw new Error(event.error && event.error.message ? event.error.message : 'Codex turn failed');
504
+ }
505
+ }
506
+ console.log(scope, stage + ' completed', Object.assign({}, context, {
507
+ elapsedMs: Date.now() - startedAt,
508
+ usage: usage,
509
+ responseLength: finalResponse.length,
510
+ }));
511
+ return {
512
+ finalResponse: finalResponse,
513
+ usage: usage,
514
+ };
515
+ } catch (error) {
516
+ console.error(scope, stage + ' failed', Object.assign({}, context, {
517
+ elapsedMs: Date.now() - startedAt,
518
+ error: error instanceof Error ? error.message : String(error),
519
+ }));
520
+ throw error;
521
+ } finally {
522
+ clearInterval(timer);
523
+ }
524
+ }
525
+
526
+ async function runRepairPlan(reportText, reportPath, options) {
527
+ var proxy = resolveCodexProxy(options.proxy);
528
+ var logScope = '[backtrace-route][fix-plan]';
529
+ console.log(logScope, 'creating codex client for generate', { reportPath: reportPath, proxy: proxy });
530
+ var codex = await withHeartbeat(logScope, 'generate:create-client', { reportPath: reportPath, proxy: proxy }, function() {
531
+ return getCodex(proxy);
532
+ });
533
+ console.log(logScope, 'codex client ready for generate', { reportPath: reportPath, proxy: proxy });
534
+ var thread = codex.startThread({
535
+ workingDirectory: options.workdir,
536
+ skipGitRepoCheck: true,
537
+ sandboxMode: 'read-only',
538
+ approvalPolicy: 'never',
539
+ modelReasoningEffort: 'high',
540
+ });
541
+
542
+ var prompt = [
543
+ '你是一名资深工程师,现在处于计划模式,只输出修复方案,不执行任何代码修改。',
544
+ '请根据下面这份崩溃分析报告,生成一个明确的修复方案。',
545
+ '输出内容请包含以下标题:',
546
+ '1. 修复目标',
547
+ '2. 需要修改的文件',
548
+ '3. 每个文件的具体修改方案',
549
+ '4. 风险与回归点',
550
+ '5. 验证方法',
551
+ '如果需要修改多个文件,请直接列出文件路径。',
552
+ '对于每个需要修改的文件,必须明确写出:文件路径、文件名、预计修改的起止行号或相邻定位点、变更类型(新增/删除/修改)。',
553
+ '对于每个变更点,必须给出具体代码内容,分别写明原内容和新内容;如果是新增内容,直接给出新增代码;如果是删除内容,直接给出删除代码。',
554
+ '如果无法精确确认行号,至少给出函数名、类名、代码片段等可定位信息,并说明是插入到哪里。',
555
+ '请尽量使用代码块展示代码内容,代码块前先写清楚文件路径、行号范围和变更类型。',
556
+ '不要执行修改,也不要省略文件路径、文件名、行号信息和具体代码内容。',
557
+ '',
558
+ '报告路径:' + reportPath,
559
+ '',
560
+ reportText,
561
+ ].join('\n');
562
+
563
+ console.log(logScope, 'starting thread.run for generate', { reportPath: reportPath, threadId: thread.id });
564
+ var turn = await runCodexTurnWithStreaming(thread, prompt, logScope, 'generate:run-prompt', { reportPath: reportPath, threadId: thread.id });
565
+ console.log(logScope, 'thread.run finished for generate', { reportPath: reportPath, threadId: thread.id, responseLength: (turn.finalResponse || '').length });
566
+ return {
567
+ threadId: thread.id,
568
+ planText: turn.finalResponse || 'Codex 未返回修复方案。',
569
+ };
570
+ }
571
+
572
+ async function applyRepairPlan(reportText, planText, reportPath, options) {
573
+ var proxy = resolveCodexProxy(options.proxy);
574
+ var logScope = '[backtrace-route][fix-plan]';
575
+ console.log(logScope, 'creating codex client for apply', { reportPath: reportPath, proxy: proxy });
576
+ var codex = await withHeartbeat(logScope, 'apply:create-client', { reportPath: reportPath, proxy: proxy }, function() {
577
+ return getCodex(proxy);
578
+ });
579
+ console.log(logScope, 'codex client ready for apply', { reportPath: reportPath, proxy: proxy });
580
+ var thread = codex.startThread({
581
+ workingDirectory: options.workdir,
582
+ skipGitRepoCheck: true,
583
+ sandboxMode: 'workspace-write',
584
+ approvalPolicy: 'never',
585
+ modelReasoningEffort: 'high',
586
+ });
587
+
588
+ var prompt = [
589
+ '你是一名资深工程师,现在根据下面已经确认的修复方案,直接在工作目录中实施代码修改。',
590
+ '请实际修改文件,不要只给建议。',
591
+ '修改完成后,说明你改了哪些文件、每个文件做了什么修改,以及你进行了哪些验证。',
592
+ '最后请单独列出所有实际修改过的源码文件路径,每行一个,尽量使用相对工作目录的路径。',
593
+ '',
594
+ '分析报告路径:' + reportPath,
595
+ '',
596
+ '原始分析报告:',
597
+ reportText,
598
+ '',
599
+ '已确认的修复方案:',
600
+ planText,
601
+ ].join('\n');
602
+
603
+ console.log(logScope, 'starting thread.run for apply', { reportPath: reportPath, threadId: thread.id });
604
+ var turn = await runCodexTurnWithStreaming(thread, prompt, logScope, 'apply:run-prompt', { reportPath: reportPath, threadId: thread.id });
605
+ console.log(logScope, 'thread.run finished for apply', { reportPath: reportPath, threadId: thread.id, responseLength: (turn.finalResponse || '').length });
606
+ return {
607
+ threadId: thread.id,
608
+ resultText: turn.finalResponse || 'Codex 未返回应用结果。',
609
+ };
610
+ }
611
+
612
+ router.post('/query/fingerprints', async function(req, res) {
613
+ var body = req.body || {};
614
+ var queryUrl = body.queryUrl || DEFAULT_QUERY_URL;
615
+ var from = String(body.from || '1');
616
+ var to = String(body.to || Math.floor(Date.now() / 1000));
617
+ var offset = Number(body.offset || 0);
618
+ var limit = Number(body.limit || 10);
619
+
620
+ try {
621
+ var token = await loginBacktraceSession(queryUrl);
622
+ var queryBody = {
623
+ group: ['fingerprint'],
624
+ offset: offset,
625
+ limit: limit,
626
+ filter: [
627
+ {
628
+ timestamp: [
629
+ ['at-most', String(to)],
630
+ ['at-least', String(from)],
631
+ ],
632
+ },
633
+ ],
634
+ };
635
+
636
+ var payload = await requestBacktraceQuery(queryUrl, token, queryBody);
637
+ return res.json({
638
+ ok: true,
639
+ totalRows: payload && payload._ && payload._.runtime && payload._.runtime.filter ? payload._.runtime.filter.rows : 0,
640
+ totalGroups: payload && payload.response && payload.response.cardinalities && payload.response.cardinalities.pagination ? payload.response.cardinalities.pagination.groups : 0,
641
+ fingerprints: extractGroupFingerprints(payload),
642
+ raw: payload,
643
+ });
644
+ } catch (error) {
645
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
646
+ }
647
+ });
648
+
649
+ router.post('/query/objects', async function(req, res) {
650
+ var body = req.body || {};
651
+ var queryUrl = body.queryUrl || DEFAULT_QUERY_URL;
652
+ var fingerprint = String(body.fingerprint || '').trim();
653
+ var from = String(body.from || '1');
654
+ var to = String(body.to || Math.floor(Date.now() / 1000));
655
+ var offset = Number(body.offset || 0);
656
+ var limit = Number(body.limit || 10);
657
+
658
+ if (!fingerprint) {
659
+ return res.status(400).json({ ok: false, error: 'fingerprint is required' });
660
+ }
661
+
662
+ try {
663
+ var token = await loginBacktraceSession(queryUrl);
664
+ var queryBody = {
665
+ select: ['fingerprint'],
666
+ offset: offset,
667
+ limit: limit,
668
+ filter: [
669
+ {
670
+ fingerprint: [
671
+ ['contains', fingerprint],
672
+ ],
673
+ timestamp: [
674
+ ['at-most', String(to)],
675
+ ['at-least', String(from)],
676
+ ],
677
+ },
678
+ ],
679
+ };
680
+
681
+ var payload = await requestBacktraceQuery(queryUrl, token, queryBody);
682
+ var decimalIds = extractObjectIds(payload && payload.response ? payload.response.objects : []);
683
+ return res.json({
684
+ ok: true,
685
+ totalRows: payload && payload._ && payload._.runtime && payload._.runtime.filter ? payload._.runtime.filter.rows : 0,
686
+ fingerprint: fingerprint,
687
+ objectIds: decimalIds.map(function(value) {
688
+ return { decimal: value, hex: toHexObjectId(value) };
689
+ }),
690
+ raw: payload,
691
+ });
692
+ } catch (error) {
693
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
694
+ }
695
+ });
696
+
697
+ router.get('/files/index', async function(req, res) {
698
+ try {
699
+ var directories = await listTopLevelMergedDirectories();
700
+ return res.json({ ok: true, directories: directories });
701
+ } catch (error) {
702
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
703
+ }
704
+ });
705
+
706
+ router.get('/files/list', async function(req, res) {
707
+ var topPath = String(req.query.path || '');
708
+ var logDir = String(req.query.logDir || '');
709
+
710
+ try {
711
+ var logDirectories = (await listImmediateDirectories(FINGERPRINTS_ROOT, path.join(topPath, 'logs'))).sort(function(a, b) { return a.name.localeCompare(b.name); });
712
+ var selectedLogDir = logDir || (logDirectories[0] ? logDirectories[0].name : '');
713
+ var logFiles = selectedLogDir ? await listImmediateFiles(FINGERPRINTS_ROOT, path.join(topPath, 'logs', selectedLogDir)) : [];
714
+ var reportFiles = await listImmediateFiles(FINGERPRINTS_ROOT, path.join(topPath, 'reports'));
715
+ var repairStatus = await readRepairStatus(topPath);
716
+ var hasCompletedRepair = !!(repairStatus && repairStatus.completed);
717
+ return res.json({
718
+ ok: true,
719
+ path: topPath,
720
+ logDirectories: logDirectories,
721
+ selectedLogDir: selectedLogDir,
722
+ logFiles: logFiles,
723
+ reportFiles: reportFiles,
724
+ reportStatus: buildFingerprintStatus(reportFiles.length > 0, hasCompletedRepair),
725
+ hasCompletedRepair: hasCompletedRepair,
726
+ });
727
+ } catch (error) {
728
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
729
+ }
730
+ });
731
+
732
+ router.get('/files/content', async function(req, res) {
733
+ var kind = req.query.kind;
734
+ var relativePath = req.query.path;
735
+ var rootDir = kind === 'report' || kind === 'logs' ? FINGERPRINTS_ROOT : '';
736
+
737
+ if (!rootDir || !relativePath) {
738
+ return res.status(400).json({ ok: false, error: 'kind and path are required' });
739
+ }
740
+
741
+ try {
742
+ var file = await readFilePreview(rootDir, relativePath);
743
+ return res.json({ ok: true, kind: kind, file: file });
744
+ } catch (error) {
745
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
746
+ }
747
+ });
748
+
749
+ router.get('/files/download', async function(req, res) {
750
+ var kind = req.query.kind;
751
+ var relativePath = req.query.path;
752
+ var rootDir = kind === 'report' || kind === 'logs' ? FINGERPRINTS_ROOT : '';
753
+
754
+ if (!rootDir || !relativePath) {
755
+ return res.status(400).json({ ok: false, error: 'kind and path are required' });
756
+ }
757
+
758
+ try {
759
+ var targetPath = toSafeAbsolute(rootDir, relativePath);
760
+ return res.download(targetPath, path.basename(targetPath));
761
+ } catch (error) {
762
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
763
+ }
764
+ });
765
+
766
+ router.post('/fix-plan/generate', async function(req, res) {
767
+ var body = req.body || {};
768
+ var reportPath = body.reportPath;
769
+ console.log('[backtrace-route][fix-plan] generate requested', { reportPath: reportPath });
770
+ if (!reportPath) {
771
+ return res.status(400).json({ ok: false, error: 'reportPath is required' });
772
+ }
773
+
774
+ try {
775
+ console.log('[backtrace-route][fix-plan] starting codex plan generation', {
776
+ reportPath: reportPath,
777
+ proxy: resolveCodexProxyShared(body.proxy),
778
+ });
779
+ var result = await generateRepairPlanShared({
780
+ reportPath: reportPath,
781
+ }, {
782
+ rootDir: ROOT_DIR,
783
+ fingerprintsRoot: FINGERPRINTS_ROOT,
784
+ workdir: body.workdir || DEFAULT_WORKDIR,
785
+ proxy: resolveCodexProxyShared(body.proxy),
786
+ });
787
+ console.log('[backtrace-route][fix-plan] codex plan generation completed', { reportPath: reportPath, repairVersion: result.repairVersion, threadId: result.threadId, repairPlanPath: result.repairPlanPath });
788
+ return res.json(result);
789
+ } catch (error) {
790
+ console.error('[backtrace-route][fix-plan] generate failed', { reportPath: reportPath, error: error instanceof Error ? error.message : String(error) });
791
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
792
+ }
793
+ });
794
+
795
+ router.post('/fix-plan/apply', async function(req, res) {
796
+ var body = req.body || {};
797
+ var reportPath = body.reportPath;
798
+ var planText = body.planText;
799
+ if (!reportPath || !planText) {
800
+ return res.status(400).json({ ok: false, error: 'reportPath and planText are required' });
801
+ }
802
+
803
+ try {
804
+ console.log('[backtrace-route][fix-plan] starting codex apply generation', {
805
+ reportPath: reportPath,
806
+ repairVersion: body.repairVersion,
807
+ proxy: resolveCodexProxyShared(body.proxy),
808
+ });
809
+ var result = await applyRepairPlanShared({
810
+ reportPath: reportPath,
811
+ planText: planText,
812
+ repairVersion: body.repairVersion,
813
+ repairPlanPath: body.repairPlanPath,
814
+ }, {
815
+ rootDir: ROOT_DIR,
816
+ fingerprintsRoot: FINGERPRINTS_ROOT,
817
+ workdir: body.workdir || DEFAULT_WORKDIR,
818
+ proxy: resolveCodexProxyShared(body.proxy),
819
+ });
820
+ return res.json(result);
821
+ } catch (error) {
822
+ return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
823
+ }
824
+ });
825
+
826
+ router.post('/run', async function(req, res) {
827
+ var body = req.body || {};
828
+ var command = body.command || 'collect-all';
829
+ var tool = new BacktraceCodexTool();
830
+
831
+ try {
832
+ var result = await tool.run(command, body);
833
+ return res.json({ ok: true, command: command, result: result });
834
+ } catch (error) {
835
+ return res.status(500).json({ ok: false, command: command, error: error instanceof Error ? error.message : String(error) });
836
+ }
837
+ });
838
+
839
+ router.get('/list', async function(req, res) {
840
+ var tool = new BacktraceCodexTool();
841
+ var overrides = {
842
+ from: req.query.from,
843
+ to: req.query.to,
844
+ limit: req.query.limit,
845
+ offset: req.query.offset,
846
+ select: req.query.select,
847
+ };
848
+
849
+ try {
850
+ var result = await tool.list(overrides);
851
+ return res.json({ ok: true, command: 'list', result: result });
852
+ } catch (error) {
853
+ return res.status(500).json({ ok: false, command: 'list', error: error instanceof Error ? error.message : String(error) });
854
+ }
855
+ });
856
+
857
+ module.exports = router;
858
+
859
+
860
+
861
+
862
+
863
+
864
+