Files
Profeto/frontend/src/admin/dal.ts
T
shangfangjian 786f10aa11 feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲
- crypto.py: API Key 加密/解密工具
- runtime_config.py: 运行时动态配置管理
- log_buffer.py: 内存日志缓冲区
- config.py: 新增加密配置项
- http_client.py: 增强重试和错误处理
2026-09-19 11:58:03 +08:00

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 },
]
}