import { useState, useEffect, useCallback } from 'react'; import './Developer.css'; import { formatBjTime } from '@/utils/datetime'; const API_BASE = '/app/api'; interface Stats { api_keys: { total: number; active: number; revoked: number }; openapi_clients: { total: number; active: number }; call_stats: { total_calls: number; today_calls: number; total_errors: number; error_rate: number; avg_response_time: number }; slow_logs: { total: number; today: number }; top_endpoints: { endpoint: string; method: string; call_count: number; error_count: number }[]; server_time: string; } interface ApiKey { id: string; user_id: string; username: string | null; api_key: string; name: string; description: string; rate_limit: number; total_calls: number; status: string; last_used_at: string | null; created_at: string; expired_at: string | null; } interface OpenApiClient { id: string; user_id: string; username: string | null; name: string; client_id: string; allowed_models: string[]; rate_limit: number; total_calls: number; last_used_at: string | null; status: string; created_at: string; } interface CallStat { id: string; endpoint: string; method: string; call_count: number; error_count: number; error_rate: number; avg_response_time: number; total_response_time: number; last_called_at: string | null; created_at: string; updated_at: string; } interface SlowLog { id: string; request_id: string | null; method: string; endpoint: string; status_code: number; response_time: number; threshold: number; created_at: string; } interface PageData { total: number; page: number; page_size: number; items: T[] } async function api(path: string, options?: RequestInit) { const token = localStorage.getItem('token'); const res = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...(options?.headers || {}), }, }); if (res.status === 307) { const location = res.headers.get('Location'); if (location) return api(location.replace(API_BASE, ''), options); } const data = await res.json(); if (!res.ok) throw new Error(data.detail || `HTTP ${res.status}`); return data; } export default function AdminDeveloper() { const [activeTab, setActiveTab] = useState<'keys' | 'clients' | 'stats' | 'logs'>('keys'); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(false); // API Keys state const [keys, setKeys] = useState>({ total: 0, page: 1, page_size: 20, items: [] }); const [keySearch, setKeySearch] = useState(''); const [keyStatusFilter, setKeyStatusFilter] = useState(''); const [showKeyModal, setShowKeyModal] = useState(false); const [keyForm, setKeyForm] = useState({ user_id: '', name: '', description: '', rate_limit: 60 }); // OpenAPI Clients state const [clients, setClients] = useState>({ total: 0, page: 1, page_size: 20, items: [] }); const [clientSearch, setClientSearch] = useState(''); const [showClientModal, setShowClientModal] = useState(false); const [clientForm, setClientForm] = useState({ user_id: '', name: '', allowed_models: '', rate_limit: 60 }); // Call Stats state const [callStats, setCallStats] = useState>({ total: 0, page: 1, page_size: 20, items: [] }); const [statSearch, setStatSearch] = useState(''); const [statSortBy, setStatSortBy] = useState('call_count'); // Slow Logs state const [slowLogs, setSlowLogs] = useState>({ total: 0, page: 1, page_size: 20, items: [] }); const [logSearch, setLogSearch] = useState(''); const loadStats = useCallback(async () => { try { const res = await api('/admin/developer/stats/overview'); if (res.code === 0) setStats(res.data); } catch (e) { console.error('Stats error:', e); } }, []); const loadKeys = useCallback(async (page = 1) => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), page_size: '20' }); if (keyStatusFilter) params.set('status', keyStatusFilter); if (keySearch) params.set('keyword', keySearch); const res = await api(`/admin/developer/api-keys?${params}`); if (res.code === 0) setKeys(res.data); } catch (e) { console.error('Keys error:', e); } setLoading(false); }, [keySearch, keyStatusFilter]); const loadClients = useCallback(async (page = 1) => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), page_size: '20' }); if (clientSearch) params.set('keyword', clientSearch); const res = await api(`/admin/developer/openapi-clients?${params}`); if (res.code === 0) setClients(res.data); } catch (e) { console.error('Clients error:', e); } setLoading(false); }, [clientSearch]); const loadCallStats = useCallback(async (page = 1) => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), page_size: '20', sort_by: statSortBy }); if (statSearch) params.set('keyword', statSearch); const res = await api(`/admin/developer/call-stats?${params}`); if (res.code === 0) setCallStats(res.data); } catch (e) { console.error('Stats error:', e); } setLoading(false); }, [statSearch, statSortBy]); const loadSlowLogs = useCallback(async (page = 1) => { setLoading(true); try { const params = new URLSearchParams({ page: String(page), page_size: '20' }); if (logSearch) params.set('keyword', logSearch); const res = await api(`/admin/developer/slow-logs?${params}`); if (res.code === 0) setSlowLogs(res.data); } catch (e) { console.error('Logs error:', e); } setLoading(false); }, [logSearch]); useEffect(() => { loadStats(); }, [loadStats]); useEffect(() => { if (activeTab === 'keys') loadKeys(); }, [activeTab, loadKeys]); useEffect(() => { if (activeTab === 'clients') loadClients(); }, [activeTab, loadClients]); useEffect(() => { if (activeTab === 'stats') loadCallStats(); }, [activeTab, loadCallStats]); useEffect(() => { if (activeTab === 'logs') loadSlowLogs(); }, [activeTab, loadSlowLogs]); const handleCreateKey = async () => { try { const body: any = { user_id: keyForm.user_id, name: keyForm.name, description: keyForm.description, rate_limit: keyForm.rate_limit }; await api('/admin/developer/api-keys/', { method: 'POST', body: JSON.stringify(body) }); setShowKeyModal(false); setKeyForm({ user_id: '', name: '', description: '', rate_limit: 60 }); loadKeys(); loadStats(); } catch (e: any) { alert(e.message); } }; const handleRevokeKey = async (id: string) => { if (!confirm('Revoke this API key?')) return; try { await api(`/admin/developer/api-keys/${id}/revoke`, { method: 'POST' }); loadKeys(); loadStats(); } catch (e: any) { alert(e.message); } }; const handleDeleteKey = async (id: string) => { if (!confirm('Permanently delete this API key?')) return; try { await api(`/admin/developer/api-keys/${id}`, { method: 'DELETE' }); loadKeys(); loadStats(); } catch (e: any) { alert(e.message); } }; const handleCreateClient = async () => { try { const models = clientForm.allowed_models ? clientForm.allowed_models.split(',').map(s => s.trim()).filter(Boolean) : null; const body: any = { user_id: clientForm.user_id, name: clientForm.name, allowed_models: models, rate_limit: clientForm.rate_limit }; const res = await api('/admin/developer/openapi-clients/', { method: 'POST', body: JSON.stringify(body) }); setShowClientModal(false); setClientForm({ user_id: '', name: '', allowed_models: '', rate_limit: 60 }); loadClients(); loadStats(); if (res.data?.client_secret) { alert(`Client created! Secret: ${res.data.client_secret}\nSave this secret - it won't be shown again.`); } } catch (e: any) { alert(e.message); } }; const handleToggleClient = async (client: OpenApiClient) => { const newStatus = client.status === 'active' ? 'disabled' : 'active'; try { await api(`/admin/developer/openapi-clients/${client.id}`, { method: 'PUT', body: JSON.stringify({ status: newStatus }) }); loadClients(); } catch (e: any) { alert(e.message); } }; const handleDeleteClient = async (id: string) => { if (!confirm('Delete this OpenAPI client?')) return; try { await api(`/admin/developer/openapi-clients/${id}`, { method: 'DELETE' }); loadClients(); loadStats(); } catch (e: any) { alert(e.message); } }; const handleResetSecret = async (id: string) => { if (!confirm('Reset client secret? The old secret will be invalidated.')) return; try { const res = await api(`/admin/developer/openapi-clients/${id}/reset-secret`, { method: 'POST' }); if (res.data?.client_secret) alert(`New secret: ${res.data.client_secret}`); } catch (e: any) { alert(e.message); } }; const handleClearLogs = async () => { if (!confirm('Clear ALL slow request logs?')) return; try { await api('/admin/developer/slow-logs', { method: 'DELETE' }); loadSlowLogs(); loadStats(); } catch (e: any) { alert(e.message); } }; const fmtTime = (t: string | null) => { if (!t) return '-'; try { return formatBjTime(t); } catch { return t; } }; const statusBadge = (status: string) => { const cls = status === 'active' ? 'badge-active' : status === 'revoked' ? 'badge-revoked' : 'badge-disabled'; return {status}; }; return (

开发者API管理

管理API密钥、OpenAPI客户端、调用统计和慢请求日志

{/* Stats Cards */} {stats && (
🔑
{stats.api_keys.total} API密钥总数 活跃 {stats.api_keys.active} / 已撤销 {stats.api_keys.revoked}
🔌
{stats.openapi_clients.total} OpenAPI客户端 活跃 {stats.openapi_clients.active}
📞
{stats.call_stats.total_calls.toLocaleString()} API总调用 今日 {stats.call_stats.today_calls.toLocaleString()}
⚠️
{stats.call_stats.error_rate}% 错误率 错误 {stats.call_stats.total_errors.toLocaleString()} 次
⏱️
{stats.call_stats.avg_response_time}s 平均响应时间 慢请求 {stats.slow_logs.total} 条
)} {/* Tab Navigation */}
{/* Tab Content */}
{loading &&
加载中...
} {/* API Keys Tab */} {activeTab === 'keys' && !loading && (
setKeySearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && loadKeys()} />
{keys.items.map(k => ( ))} {keys.items.length === 0 && }
名称API Key用户速率限制状态最后使用创建时间操作
{k.name || '-'} {k.api_key.slice(0, 12)}... {k.username || k.user_id.slice(0, 8)} {k.rate_limit}/min {statusBadge(k.status)} {fmtTime(k.last_used_at)} {fmtTime(k.created_at)} {k.status === 'active' && }
暂无API密钥
{keys.total > 20 && (
第 {keys.page} 页 / 共 {Math.ceil(keys.total / 20)} 页
)}
)} {/* OpenAPI Clients Tab */} {activeTab === 'clients' && !loading && (
setClientSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && loadClients()} />
{clients.items.map(c => ( ))} {clients.items.length === 0 && }
名称Client ID用户允许模型速率限制状态创建时间操作
{c.name} {c.client_id} {c.username || c.user_id.slice(0, 8)} {c.allowed_models?.length > 0 ? `${c.allowed_models.length} 个模型` : '全部'} {c.rate_limit}/min {statusBadge(c.status)} {fmtTime(c.created_at)}
暂无OpenAPI客户端
{clients.total > 20 && (
第 {clients.page} 页 / 共 {Math.ceil(clients.total / 20)} 页
)}
)} {/* Call Stats Tab */} {activeTab === 'stats' && !loading && (
setStatSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && loadCallStats()} />
{callStats.items.map(s => ( ))} {callStats.items.length === 0 && }
端点方法调用次数错误数错误率平均耗时最后调用
{s.endpoint} {s.method} {s.call_count.toLocaleString()} 0 ? 'text-danger' : ''}>{s.error_count} 5 ? 'text-danger' : ''}>{s.error_rate}% {s.avg_response_time}s {fmtTime(s.last_called_at)}
暂无调用统计
{callStats.total > 20 && (
第 {callStats.page} 页 / 共 {Math.ceil(callStats.total / 20)} 页
)}
)} {/* Slow Logs Tab */} {activeTab === 'logs' && !loading && (
setLogSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && loadSlowLogs()} />
{slowLogs.items.map(l => ( ))} {slowLogs.items.length === 0 && }
请求ID方法端点状态码响应时间阈值时间
{l.request_id?.slice(0, 12) || '-'} {l.method} {l.endpoint.length > 60 ? l.endpoint.slice(0, 60) + '...' : l.endpoint} = 400 ? 'text-danger' : ''}>{l.status_code} {l.response_time}s {l.threshold}s {fmtTime(l.created_at)}
暂无慢请求日志
{slowLogs.total > 20 && (
第 {slowLogs.page} 页 / 共 {Math.ceil(slowLogs.total / 20)} 页
)}
)}
{/* Create API Key Modal */} {showKeyModal && (
setShowKeyModal(false)}>
e.stopPropagation()}>

新建API密钥

setKeyForm({ ...keyForm, user_id: e.target.value })} placeholder="输入用户ID" />
setKeyForm({ ...keyForm, name: e.target.value })} placeholder="给密钥起个名字" />
setKeyForm({ ...keyForm, description: e.target.value })} placeholder="密钥用途描述" />
setKeyForm({ ...keyForm, rate_limit: parseInt(e.target.value) || 60 })} />
)} {/* Create Client Modal */} {showClientModal && (
setShowClientModal(false)}>
e.stopPropagation()}>

新建OpenAPI客户端

setClientForm({ ...clientForm, user_id: e.target.value })} placeholder="输入用户ID" />
setClientForm({ ...clientForm, name: e.target.value })} placeholder="客户端名称" />
setClientForm({ ...clientForm, allowed_models: e.target.value })} placeholder="deepseek-v3, gpt-4o" />
setClientForm({ ...clientForm, rate_limit: parseInt(e.target.value) || 60 })} />
)}
); }