backtrace-console 0.0.2 → 0.0.3
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/README.md +26 -9
- package/bin/backtrace-server.js +30 -26
- package/package.json +5 -10
- package/app.js +0 -23
- package/bin/backtrace-cli.js +0 -22
- package/bin/www +0 -93
- package/lib/BacktraceCodexTool.js +0 -32
- package/lib/backtrace/analysis.js +0 -356
- package/lib/backtrace/constants.js +0 -27
- package/lib/backtrace/options.js +0 -278
- package/lib/backtrace/query-download.js +0 -117
- package/lib/backtrace/query-session.js +0 -229
- package/lib/backtrace/query.js +0 -506
- package/lib/backtrace/repair-fingerprint.js +0 -405
- package/lib/backtrace/repair.js +0 -530
- package/lib/backtrace/tool.js +0 -364
- package/lib/backtrace/utils.js +0 -297
- package/lib/cli/args.js +0 -177
- package/lib/cli/run.js +0 -191
- package/lib/feishu.js +0 -66
- package/lib/scheduler.js +0 -126
- package/public/chat-components.css +0 -569
- package/public/chat-core.js +0 -635
- package/public/chat-layout.css +0 -290
- package/public/chat-render.js +0 -308
- package/public/chat-send.js +0 -230
- package/public/chat.html +0 -69
- package/public/index-page.js +0 -504
- package/public/index.html +0 -138
- package/public/stylesheets/style.css +0 -186
- package/routes/backtrace-chat.js +0 -389
- package/routes/backtrace-files.js +0 -88
- package/routes/backtrace-fix-plan.js +0 -53
- package/routes/backtrace-run.js +0 -128
- package/routes/backtrace-shared.js +0 -202
- package/routes/backtrace.js +0 -10
- package/routes/index.js +0 -9
- package/routes/users.js +0 -9
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--panel: rgba(255, 252, 248, 0.86);
|
|
3
|
-
--ink: #1f2430;
|
|
4
|
-
--muted: #6d7482;
|
|
5
|
-
--line: rgba(31, 36, 48, 0.1);
|
|
6
|
-
--accent: #bb5a2c;
|
|
7
|
-
--accent-strong: #8f3a14;
|
|
8
|
-
--accent-soft: rgba(187, 90, 44, 0.12);
|
|
9
|
-
--shadow: 0 24px 70px rgba(35, 31, 24, 0.14);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
* { box-sizing: border-box; }
|
|
13
|
-
html, body { margin: 0; min-height: 100%; }
|
|
14
|
-
body {
|
|
15
|
-
font-family: "Noto Sans SC", sans-serif;
|
|
16
|
-
color: var(--ink);
|
|
17
|
-
background: radial-gradient(circle at top left, rgba(255,255,255,0.9), transparent 24%), radial-gradient(circle at bottom right, rgba(187,90,44,0.15), transparent 22%), linear-gradient(135deg, #efe3d2 0%, #f6f1ea 48%, #e7ddd2 100%);
|
|
18
|
-
}
|
|
19
|
-
body::before {
|
|
20
|
-
content: "";
|
|
21
|
-
position: fixed;
|
|
22
|
-
inset: 0;
|
|
23
|
-
pointer-events: none;
|
|
24
|
-
background-image: linear-gradient(rgba(31, 36, 48, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(31, 36, 48, 0.03) 1px, transparent 1px);
|
|
25
|
-
background-size: 24px 24px;
|
|
26
|
-
mask-image: linear-gradient(to bottom, rgba(0,0,0,0.75), transparent);
|
|
27
|
-
}
|
|
28
|
-
code, pre { font-family: Consolas, Monaco, monospace; }
|
|
29
|
-
.app-shell { width: min(1480px, calc(100% - 28px)); margin: 0 auto; padding: 22px 0 28px; }
|
|
30
|
-
.topbar {
|
|
31
|
-
display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; margin-bottom: 18px; padding: 28px 30px;
|
|
32
|
-
border: 1px solid rgba(255,255,255,0.42); border-radius: 30px; background: linear-gradient(145deg, rgba(18, 28, 43, 0.94), rgba(44, 61, 81, 0.9)); color: #fbf3ea; box-shadow: var(--shadow);
|
|
33
|
-
}
|
|
34
|
-
.eyebrow, .panel-kicker { margin: 0 0 10px; font-size: 12px; letter-spacing: 0.26em; text-transform: uppercase; opacity: 0.72; }
|
|
35
|
-
.topbar h1, .panel-head h2, .status-chip strong, .tab-button { font-family: "Space Grotesk", sans-serif; }
|
|
36
|
-
.topbar h1 { margin: 0; font-size: clamp(34px, 4vw, 54px); line-height: 0.94; }
|
|
37
|
-
.subcopy { margin: 16px 0 0; max-width: 72ch; line-height: 1.7; color: rgba(251, 243, 234, 0.78); }
|
|
38
|
-
.toolbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
|
39
|
-
.toolbar-field {
|
|
40
|
-
display: flex;
|
|
41
|
-
flex-direction: column;
|
|
42
|
-
gap: 6px;
|
|
43
|
-
min-width: 220px;
|
|
44
|
-
color: rgba(251, 243, 234, 0.82);
|
|
45
|
-
}
|
|
46
|
-
.toolbar-field span {
|
|
47
|
-
font-size: 12px;
|
|
48
|
-
letter-spacing: 0.12em;
|
|
49
|
-
text-transform: uppercase;
|
|
50
|
-
}
|
|
51
|
-
.toolbar-input {
|
|
52
|
-
width: 100%;
|
|
53
|
-
min-height: 42px;
|
|
54
|
-
padding: 10px 14px;
|
|
55
|
-
border: 1px solid rgba(255,255,255,0.18);
|
|
56
|
-
border-radius: 14px;
|
|
57
|
-
background: rgba(255,255,255,0.08);
|
|
58
|
-
color: #fbf3ea;
|
|
59
|
-
font: inherit;
|
|
60
|
-
}
|
|
61
|
-
.toolbar-input::placeholder {
|
|
62
|
-
color: rgba(251, 243, 234, 0.5);
|
|
63
|
-
}
|
|
64
|
-
.toolbar-input:disabled {
|
|
65
|
-
opacity: 0.6;
|
|
66
|
-
cursor: not-allowed;
|
|
67
|
-
}
|
|
68
|
-
.action-button {
|
|
69
|
-
appearance: none; border: none; border-radius: 999px; padding: 13px 20px; font: inherit; font-weight: 700; color: #fff8f2;
|
|
70
|
-
background: linear-gradient(135deg, var(--accent), var(--accent-strong)); cursor: pointer; box-shadow: 0 14px 30px rgba(143, 58, 20, 0.24);
|
|
71
|
-
}
|
|
72
|
-
.secondary-action { padding: 11px 16px; background: rgba(31, 36, 48, 0.08); color: var(--ink); box-shadow: none; }
|
|
73
|
-
.secondary-action:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
74
|
-
.status-chip { display: flex; flex-direction: column; gap: 4px; min-width: 160px; padding: 12px 16px; border-radius: 18px; background: rgba(255,255,255,0.12); }
|
|
75
|
-
.status-chip span { font-size: 12px; text-transform: uppercase; letter-spacing: 0.16em; opacity: 0.72; }
|
|
76
|
-
#statusText[data-mode="success"] { color: #90ffd5; }
|
|
77
|
-
#statusText[data-mode="error"] { color: #ffb3b3; }
|
|
78
|
-
#statusText[data-mode="loading"] { color: #ffd89c; }
|
|
79
|
-
.browser-layout { display: grid; grid-template-columns: minmax(280px, 320px) minmax(0, 1fr); gap: 18px; min-height: calc(100vh - 170px); }
|
|
80
|
-
.panel-surface { border: 1px solid rgba(255,255,255,0.48); border-radius: 28px; background: var(--panel); backdrop-filter: blur(16px); box-shadow: var(--shadow); }
|
|
81
|
-
.browser-sidebar { padding: 18px 16px; overflow: hidden; display: flex; flex-direction: column; }
|
|
82
|
-
.browser-main { padding: 18px; display: flex; flex-direction: column; min-width: 0; gap: 16px; }
|
|
83
|
-
.panel-head { display: flex; justify-content: space-between; gap: 14px; align-items: center; }
|
|
84
|
-
.panel-head h2 { margin: 0; font-size: 28px; }
|
|
85
|
-
.sticky-head, .viewer-head, .file-panel-head { padding-bottom: 14px; border-bottom: 1px solid var(--line); }
|
|
86
|
-
.tree-root { margin-top: 16px; padding: 10px; border-radius: 20px; border: 1px solid var(--line); background: rgba(255,255,255,0.56); overflow: auto; max-height: calc(100vh - 290px); }
|
|
87
|
-
.dir-items, .file-items { display: grid; gap: 10px; }
|
|
88
|
-
.tree-directory-button, .log-directory-button, .file-row {
|
|
89
|
-
width: 100%; display: flex; align-items: center; gap: 10px; padding: 12px 14px; border: 1px solid transparent; border-radius: 16px;
|
|
90
|
-
background: rgba(255,255,255,0.62); text-align: left; font: inherit; color: var(--ink); cursor: pointer;
|
|
91
|
-
}
|
|
92
|
-
.tree-directory-button:hover, .tree-directory-button.is-active, .log-directory-button:hover, .log-directory-button.is-active, .file-row:hover, .file-row.is-active {
|
|
93
|
-
border-color: rgba(187, 90, 44, 0.18); background: rgba(187, 90, 44, 0.08);
|
|
94
|
-
}
|
|
95
|
-
.node-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; height: 24px; padding: 0 8px; border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; }
|
|
96
|
-
.node-badge.dir { background: rgba(31, 36, 48, 0.08); color: var(--muted); }
|
|
97
|
-
.node-badge.file { background: var(--accent-soft); color: var(--accent-strong); }
|
|
98
|
-
.file-row-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
99
|
-
.file-row-main strong, .file-row-main small { overflow: hidden; text-overflow: ellipsis; }
|
|
100
|
-
.file-row-main small { color: var(--muted); }
|
|
101
|
-
.empty-tree { padding: 14px; color: var(--muted); }
|
|
102
|
-
.viewer-side-actions { display: flex; align-items: center; gap: 12px; }
|
|
103
|
-
.viewer-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; color: var(--muted); text-align: right; }
|
|
104
|
-
.viewer-meta code { max-width: 460px; word-break: break-all; padding: 8px 10px; border-radius: 12px; background: rgba(31, 36, 48, 0.06); color: var(--ink); }
|
|
105
|
-
.tabs-shell { border: 1px solid var(--line); border-radius: 22px; background: rgba(255,255,255,0.56); overflow: hidden; }
|
|
106
|
-
.tabs-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px; border-bottom: 1px solid var(--line); background: rgba(255,255,255,0.5); }
|
|
107
|
-
.tabs-nav { display: flex; gap: 8px; }
|
|
108
|
-
.tab-button { appearance: none; border: none; border-radius: 999px; padding: 10px 16px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
109
|
-
.tab-button.is-active { background: rgba(187, 90, 44, 0.12); color: var(--accent-strong); }
|
|
110
|
-
.tab-panel { display: none; padding: 14px; }
|
|
111
|
-
.tab-panel.is-active { display: block; }
|
|
112
|
-
.logs-split { display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); gap: 14px; }
|
|
113
|
-
.subpanel { border: 1px solid var(--line); border-radius: 18px; background: rgba(255,255,255,0.52); overflow: hidden; }
|
|
114
|
-
.subpanel-head { padding: 12px 14px; border-bottom: 1px solid var(--line); font-weight: 700; color: var(--muted); }
|
|
115
|
-
.list-box { height: 320px; padding: 12px; overflow: auto; }
|
|
116
|
-
.report-panel .list-box { height: 320px; }
|
|
117
|
-
.viewer-body { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
118
|
-
.file-viewer {
|
|
119
|
-
flex: 0 0 420px;
|
|
120
|
-
height: 420px;
|
|
121
|
-
min-height: 420px;
|
|
122
|
-
margin: 0;
|
|
123
|
-
padding: 22px;
|
|
124
|
-
overflow: auto;
|
|
125
|
-
border-radius: 22px;
|
|
126
|
-
border: 1px solid rgba(18, 28, 43, 0.08);
|
|
127
|
-
background: linear-gradient(180deg, rgba(16, 24, 36, 0.96), rgba(28, 39, 56, 0.96));
|
|
128
|
-
color: #dbe8ff;
|
|
129
|
-
white-space: pre-wrap;
|
|
130
|
-
word-break: break-word;
|
|
131
|
-
line-height: 1.62;
|
|
132
|
-
}
|
|
133
|
-
@media (max-width: 1100px) {
|
|
134
|
-
.browser-layout { grid-template-columns: 1fr; }
|
|
135
|
-
.tree-root { max-height: 28vh; }
|
|
136
|
-
.logs-split { grid-template-columns: 1fr; }
|
|
137
|
-
}
|
|
138
|
-
@media (max-width: 720px) {
|
|
139
|
-
.app-shell { width: min(100% - 18px, 1480px); padding-top: 12px; }
|
|
140
|
-
.topbar { padding: 20px; border-radius: 24px; flex-direction: column; }
|
|
141
|
-
.toolbar, .viewer-meta, .viewer-side-actions, .tabs-header { width: 100%; align-items: stretch; text-align: left; }
|
|
142
|
-
.viewer-side-actions, .tabs-header { flex-direction: column; }
|
|
143
|
-
.browser-main, .browser-sidebar { padding: 14px; }
|
|
144
|
-
.file-viewer { padding: 16px; }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
#reportStatus {
|
|
149
|
-
color: var(--accent-strong);
|
|
150
|
-
font-family: "Space Grotesk", sans-serif;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.inner-panel {
|
|
154
|
-
padding: 18px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.fix-viewer {
|
|
158
|
-
flex: 0 0 320px;
|
|
159
|
-
height: 320px;
|
|
160
|
-
min-height: 320px;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.status-tag {
|
|
164
|
-
display: inline-flex;
|
|
165
|
-
align-items: center;
|
|
166
|
-
justify-content: center;
|
|
167
|
-
width: fit-content;
|
|
168
|
-
min-height: 22px;
|
|
169
|
-
padding: 0 10px;
|
|
170
|
-
border-radius: 999px;
|
|
171
|
-
font-size: 12px;
|
|
172
|
-
font-weight: 700;
|
|
173
|
-
letter-spacing: 0.02em;
|
|
174
|
-
}
|
|
175
|
-
.status-tag.empty {
|
|
176
|
-
background: rgba(120, 128, 145, 0.14);
|
|
177
|
-
color: #5f6878;
|
|
178
|
-
}
|
|
179
|
-
.status-tag.pending {
|
|
180
|
-
background: rgba(222, 144, 32, 0.16);
|
|
181
|
-
color: #9c5d00;
|
|
182
|
-
}
|
|
183
|
-
.status-tag.done {
|
|
184
|
-
background: rgba(42, 148, 92, 0.16);
|
|
185
|
-
color: #1f7a4d;
|
|
186
|
-
}
|
package/routes/backtrace-chat.js
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
var express = require('express');
|
|
2
|
-
var fs = require('node:fs/promises');
|
|
3
|
-
var path = require('node:path');
|
|
4
|
-
var os = require('node:os');
|
|
5
|
-
var shared = require('./backtrace-shared');
|
|
6
|
-
|
|
7
|
-
var router = express.Router();
|
|
8
|
-
|
|
9
|
-
async function getChatSessionIds(relativeFingerprintPath) {
|
|
10
|
-
var entries = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
11
|
-
return entries.filter(function(entry) { return entry.ext === '.json'; })
|
|
12
|
-
.map(function(entry) { return path.basename(entry.name, '.json'); })
|
|
13
|
-
.sort(function(a, b) { return b.localeCompare(a); });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function buildContextMessage(meta) {
|
|
17
|
-
var errorMessage = meta && meta.errorMessage ? meta.errorMessage : '-';
|
|
18
|
-
var classifiers = meta && meta.classifiers ? meta.classifiers : '-';
|
|
19
|
-
return '你是一个非常专业并资深的C++开发工程师和UE工程师。当前崩溃的关键信息如下:\n'
|
|
20
|
-
+ 'Error Message: ' + errorMessage + '\n'
|
|
21
|
-
+ 'Classifiers: ' + classifiers + '\n\n'
|
|
22
|
-
+ '请结合这些上下文,帮助用户定位问题并给出修复建议。';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function readChatSession(relativeFingerprintPath, sessionId) {
|
|
26
|
-
var relativePath = path.join(relativeFingerprintPath, 'messages', sessionId + '.json');
|
|
27
|
-
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, relativePath);
|
|
28
|
-
var raw = await fs.readFile(absolutePath, 'utf8').catch(function(error) {
|
|
29
|
-
if (error && error.code === 'ENOENT') return '';
|
|
30
|
-
throw error;
|
|
31
|
-
});
|
|
32
|
-
return raw ? JSON.parse(raw) : null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function writeChatSession(relativeFingerprintPath, session) {
|
|
36
|
-
var messagesDir = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages'));
|
|
37
|
-
var absolutePath = shared.toSafeAbsolute(shared.FINGERPRINTS_ROOT, path.join(relativeFingerprintPath, 'messages', session.sessionId + '.json'));
|
|
38
|
-
await fs.mkdir(messagesDir, { recursive: true });
|
|
39
|
-
await fs.writeFile(absolutePath, JSON.stringify(session, null, 2), 'utf8');
|
|
40
|
-
return absolutePath;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function listChatSessions(relativeFingerprintPath) {
|
|
44
|
-
var sessionIds = await getChatSessionIds(relativeFingerprintPath);
|
|
45
|
-
var sessions = [];
|
|
46
|
-
for (var i = 0; i < sessionIds.length; i += 1) {
|
|
47
|
-
var session = await readChatSession(relativeFingerprintPath, sessionIds[i]);
|
|
48
|
-
if (!session) continue;
|
|
49
|
-
sessions.push({
|
|
50
|
-
sessionId: session.sessionId,
|
|
51
|
-
threadId: session.threadId || null,
|
|
52
|
-
title: session.title || session.sessionId,
|
|
53
|
-
createdAt: session.createdAt || null,
|
|
54
|
-
updatedAt: session.updatedAt || session.createdAt || null,
|
|
55
|
-
messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return sessions;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function createChatSession(relativeFingerprintPath) {
|
|
62
|
-
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
63
|
-
if (!meta) throw new Error('fingerprint not found');
|
|
64
|
-
var now = new Date();
|
|
65
|
-
var sessionId = shared.formatChatSessionId(now);
|
|
66
|
-
var session = {
|
|
67
|
-
sessionId: sessionId,
|
|
68
|
-
fingerprint: relativeFingerprintPath,
|
|
69
|
-
threadId: null,
|
|
70
|
-
createdAt: now.toISOString(),
|
|
71
|
-
updatedAt: now.toISOString(),
|
|
72
|
-
title: '新对话 ' + sessionId,
|
|
73
|
-
messages: [{ kind: 'context', role: 'system', text: buildContextMessage(meta), createdAt: now.toISOString() }],
|
|
74
|
-
};
|
|
75
|
-
await writeChatSession(relativeFingerprintPath, session);
|
|
76
|
-
return session;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function appendChatSessionEvent(relativeFingerprintPath, sessionId, event) {
|
|
80
|
-
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
81
|
-
if (!session) throw new Error('chat session not found');
|
|
82
|
-
if (!Array.isArray(session.messages)) session.messages = [];
|
|
83
|
-
session.messages.push(event);
|
|
84
|
-
session.updatedAt = new Date().toISOString();
|
|
85
|
-
if ((!session.title || session.title.indexOf('新对话') === 0) && event.role === 'user' && event.text) {
|
|
86
|
-
session.title = String(event.text).slice(0, 80);
|
|
87
|
-
}
|
|
88
|
-
await writeChatSession(relativeFingerprintPath, session);
|
|
89
|
-
return session;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function updateChatSessionThread(relativeFingerprintPath, sessionId, threadId) {
|
|
93
|
-
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
94
|
-
if (!session) throw new Error('chat session not found');
|
|
95
|
-
session.threadId = typeof threadId === 'string' ? threadId : null;
|
|
96
|
-
session.updatedAt = new Date().toISOString();
|
|
97
|
-
await writeChatSession(relativeFingerprintPath, session);
|
|
98
|
-
return session;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text, extraMeta) {
|
|
102
|
-
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
103
|
-
if (!session) throw new Error('chat session not found');
|
|
104
|
-
if (!Array.isArray(session.messages)) session.messages = [];
|
|
105
|
-
var now = new Date().toISOString();
|
|
106
|
-
var draft = session.messages.find(function(message) {
|
|
107
|
-
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
108
|
-
});
|
|
109
|
-
if (!draft) {
|
|
110
|
-
draft = { kind: 'agent_draft', role: 'agent', text: '', createdAt: now, updatedAt: now, meta: { draftId: draftId, status: 'streaming' } };
|
|
111
|
-
session.messages.push(draft);
|
|
112
|
-
}
|
|
113
|
-
draft.text = String(text || '');
|
|
114
|
-
draft.updatedAt = now;
|
|
115
|
-
draft.meta = Object.assign({}, draft.meta || {}, extraMeta || {}, { draftId: draftId, status: 'streaming' });
|
|
116
|
-
session.updatedAt = now;
|
|
117
|
-
await writeChatSession(relativeFingerprintPath, session);
|
|
118
|
-
return session;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, text) {
|
|
122
|
-
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
123
|
-
if (!session) throw new Error('chat session not found');
|
|
124
|
-
if (!Array.isArray(session.messages)) session.messages = [];
|
|
125
|
-
var now = new Date().toISOString();
|
|
126
|
-
var draft = session.messages.find(function(message) {
|
|
127
|
-
return message && message.kind === 'agent_draft' && message.meta && message.meta.draftId === draftId;
|
|
128
|
-
});
|
|
129
|
-
if (!draft) {
|
|
130
|
-
draft = { createdAt: now, meta: { draftId: draftId } };
|
|
131
|
-
session.messages.push(draft);
|
|
132
|
-
}
|
|
133
|
-
draft.kind = 'message';
|
|
134
|
-
draft.role = 'agent';
|
|
135
|
-
draft.text = String(text || '');
|
|
136
|
-
draft.updatedAt = now;
|
|
137
|
-
draft.meta = Object.assign({}, draft.meta || {}, { draftId: draftId, status: 'completed' });
|
|
138
|
-
session.updatedAt = now;
|
|
139
|
-
await writeChatSession(relativeFingerprintPath, session);
|
|
140
|
-
return session;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function buildCodexEventMeta(event) {
|
|
144
|
-
var meta = {};
|
|
145
|
-
if (!event || typeof event !== 'object') return meta;
|
|
146
|
-
if (event.type) meta.type = event.type;
|
|
147
|
-
if (event.thread_id) meta.threadId = event.thread_id;
|
|
148
|
-
if (event.item && event.item.type) meta.itemType = event.item.type;
|
|
149
|
-
if (event.item && event.item.id) meta.itemId = event.item.id;
|
|
150
|
-
if (event.usage) meta.usage = event.usage;
|
|
151
|
-
if (event.error && event.error.message) meta.error = event.error.message;
|
|
152
|
-
return meta;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function extractCodexToolArgs(event) {
|
|
156
|
-
if (!event || !event.item) return null;
|
|
157
|
-
return event.item.arguments || event.item.input || event.item.args || null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function extractCodexToolResult(event) {
|
|
161
|
-
if (!event || !event.item) return '';
|
|
162
|
-
return event.item.result || event.item.output || event.item.text || '';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function extractCodexEventText(event) {
|
|
166
|
-
if (!event || typeof event !== 'object') return '';
|
|
167
|
-
return event.text || event.delta || (event.item && (event.item.text || event.item.delta)) || event.message || '';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function formatToolCompletedResult(toolName, args, result, eventMeta) {
|
|
171
|
-
var text = result;
|
|
172
|
-
if (text && typeof text === 'string' && text.trim() && text.trim() !== toolName) return text;
|
|
173
|
-
var parts = [];
|
|
174
|
-
if (args) parts.push('args: ' + (typeof args === 'string' ? args : JSON.stringify(args, null, 2)));
|
|
175
|
-
if (eventMeta && eventMeta.error) parts.push('error: ' + eventMeta.error);
|
|
176
|
-
return parts.length > 0 ? parts.join('\n\n') : (text || toolName);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
router.get('/chat-sessions', async function(req, res) {
|
|
180
|
-
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
181
|
-
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
182
|
-
try {
|
|
183
|
-
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
184
|
-
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
185
|
-
if (!meta) return res.status(404).json({ ok: false, error: 'fingerprint not found' });
|
|
186
|
-
var sessions = await listChatSessions(relativeFingerprintPath);
|
|
187
|
-
var activeSessionId = meta.activeSessionId || null;
|
|
188
|
-
if (activeSessionId && !sessions.some(function(session) { return session.sessionId === activeSessionId; })) {
|
|
189
|
-
activeSessionId = null;
|
|
190
|
-
}
|
|
191
|
-
return res.json({ ok: true, fingerprint: fingerprint, activeSessionId: activeSessionId, sessions: sessions });
|
|
192
|
-
} catch (error) {
|
|
193
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
router.get('/chat-session', async function(req, res) {
|
|
198
|
-
var fingerprint = String(req.query.fingerprint || '').trim();
|
|
199
|
-
var sessionId = String(req.query.sessionId || '').trim();
|
|
200
|
-
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
201
|
-
try {
|
|
202
|
-
var session = await readChatSession(shared.normalizeFingerprintPath(fingerprint), sessionId);
|
|
203
|
-
if (!session) return res.status(404).json({ ok: false, error: 'chat session not found' });
|
|
204
|
-
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
205
|
-
} catch (error) {
|
|
206
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
router.post('/chat-session/create', async function(req, res) {
|
|
211
|
-
var fingerprint = String((req.body || {}).fingerprint || '').trim();
|
|
212
|
-
if (!fingerprint) return res.status(400).json({ ok: false, error: 'fingerprint is required' });
|
|
213
|
-
try {
|
|
214
|
-
var relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
215
|
-
var session = await createChatSession(relativeFingerprintPath);
|
|
216
|
-
var meta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
217
|
-
meta.activeSessionId = session.sessionId;
|
|
218
|
-
meta.threadId = null;
|
|
219
|
-
await shared.writeFingerprintMeta(relativeFingerprintPath, meta);
|
|
220
|
-
return res.json({ ok: true, fingerprint: fingerprint, session: session });
|
|
221
|
-
} catch (error) {
|
|
222
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
router.post('/chat', async function(req, res) {
|
|
227
|
-
var body = req.body || {};
|
|
228
|
-
var prompt = body.prompt;
|
|
229
|
-
var threadId = body.threadId;
|
|
230
|
-
var fingerprint = String(body.fingerprint || '').trim();
|
|
231
|
-
var sessionId = String(body.sessionId || '').trim();
|
|
232
|
-
var images = Array.isArray(body.images) ? body.images : [];
|
|
233
|
-
var relativeFingerprintPath = '';
|
|
234
|
-
var draftId = '';
|
|
235
|
-
var finalResponse = '';
|
|
236
|
-
var tempImagePaths = [];
|
|
237
|
-
if (!prompt) return res.status(400).json({ ok: false, error: 'prompt is required' });
|
|
238
|
-
if (!fingerprint || !sessionId) return res.status(400).json({ ok: false, error: 'fingerprint and sessionId are required' });
|
|
239
|
-
try {
|
|
240
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
241
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
242
|
-
res.setHeader('Connection', 'keep-alive');
|
|
243
|
-
res.write('event: ping\ndata: \n\n');
|
|
244
|
-
|
|
245
|
-
relativeFingerprintPath = shared.normalizeFingerprintPath(fingerprint);
|
|
246
|
-
var existingMeta = await shared.readFingerprintMeta(relativeFingerprintPath);
|
|
247
|
-
if (!existingMeta) throw new Error('fingerprint not found');
|
|
248
|
-
var session = await readChatSession(relativeFingerprintPath, sessionId);
|
|
249
|
-
if (!session) throw new Error('chat session not found');
|
|
250
|
-
threadId = threadId || session.threadId || existingMeta.threadId;
|
|
251
|
-
|
|
252
|
-
await appendChatSessionEvent(relativeFingerprintPath, sessionId, {
|
|
253
|
-
kind: 'message',
|
|
254
|
-
role: 'user',
|
|
255
|
-
text: body.userMessage || prompt,
|
|
256
|
-
createdAt: new Date().toISOString(),
|
|
257
|
-
meta: images.length > 0 ? { imageCount: images.length } : null,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// 处理图片:base64 解码写到临时文件
|
|
261
|
-
for (var i = 0; i < images.length; i++) {
|
|
262
|
-
var img = images[i];
|
|
263
|
-
if (!img.data || !img.mimeType) continue;
|
|
264
|
-
var ext = img.mimeType.split('/')[1] || 'png';
|
|
265
|
-
var tempPath = path.join(os.tmpdir(), 'codex-image-' + Date.now() + '-' + i + '.' + ext);
|
|
266
|
-
var buffer = Buffer.from(img.data, 'base64');
|
|
267
|
-
await fs.writeFile(tempPath, buffer);
|
|
268
|
-
tempImagePaths.push(tempPath);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
var workdir = body.workdir || shared.DEFAULT_WORKDIR;
|
|
272
|
-
var proxy = shared.resolveCodexProxy(body.proxy);
|
|
273
|
-
var mod = await import('@openai/codex-sdk');
|
|
274
|
-
var codex = new mod.Codex({ env: Object.assign({}, process.env, { HTTP_PROXY: proxy, HTTPS_PROXY: proxy, ALL_PROXY: proxy }) });
|
|
275
|
-
var thread = threadId ? codex.resumeThread(threadId, {
|
|
276
|
-
workingDirectory: workdir,
|
|
277
|
-
skipGitRepoCheck: true,
|
|
278
|
-
sandboxMode: 'workspace-write',
|
|
279
|
-
approvalPolicy: 'never',
|
|
280
|
-
modelReasoningEffort: 'high',
|
|
281
|
-
}) : codex.startThread({
|
|
282
|
-
workingDirectory: workdir,
|
|
283
|
-
skipGitRepoCheck: true,
|
|
284
|
-
sandboxMode: 'workspace-write',
|
|
285
|
-
approvalPolicy: 'never',
|
|
286
|
-
modelReasoningEffort: 'high',
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// 构造 UserInput[]:文本 + 图片
|
|
290
|
-
var userInput = [{ type: 'text', text: shared.buildCodexChatPrompt(prompt, { workdir: workdir }) }];
|
|
291
|
-
for (var j = 0; j < tempImagePaths.length; j++) {
|
|
292
|
-
userInput.push({ type: 'local_image', path: tempImagePaths[j] });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
var stream = await thread.runStreamed(userInput);
|
|
296
|
-
finalResponse = '';
|
|
297
|
-
var lastSentText = '';
|
|
298
|
-
var toolBuffers = {};
|
|
299
|
-
draftId = 'draft-' + Date.now() + '-' + Math.random().toString(16).slice(2, 10);
|
|
300
|
-
var lastDraftWriteAt = 0;
|
|
301
|
-
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, '', { threadId: thread.id });
|
|
302
|
-
|
|
303
|
-
function writeSse(payload) {
|
|
304
|
-
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
|
305
|
-
}
|
|
306
|
-
async function persistDraft(force) {
|
|
307
|
-
var now = Date.now();
|
|
308
|
-
if (!force && now - lastDraftWriteAt < 600) return;
|
|
309
|
-
lastDraftWriteAt = now;
|
|
310
|
-
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, { threadId: thread.id });
|
|
311
|
-
}
|
|
312
|
-
function writeSseText(nextText, replace) {
|
|
313
|
-
if (typeof nextText !== 'string') return;
|
|
314
|
-
if (!replace && nextText.startsWith(lastSentText)) {
|
|
315
|
-
var deltaText = nextText.substring(lastSentText.length);
|
|
316
|
-
if (!deltaText) return;
|
|
317
|
-
lastSentText = nextText;
|
|
318
|
-
writeSse({ kind: 'agent_text', text: deltaText, replace: false });
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
lastSentText = nextText;
|
|
322
|
-
writeSse({ kind: 'agent_text', text: nextText, replace: true });
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
for await (var event of stream.events) {
|
|
326
|
-
var eventMeta = buildCodexEventMeta(event);
|
|
327
|
-
if (event.type === 'item.stream.delta' || event.type === 'text.delta' || event.type === 'turn.stream.delta') {
|
|
328
|
-
var delta = event.delta || (event.item && event.item.delta);
|
|
329
|
-
if (typeof delta === 'string') {
|
|
330
|
-
finalResponse += delta;
|
|
331
|
-
writeSseText(finalResponse, false);
|
|
332
|
-
await persistDraft(false);
|
|
333
|
-
}
|
|
334
|
-
} else if (event.type === 'item.completed' && event.item && event.item.type === 'agent_message' && event.item.text) {
|
|
335
|
-
finalResponse = event.item.text;
|
|
336
|
-
writeSseText(finalResponse, false);
|
|
337
|
-
await persistDraft(true);
|
|
338
|
-
} else if (event.type === 'turn.failed') {
|
|
339
|
-
throw new Error(event.error && event.error.message ? event.error.message : 'Codex turn failed');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (event.item && event.item.type && event.item.type !== 'agent_message' && event.item.type !== 'user_message') {
|
|
343
|
-
var toolId = event.item.id || eventMeta.itemId || ('tool-' + Date.now());
|
|
344
|
-
var toolText = extractCodexEventText(event);
|
|
345
|
-
if (!toolBuffers[toolId]) {
|
|
346
|
-
toolBuffers[toolId] = { itemType: event.item.type, text: '', args: extractCodexToolArgs(event), result: extractCodexToolResult(event) };
|
|
347
|
-
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_started', role: 'tool', text: event.item.type, createdAt: new Date().toISOString(), meta: Object.assign({ args: toolBuffers[toolId].args }, eventMeta) });
|
|
348
|
-
writeSse({ kind: 'tool_call_started', toolId: toolId, toolName: event.item.type, args: toolBuffers[toolId].args, text: event.item.type, meta: eventMeta });
|
|
349
|
-
}
|
|
350
|
-
if (extractCodexToolResult(event)) toolBuffers[toolId].result = extractCodexToolResult(event);
|
|
351
|
-
if (toolText) {
|
|
352
|
-
toolBuffers[toolId].text += toolText;
|
|
353
|
-
if (!toolBuffers[toolId].result) toolBuffers[toolId].result = toolBuffers[toolId].text;
|
|
354
|
-
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_output', role: 'tool', text: toolText, createdAt: new Date().toISOString(), meta: Object.assign({ toolId: toolId }, eventMeta) });
|
|
355
|
-
writeSse({ kind: 'tool_call_output', toolId: toolId, toolName: event.item.type, output: toolBuffers[toolId].text, text: toolText, meta: eventMeta });
|
|
356
|
-
}
|
|
357
|
-
if (event.type === 'item.completed') {
|
|
358
|
-
var completedResult = formatToolCompletedResult(event.item.type, toolBuffers[toolId].args, toolBuffers[toolId].result || toolBuffers[toolId].text, eventMeta);
|
|
359
|
-
await appendChatSessionEvent(relativeFingerprintPath, sessionId, { kind: 'tool_call_completed', role: 'tool', text: completedResult, createdAt: new Date().toISOString(), meta: Object.assign({ toolId: toolId }, eventMeta) });
|
|
360
|
-
writeSse({ kind: 'tool_call_completed', toolId: toolId, toolName: event.item.type, result: completedResult, text: completedResult, meta: eventMeta });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
await finalizeAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse);
|
|
366
|
-
await updateChatSessionThread(relativeFingerprintPath, sessionId, thread.id);
|
|
367
|
-
existingMeta.threadId = thread.id;
|
|
368
|
-
existingMeta.activeSessionId = sessionId;
|
|
369
|
-
await shared.writeFingerprintMeta(relativeFingerprintPath, existingMeta);
|
|
370
|
-
writeSse({ kind: 'done', threadId: thread.id, sessionId: sessionId });
|
|
371
|
-
res.end();
|
|
372
|
-
} catch (error) {
|
|
373
|
-
if (relativeFingerprintPath && sessionId && draftId) {
|
|
374
|
-
await upsertAgentDraftMessage(relativeFingerprintPath, sessionId, draftId, finalResponse, {
|
|
375
|
-
status: 'interrupted',
|
|
376
|
-
error: error instanceof Error ? error.message : String(error),
|
|
377
|
-
}).catch(function() {});
|
|
378
|
-
}
|
|
379
|
-
res.write('event: error\ndata: ' + JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) + '\n\n');
|
|
380
|
-
res.end();
|
|
381
|
-
} finally {
|
|
382
|
-
// 清理临时图片文件
|
|
383
|
-
for (var k = 0; k < tempImagePaths.length; k++) {
|
|
384
|
-
fs.unlink(tempImagePaths[k]).catch(function() {});
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
module.exports = router;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
var express = require('express');
|
|
2
|
-
var path = require('node:path');
|
|
3
|
-
var shared = require('./backtrace-shared');
|
|
4
|
-
|
|
5
|
-
var router = express.Router();
|
|
6
|
-
|
|
7
|
-
router.get('/files/index', async function(req, res) {
|
|
8
|
-
try {
|
|
9
|
-
var directories = await shared.listTopLevelMergedDirectories();
|
|
10
|
-
return res.json({ ok: true, directories: directories });
|
|
11
|
-
} catch (error) {
|
|
12
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
router.get('/files/list', async function(req, res) {
|
|
17
|
-
var topPath = String(req.query.path || '');
|
|
18
|
-
var logDir = String(req.query.logDir || '');
|
|
19
|
-
try {
|
|
20
|
-
var logDirectories = await shared.listImmediateDirectories(shared.FINGERPRINTS_ROOT, path.join(topPath, 'logs'));
|
|
21
|
-
var selectedLogDir = logDir || (logDirectories[0] ? logDirectories[0].name : '');
|
|
22
|
-
var logFiles = selectedLogDir ? await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(topPath, 'logs', selectedLogDir)) : [];
|
|
23
|
-
var reportFiles = await shared.listImmediateFiles(shared.FINGERPRINTS_ROOT, path.join(topPath, 'reports'));
|
|
24
|
-
var repairStatus = await shared.readRepairStatus(topPath);
|
|
25
|
-
var hasCompletedRepair = !!(repairStatus && repairStatus.completed);
|
|
26
|
-
return res.json({
|
|
27
|
-
ok: true,
|
|
28
|
-
path: topPath,
|
|
29
|
-
selectedLogDir: selectedLogDir,
|
|
30
|
-
logDirectories: logDirectories,
|
|
31
|
-
logFiles: logFiles,
|
|
32
|
-
reportFiles: reportFiles,
|
|
33
|
-
reportStatus: shared.buildFingerprintStatus(reportFiles.length > 0, hasCompletedRepair),
|
|
34
|
-
hasCompletedRepair: hasCompletedRepair,
|
|
35
|
-
});
|
|
36
|
-
} catch (error) {
|
|
37
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
router.get('/files/content', async function(req, res) {
|
|
42
|
-
var kind = req.query.kind;
|
|
43
|
-
var relativePath = req.query.path;
|
|
44
|
-
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
45
|
-
if (!rootDir || !relativePath) {
|
|
46
|
-
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
50
|
-
var content = await require('node:fs/promises').readFile(targetPath, 'utf8');
|
|
51
|
-
return res.json({ ok: true, kind: kind, path: relativePath, content: content });
|
|
52
|
-
} catch (error) {
|
|
53
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
router.get('/files/download', function(req, res) {
|
|
58
|
-
var kind = req.query.kind;
|
|
59
|
-
var relativePath = req.query.path;
|
|
60
|
-
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
61
|
-
if (!rootDir || !relativePath) {
|
|
62
|
-
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
66
|
-
return res.download(targetPath);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
router.get('/files/view', function(req, res) {
|
|
73
|
-
var kind = req.query.kind;
|
|
74
|
-
var relativePath = req.query.path;
|
|
75
|
-
var rootDir = kind === 'report' || kind === 'logs' ? shared.FINGERPRINTS_ROOT : '';
|
|
76
|
-
if (!rootDir || !relativePath) {
|
|
77
|
-
return res.status(400).json({ ok: false, error: 'kind and path are required' });
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
var targetPath = shared.toSafeAbsolute(rootDir, relativePath);
|
|
81
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
82
|
-
return res.sendFile(targetPath);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
return res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
module.exports = router;
|