Skip to content

Commit 9489227

Browse files
authored
Merge pull request #12 from weiruchenai1/experimental
实现日志功能并适配移动端
2 parents feb8a70 + 3f7db33 commit 9489227

File tree

14 files changed

+1627
-522
lines changed

14 files changed

+1627
-522
lines changed

public/worker.js

Lines changed: 554 additions & 320 deletions
Large diffs are not rendered by default.

src/components/features/Controls/index.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useLanguage } from '../../../hooks/useLanguage';
33
import { useAppState } from '../../../contexts/AppStateContext';
44
import { useApiTester } from '../../../hooks/useApiTester';
55
import { deduplicateAndCleanKeys } from '../../../utils/keyProcessor';
6+
import { getLogCollector } from '../../../utils/logCollector';
67

78
const Controls = () => {
89
const { t } = useLanguage();
@@ -76,6 +77,10 @@ const Controls = () => {
7677
}
7778

7879
dispatch({ type: 'CLEAR_ALL' });
80+
const collector = getLogCollector && getLogCollector();
81+
if (collector && typeof collector.clearLogs === 'function') {
82+
collector.clearLogs();
83+
}
7984
alert(t('cleared') || '已清空所有内容。');
8085
};
8186

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
import { useAppState } from '../../../contexts/AppStateContext';
3+
import { useLanguage } from '../../../hooks/useLanguage';
4+
import { getLogEntryByKey } from '../../../utils/logStorage';
5+
6+
const formatTimestamp = (timestamp) => {
7+
if (!timestamp) return '';
8+
try {
9+
return new Date(timestamp).toLocaleString();
10+
} catch (error) {
11+
return String(timestamp);
12+
}
13+
};
14+
15+
const formatDuration = (ms) => {
16+
if (!ms && ms !== 0) return '';
17+
if (ms < 1000) return ms + ' ms';
18+
return (ms / 1000).toFixed(2) + ' s';
19+
};
20+
21+
const tryParseJson = (str) => {
22+
try {
23+
return JSON.parse(str);
24+
} catch (error) {
25+
return null;
26+
}
27+
};
28+
29+
const normalizePayload = (input) => {
30+
if (input == null) return input;
31+
32+
if (typeof input === 'string') {
33+
const trimmed = input.trim();
34+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
35+
const parsed = tryParseJson(trimmed);
36+
if (parsed !== null) {
37+
return normalizePayload(parsed);
38+
}
39+
}
40+
return input;
41+
}
42+
43+
if (Array.isArray(input)) {
44+
return input.map((item) => normalizePayload(item));
45+
}
46+
47+
if (typeof input === 'object') {
48+
const result = {};
49+
Object.entries(input).forEach(([key, value]) => {
50+
result[key] = normalizePayload(value);
51+
});
52+
return result;
53+
}
54+
55+
return input;
56+
};
57+
58+
const stringify = (value) => {
59+
if (value == null) return '';
60+
61+
const normalized = normalizePayload(value);
62+
63+
if (typeof normalized === 'string') {
64+
return normalized;
65+
}
66+
67+
try {
68+
return JSON.stringify(normalized, null, 2);
69+
} catch (error) {
70+
return String(normalized);
71+
}
72+
};
73+
const stageLabel = (stage, t) => {
74+
const labels = {
75+
test_start: t('logViewer.stages.testStart') || '开始测试',
76+
attempt_start: t('logViewer.stages.attemptStart') || '开始尝试',
77+
attempt_result: t('logViewer.stages.attemptResult') || '尝试结果',
78+
retry_wait: t('logViewer.stages.retryWait') || '等待重试',
79+
retry_scheduled: t('logViewer.stages.retryScheduled') || '准备重试',
80+
retry: t('logViewer.stages.retry') || '重试',
81+
attempt_exception: t('logViewer.stages.attemptException') || '尝试异常',
82+
paid_detection: t('logViewer.stages.paidDetection') || '付费检测',
83+
final: t('logViewer.stages.final') || '最终结果',
84+
cancelled: t('logViewer.stages.cancelled') || '已取消'
85+
};
86+
return labels[stage] || stage;
87+
};
88+
89+
const statusLabel = (status, t) => {
90+
const map = {
91+
valid: t('statusValid') || '有效',
92+
success: t('statusValid') || '有效',
93+
paid: t('paidKeys') || '付费',
94+
free: t('logViewer.freeKey') || '免费',
95+
invalid: t('statusInvalid') || '无效',
96+
error: t('logViewer.errorStatus') || '错误',
97+
retrying: t('statusRetrying') || '重试中',
98+
testing: t('statusTesting') || '测试中',
99+
'rate-limited': t('statusRateLimit') || '速率限制',
100+
cancelled: t('logViewer.cancelled') || '已取消'
101+
};
102+
return map[status] || status;
103+
};
104+
105+
const KeyLogModal = () => {
106+
const { state, dispatch } = useAppState();
107+
const { t } = useLanguage();
108+
109+
const [persistedLog, setPersistedLog] = useState(null);
110+
const [isLoading, setIsLoading] = useState(false);
111+
112+
const isOpen = state.isLogModalOpen;
113+
const activeKey = state.activeLogKey;
114+
const stateLogEntry = useMemo(() => {
115+
if (!activeKey) return null;
116+
const sourceLogs = state.logs || [];
117+
return sourceLogs.find((log) => log.key === activeKey || log.keyId === activeKey) || null;
118+
}, [activeKey, state.logs]);
119+
120+
const logEntry = stateLogEntry || persistedLog;
121+
122+
const events = useMemo(() => {
123+
if (!logEntry || !logEntry.events) return [];
124+
return [...(logEntry.events || [])]
125+
.filter((event) => event.stage !== 'test_start')
126+
.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
127+
}, [logEntry]);
128+
129+
useEffect(() => {
130+
let cancelled = false;
131+
132+
if (!isOpen || !activeKey) {
133+
setPersistedLog(null);
134+
setIsLoading(false);
135+
return;
136+
}
137+
138+
if (stateLogEntry) {
139+
setPersistedLog(null);
140+
setIsLoading(false);
141+
return;
142+
}
143+
144+
setIsLoading(true);
145+
getLogEntryByKey(activeKey)
146+
.then((entry) => {
147+
if (!cancelled) {
148+
setPersistedLog(entry || null);
149+
}
150+
})
151+
.catch(() => {
152+
if (!cancelled) {
153+
setPersistedLog(null);
154+
}
155+
})
156+
.finally(() => {
157+
if (!cancelled) {
158+
setIsLoading(false);
159+
}
160+
});
161+
162+
return () => {
163+
cancelled = true;
164+
};
165+
}, [isOpen, activeKey, stateLogEntry]);
166+
167+
if (!isOpen) return null;
168+
169+
const handleClose = () => {
170+
dispatch({ type: 'CLOSE_LOG_MODAL' });
171+
};
172+
173+
const finalStatus = logEntry && (logEntry.finalStatus || logEntry.status);
174+
const metadata = (logEntry && logEntry.metadata) || {};
175+
176+
return (
177+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style={{ zIndex: 10000 }} onClick={handleClose}>
178+
<div className="card-base log-modal-content max-w-2xl m-md" onClick={(e) => e.stopPropagation()}>
179+
<div className="flex items-center justify-between p-lg border-b">
180+
<div>
181+
<h3 className="text-lg font-semibold text-primary">{t('logViewer.title') || '日志详情'}</h3>
182+
<div className="text-secondary text-sm font-mono break-all">{activeKey}</div>
183+
</div>
184+
<button
185+
className="btn-base btn-ghost btn-sm w-8 h-8 flex items-center justify-center"
186+
onClick={handleClose}
187+
aria-label={t('close') || '关闭'}
188+
>
189+
×
190+
</button>
191+
</div>
192+
193+
<div className="log-modal-body p-lg space-y-lg">
194+
{isLoading ? (
195+
<div className="empty-state">
196+
<div className="empty-state-text">{t('loading') || '加载中...'}</div>
197+
</div>
198+
) : logEntry ? (
199+
<>
200+
<div className="grid gap-md grid-cols-1 md:grid-cols-2">
201+
<div className="card-base card-padding">
202+
<div className="text-sm text-secondary mb-xs">{t('logViewer.summary.status') || '最终状态'}</div>
203+
<div className="text-base font-semibold">{statusLabel(finalStatus, t) || '-'}</div>
204+
{logEntry.totalDurationMs ? (
205+
<div className="text-xs text-secondary mt-xs">
206+
{(t('logViewer.summary.duration') || '总耗时') + ': ' + formatDuration(logEntry.totalDurationMs)}
207+
</div>
208+
) : null}
209+
<div className="text-xs text-secondary mt-xs">
210+
{(t('logViewer.summary.attempts') || '尝试次数') + ': ' + (logEntry.attempts || events.length || 0)}
211+
</div>
212+
{logEntry.lastError && logEntry.lastError.message ? (
213+
<div className="text-xs text-error mt-xs">{logEntry.lastError.message}</div>
214+
) : null}
215+
</div>
216+
217+
<div className="card-base card-padding">
218+
<div className="text-sm text-secondary mb-xs">{t('logViewer.summary.context') || '上下文'}</div>
219+
<div className="text-xs text-secondary">
220+
{(t('selectApi') || '选择 API 类型') + ': ' + (logEntry.apiType || '-')}
221+
</div>
222+
<div className="text-xs text-secondary">
223+
{(t('selectModel') || '测试模型') + ': ' + (logEntry.model || '-')}
224+
</div>
225+
{metadata.proxyUrl ? (
226+
<div className="text-xs text-secondary">Proxy: {metadata.proxyUrl}</div>
227+
) : null}
228+
{metadata.enablePaidDetection ? (
229+
<div className="text-xs text-secondary">{t('paidDetectionEnabled') || '已开启付费检测'}</div>
230+
) : null}
231+
</div>
232+
</div>
233+
234+
<div className="space-y-md">
235+
{events.length === 0 ? (
236+
<div className="empty-state">
237+
<div className="empty-state-text">{t('logViewer.noEvents') || '暂无事件记录'}</div>
238+
</div>
239+
) : (
240+
events.map((event) => (
241+
<div key={event.id || event.timestamp} className="card-base card-padding">
242+
<div className="flex items-center justify-between mb-sm">
243+
<div>
244+
<div className="text-sm font-semibold text-primary">{stageLabel(event.stage, t)}</div>
245+
<div className="text-xs text-secondary">{formatTimestamp(event.timestamp)}</div>
246+
</div>
247+
<div className="text-xs text-secondary text-right">
248+
{event.attempt ? (t('logViewer.attempt') || '尝试') + ' #' + event.attempt : null}
249+
{event.durationMs != null ? <div>{(t('logViewer.duration') || '耗时') + ': ' + formatDuration(event.durationMs)}</div> : null}
250+
{event.status ? <div>{statusLabel(event.status, t)}</div> : null}
251+
</div>
252+
</div>
253+
254+
{event.message ? (
255+
<div className="text-sm text-secondary mb-sm whitespace-pre-wrap">{event.message}</div>
256+
) : null}
257+
258+
{event.error ? (
259+
<div className="text-sm text-error mb-sm whitespace-pre-wrap">
260+
{typeof event.error === 'string' ? event.error : stringify(event.error)}
261+
</div>
262+
) : null}
263+
264+
{event.request ? (
265+
<details className="mb-sm">
266+
<summary className="text-sm font-semibold cursor-pointer">{t('logViewer.request') || '请求'}</summary>
267+
<pre className="bg-surface mt-xs p-sm rounded text-xs overflow-x-auto whitespace-pre-wrap">{stringify(event.request)}</pre>
268+
</details>
269+
) : null}
270+
271+
{event.response ? (
272+
<details>
273+
<summary className="text-sm font-semibold cursor-pointer">{t('logViewer.response') || '响应'}</summary>
274+
<pre className="bg-surface mt-xs p-sm rounded text-xs overflow-x-auto whitespace-pre-wrap">{stringify(event.response)}</pre>
275+
</details>
276+
) : null}
277+
</div>
278+
))
279+
)}
280+
</div>
281+
</>
282+
) : (
283+
<div className="empty-state">
284+
<div className="empty-state-text">{t('logViewer.noData') || '暂无日志数据'}</div>
285+
</div>
286+
)}
287+
</div>
288+
</div>
289+
</div>
290+
);
291+
};
292+
293+
export default KeyLogModal;
294+
295+
296+

src/components/features/LogsPreview/LogsPreviewPanel.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ const LogsPreviewPanel = ({ logs = [], onExpandLogs }) => {
2323
// 脱敏API密钥
2424
const maskApiKey = (key) => {
2525
if (!key || typeof key !== 'string') return 'Unknown';
26-
if (key.length <= 8) return key;
27-
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
26+
return key;
2827
};
2928

3029
// 过滤和搜索日志

src/components/features/Results/VirtualizedList.jsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useVirtualization } from '../../../hooks/useVirtualization';
66

77
const KeyItem = ({ index, style, data }) => {
88
const { t } = useLanguage();
9-
const { state } = useAppState();
9+
const { state, dispatch } = useAppState();
1010
const keyData = data[index];
1111

1212
if (!keyData) {
@@ -70,9 +70,26 @@ const KeyItem = ({ index, style, data }) => {
7070
return null;
7171
};
7272

73+
const handleOpenLogs = () => {
74+
dispatch({ type: 'OPEN_LOG_MODAL', payload: keyData.key });
75+
};
76+
77+
const handleKeyDown = (event) => {
78+
if (event.key === 'Enter' || event.key === ' ') {
79+
event.preventDefault();
80+
handleOpenLogs();
81+
}
82+
};
83+
7384
return (
7485
<div style={style} className="key-item-wrapper">
75-
<div className="key-item">
86+
<div
87+
className="key-item"
88+
role="button"
89+
tabIndex={0}
90+
onClick={handleOpenLogs}
91+
onKeyDown={handleKeyDown}
92+
>
7693
<div className="key-content">
7794
<div className="key-text">{keyData.key}</div>
7895
{keyData.model && (

0 commit comments

Comments
 (0)