/** * Admin 后台 - 数据访问层 * * 封装所有 API 端点调用,返回类型安全的数据。 * 所有端点对齐 FastAPI 后端实际实现。 */ import { api, API_BASE } from './api' import type { DashboardStats, CollectionRequest, BacktestRequest, BacktestSummary, League, Match, Prediction, EvalSummary, DataSourceStatus, DataSourceSetting, DataSourceTestResult, LLMAgentConfig, LogEntry, IngestSourceStatus, MatchDetailOut, MatchContextOut, AdminStats, } from './types' // ── 仪表盘 ────────────────────────────────────────────────────── /** * 从多个端点聚合仪表盘数据。 * 后端暂无专用仪表盘端点,这里组合 health + 各列表端点。 * * P2-8 修复: 使用 items.length 替代不存在的 total 字段, * 并扩大 limit 以获得更有参考价值的数量。 */ export async function fetchDashboard(): Promise { // 并行获取各端点数据 // matches 返回 {items, next_cursor, has_more}, predictions 返回数组 const [leagues, matches, predictions, health] = await Promise.allSettled([ api.get(`${API_BASE}/leagues`), api.get<{ items: Match[]; has_more: boolean }>(`${API_BASE}/matches?limit=100`), api.get(`${API_BASE}/predictions?limit=100`), api.get<{ status: string }>('/health'), ]) return { leagues: leagues.status === 'fulfilled' ? leagues.value : [], // P2-8: matches 无 total 字段,用 items.length 近似(上限 100) total_matches: matches.status === 'fulfilled' ? (matches.value as any)?.items?.length ?? 0 : 0, // predictions 直接返回数组 total_predictions: predictions.status === 'fulfilled' ? (predictions.value as any)?.length ?? 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 body: Record = { leagues: req.leagues, date_from: req.date_from, date_to: req.date_to, status: req.status || undefined, task: req.task || 'events', limit: req.limit || 100, season: req.season || undefined, } return api.post(`${API_BASE}/ingest/bzzoiro`, 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(params: { limit?: number provider?: string model?: string prompt_version?: string mode?: string league_code?: string } = {}): Promise { const sp = new URLSearchParams() if (params.limit) sp.set('limit', String(params.limit)) if (params.provider) sp.set('provider', params.provider) if (params.model) sp.set('model', params.model) if (params.prompt_version) sp.set('prompt_version', params.prompt_version) if (params.mode) sp.set('mode', params.mode) if (params.league_code) sp.set('league_code', params.league_code) try { return await api.get(`${API_BASE}/eval/summary?${sp}`) } 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 interface DataCompletenessResponse { generated_at: string totals: { finished_matches: number; stats_rows: number; stats_coverage_pct: number } issues: string[] leagues: Array<{ code: string name: string country?: string matches: { total: number; finished: number; scheduled: number; with_source_id: number; earliest_match?: string; latest_match?: string } stats: { rows: number fields: Record } standings: { rows: number; latest_retrieved?: string } }> } export async function fetchDataCompleteness(): Promise { return api.get(`${API_BASE}/admin/data-completeness`) } // ── 积分榜(主站 + 管理后台共用) ───────────────────────────────── export interface StandingRow { position: number team: string team_en: string played: number won: number drawn: number lost: number goals_for: number goals_against: number goal_diff: number points: number xg_for: number | null xg_against: number | null form: string | null zone: string | null } export interface StandingsLeague { league_code: string league_name: string season: string retrieved_at: string | null rows: StandingRow[] } export async function fetchStandings(league?: string, season?: string): Promise<{ leagues: StandingsLeague[] }> { const sp = new URLSearchParams() if (league) sp.set('league', league) if (season) sp.set('season', season) const qs = sp.toString() return api.get<{ leagues: StandingsLeague[] }>(`${API_BASE}/standings${qs ? `?${qs}` : ''}`) } // ── 数据源管理 ────────────────────────────────────────────────── /** * 测试数据源连通性 — 后端真实请求上游一次,不触发入库 */ 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 { // F4 修复: 优先使用不依赖比赛的 ping 端点 try { return await api.post(`${API_BASE}/admin/llm/ping`, {}) } catch { // 回退到旧方式(兼容) 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: [], } } } // ── 系统配置 ──────────────────────────────────────────────────── /** * P2-9 修复: 获取系统配置列表,从后端 /admin/settings 读取真实值(脱敏)。 * 字段名对齐 Config.tsx 中使用的 { key, value_masked, description, is_sensitive } 格式。 */ export async function fetchSystemConfig(): Promise { try { const settings = await fetchSettings() return settings.map(s => ({ key: s.key, value_masked: s.masked, description: s.description, is_sensitive: s.sensitive, })) } catch { // 后端不可用时返回空列表,Config.tsx 会显示空状态 return [] } } /** * 数据源健康/最近采集状态(只读,不触发采集) */ export function fetchIngestStatus(): Promise<{ sources: IngestSourceStatus[] }> { return api.get<{ sources: IngestSourceStatus[] }>(`${API_BASE}/admin/ingest/status`) } /** * 比赛详情(含最近预测摘要) */ export function fetchMatchDetail(id: number): Promise { return api.get(`${API_BASE}/matches/${id}`) } /** * 比赛上下文(双方近况 + 历史交锋,只读) */ export function fetchMatchContext(id: number): Promise { return api.get(`${API_BASE}/matches/${id}/context`) } /** * 管理区统计(只读):近 24h/7d 预测次数 */ export function fetchAdminStats(): Promise { return api.get(`${API_BASE}/admin/stats`) } // ── API Key 轮换环 ────────────────────────────────────────────── export interface KeyRingKeyStatus { masked: string blocked_remaining: number } export interface KeyRingStatusResponse { base_url: string total: number has_multiple: boolean cooldown_seconds: number active_index: number active_key: string | null keys: KeyRingKeyStatus[] } export async function fetchKeyRingStatus(): Promise { return api.get(`${API_BASE}/admin/keyring/status`) } export async function resetKeyRingCooldown(): Promise<{ ok: boolean; message: string; stats: KeyRingStatusResponse }> { return api.post<{ ok: boolean; message: string; stats: KeyRingStatusResponse }>(`${API_BASE}/admin/keyring/cooldown/reset`) } // ── 定时任务 ──────────────────────────────────────────────────── export interface ScheduleItem { id: string task: string cron: string leagues?: string enabled: boolean last_run_at?: string | null last_status?: string | null } export async function fetchSchedules(): Promise { return api.get(`${API_BASE}/admin/schedules`) } export async function createSchedule(data: { id: string; task: string; cron: string; leagues?: string; enabled: boolean }): Promise<{ ok: boolean }> { return api.post<{ ok: boolean }>(`${API_BASE}/admin/schedules`, data) } export async function updateSchedule(id: string, data: Partial): Promise<{ ok: boolean }> { return api.put<{ ok: boolean }>(`${API_BASE}/admin/schedules/${id}`, data) } export async function deleteSchedule(id: string): Promise<{ ok: boolean }> { return api.delete<{ ok: boolean }>(`${API_BASE}/admin/schedules/${id}`) } export async function runScheduleNow(id: string): Promise<{ ok: boolean; message: string }> { return api.post<{ ok: boolean; message: string }>(`${API_BASE}/admin/schedules/${id}/run`) }