claude-code-inspector 0.1.0
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/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish-npm.yml +33 -0
- package/README.md +199 -0
- package/app/api/events/route.ts +35 -0
- package/app/api/proxy/route.ts +42 -0
- package/app/api/requests/[id]/route.ts +82 -0
- package/app/api/requests/export/route.ts +124 -0
- package/app/api/requests/route.ts +32 -0
- package/app/dashboard/page.tsx +562 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +26 -0
- package/app/layout.tsx +34 -0
- package/app/page.tsx +5 -0
- package/app/v1/messages/route.ts +30 -0
- package/components/JsonModal.tsx +155 -0
- package/components/JsonViewer.tsx +185 -0
- package/dev.sh +19 -0
- package/eslint.config.mjs +18 -0
- package/lib/env.ts +52 -0
- package/lib/pricing.ts +131 -0
- package/lib/proxy/forwarder.test.ts +171 -0
- package/lib/proxy/forwarder.ts +96 -0
- package/lib/proxy/handlers.test.ts +276 -0
- package/lib/proxy/handlers.ts +340 -0
- package/lib/proxy/ws-server.ts +76 -0
- package/lib/recorder/index.ts +152 -0
- package/lib/recorder/schema.ts +41 -0
- package/lib/recorder/store.ts +141 -0
- package/next.config.ts +59 -0
- package/package.json +42 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server.ts +64 -0
- package/tsconfig.json +34 -0
- package/tsconfig.server.json +11 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { extractSessionId, extractUsage, isSseResponse, forwardRequest, UpstreamConfig } from './forwarder';
|
|
2
|
+
import { recordRequest, updateRequestResponse, recordSseEvent } from '../recorder';
|
|
3
|
+
import { broadcastNewRequest, broadcastRequestUpdate, broadcastSseEvent } from './ws-server';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { calculateCost } from '../pricing';
|
|
6
|
+
import { initEnv } from '../../lib/env';
|
|
7
|
+
|
|
8
|
+
// 确保环境变量已初始化(兼容 dev 和 start 模式)
|
|
9
|
+
let envInitialized = false;
|
|
10
|
+
function ensureEnvInitialized() {
|
|
11
|
+
if (!envInitialized) {
|
|
12
|
+
initEnv();
|
|
13
|
+
envInitialized = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 处理 /v1/messages 请求 (Claude API)
|
|
19
|
+
*/
|
|
20
|
+
export async function handleMessages(
|
|
21
|
+
request: Request,
|
|
22
|
+
body: any,
|
|
23
|
+
headers: Record<string, string>
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
// 确保环境变量已初始化
|
|
26
|
+
ensureEnvInitialized();
|
|
27
|
+
|
|
28
|
+
const requestId = uuidv4();
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
// 提取 session_id
|
|
32
|
+
const sessionId = extractSessionId(headers, body);
|
|
33
|
+
|
|
34
|
+
// 提取 model
|
|
35
|
+
const model = body?.model || '';
|
|
36
|
+
|
|
37
|
+
// 1. 记录请求
|
|
38
|
+
await recordRequest({
|
|
39
|
+
id: requestId,
|
|
40
|
+
session_id: sessionId || undefined,
|
|
41
|
+
endpoint: '/v1/messages',
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers,
|
|
44
|
+
body,
|
|
45
|
+
model,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 广播新请求
|
|
49
|
+
broadcastNewRequest({
|
|
50
|
+
id: requestId,
|
|
51
|
+
session_id: sessionId,
|
|
52
|
+
endpoint: '/v1/messages',
|
|
53
|
+
method: 'POST',
|
|
54
|
+
input_tokens: 0,
|
|
55
|
+
output_tokens: 0,
|
|
56
|
+
created_at: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 2. 获取上游配置
|
|
60
|
+
// UPSTREAM_* 用于转发,ANTHROPIC_* 作为 fallback
|
|
61
|
+
const config: UpstreamConfig = {
|
|
62
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || 'https://api.anthropic.com',
|
|
63
|
+
apiKey: process.env.UPSTREAM_API_KEY || process.env.ANTHROPIC_API_KEY || '',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// 3. 转发请求
|
|
69
|
+
const response = await forwardRequest('/v1/messages', body, headers, config);
|
|
70
|
+
|
|
71
|
+
// 4. 处理响应
|
|
72
|
+
if (isSseResponse(response)) {
|
|
73
|
+
return handleStreamingResponse(response, requestId, startTime, body);
|
|
74
|
+
} else {
|
|
75
|
+
return handleNonStreamingResponse(response, requestId, startTime, body);
|
|
76
|
+
}
|
|
77
|
+
} catch (error: any) {
|
|
78
|
+
// 5. 处理错误
|
|
79
|
+
const errorMessage = error?.response?.data?.error?.message || error?.message || 'Unknown error';
|
|
80
|
+
await updateRequestResponse({
|
|
81
|
+
id: requestId,
|
|
82
|
+
status: error?.response?.status || 500,
|
|
83
|
+
headers: {},
|
|
84
|
+
body: { error: errorMessage },
|
|
85
|
+
latency_ms: Date.now() - startTime,
|
|
86
|
+
input_tokens: 0,
|
|
87
|
+
output_tokens: 0,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
91
|
+
status: error?.response?.status || 500,
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 从 SSE 事件中提取 usage 信息(兼容多种 API 格式)
|
|
99
|
+
*/
|
|
100
|
+
function extractUsageFromSse(json: any): { input_tokens: number; output_tokens: number } {
|
|
101
|
+
// Claude/智谱 GLM 格式: message_start 事件中的 message.usage
|
|
102
|
+
if (json.type === 'message_start' && json.message?.usage) {
|
|
103
|
+
return {
|
|
104
|
+
input_tokens: json.message.usage.input_tokens || 0,
|
|
105
|
+
output_tokens: json.message.usage.output_tokens || 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Claude/智谱 GLM 格式: message_delta 事件中的 usage
|
|
110
|
+
if (json.type === 'message_delta' && json.usage) {
|
|
111
|
+
return {
|
|
112
|
+
input_tokens: json.usage.input_tokens || 0,
|
|
113
|
+
output_tokens: json.usage.output_tokens || 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// OpenAI / Anthropic 格式
|
|
118
|
+
if (json.usage) {
|
|
119
|
+
return {
|
|
120
|
+
input_tokens: json.usage.input_tokens || json.usage.prompt_tokens || 0,
|
|
121
|
+
output_tokens: json.usage.output_tokens || json.usage.completion_tokens || 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 智谱 GLM 格式 (usage 在顶层)
|
|
126
|
+
if (json.input_tokens !== undefined || json.output_tokens !== undefined) {
|
|
127
|
+
return {
|
|
128
|
+
input_tokens: json.input_tokens || 0,
|
|
129
|
+
output_tokens: json.output_tokens || 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { input_tokens: 0, output_tokens: 0 };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 处理流式响应
|
|
138
|
+
*/
|
|
139
|
+
async function handleStreamingResponse(
|
|
140
|
+
response: any,
|
|
141
|
+
requestId: string,
|
|
142
|
+
startTime: number,
|
|
143
|
+
body: any
|
|
144
|
+
): Promise<Response> {
|
|
145
|
+
const firstTokenTime = { recorded: false, value: 0 };
|
|
146
|
+
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
147
|
+
const allEvents: any[] = []; // 收集所有事件用于记录完整响应
|
|
148
|
+
|
|
149
|
+
// 提取上游响应头
|
|
150
|
+
const upstreamHeaders: Record<string, string> = {};
|
|
151
|
+
if (response.headers) {
|
|
152
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
153
|
+
upstreamHeaders[key] = String(value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 使用 ReadableStream 包装 Node.js stream
|
|
158
|
+
const nodeStream = response.data;
|
|
159
|
+
|
|
160
|
+
const stream = new ReadableStream({
|
|
161
|
+
async start(controller) {
|
|
162
|
+
try {
|
|
163
|
+
for await (const chunk of nodeStream) {
|
|
164
|
+
const text = chunk.toString();
|
|
165
|
+
const lines = text.split('\n').filter((line: string) => line.trim());
|
|
166
|
+
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
// 处理 data: 行(支持 "data:" 和 "data: " 两种格式)
|
|
169
|
+
if (line.startsWith('data:')) {
|
|
170
|
+
// 提取 data 后的内容(跳过可能的空格)
|
|
171
|
+
const data = line.startsWith('data: ') ? line.slice(6) : line.slice(5);
|
|
172
|
+
|
|
173
|
+
if (data === '[DONE]') {
|
|
174
|
+
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const json = JSON.parse(data);
|
|
180
|
+
allEvents.push(json);
|
|
181
|
+
|
|
182
|
+
// 记录首个 token 时间(兼容多种格式)
|
|
183
|
+
if (!firstTokenTime.recorded) {
|
|
184
|
+
// Claude 格式: content_block_delta 中的 delta.text 或 delta.thinking
|
|
185
|
+
const hasContent =
|
|
186
|
+
json.delta?.text ||
|
|
187
|
+
json.delta?.thinking ||
|
|
188
|
+
json.choices?.[0]?.delta?.content ||
|
|
189
|
+
json.content;
|
|
190
|
+
if (hasContent) {
|
|
191
|
+
firstTokenTime.recorded = true;
|
|
192
|
+
firstTokenTime.value = Date.now() - startTime;
|
|
193
|
+
await recordSseEvent(requestId, { type: 'first_token', latency: firstTokenTime.value });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 提取 usage(兼容多种格式)
|
|
198
|
+
const extractedUsage = extractUsageFromSse(json);
|
|
199
|
+
if (extractedUsage.input_tokens > 0 || extractedUsage.output_tokens > 0) {
|
|
200
|
+
usage.input_tokens = extractedUsage.input_tokens;
|
|
201
|
+
usage.output_tokens = extractedUsage.output_tokens;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 记录 SSE 事件
|
|
205
|
+
await recordSseEvent(requestId, json);
|
|
206
|
+
|
|
207
|
+
// 广播 SSE 事件
|
|
208
|
+
broadcastSseEvent(requestId, json);
|
|
209
|
+
} catch (parseError) {
|
|
210
|
+
// 记录解析错误以便调试
|
|
211
|
+
console.error('[SSE Parse Error] Failed to parse:', data.substring(0, 100));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 转发所有行(包括 event: 行)
|
|
215
|
+
controller.enqueue(new TextEncoder().encode(line + '\n'));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Stream error:', error);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 完成时更新数据库
|
|
223
|
+
const costUsd = calculateCost(
|
|
224
|
+
body.model || '',
|
|
225
|
+
usage.input_tokens,
|
|
226
|
+
usage.output_tokens
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await updateRequestResponse({
|
|
230
|
+
id: requestId,
|
|
231
|
+
status: 200,
|
|
232
|
+
headers: upstreamHeaders,
|
|
233
|
+
body: { usage, events: allEvents },
|
|
234
|
+
latency_ms: Date.now() - startTime,
|
|
235
|
+
first_token_ms: firstTokenTime.value,
|
|
236
|
+
input_tokens: usage.input_tokens,
|
|
237
|
+
output_tokens: usage.output_tokens,
|
|
238
|
+
model: body.model || '',
|
|
239
|
+
cost_usd: costUsd,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 广播请求完成
|
|
243
|
+
broadcastRequestUpdate({
|
|
244
|
+
id: requestId,
|
|
245
|
+
status: 200,
|
|
246
|
+
latency_ms: Date.now() - startTime,
|
|
247
|
+
first_token_ms: firstTokenTime.value,
|
|
248
|
+
input_tokens: usage.input_tokens,
|
|
249
|
+
output_tokens: usage.output_tokens,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
controller.close();
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return new Response(stream, {
|
|
257
|
+
status: 200,
|
|
258
|
+
headers: {
|
|
259
|
+
'Content-Type': 'text/event-stream',
|
|
260
|
+
'Cache-Control': 'no-cache',
|
|
261
|
+
Connection: 'keep-alive',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 从 stream 中读取所有数据
|
|
268
|
+
*/
|
|
269
|
+
async function readStream(stream: any): Promise<string> {
|
|
270
|
+
const chunks: Buffer[] = [];
|
|
271
|
+
for await (const chunk of stream) {
|
|
272
|
+
chunks.push(chunk);
|
|
273
|
+
}
|
|
274
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 处理非流式响应
|
|
279
|
+
*/
|
|
280
|
+
async function handleNonStreamingResponse(
|
|
281
|
+
response: any,
|
|
282
|
+
requestId: string,
|
|
283
|
+
startTime: number,
|
|
284
|
+
body: any
|
|
285
|
+
): Promise<Response> {
|
|
286
|
+
// 从 stream 中读取数据
|
|
287
|
+
const rawData = await readStream(response.data);
|
|
288
|
+
const data = JSON.parse(rawData);
|
|
289
|
+
const usage = extractUsage(data);
|
|
290
|
+
|
|
291
|
+
// 提取 headers 为简单对象(避免循环引用)
|
|
292
|
+
const responseHeaders: Record<string, string> = {};
|
|
293
|
+
if (response.headers) {
|
|
294
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
295
|
+
responseHeaders[key] = String(value);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 计算成本
|
|
300
|
+
const costUsd = calculateCost(
|
|
301
|
+
body.model || '',
|
|
302
|
+
usage.input_tokens,
|
|
303
|
+
usage.output_tokens,
|
|
304
|
+
usage.cache_read_tokens,
|
|
305
|
+
usage.cache_creation_tokens
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// 更新响应记录
|
|
309
|
+
await updateRequestResponse({
|
|
310
|
+
id: requestId,
|
|
311
|
+
status: response.status,
|
|
312
|
+
headers: responseHeaders,
|
|
313
|
+
body: data,
|
|
314
|
+
latency_ms: Date.now() - startTime,
|
|
315
|
+
input_tokens: usage.input_tokens,
|
|
316
|
+
output_tokens: usage.output_tokens,
|
|
317
|
+
cache_read_tokens: usage.cache_read_tokens,
|
|
318
|
+
cache_creation_tokens: usage.cache_creation_tokens,
|
|
319
|
+
model: body.model || '',
|
|
320
|
+
cost_usd: costUsd,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// 广播请求完成
|
|
324
|
+
broadcastRequestUpdate({
|
|
325
|
+
id: requestId,
|
|
326
|
+
status: response.status,
|
|
327
|
+
latency_ms: Date.now() - startTime,
|
|
328
|
+
input_tokens: usage.input_tokens,
|
|
329
|
+
output_tokens: usage.output_tokens,
|
|
330
|
+
cache_read_tokens: usage.cache_read_tokens,
|
|
331
|
+
cache_creation_tokens: usage.cache_creation_tokens,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return new Response(JSON.stringify(data), {
|
|
335
|
+
status: response.status,
|
|
336
|
+
headers: {
|
|
337
|
+
'Content-Type': 'application/json',
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
|
|
3
|
+
// 全局 WebSocket 服务器实例(由 server.ts 设置)
|
|
4
|
+
let wssInstance: WebSocketServer | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 设置 WebSocket 服务器实例(由 server.ts 调用)
|
|
8
|
+
*/
|
|
9
|
+
export function setWssInstance(wss: WebSocketServer) {
|
|
10
|
+
wssInstance = wss;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取 WebSocket 服务器实例
|
|
15
|
+
*/
|
|
16
|
+
export function getWssInstance(): WebSocketServer | null {
|
|
17
|
+
return wssInstance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 广播 SSE 事件到所有客户端
|
|
22
|
+
*/
|
|
23
|
+
export function broadcastSseEvent(requestId: string, event: any) {
|
|
24
|
+
if (!wssInstance) return;
|
|
25
|
+
|
|
26
|
+
const message = JSON.stringify({
|
|
27
|
+
type: 'sse_event',
|
|
28
|
+
requestId,
|
|
29
|
+
event,
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
wssInstance.clients.forEach((client: any) => {
|
|
34
|
+
if (client.readyState === 1) {
|
|
35
|
+
client.send(message);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 广播新请求到所有客户端
|
|
42
|
+
*/
|
|
43
|
+
export function broadcastNewRequest(data: any) {
|
|
44
|
+
if (!wssInstance) return;
|
|
45
|
+
|
|
46
|
+
const message = JSON.stringify({
|
|
47
|
+
type: 'new_request',
|
|
48
|
+
data,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
wssInstance.clients.forEach((client: any) => {
|
|
53
|
+
if (client.readyState === 1) {
|
|
54
|
+
client.send(message);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 广播请求更新到所有客户端
|
|
61
|
+
*/
|
|
62
|
+
export function broadcastRequestUpdate(data: any) {
|
|
63
|
+
if (!wssInstance) return;
|
|
64
|
+
|
|
65
|
+
const message = JSON.stringify({
|
|
66
|
+
type: 'request_update',
|
|
67
|
+
data,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
wssInstance.clients.forEach((client: any) => {
|
|
72
|
+
if (client.readyState === 1) {
|
|
73
|
+
client.send(message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { RequestStore, RequestLog } from './store';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// 懒加载 store 实例
|
|
5
|
+
let _requestStore: RequestStore | null = null;
|
|
6
|
+
|
|
7
|
+
export function getStore(): RequestStore {
|
|
8
|
+
if (!_requestStore) {
|
|
9
|
+
const dbPath = path.resolve(process.cwd(), 'db/inspector.sqlite');
|
|
10
|
+
_requestStore = new RequestStore(dbPath);
|
|
11
|
+
}
|
|
12
|
+
return _requestStore;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 记录请求
|
|
17
|
+
*/
|
|
18
|
+
export async function recordRequest(data: {
|
|
19
|
+
id: string;
|
|
20
|
+
session_id?: string;
|
|
21
|
+
endpoint: string;
|
|
22
|
+
method: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
body: any;
|
|
25
|
+
model?: string;
|
|
26
|
+
}): Promise<void> {
|
|
27
|
+
const log: RequestLog = {
|
|
28
|
+
id: data.id,
|
|
29
|
+
session_id: data.session_id,
|
|
30
|
+
endpoint: data.endpoint,
|
|
31
|
+
method: data.method,
|
|
32
|
+
request_headers: JSON.stringify(data.headers),
|
|
33
|
+
request_body: JSON.stringify(data.body),
|
|
34
|
+
response_status: 0,
|
|
35
|
+
response_headers: null,
|
|
36
|
+
response_body: null,
|
|
37
|
+
streaming_events: null,
|
|
38
|
+
input_tokens: 0,
|
|
39
|
+
output_tokens: 0,
|
|
40
|
+
cache_read_tokens: 0,
|
|
41
|
+
cache_creation_tokens: 0,
|
|
42
|
+
latency_ms: 0,
|
|
43
|
+
first_token_ms: 0,
|
|
44
|
+
status_code: 0,
|
|
45
|
+
error_message: null,
|
|
46
|
+
model: data.model || null,
|
|
47
|
+
cost_usd: 0,
|
|
48
|
+
created_at: new Date().toISOString(),
|
|
49
|
+
};
|
|
50
|
+
getStore().insert(log);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 更新请求响应
|
|
55
|
+
*/
|
|
56
|
+
export async function updateRequestResponse(data: {
|
|
57
|
+
id: string;
|
|
58
|
+
status: number;
|
|
59
|
+
headers: Record<string, string>;
|
|
60
|
+
body: any;
|
|
61
|
+
latency_ms: number;
|
|
62
|
+
first_token_ms?: number;
|
|
63
|
+
input_tokens: number;
|
|
64
|
+
output_tokens: number;
|
|
65
|
+
cache_read_tokens?: number;
|
|
66
|
+
cache_creation_tokens?: number;
|
|
67
|
+
model?: string;
|
|
68
|
+
cost_usd?: number;
|
|
69
|
+
}): Promise<void> {
|
|
70
|
+
const store = getStore() as any;
|
|
71
|
+
const db = store.db as any;
|
|
72
|
+
const stmt = db.prepare(`
|
|
73
|
+
UPDATE request_logs
|
|
74
|
+
SET
|
|
75
|
+
response_status = ?,
|
|
76
|
+
response_headers = ?,
|
|
77
|
+
response_body = ?,
|
|
78
|
+
latency_ms = ?,
|
|
79
|
+
first_token_ms = ?,
|
|
80
|
+
input_tokens = ?,
|
|
81
|
+
output_tokens = ?,
|
|
82
|
+
cache_read_tokens = ?,
|
|
83
|
+
cache_creation_tokens = ?,
|
|
84
|
+
model = ?,
|
|
85
|
+
cost_usd = ?,
|
|
86
|
+
status_code = ?
|
|
87
|
+
WHERE id = ?
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
stmt.run(
|
|
91
|
+
data.status,
|
|
92
|
+
JSON.stringify(data.headers),
|
|
93
|
+
JSON.stringify(data.body),
|
|
94
|
+
data.latency_ms,
|
|
95
|
+
data.first_token_ms || null,
|
|
96
|
+
data.input_tokens,
|
|
97
|
+
data.output_tokens,
|
|
98
|
+
data.cache_read_tokens || 0,
|
|
99
|
+
data.cache_creation_tokens || 0,
|
|
100
|
+
data.model || null,
|
|
101
|
+
data.cost_usd || 0,
|
|
102
|
+
data.status,
|
|
103
|
+
data.id
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 记录 SSE 事件
|
|
109
|
+
*/
|
|
110
|
+
export async function recordSseEvent(requestId: string, event: any): Promise<void> {
|
|
111
|
+
const store = getStore() as any;
|
|
112
|
+
const db = store.db as any;
|
|
113
|
+
|
|
114
|
+
// 获取现 streaming_events
|
|
115
|
+
const current = db.prepare('SELECT streaming_events FROM request_logs WHERE id = ?').get(requestId) as any;
|
|
116
|
+
let events: any[] = [];
|
|
117
|
+
|
|
118
|
+
if (current?.streaming_events) {
|
|
119
|
+
try {
|
|
120
|
+
events = JSON.parse(current.streaming_events);
|
|
121
|
+
} catch {
|
|
122
|
+
events = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
events.push({
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
data: event,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const stmt = db.prepare(`
|
|
132
|
+
UPDATE request_logs
|
|
133
|
+
SET streaming_events = ?
|
|
134
|
+
WHERE id = ?
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
stmt.run(JSON.stringify(events), requestId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 记录错误
|
|
142
|
+
*/
|
|
143
|
+
export async function recordError(requestId: string, error: string): Promise<void> {
|
|
144
|
+
const store = getStore() as any;
|
|
145
|
+
const db = store.db as any;
|
|
146
|
+
const stmt = db.prepare(`
|
|
147
|
+
UPDATE request_logs
|
|
148
|
+
SET error_message = ?, status_code = 500
|
|
149
|
+
WHERE id = ?
|
|
150
|
+
`);
|
|
151
|
+
stmt.run(error, requestId);
|
|
152
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库表结构定义
|
|
3
|
+
*/
|
|
4
|
+
export const CREATE_TABLES = `
|
|
5
|
+
-- 请求日志表
|
|
6
|
+
CREATE TABLE IF NOT EXISTS request_logs (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
session_id TEXT,
|
|
9
|
+
endpoint TEXT NOT NULL,
|
|
10
|
+
method TEXT NOT NULL,
|
|
11
|
+
request_headers TEXT,
|
|
12
|
+
request_body TEXT,
|
|
13
|
+
response_status INTEGER,
|
|
14
|
+
response_headers TEXT,
|
|
15
|
+
response_body TEXT,
|
|
16
|
+
streaming_events TEXT,
|
|
17
|
+
input_tokens INTEGER DEFAULT 0,
|
|
18
|
+
output_tokens INTEGER DEFAULT 0,
|
|
19
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
20
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
21
|
+
latency_ms INTEGER,
|
|
22
|
+
first_token_ms INTEGER,
|
|
23
|
+
status_code INTEGER,
|
|
24
|
+
error_message TEXT,
|
|
25
|
+
model TEXT,
|
|
26
|
+
cost_usd REAL DEFAULT 0,
|
|
27
|
+
created_at TEXT NOT NULL
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
-- 索引
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_request_logs_session ON request_logs(session_id);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_request_logs_endpoint ON request_logs(endpoint);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON request_logs(created_at DESC);
|
|
34
|
+
|
|
35
|
+
-- 设置表
|
|
36
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
37
|
+
key TEXT PRIMARY KEY,
|
|
38
|
+
value TEXT NOT NULL,
|
|
39
|
+
updated_at TEXT NOT NULL
|
|
40
|
+
);
|
|
41
|
+
`;
|