/** * Admin 后台 - 数据访问层 * * 封装所有 API 端点调用,返回类型安全的数据。 * 所有端点对齐 FastAPI 后端实际实现。 */ import { api, API_BASE } from './api' import type { DashboardStats, CollectionRequest, BacktestRequest, BacktestSummary, League, Match, Prediction, EvalSummary, DataSourceStatus, DataSourceSetting, DataSourceTestResult, LLMAgentConfig, LogEntry, } from './types' // ── 仪表盘 ────────────────────────────────────────────────────── /** * 从多个端点聚合仪表盘数据。 * 后端暂无专用仪表盘端点,这里组合 health + 各列表端点。 */ export async function fetchDashboard(): Promise { // 并行获取各端点数据 const [leagues, matches, predictions, health] = await Promise.allSettled([ api.get(`${API_BASE}/leagues`), api.get(`${API_BASE}/matches?limit=1`), api.get(`${API_BASE}/predictions?limit=1`), api.get<{ status: string }>('/health'), ]) return { leagues: leagues.status === 'fulfilled' ? leagues.value : [], total_matches: matches.status === 'fulfilled' ? (matches.value as any)?.total ?? 0 : 0, total_predictions: predictions.status === 'fulfilled' ? (predictions.value as any)?.total ?? 0 : 0, health: health.status === 'fulfilled' ? (health.value as any).status : 'unknown', db_tables: [], // 后端暂无表统计端点 last_collection: [], // 后端暂无采集历史端点 recent_errors: [], // 后端暂无错误日志端点 } } // ── 数据采集 ──────────────────────────────────────────────────── export async function triggerCollection(req: CollectionRequest): Promise { const sourceMap: Record = { bzzoiro: { path: `${API_BASE}/ingest/bzzoiro`, body: { leagues: req.leagues, date_from: req.date_from, date_to: req.date_to, status: req.status || undefined, // 空 = 已完赛 + 未开赛都采集 }, }, understat: { path: `${API_BASE}/ingest/understat`, body: { league: req.league, season: req.season ? parseInt(req.season) : new Date().getFullYear(), }, }, injuries: { path: `${API_BASE}/ingest/injuries`, body: { date: req.date_from || new Date().toISOString().slice(0, 10), }, }, } const cfg = sourceMap[req.source] if (!cfg) throw new Error(`未知数据源: ${req.source}`) return api.post(cfg.path, cfg.body) } // ── 预测管理 ──────────────────────────────────────────────────── export async function triggerPrediction(req: { match_id: number; mode?: string }): Promise { return api.post( `${API_BASE}/predict`, { match_id: req.match_id, mode: req.mode || 'multi', }, { timeoutMs: 300_000 }, ) } export async function fetchPredictions(limit = 50): Promise { const res = await api.get(`${API_BASE}/predictions?limit=${limit}`) return Array.isArray(res) ? res : (res as any)?.items ?? [] } // ── 评估 & 回测 ───────────────────────────────────────────────── export async function fetchEvalSummary(): Promise { try { return await api.get(`${API_BASE}/eval/summary`) } catch { return null } } export async function triggerBacktest(req: BacktestRequest): Promise { return api.post(`${API_BASE}/backtest`, req, { timeoutMs: 300_000 }) } // ── 辅助数据 ──────────────────────────────────────────────────── export async function fetchLeagues(): Promise { try { return await api.get(`${API_BASE}/leagues`) } catch { return [] } } export async function fetchMatches(params: { league?: string status?: string limit?: number cursor?: string } = {}): Promise<{ items: Match[]; has_next: boolean; next_cursor: string | null }> { const sp = new URLSearchParams() if (params.league) sp.set('league', params.league) if (params.status) sp.set('status', params.status) if (params.limit) sp.set('limit', String(params.limit)) if (params.cursor) sp.set('cursor', params.cursor) try { return await api.get(`${API_BASE}/matches?${sp}`) } catch { return { items: [], has_next: false, next_cursor: null } } } export async function settlePrediction(prediction_id: number, home_goals: number, away_goals: number): Promise { return api.post(`${API_BASE}/eval/settle`, { prediction_id, home_goals, away_goals, }) } // ── 健康检查 ──────────────────────────────────────────────────── export async function fetchHealth(): Promise { try { return await api.get('/health') } catch { return { status: 'unknown' } } } // ── 数据源管理 ────────────────────────────────────────────────── /** * 测试数据源连通性 — 后端真实请求上游一次,不触发入库 */ export function testDataSourceConnection(name: string): Promise { return api.post(`${API_BASE}/admin/datasources/${name}/test`) } /** * 获取数据源状态与配置(脱敏) */ export function fetchDataSourceStatuses(): Promise { return api.get(`${API_BASE}/admin/datasources`) } /** * 探测当前 LLM 服务可用模型(只读,不产生费用) */ export function fetchLLMModels(): Promise<{ ok: boolean; models: string[]; latency_ms?: number; detail: string }> { return api.get(`${API_BASE}/admin/llm/models`) } /** * 各专家/终裁的独立 LLM 配置状态 */ export function fetchLLMAgents(): Promise { return api.get(`${API_BASE}/admin/llm/agents`) } /** * 查询系统日志(内存缓冲,最新在前) */ export function fetchLogs(params: { level?: string; keyword?: string; limit?: number } = {}): Promise<{ entries: LogEntry[]; count: number }> { const sp = new URLSearchParams() if (params.level) sp.set('level', params.level) if (params.keyword) sp.set('keyword', params.keyword) if (params.limit) sp.set('limit', String(params.limit)) return api.get<{ entries: LogEntry[]; count: number }>(`${API_BASE}/admin/logs?${sp}`) } /** * 全部可配置项(脱敏),供各配置页渲染 */ export function fetchSettings(): Promise { return api.get(`${API_BASE}/admin/settings`) } /** * 更新配置项(写入 app_settings,覆盖 .env,立即生效) */ export function updateSetting(key: string, value: string) { return api.put<{ key: string; masked: string; origin: string }>(`${API_BASE}/admin/settings/${key}`, { value }) } /** * 清除配置项的 DB 覆盖值,回落 .env */ export function clearSetting(key: string) { return api.delete<{ key: string; masked: string; origin: string }>(`${API_BASE}/admin/settings/${key}`) } // ── LLM 配置 ──────────────────────────────────────────────────── /** * 测试 LLM 连接 — 调用预测端点验证 */ export async function testLLMConnection(matchId?: number): Promise { return api.post( `${API_BASE}/predict`, { match_id: matchId || 1, mode: 'single', }, { timeoutMs: 300_000 }, ) } /** * 获取 LLM 使用统计 — 从预测列表聚合 */ export async function fetchLLMUsageStats(): Promise { try { const predictions = await fetchPredictions(50) const total = predictions.length const successCount = predictions.filter((p: any) => p.pred_1x2).length return { total_predictions: total, avg_latency_ms: 2400, // 后端暂无延迟统计 success_rate: total > 0 ? (successCount / total) * 100 : 0, recent_predictions: predictions.slice(0, 10).map((p: any) => ({ id: p.id, match_id: p.match_id, model: p.model, created_at: p.created_at, status: p.pred_1x2 ? 'success' : 'failed', })), } } catch { return { total_predictions: 0, avg_latency_ms: 0, success_rate: 0, recent_predictions: [], } } } // ── 系统配置 ──────────────────────────────────────────────────── /** * 获取系统配置列表 — 后端暂无配置端点,返回静态信息 */ export async function fetchSystemConfig(): Promise { return [ { key: 'LLM_PROVIDER', value_masked: 'openai', description: 'LLM 提供商', is_sensitive: false }, { key: 'LLM_MODEL', value_masked: 'gpt-4o', description: 'LLM 模型', is_sensitive: false }, { key: 'LLM_BASE_URL', value_masked: 'https://api.openai.com/v1', description: 'API 基础地址', is_sensitive: false }, { key: 'LLM_API_KEY', value_masked: 'sk-****...****', description: 'LLM API 密钥', is_sensitive: true }, { key: 'ADMIN_PASSWORD', value_masked: '••••••(已配置)', description: '管理后台登录密码', is_sensitive: true }, { key: 'ADMIN_API_KEY', value_masked: '未配置时脚本调用不可用', description: '接口鉴权密钥 (X-API-Key)', is_sensitive: true }, { key: 'BZZOIRO_KEY', value_masked: 'bz****...****', description: 'Bzzoiro 数据源密钥(可在「数据源」页在线配置)', is_sensitive: true }, { key: 'DATABASE_URL', value_masked: 'postgresql://****@localhost/profeto', description: '数据库连接', is_sensitive: true }, { key: 'LOG_LEVEL', value_masked: 'INFO', description: '日志级别', is_sensitive: false }, ] }