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.
@@ -0,0 +1,562 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef, useCallback } from 'react';
4
+ import JsonViewer from '@/components/JsonViewer';
5
+ import JsonModal from '@/components/JsonModal';
6
+
7
+ interface RequestLog {
8
+ id: string;
9
+ session_id?: string;
10
+ endpoint: string;
11
+ method: string;
12
+ response_status?: number;
13
+ input_tokens: number;
14
+ output_tokens: number;
15
+ cache_read_tokens: number;
16
+ cache_creation_tokens: number;
17
+ latency_ms?: number;
18
+ first_token_ms?: number;
19
+ model?: string;
20
+ cost_usd?: number;
21
+ created_at: string;
22
+ }
23
+
24
+ interface RequestDetail extends RequestLog {
25
+ request_headers?: Record<string, string> | null;
26
+ request_body?: any;
27
+ response_headers?: Record<string, string> | null;
28
+ response_body?: any;
29
+ streaming_events?: any[] | null;
30
+ error_message?: string | null;
31
+ }
32
+
33
+ export default function Dashboard() {
34
+ const [requests, setRequests] = useState<RequestLog[]>([]);
35
+ const [selectedRequest, setSelectedRequest] = useState<RequestLog | null>(null);
36
+ const [requestDetail, setRequestDetail] = useState<RequestDetail | null>(null);
37
+ const [detailLoading, setDetailLoading] = useState(false);
38
+ const [activeTab, setActiveTab] = useState<'overview' | 'request' | 'response' | 'streaming'>('request');
39
+ const [modalData, setModalData] = useState<any>(null); // 用于浮窗显示的数据
40
+ const [modalSnapshot, setModalSnapshot] = useState<any>(null); // 浮窗快照,防止刷新
41
+ const [stats, setStats] = useState({ total: 0, inputTokens: 0, outputTokens: 0, totalCost: 0 });
42
+ const [filter, setFilter] = useState({ endpoint: '', status: '', search: '' });
43
+ const wsRef = useRef<WebSocket | null>(null);
44
+
45
+ // 打开浮窗时保存快照,防止后续刷新丢失状态
46
+ const handleExpandModal = useCallback((data: any) => {
47
+ const snapshot = JSON.parse(JSON.stringify(data));
48
+ setModalData(data);
49
+ setModalSnapshot(snapshot);
50
+ isModalOpenRef.current = true;
51
+ }, []);
52
+
53
+ // 关闭浮窗时清除快照 - 使用 useCallback 稳定引用
54
+ const handleCloseModal = useCallback(() => {
55
+ setModalData(null);
56
+ setModalSnapshot(null);
57
+ isModalOpenRef.current = false;
58
+ }, []);
59
+
60
+ // 防止 WebSocket 更新时意外关闭浮窗
61
+ // 使用 useRef 跟踪浮窗是否正在被用户查看
62
+ const isModalOpenRef = useRef(false);
63
+
64
+ // 获取最近请求
65
+ const fetchRecentRequests = async () => {
66
+ try {
67
+ const res = await fetch('/api/requests');
68
+ const data = await res.json();
69
+ setRequests(data);
70
+ updateStats(data);
71
+ } catch (error) {
72
+ console.error('Failed to fetch requests:', error);
73
+ }
74
+ };
75
+
76
+ // 更新统计
77
+ const updateStats = (reqs: RequestLog[]) => {
78
+ const total = reqs.length;
79
+ const inputTokens = reqs.reduce((sum, r) => sum + r.input_tokens, 0);
80
+ const outputTokens = reqs.reduce((sum, r) => sum + r.output_tokens, 0);
81
+ const totalCost = reqs.reduce((sum, r) => sum + (r.cost_usd || 0), 0);
82
+ setStats({ total, inputTokens, outputTokens, totalCost } as any);
83
+ };
84
+
85
+ // 获取请求详情
86
+ const fetchRequestDetail = async (id: string) => {
87
+ setDetailLoading(true);
88
+ try {
89
+ const res = await fetch(`/api/requests/${id}`);
90
+ const data = await res.json();
91
+ setRequestDetail(data);
92
+ } catch (error) {
93
+ console.error('Failed to fetch request detail:', error);
94
+ } finally {
95
+ setDetailLoading(false);
96
+ }
97
+ };
98
+
99
+ // 选中请求变化时获取详情
100
+ useEffect(() => {
101
+ if (selectedRequest) {
102
+ fetchRequestDetail(selectedRequest.id);
103
+ setActiveTab('request');
104
+ } else {
105
+ setRequestDetail(null);
106
+ }
107
+ }, [selectedRequest?.id]);
108
+
109
+ // WebSocket 连接 - 已临时注释用于测试
110
+ // useEffect(() => {
111
+ // // 初始加载
112
+ // fetchRecentRequests();
113
+
114
+ // // 连接 WebSocket
115
+ // const connectWebSocket = () => {
116
+ // const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
117
+ // wsRef.current = new WebSocket(`${protocol}//${window.location.host}/api/ws`);
118
+
119
+ // wsRef.current.onopen = () => {
120
+ // console.log('WebSocket connected');
121
+ // };
122
+
123
+ // wsRef.current.onmessage = (event) => {
124
+ // const message: { type: string; data?: RequestLog; requestId?: string; event?: any } = JSON.parse(event.data);
125
+
126
+ // if (message.type === 'new_request' && message.data) {
127
+ // setRequests((prev) => [message.data!, ...prev]);
128
+ // } else if (message.type === 'request_update' && message.data) {
129
+ // setRequests((prev) => prev.map((r) => (r.id === message.data!.id ? message.data! : r)));
130
+ // // 注意:不更新 requestDetail,保持打开浮层时的数据快照
131
+ // // 用户可手动刷新或重新选择查看最新数据
132
+ // } else if (message.type === 'sse_event') {
133
+ // console.log('SSE Event:', message);
134
+ // }
135
+ // };
136
+
137
+ // wsRef.current.onclose = () => {
138
+ // console.log('WebSocket disconnected, reconnecting...');
139
+ // setTimeout(connectWebSocket, 3000);
140
+ // };
141
+
142
+ // wsRef.current.onerror = (error) => {
143
+ // console.error('WebSocket error:', error);
144
+ // };
145
+ // };
146
+
147
+ // connectWebSocket();
148
+
149
+ // return () => {
150
+ // if (wsRef.current) {
151
+ // wsRef.current.close();
152
+ // }
153
+ // };
154
+ // }, []);
155
+
156
+ // 临时替代:只加载一次数据,不连接 WebSocket
157
+ useEffect(() => {
158
+ fetchRecentRequests();
159
+ }, []);
160
+
161
+ // 过滤请求
162
+ const filteredRequests = requests.filter((req) => {
163
+ if (filter.endpoint && !req.endpoint.includes(filter.endpoint)) return false;
164
+ if (filter.status && req.response_status !== parseInt(filter.status)) return false;
165
+ if (filter.search) {
166
+ const searchLower = filter.search.toLowerCase();
167
+ return (
168
+ req.id.toLowerCase().includes(searchLower) ||
169
+ req.session_id?.toLowerCase().includes(searchLower)
170
+ );
171
+ }
172
+ return true;
173
+ });
174
+
175
+ // 导出功能
176
+ const exportRequests = async (format: 'json' | 'csv') => {
177
+ const ids = filteredRequests.map((r) => r.id).join(',');
178
+ const url = `/api/requests/export?format=${format}&ids=${encodeURIComponent(ids)}`;
179
+
180
+ // 创建临时链接触发下载
181
+ const link = document.createElement('a');
182
+ link.href = url;
183
+ link.download = format === 'json' ? 'cc-inspector-requests.json' : 'cc-inspector-requests.csv';
184
+ document.body.appendChild(link);
185
+ link.click();
186
+ document.body.removeChild(link);
187
+ };
188
+
189
+ // 格式化时间
190
+ const formatTime = (isoString: string) => {
191
+ const date = new Date(isoString);
192
+ return date.toLocaleTimeString();
193
+ };
194
+
195
+ // 格式化数字
196
+ const formatNumber = (num: number) => {
197
+ return num.toLocaleString();
198
+ };
199
+
200
+ // 格式化成本
201
+ const formatCost = (cost: number) => {
202
+ if (cost < 0.0001) {
203
+ return `$${cost.toFixed(6)}`;
204
+ } else if (cost < 0.01) {
205
+ return `$${cost.toFixed(5)}`;
206
+ } else if (cost < 1) {
207
+ return `$${cost.toFixed(4)}`;
208
+ } else {
209
+ return `$${cost.toFixed(2)}`;
210
+ }
211
+ };
212
+
213
+ // 获取状态颜色
214
+ const getStatusColor = (status?: number) => {
215
+ if (!status) return 'bg-gray-400';
216
+ if (status >= 200 && status < 300) return 'bg-green-500';
217
+ if (status >= 400 && status < 500) return 'bg-yellow-500';
218
+ return 'bg-red-500';
219
+ };
220
+
221
+ // 获取状态文本
222
+ const getStatusText = (status: number) => {
223
+ const statusText: Record<number, string> = {
224
+ 200: 'OK',
225
+ 201: 'Created',
226
+ 400: 'Bad Request',
227
+ 401: 'Unauthorized',
228
+ 403: 'Forbidden',
229
+ 404: 'Not Found',
230
+ 429: 'Too Many Requests',
231
+ 500: 'Internal Server Error',
232
+ 502: 'Bad Gateway',
233
+ 503: 'Service Unavailable',
234
+ };
235
+ return statusText[status] || '';
236
+ };
237
+
238
+ return (
239
+ <div className="min-h-screen bg-gray-900 text-white">
240
+ {/* 头部 */}
241
+ <header className="bg-gray-800 border-b border-gray-700 px-6 py-4">
242
+ <h1 className="text-2xl font-bold text-blue-400">CC Inspector</h1>
243
+ <p className="text-sm text-gray-400 mt-1">Claude Code 请求监控面板</p>
244
+ </header>
245
+
246
+ {/* 统计卡片 */}
247
+ <div className="grid grid-cols-4 gap-4 px-6 py-4">
248
+ <div className="bg-gray-800 rounded-lg p-4">
249
+ <div className="text-gray-400 text-sm">总请求数</div>
250
+ <div className="text-2xl font-bold">{stats.total}</div>
251
+ </div>
252
+ <div className="bg-gray-800 rounded-lg p-4">
253
+ <div className="text-gray-400 text-sm">Input Tokens</div>
254
+ <div className="text-2xl font-bold text-green-400">{formatNumber(stats.inputTokens)}</div>
255
+ </div>
256
+ <div className="bg-gray-800 rounded-lg p-4">
257
+ <div className="text-gray-400 text-sm">Output Tokens</div>
258
+ <div className="text-2xl font-bold text-blue-400">{formatNumber(stats.outputTokens)}</div>
259
+ </div>
260
+ <div className="bg-gray-800 rounded-lg p-4">
261
+ <div className="text-gray-400 text-sm">总成本 (USD)</div>
262
+ <div className="text-2xl font-bold text-yellow-400">${stats.totalCost.toFixed(4)}</div>
263
+ </div>
264
+ </div>
265
+
266
+ {/* 过滤器 */}
267
+ <div className="px-6 py-2 flex gap-4 items-center">
268
+ <input
269
+ type="text"
270
+ placeholder="搜索 ID 或 Session..."
271
+ value={filter.search}
272
+ onChange={(e) => setFilter({ ...filter, search: e.target.value })}
273
+ className="bg-gray-800 border border-gray-700 rounded px-3 py-1 text-sm flex-1 max-w-xs"
274
+ />
275
+ <input
276
+ type="text"
277
+ placeholder="端点过滤 (如/v1/messages)"
278
+ value={filter.endpoint}
279
+ onChange={(e) => setFilter({ ...filter, endpoint: e.target.value })}
280
+ className="bg-gray-800 border border-gray-700 rounded px-3 py-1 text-sm w-48"
281
+ />
282
+ <select
283
+ value={filter.status}
284
+ onChange={(e) => setFilter({ ...filter, status: e.target.value })}
285
+ className="bg-gray-800 border border-gray-700 rounded px-3 py-1 text-sm w-32"
286
+ >
287
+ <option value="">全部状态</option>
288
+ <option value="200">200 OK</option>
289
+ <option value="400">400</option>
290
+ <option value="401">401</option>
291
+ <option value="429">429</option>
292
+ <option value="500">500</option>
293
+ </select>
294
+ <button
295
+ onClick={() => setFilter({ endpoint: '', status: '', search: '' })}
296
+ className="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 rounded"
297
+ >
298
+ 清除
299
+ </button>
300
+ <div className="flex-1" />
301
+ <button
302
+ onClick={() => exportRequests('json')}
303
+ className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-500 rounded"
304
+ >
305
+ 导出 JSON
306
+ </button>
307
+ <button
308
+ onClick={() => exportRequests('csv')}
309
+ className="px-3 py-1 text-sm bg-green-600 hover:bg-green-500 rounded"
310
+ >
311
+ 导出 CSV
312
+ </button>
313
+ </div>
314
+
315
+ {/* 主内容区 */}
316
+ <div className="flex h-[calc(100vh-200px)]">
317
+ {/* 请求列表 */}
318
+ <div className="w-1/2 border-r border-gray-700 overflow-auto">
319
+ <div className="px-4 py-2 text-xs text-gray-500">
320
+ 显示 {filteredRequests.length} / {requests.length} 个请求
321
+ </div>
322
+ <table className="w-full">
323
+ <thead className="bg-gray-800 sticky top-0">
324
+ <tr>
325
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">状态</th>
326
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">端点</th>
327
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">模型</th>
328
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">Tokens</th>
329
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">延迟</th>
330
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">成本</th>
331
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-400 uppercase">时间</th>
332
+ </tr>
333
+ </thead>
334
+ <tbody>
335
+ {filteredRequests.map((req) => (
336
+ <tr
337
+ key={req.id}
338
+ className={`border-b border-gray-800 cursor-pointer hover:bg-gray-800 ${
339
+ selectedRequest?.id === req.id ? 'bg-gray-700' : ''
340
+ }`}
341
+ onClick={() => setSelectedRequest(req)}
342
+ >
343
+ <td className="px-4 py-3">
344
+ <span className={`inline-block w-3 h-3 rounded-full ${getStatusColor(req.response_status)}`} />
345
+ </td>
346
+ <td className="px-4 py-3 text-sm">{req.endpoint}</td>
347
+ <td className="px-4 py-3 text-sm text-gray-400">
348
+ {req.model || '-'}
349
+ </td>
350
+ <td className="px-4 py-3 text-sm">
351
+ <span className="text-green-400">{formatNumber(req.input_tokens)}</span>
352
+ <span className="text-gray-500 mx-1">/</span>
353
+ <span className="text-blue-400">{formatNumber(req.output_tokens)}</span>
354
+ </td>
355
+ <td className="px-4 py-3 text-sm text-gray-400">
356
+ {req.latency_ms ? `${req.latency_ms}ms` : '-'}
357
+ </td>
358
+ <td className="px-4 py-3 text-sm text-yellow-400">
359
+ {req.cost_usd ? formatCost(req.cost_usd) : '-'}
360
+ </td>
361
+ <td className="px-4 py-3 text-sm text-gray-400">{formatTime(req.created_at)}</td>
362
+ </tr>
363
+ ))}
364
+ </tbody>
365
+ </table>
366
+ {filteredRequests.length === 0 && (
367
+ <div className="text-center py-12 text-gray-500">
368
+ {requests.length === 0 ? '暂无请求,请在 Claude Code 中发起请求' : '没有符合过滤条件的请求'}
369
+ </div>
370
+ )}
371
+ </div>
372
+
373
+ {/* 请求详情 */}
374
+ <div className="w-1/2 overflow-auto p-4">
375
+ {selectedRequest ? (
376
+ <div>
377
+ <div className="flex items-center justify-between mb-4">
378
+ <h2 className="text-lg font-semibold">请求详情</h2>
379
+ <div className="flex items-center gap-2">
380
+ <span className={`inline-block w-3 h-3 rounded-full ${getStatusColor(selectedRequest.response_status)}`} />
381
+ <span className="text-sm">{selectedRequest.response_status || 'Pending'}</span>
382
+ </div>
383
+ </div>
384
+
385
+ {/* 基本信息 */}
386
+ <div className="bg-gray-800 rounded-lg p-4 mb-4">
387
+ <div className="grid grid-cols-2 gap-4 text-sm">
388
+ <div>
389
+ <span className="text-gray-400">Model:</span>
390
+ <span className="ml-2 font-mono">{selectedRequest.model || '-'}</span>
391
+ </div>
392
+ <div>
393
+ <span className="text-gray-400">Latency:</span>
394
+ <span className="ml-2 font-mono">{selectedRequest.latency_ms ? `${selectedRequest.latency_ms}ms` : '-'}</span>
395
+ </div>
396
+ <div>
397
+ <span className="text-gray-400">Input:</span>
398
+ <span className="ml-2 text-green-400 font-mono">{formatNumber(selectedRequest.input_tokens)}</span>
399
+ </div>
400
+ <div>
401
+ <span className="text-gray-400">Output:</span>
402
+ <span className="ml-2 text-blue-400 font-mono">{formatNumber(selectedRequest.output_tokens)}</span>
403
+ </div>
404
+ <div>
405
+ <span className="text-gray-400">Cost:</span>
406
+ <span className="ml-2 text-yellow-400 font-mono">{selectedRequest.cost_usd ? formatCost(selectedRequest.cost_usd) : '-'}</span>
407
+ </div>
408
+ <div>
409
+ <span className="text-gray-400">Session:</span>
410
+ <span className="ml-2 font-mono text-xs">{selectedRequest.session_id?.slice(0, 8) || '-'}</span>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ {/* Tab 导航 */}
416
+ <div className="flex border-b border-gray-700 mb-4">
417
+ <button
418
+ onClick={() => setActiveTab('request')}
419
+ className={`px-4 py-2 text-sm ${activeTab === 'request' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:text-white'}`}
420
+ >
421
+ Request
422
+ </button>
423
+ <button
424
+ onClick={() => setActiveTab('response')}
425
+ className={`px-4 py-2 text-sm ${activeTab === 'response' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:text-white'}`}
426
+ >
427
+ Response
428
+ </button>
429
+ <button
430
+ onClick={() => setActiveTab('streaming')}
431
+ className={`px-4 py-2 text-sm ${activeTab === 'streaming' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:text-white'}`}
432
+ >
433
+ Streaming
434
+ </button>
435
+ <button
436
+ onClick={() => setActiveTab('overview')}
437
+ className={`px-4 py-2 text-sm ${activeTab === 'overview' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-gray-400 hover:text-white'}`}
438
+ >
439
+ Overview
440
+ </button>
441
+ </div>
442
+
443
+ {/* Tab 内容 */}
444
+ {detailLoading ? (
445
+ <div className="text-center py-8 text-gray-400">Loading...</div>
446
+ ) : (
447
+ <>
448
+ {/* Request Tab */}
449
+ {activeTab === 'request' && requestDetail && (
450
+ <div className="space-y-4">
451
+ <div>
452
+ <div className="text-gray-400 text-xs mb-1">Request Body</div>
453
+ <JsonViewer data={requestDetail.request_body} maxHeight="400px" onExpand={handleExpandModal} />
454
+ </div>
455
+ <div>
456
+ <div className="text-gray-400 text-xs mb-1">Request Headers</div>
457
+ <JsonViewer data={requestDetail.request_headers} maxHeight="200px" onExpand={handleExpandModal} />
458
+ </div>
459
+ </div>
460
+ )}
461
+
462
+ {/* Response Tab */}
463
+ {activeTab === 'response' && requestDetail && (
464
+ <div className="space-y-4">
465
+ {requestDetail.error_message && (
466
+ <div className="bg-red-900/30 border border-red-700 rounded p-3 text-red-400 text-sm">
467
+ Error: {requestDetail.error_message}
468
+ </div>
469
+ )}
470
+ <div>
471
+ <div className="text-gray-400 text-xs mb-1">Response Body</div>
472
+ <JsonViewer data={requestDetail.response_body} maxHeight="400px" onExpand={handleExpandModal} />
473
+ </div>
474
+ <div>
475
+ <div className="text-gray-400 text-xs mb-1">Response Headers</div>
476
+ <JsonViewer data={requestDetail.response_headers} maxHeight="200px" onExpand={handleExpandModal} />
477
+ </div>
478
+ </div>
479
+ )}
480
+
481
+ {/* Streaming Tab */}
482
+ {activeTab === 'streaming' && requestDetail && (
483
+ <div>
484
+ <div className="text-gray-400 text-xs mb-1">Streaming Events ({requestDetail.streaming_events?.length || 0})</div>
485
+ {requestDetail.streaming_events && requestDetail.streaming_events.length > 0 ? (
486
+ <div className="space-y-2 max-h-[500px] overflow-auto">
487
+ {requestDetail.streaming_events.map((event: any, idx: number) => (
488
+ <div key={idx} className="bg-gray-800 rounded p-2">
489
+ <div className="text-xs text-gray-500 mb-1">Event #{idx + 1}</div>
490
+ <JsonViewer data={event} maxHeight="200px" onExpand={handleExpandModal} />
491
+ </div>
492
+ ))}
493
+ </div>
494
+ ) : (
495
+ <div className="text-gray-500 text-sm">No streaming events</div>
496
+ )}
497
+ </div>
498
+ )}
499
+
500
+ {/* Overview Tab */}
501
+ {activeTab === 'overview' && (
502
+ <div className="space-y-4">
503
+ <div className="bg-gray-800 rounded-lg p-4">
504
+ <h3 className="text-sm font-semibold mb-3">Token 统计</h3>
505
+ <div className="grid grid-cols-2 gap-4">
506
+ <div>
507
+ <div className="text-gray-400 text-xs">Input Tokens</div>
508
+ <div className="text-green-400 font-mono text-lg">{formatNumber(selectedRequest.input_tokens)}</div>
509
+ </div>
510
+ <div>
511
+ <div className="text-gray-400 text-xs">Output Tokens</div>
512
+ <div className="text-blue-400 font-mono text-lg">{formatNumber(selectedRequest.output_tokens)}</div>
513
+ </div>
514
+ <div>
515
+ <div className="text-gray-400 text-xs">Cache Read</div>
516
+ <div className="text-purple-400 font-mono text-lg">{formatNumber(selectedRequest.cache_read_tokens)}</div>
517
+ </div>
518
+ <div>
519
+ <div className="text-gray-400 text-xs">Cache Creation</div>
520
+ <div className="text-orange-400 font-mono text-lg">{formatNumber(selectedRequest.cache_creation_tokens)}</div>
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ <div className="bg-gray-800 rounded-lg p-4">
526
+ <h3 className="text-sm font-semibold mb-3">性能指标</h3>
527
+ <div className="grid grid-cols-2 gap-4">
528
+ <div>
529
+ <div className="text-gray-400 text-xs">Latency</div>
530
+ <div className="font-mono text-lg">{selectedRequest.latency_ms ? `${selectedRequest.latency_ms}ms` : '-'}</div>
531
+ </div>
532
+ <div>
533
+ <div className="text-gray-400 text-xs">First Token</div>
534
+ <div className="font-mono text-lg">{selectedRequest.first_token_ms ? `${selectedRequest.first_token_ms}ms` : '-'}</div>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <div className="text-xs text-gray-500">
540
+ <div>Request ID: {selectedRequest.id}</div>
541
+ <div>Created: {selectedRequest.created_at}</div>
542
+ </div>
543
+ </div>
544
+ )}
545
+ </>
546
+ )}
547
+ </div>
548
+ ) : (
549
+ <div className="text-center py-12 text-gray-500">
550
+ 选择一个请求查看详情
551
+ </div>
552
+ )}
553
+ </div>
554
+ </div>
555
+
556
+ {/* JSON 浮窗 - 使用 key 防止重新挂载 */}
557
+ {modalSnapshot && (
558
+ <JsonModal key={modalSnapshot ? 'modal-active' : 'modal-closed'} data={modalSnapshot} onClose={handleCloseModal} allowClickAway={true} />
559
+ )}
560
+ </div>
561
+ );
562
+ }
Binary file
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #ffffff;
5
+ --foreground: #171717;
6
+ }
7
+
8
+ @theme inline {
9
+ --color-background: var(--background);
10
+ --color-foreground: var(--foreground);
11
+ --font-sans: var(--font-geist-sans);
12
+ --font-mono: var(--font-geist-mono);
13
+ }
14
+
15
+ @media (prefers-color-scheme: dark) {
16
+ :root {
17
+ --background: #0a0a0a;
18
+ --foreground: #ededed;
19
+ }
20
+ }
21
+
22
+ body {
23
+ background: var(--background);
24
+ color: var(--foreground);
25
+ font-family: Arial, Helvetica, sans-serif;
26
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,34 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Create Next App",
17
+ description: "Generated by create next app",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en">
27
+ <body
28
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29
+ >
30
+ {children}
31
+ </body>
32
+ </html>
33
+ );
34
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function Home() {
4
+ redirect('/dashboard');
5
+ }
@@ -0,0 +1,30 @@
1
+ import { handleMessages } from '@/lib/proxy/handlers';
2
+
3
+ /**
4
+ * 处理 Claude API /v1/messages 请求
5
+ * 这是 Claude Code 请求的入口点
6
+ */
7
+ export async function POST(request: Request) {
8
+ try {
9
+ // 提取请求头
10
+ const headers: Record<string, string> = {};
11
+ request.headers.forEach((value, key) => {
12
+ headers[key] = value;
13
+ });
14
+
15
+ const body = await request.json();
16
+
17
+ // 转发到处理器
18
+ return handleMessages(request, body, headers);
19
+ } catch (error: any) {
20
+ console.error('/v1/messages error:', error);
21
+ console.error('Stack:', error?.stack);
22
+ return new Response(
23
+ JSON.stringify({ error: error?.message || 'Proxy error', stack: error?.stack }),
24
+ {
25
+ status: 500,
26
+ headers: { 'Content-Type': 'application/json' },
27
+ }
28
+ );
29
+ }
30
+ }