aicodeswitch 1.4.1 → 1.5.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/CLAUDE.md +1 -0
- package/dist/server/version-check.js +1 -2
- package/package.json +2 -2
- package/src/server/auth.ts +79 -0
- package/src/server/database.ts +809 -0
- package/src/server/main.ts +514 -0
- package/src/server/proxy-server.ts +1301 -0
- package/src/server/transformers/chunk-collector.ts +202 -0
- package/src/server/transformers/claude-openai.ts +261 -0
- package/src/server/transformers/openai-responses.ts +440 -0
- package/src/server/transformers/streaming.ts +775 -0
- package/src/server/version-check.ts +108 -0
- package/src/types/index.ts +217 -0
- package/src/ui/App.tsx +342 -0
- package/src/ui/api/client.ts +179 -0
- package/src/ui/components/JSONViewer.tsx +89 -0
- package/src/ui/constants/index.ts +4 -0
- package/src/ui/docs/vendors-recommand.md +13 -0
- package/src/ui/main.tsx +10 -0
- package/src/ui/pages/LogsPage.tsx +702 -0
- package/src/ui/pages/RoutesPage.tsx +552 -0
- package/src/ui/pages/SettingsPage.tsx +206 -0
- package/src/ui/pages/StatisticsPage.tsx +620 -0
- package/src/ui/pages/UsagePage.tsx +13 -0
- package/src/ui/pages/VendorsPage.tsx +490 -0
- package/src/ui/pages/WriteConfigPage.tsx +198 -0
- package/src/ui/styles/App.css +831 -0
- package/src/ui/styles/index.css +137 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { api } from '../api/client';
|
|
3
|
+
import type { RequestLog, AccessLog, ErrorLog, Vendor, APIService } from '../../types';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import JSONViewer from '../components/JSONViewer';
|
|
6
|
+
import { TARGET_TYPE } from '../constants';
|
|
7
|
+
|
|
8
|
+
type LogTab = 'request' | 'access' | 'error';
|
|
9
|
+
|
|
10
|
+
function LogsPage() {
|
|
11
|
+
const [activeTab, setActiveTab] = useState<LogTab>('request');
|
|
12
|
+
const [requestLogs, setRequestLogs] = useState<RequestLog[]>([]);
|
|
13
|
+
const [accessLogs, setAccessLogs] = useState<AccessLog[]>([]);
|
|
14
|
+
const [errorLogs, setErrorLogs] = useState<ErrorLog[]>([]);
|
|
15
|
+
const [selectedRequestLog, setSelectedRequestLog] = useState<RequestLog | null>(null);
|
|
16
|
+
const [selectedAccessLog, setSelectedAccessLog] = useState<AccessLog | null>(null);
|
|
17
|
+
const [selectedErrorLog, setSelectedErrorLog] = useState<ErrorLog | null>(null);
|
|
18
|
+
const [chunksExpanded, setChunksExpanded] = useState<boolean>(false);
|
|
19
|
+
const limit = 100;
|
|
20
|
+
const offset = 0;
|
|
21
|
+
|
|
22
|
+
// 筛选器相关state
|
|
23
|
+
const [vendors, setVendors] = useState<Vendor[]>([]);
|
|
24
|
+
const [services, setServices] = useState<APIService[]>([]);
|
|
25
|
+
const [filterTargetType, setFilterTargetType] = useState<string>('');
|
|
26
|
+
const [filterVendorId, setFilterVendorId] = useState<string>('');
|
|
27
|
+
const [filterServiceId, setFilterServiceId] = useState<string>('');
|
|
28
|
+
const [filterModel, setFilterModel] = useState<string>('');
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
loadLogs();
|
|
32
|
+
loadVendorsAndServices();
|
|
33
|
+
}, [activeTab]);
|
|
34
|
+
|
|
35
|
+
const loadLogs = async () => {
|
|
36
|
+
if (activeTab === 'request') {
|
|
37
|
+
const data = await api.getLogs(limit, offset);
|
|
38
|
+
setRequestLogs(data);
|
|
39
|
+
} else if (activeTab === 'access') {
|
|
40
|
+
const data = await api.getAccessLogs(limit, offset);
|
|
41
|
+
setAccessLogs(data);
|
|
42
|
+
} else if (activeTab === 'error') {
|
|
43
|
+
const data = await api.getErrorLogs(limit, offset);
|
|
44
|
+
setErrorLogs(data);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const loadVendorsAndServices = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const [vendorsData, servicesData] = await Promise.all([
|
|
51
|
+
api.getVendors(),
|
|
52
|
+
api.getAPIServices()
|
|
53
|
+
]);
|
|
54
|
+
setVendors(vendorsData);
|
|
55
|
+
setServices(servicesData);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Failed to load vendors and services:', error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleClearLogs = async () => {
|
|
62
|
+
if (confirm('确定要清空当前类型的所有日志吗?')) {
|
|
63
|
+
if (activeTab === 'request') {
|
|
64
|
+
await api.clearLogs();
|
|
65
|
+
setRequestLogs([]);
|
|
66
|
+
setSelectedRequestLog(null);
|
|
67
|
+
} else if (activeTab === 'access') {
|
|
68
|
+
await api.clearAccessLogs();
|
|
69
|
+
setAccessLogs([]);
|
|
70
|
+
setSelectedAccessLog(null);
|
|
71
|
+
} else if (activeTab === 'error') {
|
|
72
|
+
await api.clearErrorLogs();
|
|
73
|
+
setErrorLogs([]);
|
|
74
|
+
setSelectedErrorLog(null);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const getStatusBadge = (statusCode: number | undefined) => {
|
|
80
|
+
if (!statusCode) return 'badge-danger';
|
|
81
|
+
if (statusCode >= 200 && statusCode < 300) return 'badge-success';
|
|
82
|
+
if (statusCode >= 400 && statusCode < 500) return 'badge-warning';
|
|
83
|
+
return 'badge-danger';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 筛选相关函数
|
|
87
|
+
const handleVendorChange = (vendorId: string) => {
|
|
88
|
+
setFilterVendorId(vendorId);
|
|
89
|
+
setFilterServiceId(''); // 重置服务选择
|
|
90
|
+
setFilterModel(''); // 重置模型选择
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleServiceChange = (serviceId: string) => {
|
|
94
|
+
setFilterServiceId(serviceId);
|
|
95
|
+
setFilterModel(''); // 重置模型选择
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getFilteredServices = () => {
|
|
99
|
+
if (!filterVendorId) return [];
|
|
100
|
+
return services.filter(s => s.vendorId === filterVendorId);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getAvailableModels = () => {
|
|
104
|
+
if (!filterServiceId) return [];
|
|
105
|
+
const service = services.find(s => s.id === filterServiceId);
|
|
106
|
+
return service?.supportedModels || [];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const filterRequestLogs = (logs: RequestLog[]) => {
|
|
110
|
+
return logs.filter(log => {
|
|
111
|
+
if (filterTargetType && log.targetType !== filterTargetType) return false;
|
|
112
|
+
if (filterVendorId && log.vendorId !== filterVendorId) return false;
|
|
113
|
+
if (filterServiceId && log.targetServiceId !== filterServiceId) return false;
|
|
114
|
+
if (filterModel && log.targetModel !== filterModel) return false;
|
|
115
|
+
return true;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const filteredRequestLogs = filterRequestLogs(requestLogs);
|
|
120
|
+
|
|
121
|
+
const renderRequestLogs = () => {
|
|
122
|
+
if (filteredRequestLogs.length === 0) {
|
|
123
|
+
return <div className="empty-state"><p>暂无请求日志</p></div>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<table>
|
|
128
|
+
<thead>
|
|
129
|
+
<tr>
|
|
130
|
+
<th>时间</th>
|
|
131
|
+
<th>来源对象类型</th>
|
|
132
|
+
<th>路径</th>
|
|
133
|
+
<th>状态</th>
|
|
134
|
+
<th>响应时间</th>
|
|
135
|
+
<th>Tokens信息</th>
|
|
136
|
+
<th>操作</th>
|
|
137
|
+
</tr>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>
|
|
140
|
+
{filteredRequestLogs.map((log) => (
|
|
141
|
+
<tr key={log.id}>
|
|
142
|
+
<td>{dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')}</td>
|
|
143
|
+
<td>
|
|
144
|
+
{TARGET_TYPE[log.targetType!] ? (
|
|
145
|
+
<span className="badge badge-info">
|
|
146
|
+
{TARGET_TYPE[log.targetType!]}
|
|
147
|
+
</span>
|
|
148
|
+
) : '-'}
|
|
149
|
+
</td>
|
|
150
|
+
<td>{log.path}</td>
|
|
151
|
+
<td>
|
|
152
|
+
<span className={`badge ${getStatusBadge(log.statusCode)}`}>
|
|
153
|
+
{log.statusCode || 'Error'}
|
|
154
|
+
</span>
|
|
155
|
+
</td>
|
|
156
|
+
<td>{log.responseTime ? `${log.responseTime}ms` : '-'}</td>
|
|
157
|
+
<td>
|
|
158
|
+
{log.usage ? (
|
|
159
|
+
<span>
|
|
160
|
+
{log.usage.totalTokens ? log.usage.totalTokens : log.usage.inputTokens + log.usage.outputTokens} tokens
|
|
161
|
+
</span>
|
|
162
|
+
) : '-'}
|
|
163
|
+
</td>
|
|
164
|
+
<td>
|
|
165
|
+
<button className="btn btn-secondary" onClick={() => setSelectedRequestLog(log)}>详情</button>
|
|
166
|
+
</td>
|
|
167
|
+
</tr>
|
|
168
|
+
))}
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const renderAccessLogs = () => {
|
|
175
|
+
if (accessLogs.length === 0) {
|
|
176
|
+
return <div className="empty-state"><p>暂无访问日志</p></div>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<table>
|
|
181
|
+
<thead>
|
|
182
|
+
<tr>
|
|
183
|
+
<th>时间</th>
|
|
184
|
+
<th>方法</th>
|
|
185
|
+
<th>路径</th>
|
|
186
|
+
<th>状态</th>
|
|
187
|
+
<th>响应时间</th>
|
|
188
|
+
<th>客户端IP</th>
|
|
189
|
+
<th>操作</th>
|
|
190
|
+
</tr>
|
|
191
|
+
</thead>
|
|
192
|
+
<tbody>
|
|
193
|
+
{accessLogs.map((log) => (
|
|
194
|
+
<tr key={log.id}>
|
|
195
|
+
<td>{dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')}</td>
|
|
196
|
+
<td><span className="badge badge-success">{log.method}</span></td>
|
|
197
|
+
<td>{log.path}</td>
|
|
198
|
+
<td>
|
|
199
|
+
<span className={`badge ${getStatusBadge(log.statusCode)}`}>
|
|
200
|
+
{log.statusCode || '-'}
|
|
201
|
+
</span>
|
|
202
|
+
</td>
|
|
203
|
+
<td>{log.responseTime ? `${log.responseTime}ms` : '-'}</td>
|
|
204
|
+
<td style={{ fontSize: '12px' }}>{log.clientIp || '-'}</td>
|
|
205
|
+
<td>
|
|
206
|
+
<button className="btn btn-secondary" onClick={() => setSelectedAccessLog(log)}>详情</button>
|
|
207
|
+
</td>
|
|
208
|
+
</tr>
|
|
209
|
+
))}
|
|
210
|
+
</tbody>
|
|
211
|
+
</table>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const renderErrorLogs = () => {
|
|
216
|
+
if (errorLogs.length === 0) {
|
|
217
|
+
return <div className="empty-state"><p>暂无错误日志</p></div>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<table>
|
|
222
|
+
<thead>
|
|
223
|
+
<tr>
|
|
224
|
+
<th>时间</th>
|
|
225
|
+
<th>方法</th>
|
|
226
|
+
<th>路径</th>
|
|
227
|
+
<th>状态</th>
|
|
228
|
+
<th>错误信息</th>
|
|
229
|
+
<th>操作</th>
|
|
230
|
+
</tr>
|
|
231
|
+
</thead>
|
|
232
|
+
<tbody>
|
|
233
|
+
{errorLogs.map((log) => (
|
|
234
|
+
<tr key={log.id}>
|
|
235
|
+
<td>{dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss')}</td>
|
|
236
|
+
<td><span className="badge badge-danger">{log.method}</span></td>
|
|
237
|
+
<td>{log.path}</td>
|
|
238
|
+
<td>
|
|
239
|
+
<span className="badge badge-danger">
|
|
240
|
+
{log.statusCode || '-'}
|
|
241
|
+
</span>
|
|
242
|
+
</td>
|
|
243
|
+
<td style={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: '#e74c3c' }}>
|
|
244
|
+
{log.errorMessage}
|
|
245
|
+
</td>
|
|
246
|
+
<td>
|
|
247
|
+
<button className="btn btn-secondary" onClick={() => setSelectedErrorLog(log)}>详情</button>
|
|
248
|
+
</td>
|
|
249
|
+
</tr>
|
|
250
|
+
))}
|
|
251
|
+
</tbody>
|
|
252
|
+
</table>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div>
|
|
258
|
+
<div className="page-header">
|
|
259
|
+
<h1>请求日志</h1>
|
|
260
|
+
<p>查看所有API请求日志</p>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div className="card">
|
|
264
|
+
<div style={{ borderBottom: '1px solid #ecf0f1', marginBottom: '20px' }}>
|
|
265
|
+
<div style={{ display: 'flex', gap: '0' }}>
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => setActiveTab('request')}
|
|
268
|
+
style={{
|
|
269
|
+
padding: '12px 24px',
|
|
270
|
+
border: 'none',
|
|
271
|
+
background: activeTab === 'request' ? '#3498db' : 'transparent',
|
|
272
|
+
color: activeTab === 'request' ? 'white' : '#7f8c8d',
|
|
273
|
+
cursor: 'pointer',
|
|
274
|
+
borderBottom: activeTab === 'request' ? '2px solid #2980b9' : '2px solid transparent',
|
|
275
|
+
fontWeight: activeTab === 'request' ? 'bold' : 'normal',
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
请求日志 ({requestLogs.length})
|
|
279
|
+
</button>
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => setActiveTab('access')}
|
|
282
|
+
style={{
|
|
283
|
+
padding: '12px 24px',
|
|
284
|
+
border: 'none',
|
|
285
|
+
background: activeTab === 'access' ? '#3498db' : 'transparent',
|
|
286
|
+
color: activeTab === 'access' ? 'white' : '#7f8c8d',
|
|
287
|
+
cursor: 'pointer',
|
|
288
|
+
borderBottom: activeTab === 'access' ? '2px solid #2980b9' : '2px solid transparent',
|
|
289
|
+
fontWeight: activeTab === 'access' ? 'bold' : 'normal',
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
访问日志 ({accessLogs.length})
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
onClick={() => setActiveTab('error')}
|
|
296
|
+
style={{
|
|
297
|
+
padding: '12px 24px',
|
|
298
|
+
border: 'none',
|
|
299
|
+
background: activeTab === 'error' ? '#e74c3c' : 'transparent',
|
|
300
|
+
color: activeTab === 'error' ? 'white' : '#7f8c8d',
|
|
301
|
+
cursor: 'pointer',
|
|
302
|
+
borderBottom: activeTab === 'error' ? '2px solid #c0392b' : '2px solid transparent',
|
|
303
|
+
fontWeight: activeTab === 'error' ? 'bold' : 'normal',
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
错误日志 ({errorLogs.length})
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<div className="toolbar">
|
|
312
|
+
<h3>
|
|
313
|
+
{activeTab === 'access' && '访问日志列表'}
|
|
314
|
+
{activeTab === 'error' && '错误日志列表'}
|
|
315
|
+
{activeTab === 'request' && '请求日志列表'}
|
|
316
|
+
</h3>
|
|
317
|
+
<div style={{ display: 'flex', gap: '10px' }}>
|
|
318
|
+
<button className="btn btn-primary" onClick={loadLogs}>刷新</button>
|
|
319
|
+
<button className="btn btn-danger" onClick={handleClearLogs}>清空日志</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* 筛选器 - 仅在请求日志tab显示 */}
|
|
324
|
+
{activeTab === 'request' && (
|
|
325
|
+
<div style={{
|
|
326
|
+
padding: '15px',
|
|
327
|
+
background: 'var(--bg-secondary)',
|
|
328
|
+
borderRadius: '12px',
|
|
329
|
+
marginBottom: '20px',
|
|
330
|
+
display: 'flex',
|
|
331
|
+
gap: '15px',
|
|
332
|
+
alignItems: 'center',
|
|
333
|
+
flexWrap: 'wrap',
|
|
334
|
+
border: '1px solid var(--border-primary)'
|
|
335
|
+
}}>
|
|
336
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
337
|
+
<label style={{ fontWeight: 'bold', minWidth: '80px', color: 'var(--text-primary)' }}>来源类型:</label>
|
|
338
|
+
<select
|
|
339
|
+
value={filterTargetType}
|
|
340
|
+
onChange={(e) => setFilterTargetType(e.target.value)}
|
|
341
|
+
style={{
|
|
342
|
+
padding: '6px 10px',
|
|
343
|
+
borderRadius: '8px',
|
|
344
|
+
border: '1px solid var(--border-primary)',
|
|
345
|
+
background: 'var(--bg-card)',
|
|
346
|
+
color: 'var(--text-primary)'
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
<option value="">全部</option>
|
|
350
|
+
<option value="claude-code">Claude Code</option>
|
|
351
|
+
<option value="codex">Codex</option>
|
|
352
|
+
</select>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
356
|
+
<label style={{ fontWeight: 'bold', minWidth: '80px', color: 'var(--text-primary)' }}>供应商:</label>
|
|
357
|
+
<select
|
|
358
|
+
value={filterVendorId}
|
|
359
|
+
onChange={(e) => handleVendorChange(e.target.value)}
|
|
360
|
+
style={{
|
|
361
|
+
padding: '6px 10px',
|
|
362
|
+
borderRadius: '8px',
|
|
363
|
+
border: '1px solid var(--border-primary)',
|
|
364
|
+
minWidth: '150px',
|
|
365
|
+
background: 'var(--bg-card)',
|
|
366
|
+
color: 'var(--text-primary)'
|
|
367
|
+
}}
|
|
368
|
+
>
|
|
369
|
+
<option value="">全部供应商</option>
|
|
370
|
+
{vendors.map(vendor => (
|
|
371
|
+
<option key={vendor.id} value={vendor.id}>{vendor.name}</option>
|
|
372
|
+
))}
|
|
373
|
+
</select>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
377
|
+
<label style={{ fontWeight: 'bold', minWidth: '80px', color: 'var(--text-primary)' }}>API服务:</label>
|
|
378
|
+
<select
|
|
379
|
+
value={filterServiceId}
|
|
380
|
+
onChange={(e) => handleServiceChange(e.target.value)}
|
|
381
|
+
disabled={!filterVendorId}
|
|
382
|
+
style={{
|
|
383
|
+
padding: '6px 10px',
|
|
384
|
+
borderRadius: '8px',
|
|
385
|
+
border: '1px solid var(--border-primary)',
|
|
386
|
+
minWidth: '150px',
|
|
387
|
+
background: filterVendorId ? 'var(--bg-card)' : 'var(--bg-secondary)',
|
|
388
|
+
color: 'var(--text-primary)',
|
|
389
|
+
cursor: filterVendorId ? 'pointer' : 'not-allowed',
|
|
390
|
+
opacity: filterVendorId ? 1 : 0.6
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
<option value="">全部服务</option>
|
|
394
|
+
{getFilteredServices().map(service => (
|
|
395
|
+
<option key={service.id} value={service.id}>{service.name}</option>
|
|
396
|
+
))}
|
|
397
|
+
</select>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
401
|
+
<label style={{ fontWeight: 'bold', minWidth: '80px', color: 'var(--text-primary)' }}>模型:</label>
|
|
402
|
+
<select
|
|
403
|
+
value={filterModel}
|
|
404
|
+
onChange={(e) => setFilterModel(e.target.value)}
|
|
405
|
+
disabled={!filterServiceId}
|
|
406
|
+
style={{
|
|
407
|
+
padding: '6px 10px',
|
|
408
|
+
borderRadius: '8px',
|
|
409
|
+
border: '1px solid var(--border-primary)',
|
|
410
|
+
minWidth: '150px',
|
|
411
|
+
background: filterServiceId ? 'var(--bg-card)' : 'var(--bg-secondary)',
|
|
412
|
+
color: 'var(--text-primary)',
|
|
413
|
+
cursor: filterServiceId ? 'pointer' : 'not-allowed',
|
|
414
|
+
opacity: filterServiceId ? 1 : 0.6
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
<option value="">全部模型</option>
|
|
418
|
+
{getAvailableModels().map(model => (
|
|
419
|
+
<option key={model} value={model}>{model}</option>
|
|
420
|
+
))}
|
|
421
|
+
</select>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{(filterTargetType || filterVendorId || filterServiceId || filterModel) && (
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => {
|
|
427
|
+
setFilterTargetType('');
|
|
428
|
+
setFilterVendorId('');
|
|
429
|
+
setFilterServiceId('');
|
|
430
|
+
setFilterModel('');
|
|
431
|
+
}}
|
|
432
|
+
style={{
|
|
433
|
+
padding: '6px 12px',
|
|
434
|
+
borderRadius: '8px',
|
|
435
|
+
border: '1px solid var(--accent-danger)',
|
|
436
|
+
background: 'var(--accent-danger)',
|
|
437
|
+
color: 'white',
|
|
438
|
+
cursor: 'pointer',
|
|
439
|
+
fontSize: '14px'
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
清除筛选
|
|
443
|
+
</button>
|
|
444
|
+
)}
|
|
445
|
+
|
|
446
|
+
<div style={{ marginLeft: 'auto', color: 'var(--text-muted)', fontSize: '14px' }}>
|
|
447
|
+
显示 {filteredRequestLogs.length} / {requestLogs.length} 条
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
|
|
452
|
+
{activeTab === 'request' && renderRequestLogs()}
|
|
453
|
+
{activeTab === 'access' && renderAccessLogs()}
|
|
454
|
+
{activeTab === 'error' && renderErrorLogs()}
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{selectedRequestLog && (
|
|
458
|
+
<div className="modal-overlay" onClick={() => setSelectedRequestLog(null)}>
|
|
459
|
+
<div className="modal" onClick={(e) => e.stopPropagation()} style={{ minWidth: '600px', maxHeight: '90vh', overflowY: 'auto' }}>
|
|
460
|
+
<div className="modal-header">
|
|
461
|
+
<h2>请求详情</h2>
|
|
462
|
+
</div>
|
|
463
|
+
<div>
|
|
464
|
+
<div className="form-group">
|
|
465
|
+
<label>日志ID</label>
|
|
466
|
+
<input type="text" value={selectedRequestLog.id} readOnly />
|
|
467
|
+
</div>
|
|
468
|
+
<div className="form-group">
|
|
469
|
+
<label>时间</label>
|
|
470
|
+
<input type="text" value={dayjs(selectedRequestLog.timestamp).format('YYYY-MM-DD HH:mm:ss')} readOnly />
|
|
471
|
+
</div>
|
|
472
|
+
{selectedRequestLog.targetType && (
|
|
473
|
+
<div className="form-group">
|
|
474
|
+
<label>来源对象类型</label>
|
|
475
|
+
<input type="text" value={TARGET_TYPE[selectedRequestLog.targetType] || '-'} readOnly />
|
|
476
|
+
</div>
|
|
477
|
+
)}
|
|
478
|
+
{selectedRequestLog.requestModel && (
|
|
479
|
+
<div className="form-group">
|
|
480
|
+
<label>请求模型</label>
|
|
481
|
+
<input type="text" value={selectedRequestLog.requestModel} readOnly />
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
{selectedRequestLog.vendorName && (
|
|
485
|
+
<div className="form-group">
|
|
486
|
+
<label>供应商</label>
|
|
487
|
+
<input type="text" value={selectedRequestLog.vendorName} readOnly />
|
|
488
|
+
</div>
|
|
489
|
+
)}
|
|
490
|
+
{selectedRequestLog.targetServiceName && (
|
|
491
|
+
<div className="form-group">
|
|
492
|
+
<label>供应商API服务</label>
|
|
493
|
+
<input type="text" value={selectedRequestLog.targetServiceName} readOnly />
|
|
494
|
+
</div>
|
|
495
|
+
)}
|
|
496
|
+
{selectedRequestLog.targetModel && (
|
|
497
|
+
<div className="form-group">
|
|
498
|
+
<label>供应商模型</label>
|
|
499
|
+
<input type="text" value={selectedRequestLog.targetModel} readOnly />
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
<div className="form-group">
|
|
503
|
+
<label>请求方法</label>
|
|
504
|
+
<input type="text" value={selectedRequestLog.method} readOnly />
|
|
505
|
+
</div>
|
|
506
|
+
<div className="form-group">
|
|
507
|
+
<label>请求路径</label>
|
|
508
|
+
<input type="text" value={selectedRequestLog.path} readOnly />
|
|
509
|
+
</div>
|
|
510
|
+
{selectedRequestLog.headers && (
|
|
511
|
+
<div className="form-group">
|
|
512
|
+
<label>请求头</label>
|
|
513
|
+
<JSONViewer data={selectedRequestLog.headers} collapsed />
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
{selectedRequestLog.body && (
|
|
517
|
+
<div className="form-group">
|
|
518
|
+
<label>请求体</label>
|
|
519
|
+
<JSONViewer data={selectedRequestLog.body} />
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
<div className="form-group">
|
|
523
|
+
<label>状态码</label>
|
|
524
|
+
<input type="text" value={selectedRequestLog.statusCode || 'Error'} readOnly />
|
|
525
|
+
</div>
|
|
526
|
+
<div className="form-group">
|
|
527
|
+
<label>响应时间</label>
|
|
528
|
+
<input type="text" value={selectedRequestLog.responseTime ? `${selectedRequestLog.responseTime}ms` : '-'} readOnly />
|
|
529
|
+
</div>
|
|
530
|
+
{selectedRequestLog.responseHeaders && (
|
|
531
|
+
<div className="form-group">
|
|
532
|
+
<label>响应头</label>
|
|
533
|
+
<JSONViewer data={selectedRequestLog.responseHeaders} collapsed />
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
{selectedRequestLog.responseBody && (
|
|
537
|
+
<div className="form-group">
|
|
538
|
+
<label>响应体</label>
|
|
539
|
+
<JSONViewer data={selectedRequestLog.responseBody} />
|
|
540
|
+
</div>
|
|
541
|
+
)}
|
|
542
|
+
{selectedRequestLog.streamChunks && selectedRequestLog.streamChunks.length > 0 && (
|
|
543
|
+
<div className="form-group">
|
|
544
|
+
<label>
|
|
545
|
+
Stream Chunks ({selectedRequestLog.streamChunks.length}个)
|
|
546
|
+
<button
|
|
547
|
+
onClick={() => setChunksExpanded(!chunksExpanded)}
|
|
548
|
+
style={{ marginLeft: '10px', padding: '2px 8px', fontSize: '12px' }}
|
|
549
|
+
className='btn btn-sm btn-primary'
|
|
550
|
+
>
|
|
551
|
+
{chunksExpanded ? '折叠' : '展开'}
|
|
552
|
+
</button>
|
|
553
|
+
</label>
|
|
554
|
+
{chunksExpanded && (
|
|
555
|
+
<div style={{ maxHeight: '400px', overflowY: 'auto', border: '1px solid #ddd', padding: '10px', borderRadius: '4px' }}>
|
|
556
|
+
{selectedRequestLog.streamChunks.map((chunk, index) => (
|
|
557
|
+
<div key={index} style={{ marginBottom: '10px' }}>
|
|
558
|
+
<div style={{ fontWeight: 'bold', fontSize: '12px', color: '#7f8c8d', marginBottom: '4px' }}>
|
|
559
|
+
Chunk #{index + 1}
|
|
560
|
+
</div>
|
|
561
|
+
<JSONViewer data={chunk} collapsed={true} />
|
|
562
|
+
</div>
|
|
563
|
+
))}
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
566
|
+
</div>
|
|
567
|
+
)}
|
|
568
|
+
{selectedRequestLog.usage && (
|
|
569
|
+
<div className="form-group">
|
|
570
|
+
<label>Token 使用</label>
|
|
571
|
+
<textarea
|
|
572
|
+
rows={4}
|
|
573
|
+
value={
|
|
574
|
+
`输入: ${selectedRequestLog.usage.inputTokens}\n` +
|
|
575
|
+
`输出: ${selectedRequestLog.usage.outputTokens}\n` +
|
|
576
|
+
(selectedRequestLog.usage.totalTokens !== undefined ? `总计: ${selectedRequestLog.usage.totalTokens}\n` : '') +
|
|
577
|
+
(selectedRequestLog.usage.cacheReadInputTokens !== undefined ? `缓存读取: ${selectedRequestLog.usage.cacheReadInputTokens}` : '')
|
|
578
|
+
}
|
|
579
|
+
readOnly
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
)}
|
|
583
|
+
{selectedRequestLog.error && (
|
|
584
|
+
<div className="form-group">
|
|
585
|
+
<label>错误信息</label>
|
|
586
|
+
<textarea rows={4} value={selectedRequestLog.error} readOnly style={{ color: 'red' }} />
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
<div className="modal-footer">
|
|
591
|
+
<button className="btn btn-secondary" onClick={() => setSelectedRequestLog(null)}>关闭</button>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
)}
|
|
596
|
+
|
|
597
|
+
{selectedAccessLog && (
|
|
598
|
+
<div className="modal-overlay" onClick={() => setSelectedAccessLog(null)}>
|
|
599
|
+
<div className="modal" onClick={(e) => e.stopPropagation()} style={{ minWidth: '600px' }}>
|
|
600
|
+
<div className="modal-header">
|
|
601
|
+
<h2>访问日志详情</h2>
|
|
602
|
+
</div>
|
|
603
|
+
<div>
|
|
604
|
+
<div className="form-group">
|
|
605
|
+
<label>时间</label>
|
|
606
|
+
<input type="text" value={dayjs(selectedAccessLog.timestamp).format('YYYY-MM-DD HH:mm:ss')} readOnly />
|
|
607
|
+
</div>
|
|
608
|
+
<div className="form-group">
|
|
609
|
+
<label>请求方法</label>
|
|
610
|
+
<input type="text" value={selectedAccessLog.method} readOnly />
|
|
611
|
+
</div>
|
|
612
|
+
<div className="form-group">
|
|
613
|
+
<label>请求路径</label>
|
|
614
|
+
<input type="text" value={selectedAccessLog.path} readOnly />
|
|
615
|
+
</div>
|
|
616
|
+
<div className="form-group">
|
|
617
|
+
<label>状态码</label>
|
|
618
|
+
<input type="text" value={selectedAccessLog.statusCode || '-'} readOnly />
|
|
619
|
+
</div>
|
|
620
|
+
<div className="form-group">
|
|
621
|
+
<label>响应时间</label>
|
|
622
|
+
<input type="text" value={selectedAccessLog.responseTime ? `${selectedAccessLog.responseTime}ms` : '-'} readOnly />
|
|
623
|
+
</div>
|
|
624
|
+
<div className="form-group">
|
|
625
|
+
<label>客户端IP</label>
|
|
626
|
+
<input type="text" value={selectedAccessLog.clientIp || '-'} readOnly />
|
|
627
|
+
</div>
|
|
628
|
+
<div className="form-group">
|
|
629
|
+
<label>User Agent</label>
|
|
630
|
+
<textarea rows={2} value={selectedAccessLog.userAgent || '-'} readOnly />
|
|
631
|
+
</div>
|
|
632
|
+
{selectedAccessLog.error && (
|
|
633
|
+
<div className="form-group">
|
|
634
|
+
<label>错误信息</label>
|
|
635
|
+
<textarea rows={6} value={selectedAccessLog.error} readOnly style={{ color: '#e74c3c' }} />
|
|
636
|
+
</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
<div className="modal-footer">
|
|
640
|
+
<button className="btn btn-secondary" onClick={() => setSelectedAccessLog(null)}>关闭</button>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
645
|
+
|
|
646
|
+
{selectedErrorLog && (
|
|
647
|
+
<div className="modal-overlay" onClick={() => setSelectedErrorLog(null)}>
|
|
648
|
+
<div className="modal" onClick={(e) => e.stopPropagation()} style={{ minWidth: '600px' }}>
|
|
649
|
+
<div className="modal-header">
|
|
650
|
+
<h2>错误日志详情</h2>
|
|
651
|
+
</div>
|
|
652
|
+
<div>
|
|
653
|
+
<div className="form-group">
|
|
654
|
+
<label>时间</label>
|
|
655
|
+
<input type="text" value={dayjs(selectedErrorLog.timestamp).format('YYYY-MM-DD HH:mm:ss')} readOnly />
|
|
656
|
+
</div>
|
|
657
|
+
<div className="form-group">
|
|
658
|
+
<label>请求方法</label>
|
|
659
|
+
<input type="text" value={selectedErrorLog.method} readOnly />
|
|
660
|
+
</div>
|
|
661
|
+
<div className="form-group">
|
|
662
|
+
<label>请求路径</label>
|
|
663
|
+
<input type="text" value={selectedErrorLog.path} readOnly />
|
|
664
|
+
</div>
|
|
665
|
+
<div className="form-group">
|
|
666
|
+
<label>状态码</label>
|
|
667
|
+
<input type="text" value={selectedErrorLog.statusCode || '-'} readOnly />
|
|
668
|
+
</div>
|
|
669
|
+
<div className="form-group">
|
|
670
|
+
<label>错误信息</label>
|
|
671
|
+
<textarea rows={4} value={selectedErrorLog.errorMessage} readOnly style={{ color: '#e74c3c' }} />
|
|
672
|
+
</div>
|
|
673
|
+
{selectedErrorLog.errorStack && (
|
|
674
|
+
<div className="form-group">
|
|
675
|
+
<label>错误堆栈</label>
|
|
676
|
+
<textarea rows={8} value={selectedErrorLog.errorStack} readOnly style={{ fontSize: '12px', color: '#7f8c8d' }} />
|
|
677
|
+
</div>
|
|
678
|
+
)}
|
|
679
|
+
{selectedErrorLog.requestBody && (
|
|
680
|
+
<div className="form-group">
|
|
681
|
+
<label>请求体</label>
|
|
682
|
+
<JSONViewer data={selectedErrorLog.requestBody} />
|
|
683
|
+
</div>
|
|
684
|
+
)}
|
|
685
|
+
{selectedErrorLog.requestHeaders && (
|
|
686
|
+
<div className="form-group">
|
|
687
|
+
<label>请求头</label>
|
|
688
|
+
<JSONViewer data={selectedErrorLog.requestHeaders} />
|
|
689
|
+
</div>
|
|
690
|
+
)}
|
|
691
|
+
</div>
|
|
692
|
+
<div className="modal-footer">
|
|
693
|
+
<button className="btn btn-secondary" onClick={() => setSelectedErrorLog(null)}>关闭</button>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
)}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export default LogsPage;
|