- crypto.py: API Key 加密/解密工具 - runtime_config.py: 运行时动态配置管理 - log_buffer.py: 内存日志缓冲区 - config.py: 新增加密配置项 - http_client.py: 增强重试和错误处理
288 lines
10 KiB
TypeScript
288 lines
10 KiB
TypeScript
/**
|
|
* 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<DashboardStats> {
|
|
// 并行获取各端点数据
|
|
const [leagues, matches, predictions, health] = await Promise.allSettled([
|
|
api.get<League[]>(`${API_BASE}/leagues`),
|
|
api.get<Match[]>(`${API_BASE}/matches?limit=1`),
|
|
api.get<Prediction[]>(`${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<any> {
|
|
const sourceMap: Record<string, { path: string; body: any }> = {
|
|
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<any> {
|
|
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<any[]> {
|
|
const res = await api.get<any>(`${API_BASE}/predictions?limit=${limit}`)
|
|
return Array.isArray(res) ? res : (res as any)?.items ?? []
|
|
}
|
|
|
|
// ── 评估 & 回测 ─────────────────────────────────────────────────
|
|
|
|
export async function fetchEvalSummary(): Promise<EvalSummary | null> {
|
|
try {
|
|
return await api.get<EvalSummary>(`${API_BASE}/eval/summary`)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function triggerBacktest(req: BacktestRequest): Promise<BacktestSummary> {
|
|
return api.post<BacktestSummary>(`${API_BASE}/backtest`, req, { timeoutMs: 300_000 })
|
|
}
|
|
|
|
// ── 辅助数据 ────────────────────────────────────────────────────
|
|
|
|
export async function fetchLeagues(): Promise<League[]> {
|
|
try {
|
|
return await api.get<League[]>(`${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<any>(`${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<any> {
|
|
return api.post(`${API_BASE}/eval/settle`, {
|
|
prediction_id,
|
|
home_goals,
|
|
away_goals,
|
|
})
|
|
}
|
|
|
|
// ── 健康检查 ────────────────────────────────────────────────────
|
|
|
|
export async function fetchHealth(): Promise<any> {
|
|
try {
|
|
return await api.get<any>('/health')
|
|
} catch {
|
|
return { status: 'unknown' }
|
|
}
|
|
}
|
|
|
|
// ── 数据源管理 ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 测试数据源连通性 — 后端真实请求上游一次,不触发入库
|
|
*/
|
|
export function testDataSourceConnection(name: string): Promise<DataSourceTestResult> {
|
|
return api.post<DataSourceTestResult>(`${API_BASE}/admin/datasources/${name}/test`)
|
|
}
|
|
|
|
/**
|
|
* 获取数据源状态与配置(脱敏)
|
|
*/
|
|
export function fetchDataSourceStatuses(): Promise<DataSourceStatus[]> {
|
|
return api.get<DataSourceStatus[]>(`${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<LLMAgentConfig[]> {
|
|
return api.get<LLMAgentConfig[]>(`${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<DataSourceSetting[]> {
|
|
return api.get<DataSourceSetting[]>(`${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<any> {
|
|
return api.post(
|
|
`${API_BASE}/predict`,
|
|
{
|
|
match_id: matchId || 1,
|
|
mode: 'single',
|
|
},
|
|
{ timeoutMs: 300_000 },
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 获取 LLM 使用统计 — 从预测列表聚合
|
|
*/
|
|
export async function fetchLLMUsageStats(): Promise<any> {
|
|
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<any[]> {
|
|
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 },
|
|
]
|
|
}
|