/** * Admin 后台 - LLM 配置管理页面(报刊风) * * 功能: * - 显示当前 LLM 配置(provider, model, base_url;后端暂无配置端点,当前值取自 .env 约定) * - 测试 LLM 连接(会真实调用一次 /predict,产生 LLM 调用费用) * - 显示 LLM 使用统计(从预测记录聚合) * - 可用模型列表 */ import { useEffect, useState, useCallback } from 'react' import { testLLMConnection, fetchLLMUsageStats, fetchSettings, fetchLLMModels, updateSetting, clearSetting } from '../dal' import type { LLMUsageStats } from '../types' import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components' import SettingRow from '../SettingRow' import AgentLLMCard from '../AgentLLMCard' import type { DataSourceSetting } from '../types' const LLM_SETTING_KEYS = ['LLM_API_KEY', 'LLM_BASE_URL', 'LLM_MODEL'] export default function LLMConfigPage() { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null) // LLM 连接配置(运行时配置,DB 覆盖 .env) const [llmSettings, setLlmSettings] = useState([]) const [settingsLoading, setSettingsLoading] = useState(true) const [editingKey, setEditingKey] = useState(null) const [busyKey, setBusyKey] = useState(null) const [rowNotice, setRowNotice] = useState<{ key: string; ok: boolean; text: string } | null>(null) const loadStats = useCallback(async () => { setLoading(true) try { const data = await fetchLLMUsageStats() setStats(data) } catch { setStats(null) } finally { setLoading(false) } }, []) const loadSettings = useCallback(async () => { setSettingsLoading(true) try { const all = await fetchSettings() setLlmSettings(all.filter(x => LLM_SETTING_KEYS.includes(x.key))) } catch { setLlmSettings([]) } finally { setSettingsLoading(false) } }, []) useEffect(() => { loadStats() loadSettings() }, [loadStats, loadSettings]) /** 供 LLM_MODEL 行内检测:探测当前服务可用模型,失败抛错由行内展示 */ const detectLLMModels = useCallback(async (): Promise => { const r = await fetchLLMModels() if (!r.ok) throw new Error(r.detail) return r.models }, []) async function handleSave(key: string, value: string) { setBusyKey(key) setRowNotice(null) try { await updateSetting(key, value) setRowNotice({ key, ok: true, text: '已保存,立即生效' }) setEditingKey(null) await loadSettings() } catch (err) { setRowNotice({ key, ok: false, text: err instanceof Error ? err.message.split('\n')[0] : '保存失败' }) } finally { setBusyKey(null) } } async function handleClear(key: string) { setBusyKey(key) setRowNotice(null) try { await clearSetting(key) setRowNotice({ key, ok: true, text: '已清除数据库覆盖,回落 .env 默认值' }) await loadSettings() } catch (err) { setRowNotice({ key, ok: false, text: err instanceof Error ? err.message.split('\n')[0] : '清除失败' }) } finally { setBusyKey(null) } } async function handleTest() { setTesting(true) setTestResult(null) try { await testLLMConnection() setTestResult({ success: true, message: 'LLM 连接测试成功' }) } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'LLM 连接测试失败' setTestResult({ success: false, message: msg }) } finally { setTesting(false) } } return (
{/* LLM 连接配置 */} {settingsLoading ? (<> 加载中) : '刷新'} } /> {settingsLoading ? (
{[1, 2, 3].map(i => )}
) : (
{llmSettings.map(setting => ( { setEditingKey(setting.key) setRowNotice(null) }} onCancel={() => setEditingKey(null)} onSave={v => handleSave(setting.key, v)} onClear={() => handleClear(setting.key)} detectModels={setting.key === 'LLM_MODEL' ? detectLLMModels : undefined} /> ))}
)} {rowNotice && (
)}

模式: 多专家 (5 路 + 终裁)。填入可连通的 OpenAI 兼容服务(如 DeepSeek、 智谱、通义或任意网关)后点下方「测试 LLM 连接」验证。

{/* 测试连接 */} {testResult && (
)}

测试会真实调用一次 LLM 预测,产生费用。

{/* 使用统计 */} {loading ? (<> 加载中) : '刷新'} } /> {loading ? (
) : stats ? (
{stats.total_predictions}
总预测数
{stats.avg_latency_ms > 0 ? `${(stats.avg_latency_ms / 1000).toFixed(1)}s` : '—'}
平均延迟
{stats.success_rate.toFixed(0)}%
有效率
) : (

暂无使用统计数据

)}
{/* 专家与终裁独立配置 */} {/* 最近预测 */} {loading ? (
{[1, 2, 3].map(i => ( ))}
) : stats && stats.recent_predictions.length > 0 ? (
{stats.recent_predictions.map(p => (
#{p.id} 比赛 #{p.match_id} {p.model}
{p.created_at ? new Date(p.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'} {p.status === 'success' ? 成功 : 失败}
))}
) : (

暂无预测记录

)}
) }