公开只读 API(P1-2/P1-3)+ 全套文档对齐(P1-1) #11

Merged
shangfangjian merged 3 commits from docs-public-api-fixes into main 2026-09-21 21:38:59 +08:00
10 changed files with 239 additions and 132 deletions
Showing only changes of commit 80616cf459 - Show all commits
-1
View File
@@ -40,7 +40,6 @@ LLM_TIMEOUT=60
# ---- 数据源 ---- # ---- 数据源 ----
BZZOIRO_KEY= BZZOIRO_KEY=
API_FOOTBALL_KEY=
# ---- CORS ---- # ---- CORS ----
CORS_ORIGINS=http://localhost:5173,http://localhost:3000 CORS_ORIGINS=http://localhost:5173,http://localhost:3000
+59 -35
View File
@@ -12,28 +12,30 @@
│ REST API │ REST API
┌──────────────────────▼──────────────────────────────┐ ┌──────────────────────▼──────────────────────────────┐
│ FastAPI │ │ FastAPI │
│ ├── /api/v1/matches 比赛查询 │ ├── /api/v1/matches 比赛查询(公开只读)
│ ├── /api/v1/predict LLM 预测 (单/多 Agent) │ │ ├── /api/v1/predict LLM 预测 (单/多 Agent) │
│ ├── /api/v1/ingest/* 数据采集 │ ├── /api/v1/ingest/* 数据采集(需管理员)
│ ├── /api/v1/eval/* 评估回填 │ ├── /api/v1/eval/* 评估回填(需管理员)
│ └── /api/v1/backtest 回测 │ └── /api/v1/backtest 回测(需管理员)
└──────────┬─────────────────────────────┬────────────┘ └──────────┬─────────────────────────────┬────────────┘
│ │ │ │
┌──────────▼──────────┐ ┌─────────────▼────────────┐ ┌──────────▼──────────┐ ┌─────────────▼────────────┐
│ PostgreSQL │ │ LLM (OpenAI-compatible) │ │ PostgreSQL │ │ LLM (OpenAI-compatible) │
6 张表 │ │ OpenAI / Deepseek / │ 12 张表 │ │ OpenAI / Deepseek / │
│ leagues/teams/ │ │ Ollama / 任意网关 │ │ leagues/teams/ │ │ Ollama / 任意网关 │
│ matches/match_ │ └──────────────────────────┘ │ matches/match_ │ └──────────────────────────┘
│ stats/predictions/ │ stats/standings/
injuries predictions/
│ app_settings/ │
│ schedules + │
│ raw_events 等 4 张 │
│ 数据治理表 │
└─────────────────────┘ └─────────────────────┘
│ 采集 │ 采集
┌──────────┴─────────────────────────────────────────┐ ┌──────────┴─────────────────────────────────────────┐
│ 数据源 (DataSource 协议 + 注册表) │ │ 数据源 (DataSource 协议 + 注册表) │
── bzzoiro 比分 / 统计 / xG ── bzzoiro 比分 / 赛程 / 统计 / 积分榜
│ ├── understat xG 回填 │
│ └── injuries 伤停数据 (api-football) │
└────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────┘
``` ```
@@ -55,7 +57,7 @@ API Route → Application Service → Repository → UnitOfWork → DB
比赛数据 → 切片 ─┬─→ A 近期状态专家 ─┐ 比赛数据 → 切片 ─┬─→ A 近期状态专家 ─┐
├─→ B 攻防数据专家 ─┤ ├─→ B 攻防数据专家 ─┤
├─→ C 主客因素专家 ─┼─→ 终裁专家 ─→ 最终预测 ├─→ C 主客因素专家 ─┼─→ 终裁专家 ─→ 最终预测
├─→ D 阵容完整专家 ─┤ ├─→ D 联赛排名专家 ─┤
└─→ E 历史交锋专家 ─┘ └─→ E 历史交锋专家 ─┘
``` ```
@@ -66,8 +68,7 @@ API Route → Application Service → Repository → UnitOfWork → DB
### 数据正确性保障 ### 数据正确性保障
- **Cutoff 机制**: 回测时只使用 `cutoff_at` 之前已采集的数据 - **Cutoff 机制**: 回测时只使用 `cutoff_at` 之前已采集的数据(近况/交锋/统计/积分榜切片统一生效)
- **Injury 防泄漏**: 伤停查询强制 `retrieved_at <= cutoff`
- **LLM 输出校验**: Pydantic 严格校验 + 语义一致性检查 - **LLM 输出校验**: Pydantic 严格校验 + 语义一致性检查
- **数据库约束**: CHECK 约束作为最后一道防线 - **数据库约束**: CHECK 约束作为最后一道防线
@@ -141,20 +142,29 @@ cd frontend && npm install && npm run dev
## API 概览 ## API 概览
**公开只读**(无需登录;`predict` 带内存限流):
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
|---|---|---| |---|---|---|
| GET | `/api/v1/matches` | 比赛查询(筛选/分页) | | GET | `/api/v1/leagues` | 联赛列表(仅 id/code/name/country) |
| GET | `/api/v1/leagues` | 联赛列表 | | GET | `/api/v1/matches` | 比赛查询(筛选/游标分页) |
| POST | `/api/v1/predict` | LLM 预测 (`mode=single`/`multi`) | | GET | `/api/v1/matches/{id}` | 比赛详情(含统计与最近预测) |
| GET | `/api/v1/predictions` | 预测历史 | | GET | `/api/v1/matches/{id}/context` | 比赛上下文(双方近况 + 历史交锋) |
| POST | `/api/v1/ingest/bzzoiro` | 采集比分/统计 | | GET | `/api/v1/standings` | 联赛积分榜 |
| POST | `/api/v1/ingest/understat` | 回填 xG | | POST | `/api/v1/predict` | LLM 预测 (`mode=single`/`multi`/`baseline`) |
| POST | `/api/v1/ingest/injuries` | 采集伤停 | | GET | `/health``/health/ready` | 存活 / 就绪检查(含 DB) |
**需管理员**(Cookie 会话或 `X-API-Key`):
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/v1/ingest/bzzoiro` | 采集赛果/赛程/统计/积分榜 |
| GET | `/api/v1/predictions` | 预测历史(列表) |
| GET | `/api/v1/predictions/{id}` | 单条预测详情 |
| POST | `/api/v1/eval/settle` | 回填实际结果 | | POST | `/api/v1/eval/settle` | 回填实际结果 |
| GET | `/api/v1/eval/summary` | 准确率汇总 | | GET | `/api/v1/eval/summary` | 准确率汇总 |
| POST | `/api/v1/backtest` | 历史回测 | | POST | `/api/v1/backtest` | 历史回测 |
| GET | `/health` | 存活检查 | | `/api/v1/admin/**` | 配置/采集状态/日志/定时任务/死信等 | 管理后台(router 级鉴权) |
| GET | `/health/ready` | 就绪检查(含 DB) |
## 项目结构 ## 项目结构
@@ -163,36 +173,46 @@ Profeto/
├── src/ ├── src/
│ ├── api/ # FastAPI 路由层 │ ├── api/ # FastAPI 路由层
│ │ ├── app.py # 应用工厂 + lifespan │ │ ├── app.py # 应用工厂 + lifespan
│ │ ├── deps.py # 依赖注入:鉴权 / 限流
│ │ ├── schemas.py # Pydantic 请求/响应模型 │ │ ├── schemas.py # Pydantic 请求/响应模型
│ │ └── routes/ │ │ └── routes/
│ │ ├── matches.py # 比赛查询 │ │ ├── matches.py # 比赛查询(公开只读)
│ │ ├── predict.py # 预测入口 │ │ ├── predict.py # 预测入口 + 预测历史
│ │ ├── ingest.py # 数据采集 │ │ ├── ingest.py # 数据采集(需管理员)
│ │ ├── eval.py # 评估回填 │ │ ├── eval.py # 评估回填(需管理员)
│ │ ── backtest.py # 回测 │ │ ── backtest.py # 回测(需管理员)
│ │ ├── auth.py # 登录/登出/改密
│ │ ├── admin_settings.py # /admin/** 配置/日志/数据质量(router 级鉴权)
│ │ └── schedules.py # 定时任务 + 死信重试(router 级鉴权)
│ ├── core/ # 基础设施 │ ├── core/ # 基础设施
│ │ ├── config.py # pydantic-settings 配置 │ │ ├── config.py # pydantic-settings 配置
│ │ ├── crypto.py # 加密/哈希
│ │ ├── http_client.py # 共享 httpx 客户端 │ │ ├── http_client.py # 共享 httpx 客户端
│ │ ── retry.py # 重试工具(指数退避) │ │ ── log_buffer.py # 内存日志缓冲(admin 日志页)
│ │ ├── runtime_config.py # DB 配置覆盖(.env → app_settings)
│ │ ├── scheduler.py # 进程内 cron 调度器
│ │ └── security_check.py # 启动安全校验
│ ├── data/ # 数据层 │ ├── data/ # 数据层
│ │ ├── sources.py # DataSource 协议 + 注册表 │ │ ├── sources.py # DataSource 协议 + 注册表
│ │ ├── bzzoiro.py # bzzoiro 数据源(events/standings/stats)
│ │ ├── normalize.py # 数据规范化契约 │ │ ├── normalize.py # 数据规范化契约
│ │ ├── bzzoiro.py # bzzoiro 数据源
│ │ ├── understat.py # understat xG 数据源
│ │ ├── injuries.py # 伤停数据
│ │ ├── config.py # 联赛映射常量 │ │ ├── config.py # 联赛映射常量
│ │ ── team_names.py # 队名归一化 │ │ ── key_ring.py # API Key 轮换环(429 冷却)
│ │ ├── team_names.py # 队名归一化
│ │ └── team_names_zh.py # 队名中文映射
│ ├── db/ # 数据库 │ ├── db/ # 数据库
│ │ ├── base.py # SQLAlchemy async engine │ │ ├── base.py # SQLAlchemy async engine
│ │ ├── models.py # ORM 模型 (6 表) │ │ ├── models.py # ORM 模型 (12 表)
│ │ ├── unit_of_work.py # UnitOfWork 事务封装 │ │ ├── unit_of_work.py # UnitOfWork 事务封装
│ │ └── repositories.py # Repository 数据访问 │ │ └── repositories.py # Repository 数据访问
│ └── llm/ # LLM 预测核心 │ └── llm/ # LLM 预测核心
│ ├── predict.py # 预测服务 (缓存 + 单/多模式) │ ├── predict.py # 预测服务 (缓存 + 单/多/基线模式)
│ ├── context_builder.py # 数据切片 + 上下文拼接 │ ├── context_builder.py # 数据切片 + 上下文拼接
│ ├── baseline.py # 基线预测(均值模型)
│ ├── eval.py # 评估统计 │ ├── eval.py # 评估统计
│ ├── backtest.py # 回测框架 │ ├── backtest.py # 回测框架
│ ├── provider.py # 多提供商 LLM 抽象 │ ├── provider.py # 多提供商 LLM 抽象
│ ├── utils.py # LLM 工具函数
│ ├── validation.py # LLM 输出校验 │ ├── validation.py # LLM 输出校验
│ ├── agents/ │ ├── agents/
│ │ ├── base.py # Agent 基础设施 + 解析 │ │ ├── base.py # Agent 基础设施 + 解析
@@ -200,6 +220,11 @@ Profeto/
│ └── prompts/ # Prompt 模板 │ └── prompts/ # Prompt 模板
├── alembic/ # 数据库迁移 ├── alembic/ # 数据库迁移
├── frontend/ # React 前端 ├── frontend/ # React 前端
│ └── src/
│ ├── pages/ # 公开站(赛程 Matches + 积分榜 Standings)
│ ├── admin/ # 管理后台(布局/页面/数据访问层 dal.ts)
│ ├── components/ # 共享组件
│ └── lib/http.ts # 唯一 HTTP 实现(带凭据/超时/错误处理)
├── docs/ # 详细文档 ├── docs/ # 详细文档
├── tests/ # 单元测试 ├── tests/ # 单元测试
├── docker-compose.yml ├── docker-compose.yml
@@ -234,7 +259,6 @@ Profeto/
| `LLM_SPECIALIST_MODEL` | 专家模型 (空=回落 LLM_MODEL) | | | `LLM_SPECIALIST_MODEL` | 专家模型 (空=回落 LLM_MODEL) | |
| `LLM_AGGREGATOR_MODEL` | 终裁模型 (空=回落 LLM_MODEL) | | | `LLM_AGGREGATOR_MODEL` | 终裁模型 (空=回落 LLM_MODEL) | |
| `BZZOIRO_KEY` | bzzoiro API Key | *(必填)* | | `BZZOIRO_KEY` | bzzoiro API Key | *(必填)* |
| `API_FOOTBALL_KEY` | api-football Key (伤停) | |
| `CORS_ORIGINS` | 允许的跨域来源 | `http://localhost:5173` | | `CORS_ORIGINS` | 允许的跨域来源 | `http://localhost:5173` |
## 测试 ## 测试
+35 -27
View File
@@ -13,16 +13,15 @@
│ │ │ │
│ 数据查询 预测编排 采集(手动/cron 触发) │ │ 数据查询 预测编排 采集(手动/cron 触发) │
│ ┌──────┐ ┌────────────┐ ┌───────────────────┐ │ │ ┌──────┐ ┌────────────┐ ┌───────────────────┐ │
│ │matches│ │ orchestrator│ │ bzzoiro (赛果) │ │ │ │matches│ │ orchestrator│ │ bzzoiro (唯一源) │ │
│ │leagues│ │ ┌─ 5 专家并行(便宜模型) │ │ │ │leagues│ │ ┌─ 5 专家并行(便宜模型) │ │
│ └──┬───┘ │ │ h2h / form / stats / │ │ │ └──┬───┘ │ │ h2h / form / stats / │ │
│ │ │ │ home_away / injuries │ │ │ │ │ │ home_away / standings │ │
│ │ │ └─ aggregator 终裁(强模型) │ │ │ │ │ └─ aggregator 终裁(强模型) │ │
│ │ └────────────┘ └───────────────────┘ │ │ └────────────┘ │ events / standings │
│ │ └ understat (xG) ┌──┴──────────────┴──┐ │ /stats 三条管线 │
──────────────────┐ └ injuries (伤停) │ PostgreSQL (12 张表)│ └───────────────────┘
│ PostgreSQL (6 张表) │ httpx → 外部 API │ └────────────────────┘ httpx → 外部 API │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
``` ```
@@ -31,7 +30,7 @@
1. `POST /predict {match_id}` → orchestrator 1. `POST /predict {match_id}` → orchestrator
2. `load_match_header`: 查比赛 + 双方 + 联赛(一次 eager load) 2. `load_match_header`: 查比赛 + 双方 + 联赛(一次 eager load)
3. **5 个专家 agent 并行**(`asyncio.gather`),每个: 3. **5 个专家 agent 并行**(`asyncio.gather`),每个:
- 各自的数据切片函数查库(近况/交锋/积分榜 SQL 聚合/伤停/xG) - 各自的数据切片函数查库(近况/交锋/积分榜聚合/射门控球/xG)
- 切片无数据 → **跳过 LLM**,直接 `no_data` stub(省 token、防幻觉) - 切片无数据 → **跳过 LLM**,直接 `no_data` stub(省 token、防幻觉)
- 有数据 → 专属 prompt(专家模型,便宜快)→ 结构化 JSON 报告(`home_edge` 方向性评分 + 证据) - 有数据 → 专属 prompt(专家模型,便宜快)→ 结构化 JSON 报告(`home_edge` 方向性评分 + 证据)
4. **终裁 agent**:5 份报告 + 比赛信息 → 权衡采信度(`agent_weights`)→ 最终预测 JSON 4. **终裁 agent**:5 份报告 + 比赛信息 → 权衡采信度(`agent_weights`)→ 最终预测 JSON
@@ -44,7 +43,7 @@
|---|---| |---|---|
| **多专家并行而非单次大 prompt** | 每维度独立迭代 prompt;报告可归因(哪个维度分析错了);总延迟 ≈ 2 次串行调用 | | **多专家并行而非单次大 prompt** | 每维度独立迭代 prompt;报告可归因(哪个维度分析错了);总延迟 ≈ 2 次串行调用 |
| **专家/终裁模型分档** | 专家用便宜模型快速分析,终裁用强模型汇总决策,成本与质量平衡(`LLM_SPECIALIST_MODEL` / `LLM_AGGREGATOR_MODEL`) | | **专家/终裁模型分档** | 专家用便宜模型快速分析,终裁用强模型汇总决策,成本与质量平衡(`LLM_SPECIALIST_MODEL` / `LLM_AGGREGATOR_MODEL`) |
| **no_data 门控** | 无数据维度(如伤停未接入)不调 LLM,终裁知道维度缺失,不编造 | | **no_data 门控** | 无数据维度(如积分榜未采集)不调 LLM,终裁知道维度缺失,不编造 |
| **fail-open** | 单个专家失败只标记 `status=error`,其余照常;研究场景可用性优先 | | **fail-open** | 单个专家失败只标记 `status=error`,其余照常;研究场景可用性优先 |
| **`match_date_date` 天级去重** | 不同源时间精度不同,秒级匹配会产生重复行;天级 + 数据库唯一约束 | | **`match_date_date` 天级去重** | 不同源时间精度不同,秒级匹配会产生重复行;天级 + 数据库唯一约束 |
| **积分榜 SQL 聚合 + season 过滤** | `UNION ALL` 主客双视角 + `GROUP BY` 在库内算,只算当前赛季(修复过跨赛季 bug) | | **积分榜 SQL 聚合 + season 过滤** | `UNION ALL` 主客双视角 + `GROUP BY` 在库内算,只算当前赛季(修复过跨赛季 bug) |
@@ -57,33 +56,38 @@
Profeto/ Profeto/
├── src/ ├── src/
│ ├── api/ │ ├── api/
│ │ ├── app.py # FastAPI 工厂(lifespan 仅验证 DB 连接,不建表) │ │ ├── app.py # FastAPI 工厂(lifespan:迁移校验/定时任务/生产限流提醒)
│ │ ├── deps.py # 依赖:管理接口鉴权(X-API-Key) │ │ ├── deps.py # 依赖:管理接口鉴权(Cookie/X-API-Key)+ 限流
│ │ ├── schemas.py # Pydantic v2 请求/响应 │ │ ├── schemas.py # Pydantic v2 请求/响应
│ │ └── routes/ │ │ └── routes/
│ │ ├── matches.py # 联赛/比赛查询(游标分页) │ │ ├── matches.py # 联赛/比赛/上下文/积分榜(公开只读)
│ │ ├── predict.py # 预测 + 预测历史 │ │ ├── predict.py # 预测(限流)+ 预测历史(需鉴权)
│ │ ├── ingest.py # 采集触发(自管 session,需鉴权) │ │ ├── ingest.py # 采集触发(需鉴权)
│ │ ├── eval.py # 赛后回填 + 准确率汇总 │ │ ├── eval.py # 赛后回填 + 准确率汇总(需鉴权)
│ │ ── backtest.py # 历史回测(需鉴权) │ │ ── backtest.py # 历史回测(需鉴权)
│ │ ├── auth.py # 登录/登出/改密
│ │ ├── admin_settings.py # /admin/** 配置/日志/数据质量(router 级鉴权)
│ │ └── schedules.py # 定时任务 + 死信重试(router 级鉴权)
│ ├── db/ │ ├── db/
│ │ ├── base.py # async engine + get_db/get_db_read │ │ ├── base.py # async engine + get_db/get_db_read
│ │ ├── models.py # 6 张表 ORM │ │ ├── models.py # 12 张表 ORM
│ │ ├── repositories.py # 仓储层 │ │ ├── repositories.py # 仓储层
│ │ └── unit_of_work.py # 事务边界 │ │ └── unit_of_work.py # 事务边界
│ ├── data/ │ ├── data/
│ │ ├── bzzoiro.py # 赛果采集 + 幂等入库 │ │ ├── bzzoiro.py # 唯一数据源:events/standings/stats 三管线 + Bronze 层
│ │ ├── understat.py # xG 回填
│ │ ├── injuries.py # 伤停采集(带文件缓存)
│ │ ├── normalize.py # NormalizedMatch 清洗契约 │ │ ├── normalize.py # NormalizedMatch 清洗契约
│ │ ├── team_names.py # 队名归一映射 │ │ ├── team_names.py # 队名归一映射
│ │ ├── team_names_zh.py # 队名中文名映射
│ │ ├── key_ring.py # 多 key 轮换(429 冷却,进程内)
│ │ ├── sources.py # 数据源注册表 │ │ ├── sources.py # 数据源注册表
│ │ └── config.py # 联赛代码映射 │ │ └── config.py # 联赛代码映射
│ ├── llm/ │ ├── llm/
│ │ ├── provider.py # OpenAI-compatible 抽象(共享连接池/JSON 兜底解析) │ │ ├── provider.py # OpenAI-compatible 抽象(共享连接池/JSON 兜底解析)
│ │ ├── context_builder.py # 数据切片(h2h/form/stats/home_away/injuries)+ 单 agent 拼接 │ │ ├── context_builder.py # 数据切片(h2h/form/stats/home_away/standings)+ 单 agent 拼接
│ │ ├── predict.py # 预测入口(mode 分派 + 缓存) │ │ ├── predict.py # 预测入口(mode 分派 + 缓存)
│ │ ├── baseline.py # 基线预测(均值模型,mode=baseline)
│ │ ├── eval.py # 准确率统计 │ │ ├── eval.py # 准确率统计
│ │ ├── utils.py # LLM 工具函数
│ │ ├── validation.py # LLM 输出严格校验(Pydantic) │ │ ├── validation.py # LLM 输出严格校验(Pydantic)
│ │ ├── backtest.py # 回测执行 │ │ ├── backtest.py # 回测执行
│ │ ├── agents/ │ │ ├── agents/
@@ -91,14 +95,18 @@ Profeto/
│ │ │ └── orchestrator.py # 并行专家 → 终裁 → 存库 │ │ │ └── orchestrator.py # 并行专家 → 终裁 → 存库
│ │ └── prompts/ │ │ └── prompts/
│ │ ├── match_prediction_v1/v2.md # 单 agent 模板 │ │ ├── match_prediction_v1/v2.md # 单 agent 模板
│ │ └── agents/{h2h,form,stats,home_away,injuries,aggregator}_v1.md │ │ └── agents/{form,stats,home_away,standings,h2h,aggregator}_v1.md
│ └── core/ │ └── core/
│ ├── config.py # pydantic-settings │ ├── config.py # pydantic-settings
│ ├── crypto.py # 加密/哈希
│ ├── http_client.py # 共享 httpx 客户端 │ ├── http_client.py # 共享 httpx 客户端
── retry.py # 重试工具 ── log_buffer.py # 内存日志缓冲(admin 日志页)
├── alembic/versions/ # 0001~0006(0001 建表 → 0006 漂移清理) │ ├── runtime_config.py # DB 配置覆盖(app_settings)
├── frontend/src/pages/Matches.tsx # 单页(预测面板 + 专家报告折叠区 + 游标分页) │ ├── scheduler.py # 进程内 cron 调度器
├── tests/ # 核心 + agent 测试 │ └── security_check.py # 启动安全校验
├── alembic/versions/ # 0001~0018(建表 → Bronze 层 → 单一数据源 → 基线模式等)
├── frontend/src/ # pages/(公开站) + admin/(管理后台) + lib/http.ts(唯一 HTTP 实现)
├── tests/ # 核心 + agent 测试(250+ 项,自包含)
├── docker-compose.yml # api + postgres 两容器 ├── docker-compose.yml # api + postgres 两容器
└── docs/ # 本文档 └── docs/ # 本文档
``` ```
@@ -113,5 +121,5 @@ Profeto/
| HTTP | httpx(共享连接池)/ urllib(bzzoiro 同步限速) | | HTTP | httpx(共享连接池)/ urllib(bzzoiro 同步限速) |
| LLM | OpenAI-compatible 接口(openai/deepseek/ollama 等任一) | | LLM | OpenAI-compatible 接口(openai/deepseek/ollama 等任一) |
| 前端 | Vite + React 18 + TypeScript + Tailwind | | 前端 | Vite + React 18 + TypeScript + Tailwind |
| 测试 | pytest + pytest-asyncio(33 项,自包含) | | 测试 | pytest + pytest-asyncio(250+ 项,自包含) |
| 部署 | Docker Compose(api + postgres) | | 部署 | Docker Compose(api + postgres) |
+13 -4
View File
@@ -66,6 +66,9 @@ curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
-d '{"leagues":["E0"],"date_from":"2026-08-01","date_to":"2026-09-08"}' -d '{"leagues":["E0"],"date_from":"2026-08-01","date_to":"2026-09-08"}'
``` ```
> 注:采集/评估端点需管理员凭据。本地开发环境(未配置鉴权、非 production)默认放行;
> 生产环境需先 `POST /auth/login` 取 Cookie,或带 `X-API-Key` 头。
数据量大时**直接拉整赛季**(约 380 场,含近几个赛季更好,近况/交锋/积分榜都需要历史): 数据量大时**直接拉整赛季**(约 380 场,含近几个赛季更好,近况/交锋/积分榜都需要历史):
```bash ```bash
@@ -74,14 +77,20 @@ curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
-d '{"leagues":["E0"],"date_from":"2025-08-01","date_to":"2026-09-08"}' -d '{"leagues":["E0"],"date_from":"2025-08-01","date_to":"2026-09-08"}'
``` ```
### 回填 xG(可选,让攻防数据 agent 有数据) ### 回填积分榜与统计(让攻防/排名专家有数据)
```bash ```bash
curl -X POST http://localhost:8000/api/v1/ingest/understat \ curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"league":"E0","season":2025}' -d '{"task":"standings"}'
curl -X POST http://localhost:8000/api/v1/ingest/bzzoiro \
-H "Content-Type: application/json" \
-d '{"task":"stats","leagues":["E0"],"limit":300}'
``` ```
`task=stats` 只补空字段(xG/射门/控球等),不创建比赛。
### 查比赛 ### 查比赛
浏览器打开 http://localhost:5173 ,选"英超 / 未开赛"; 浏览器打开 http://localhost:5173 ,选"英超 / 未开赛";
@@ -126,4 +135,4 @@ curl http://localhost:8000/api/v1/eval/summary
| predict 返回 502 | 看 uvicorn 日志的 LLM error;确认 `LLM_BASE_URL`/`LLM_API_KEY`;`response_format` 不兼容的网关会报错(改用支持 json mode 的模型) | | predict 返回 502 | 看 uvicorn 日志的 LLM error;确认 `LLM_BASE_URL`/`LLM_API_KEY`;`response_format` 不兼容的网关会报错(改用支持 json mode 的模型) |
| 采集 0 场 | bzzoiro Key 失效或联赛代码写错;先 `GET /api/v1/leagues` 看库里有没有联赛 | | 采集 0 场 | bzzoiro Key 失效或联赛代码写错;先 `GET /api/v1/leagues` 看库里有没有联赛 |
| 专家报告全是 no_data | 历史数据不够 —— 近况需要每队近 5 场、积分榜需要本赛季已完赛比赛,多拉几周数据 | | 专家报告全是 no_data | 历史数据不够 —— 近况需要每队近 5 场、积分榜需要本赛季已完赛比赛,多拉几周数据 |
| xg agent 报无 xG 数据 | 先跑 understat 回填;注意 understat 只有五大联赛 | | stats 专家报无 xG/统计 | 先跑 `task=stats` 回填(bzzoiro 统计管线,只补空字段) |
+80 -34
View File
@@ -4,18 +4,30 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
所有数据端点返回 JSON。错误统一为 `{"detail": "<message>"}` + 对应 HTTP 状态码。 所有数据端点返回 JSON。错误统一为 `{"detail": "<message>"}` + 对应 HTTP 状态码。
## 鉴权模型
| 级别 | 端点 | 说明 |
|---|---|---|
| **公开只读** | `GET /leagues``GET /matches``GET /matches/{id}``GET /matches/{id}/context``GET /standings``GET /health*` | 无需任何凭据;公开站直接调用 |
| **公开 + 限流** | `POST /predict` | 内存滑动窗口限流(10 次/分钟/IP) |
| **需管理员** | `GET /predictions*``POST /ingest/bzzoiro``/eval/*``POST /backtest``/admin/**` | Cookie 会话(`POST /auth/login` 颁发)或 `X-API-Key` 头 |
管理端点在生产环境未配置鉴权时 fail-closed(503),不会静默放行。
--- ---
## 数据查询 ## 数据查询(公开只读)
### `GET /api/v1/leagues` ### `GET /api/v1/leagues`
列出已入库联赛。 列出已入库联赛(P1-3: 公开站联赛筛选动态加载来源)
```json ```json
[{"id": 1, "code": "E0", "name": "Premier League", "country": "England"}] [{"id": 1, "code": "E0", "name": "Premier League", "country": "England"}]
``` ```
仅返回 `id/code/name/country` 四个展示字段,不含任何配置或密钥信息。
### `GET /api/v1/matches` ### `GET /api/v1/matches`
比赛列表,游标分页。 比赛列表,游标分页。
@@ -47,7 +59,44 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
### `GET /api/v1/matches/{id}` ### `GET /api/v1/matches/{id}`
单场比赛详情,字段同上。 单场比赛详情,字段同上,另含 `stats`(统计)与 `recent_predictions`(最近 5 条预测摘要)
### `GET /api/v1/matches/{id}/context`
比赛上下文(公开只读,P1-2: 公开站详情页「近况/交锋」数据来源;不触发 LLM):
- `home_recent`: 主队最近 5 场已完赛
- `away_recent`: 客队最近 5 场已完赛
- `h2h`: 双方最近 5 次交手
```json
{
"home_recent": [
{"match_date": "2026-09-12T14:00:00+00:00", "home_team": "阿森纳",
"away_team": "切尔西", "home_goals": 2, "away_goals": 1}
],
"away_recent": [],
"h2h": []
}
```
数据不足时对应列表为空(前端展示空态)。比赛不存在返回 404。
### `GET /api/v1/standings`
联赛积分榜(公开只读)。参数:`league`(联赛代码,空 = 全部)、`season`(空 = 各联赛最新赛季)。
```json
{"leagues": [
{"league_code": "E0", "league_name": "Premier League", "season": "2026-2027",
"retrieved_at": "2026-09-20T08:00:00+00:00",
"rows": [{"position": 1, "team": "阿森纳", "team_en": "Arsenal",
"played": 5, "won": 4, "drawn": 1, "lost": 0,
"goals_for": 11, "goals_against": 3, "goal_diff": 8,
"points": 13, "xg_for": 9.8, "xg_against": 3.9,
"form": "WWWDW", "zone": "UEFA Champions League"}]}
]}
```
--- ---
@@ -96,13 +145,13 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
"exp_home_goals": null, "exp_away_goals": null, "probable_score": null, "exp_home_goals": null, "exp_away_goals": null, "probable_score": null,
"model": "gpt-4o-mini", "latency_ms": 2100, "model": "gpt-4o-mini", "latency_ms": 2100,
"prompt_tokens": 380, "completion_tokens": 120}, "prompt_tokens": 380, "completion_tokens": 120},
{"agent": "injuries", "status": "no_data", "data_sufficiency": "none", {"agent": "standings", "status": "no_data", "data_sufficiency": "none",
"analysis": "该维度无数据,跳过分析。", "home_edge": null, "subjective_confidence": null, "analysis": "该维度无数据,跳过分析。", "home_edge": null, "subjective_confidence": null,
"key_evidence": [], "exp_home_goals": null, "exp_away_goals": null, "key_evidence": [], "exp_home_goals": null, "exp_away_goals": null,
"probable_score": null, "model": "", "latency_ms": null, "probable_score": null, "model": "", "latency_ms": null,
"prompt_tokens": null, "completion_tokens": null} "prompt_tokens": null, "completion_tokens": null}
], ],
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "injuries": 0.0, "h2h": 0.8}, "agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "standings": 0.8, "h2h": 0.8},
"context": "[5 份报告的 JSON 串]", "context": "[5 份报告的 JSON 串]",
"latency_ms": 9800 "latency_ms": 9800
} }
@@ -112,11 +161,11 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
错误:404 比赛不存在;502 LLM 调用失败(终裁失败时整体失败,专家失败不会)。 错误:404 比赛不存在;502 LLM 调用失败(终裁失败时整体失败,专家失败不会)。
### `GET /api/v1/predictions?match_id=&limit=` ### `GET /api/v1/predictions?match_id=&limit=`(需管理员)
预测历史(倒序),含 `settled` 与实际比分回填状态。 预测历史(倒序),含 `settled` 与实际比分回填状态。
### `GET /api/v1/predictions/{id}` ### `GET /api/v1/predictions/{id}`(需管理员)
单条预测详情(含完整 `agent_outputs`)。 单条预测详情(含完整 `agent_outputs`)。
@@ -124,40 +173,34 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
## 数据采集 ## 数据采集
### `POST /api/v1/ingest/bzzoiro` ### `POST /api/v1/ingest/bzzoiro`(需管理员)
从 bzzoiro 采集赛果/赛程并入库(幂等,重复跑安全)。 从 bzzoiro(唯一数据源)采集数据并入库(幂等,重复跑安全)。任务在后台异步执行,请求立即返回。
```json ```json
{"leagues": ["E0", "SP1"], "date_from": "2025-08-01", "date_to": "2026-09-08", "status": "finished"} {"task": "all", "leagues": ["E0", "SP1"], "date_from": "2025-08-01", "date_to": "2026-09-08", "status": "finished"}
``` ```
- `status` 还可传 `scheduled` 拉未来赛程 | 字段 | 默认 | 说明 |
|---|---|---|
| `task` | `events` | 采集任务:`events`(比赛)/ `standings`(积分榜)/ `stats`(统计回填,含 xG)/ `all` |
| `leagues` | 全部已知联赛 | 联赛代码列表,如 `["E0", "SP1"]` |
| `date_from` / `date_to` | 空 | 日期范围(`YYYY-MM-DD`) |
| `status` | 空(两者都采) | `finished`(已完赛)/ `scheduled`(未来赛程) |
| `season` | 当前赛季 | standings 赛季,如 `"2026-2027"` |
| `limit` | 100 | stats 回填单次最大比赛数(1500) |
- 响应含每联赛 `inserted`/`updated`/`errors` 统计 - 响应含每联赛 `inserted`/`updated`/`errors` 统计
- `task=stats` 只补空字段、不创建比赛(xG/射门/控球等统计回填)
### `POST /api/v1/ingest/understat` > 历史版本曾有独立的 understat(xG)与 injuries(伤停)采集端点,
> 已随数据源收敛为 bzzoiro 唯一来源而移除。
回填 xG(只补空字段,不创建比赛):
```json
{"league": "E0", "season": 2025}
```
`season=2025` 表示 2025-2026 赛季。仅支持五大联赛。
### `POST /api/v1/ingest/injuries`
采集伤停(需 `API_FOOTBALL_KEY`,当前只返回计数,尚未接入 context):
```json
{"date": "2026-09-10"}
```
--- ---
## 评估 ## 评估
### `POST /api/v1/eval/settle` ### `POST /api/v1/eval/settle`(需管理员)
赛后回填实际比分: 赛后回填实际比分:
@@ -165,7 +208,7 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
{"prediction_id": 7, "home_goals": 2, "away_goals": 1} {"prediction_id": 7, "home_goals": 2, "away_goals": 1}
``` ```
### `GET /api/v1/eval/summary` ### `GET /api/v1/eval/summary`(需管理员)
`provider × model` 聚合已结算预测: `provider × model` 聚合已结算预测:
@@ -183,7 +226,10 @@ Base URL: `http://localhost:8000` · 交互式文档: `/docs`(Swagger)与 `/redo
## 基础 ## 基础
| 端点 | 说明 | | 端点 | 权限 | 说明 |
|---|---| |---|---|---|
| `GET /health` | 存活检查 | | `GET /health` | 公开 | 存活检查 |
| `GET /docs` | Swagger UI | | `GET /docs``GET /redoc` | 公开 | Swagger UI / ReDoc |
| `POST /auth/login``POST /auth/logout``GET /auth/me` | 公开 | 管理员 Cookie 会话登录/登出/当前用户 |
| `POST /auth/change-password` | 需管理员 | 修改管理员密码 |
| `POST /backtest` | 需管理员 | 历史回测(对已完赛比赛批量预测并评估) |
+7 -7
View File
@@ -10,7 +10,7 @@ Profeto 的核心预测路径是 **5 个领域专家 Agent 并行分析 + 1 个
| `form` 近期状态 | 分析比分与关键事件,判断近期走势 | 两队近 N 场赛果(含 xG) | `home_edge` + 走势判断 | | `form` 近期状态 | 分析比分与关键事件,判断近期走势 | 两队近 N 场赛果(含 xG) | `home_edge` + 走势判断 |
| `stats` 攻防数据 | 评估进球、射门与控球,量化攻防强度 | 近 N 场进球/射门/控球/xG 统计 | `home_edge` + 攻防强度 | | `stats` 攻防数据 | 评估进球、射门与控球,量化攻防强度 | 近 N 场进球/射门/控球/xG 统计 | `home_edge` + 攻防强度 |
| `home_away` 主客因素 | 对比主场与客场表现,评估地理优势影响 | 主队主场战绩 + 客队客场战绩 | `home_edge` + 地理优势 | | `home_away` 主客因素 | 对比主场与客场表现,评估地理优势影响 | 主队主场战绩 + 客队客场战绩 | `home_edge` + 地理优势 |
| `injuries` 阵容完整性 | 汇总伤停与停赛名单,评估战力缺失程度 | 伤停数据(当前无源 → no_data 门控) | `home_edge``no_data` | | `standings` 联赛排名 | 结合积分榜排名、积分与分区(欧冠/欧联/降级),评估双方竞争位置 | 两队当前赛季积分榜行(排名/积分/分区/近期战绩) | `home_edge``no_data` |
| `h2h` 历史交锋 | 分析过去数年以及近期的交手数据,提取交手规律 | 近 N 次交锋(含主客方向 + 总计统计) | `home_edge` + 交手规律 | | `h2h` 历史交锋 | 分析过去数年以及近期的交手数据,提取交手规律 | 近 N 次交锋(含主客方向 + 总计统计) | `home_edge` + 交手规律 |
| `aggregator` 终裁 | 权衡 5 份报告 → 最终结论 | 5 份结构化报告 + 比赛头信息 | 最终预测 + 各报告采信度 | | `aggregator` 终裁 | 权衡 5 份报告 → 最终结论 | 5 份结构化报告 + 比赛头信息 | 最终预测 + 各报告采信度 |
@@ -25,7 +25,7 @@ POST /predict {match_id, mode: "multi"}
│ ├─ form agent ─┐ │ ├─ form agent ─┐
│ ├─ stats agent │ 每个 agent 拿到专属数据切片 │ ├─ stats agent │ 每个 agent 拿到专属数据切片
│ ├─ home_away agent │ → no_data 门控 → 调 LLM → 输出 JSON 报告 │ ├─ home_away agent │ → no_data 门控 → 调 LLM → 输出 JSON 报告
│ ├─ injuries agent │ (无数据 → 跳过 LLM,返回 stub) │ ├─ standings agent │ (无数据 → 跳过 LLM,返回 stub)
│ └─ h2h agent ─┘ │ └─ h2h agent ─┘
├─ aggregator agent(5 份报告 + 比赛头 → 最终 JSON) ├─ aggregator agent(5 份报告 + 比赛头 → 最终 JSON)
@@ -39,12 +39,12 @@ POST /predict {match_id, mode: "multi"}
5 个专家通过 `asyncio.gather` 并发,总延迟 ≈ `max(专家延迟) + 终裁延迟` ≈ 2 次串行 LLM 调用。 5 个专家通过 `asyncio.gather` 并发,总延迟 ≈ `max(专家延迟) + 终裁延迟` ≈ 2 次串行 LLM 调用。
### 2. no_data 门控(省 token、防幻觉) ### 2. no_data 门控(省 token、防幻觉)
数据切片为空时(如伤停数据源未接入),**跳过 LLM 调用**,直接返回: 数据切片为空时(如该场比赛的积分榜尚未采集),**跳过 LLM 调用**,直接返回:
```json ```json
{"agent": "injuries", "status": "no_data", "data_sufficiency": "none", {"agent": "standings", "status": "no_data", "data_sufficiency": "none",
"analysis": "该维度无数据,跳过分析。"} "analysis": "该维度无数据,跳过分析。"}
``` ```
终裁 Agent 会看到这个 `no_data` 状态,不会编造伤停分析。 终裁 Agent 会看到这个 `no_data` 状态,不会编造积分榜分析。
### 3. fail-open(单专家失败不阻断) ### 3. fail-open(单专家失败不阻断)
单个专家 LLM 调用失败 → 其报告标记 `status: error`,其余 4 份 + 终裁照常执行。 单个专家 LLM 调用失败 → 其报告标记 `status: error`,其余 4 份 + 终裁照常执行。
@@ -89,7 +89,7 @@ POST /predict {match_id, mode: "multi"}
"1x2": "1", "1x2": "1",
"subjective_confidence": 0.68, "subjective_confidence": 0.68,
"reasoning": "综合 stats 报告的攻防强度与 form 报告的三连胜势头……", "reasoning": "综合 stats 报告的攻防强度与 form 报告的三连胜势头……",
"agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "injuries": 0.0, "h2h": 0.8} "agent_weights": {"form": 0.9, "stats": 0.8, "home_away": 0.7, "standings": 0.8, "h2h": 0.8}
} }
``` ```
@@ -120,7 +120,7 @@ agents/
├── form_v1.md # 近期状态专家 ├── form_v1.md # 近期状态专家
├── stats_v1.md # 攻防数据专家 ├── stats_v1.md # 攻防数据专家
├── home_away_v1.md # 主客因素专家 ├── home_away_v1.md # 主客因素专家
├── injuries_v1.md # 阵容完整性专家 ├── standings_v1.md # 联赛排名专家
├── h2h_v1.md # 历史交锋专家 ├── h2h_v1.md # 历史交锋专家
└── aggregator_v1.md # 终裁 └── aggregator_v1.md # 终裁
``` ```
+26 -14
View File
@@ -4,9 +4,10 @@
| 数据源 | 用途 | 必需 Key | 说明 | | 数据源 | 用途 | 必需 Key | 说明 |
|---|---|---|---| |---|---|---|---|
| bzzoiro | 赛果/赛程(主源) | `BZZOIRO_KEY` | 五大联赛历史 + 实时 | | bzzoiro(唯一) | 赛果/赛程/积分榜/统计(xG、射门、控球等) | `BZZOIRO_KEY` | 五大联赛 + 欧战,历史 + 实时 |
| understat | xG 回填 | 无(公开) | 仅五大联赛,补 `match_stats.xg` |
| api-football | 伤停 | `API_FOOTBALL_KEY` | 当前只采集计数,未接入 context | > 历史版本曾有 understat(xG 回填)与 api-football(伤停)两个辅助源,
> 现已移除:数据源收敛为 bzzoiro 唯一来源,统计与积分榜均由 bzzoiro 管线采集。
### bzzoiro ### bzzoiro
@@ -28,12 +29,6 @@
> - 黄牌: `home_yellow_cards` / `away_yellow_cards` > - 黄牌: `home_yellow_cards` / `away_yellow_cards`
> - 红牌: `home_red_cards` / `away_red_cards` > - 红牌: `home_red_cards` / `away_red_cards`
### understat
- 端点:`/getLeagueData/{league}/{season}`,返回 JS 包裹的 JSON(需正则提取)
- 只回填 xG(`match_stats.home_xg`/`away_xg`),**不创建新比赛**
- 通过"天级日期 + 队名归一"匹配已有比赛
## 数据清洗契约 ## 数据清洗契约
所有数据源统一清洗为 `NormalizedMatch`(`src/data/normalize.py`),字段: 所有数据源统一清洗为 `NormalizedMatch`(`src/data/normalize.py`),字段:
@@ -70,7 +65,7 @@
## 数据库 Schema ## 数据库 Schema
6 张表: 12 张表:核心业务表 5 张见下方 DDL,其余 7 张(积分榜/配置/调度/治理)见后文表格。
```sql ```sql
-- 联赛 -- 联赛
@@ -144,12 +139,25 @@ CREATE TABLE predictions (
reasoning TEXT, reasoning TEXT,
raw_response JSONB, -- LLM 完整原始响应 raw_response JSONB, -- LLM 完整原始响应
agent_outputs JSONB, -- multi 模式: 5 份专家报告 agent_outputs JSONB, -- multi 模式: 5 份专家报告
agent_weights JSONB, -- multi 模式: 终裁给出的各专家权重
created_at TIMESTAMPTZ, created_at TIMESTAMPTZ,
actual_home_goals INT, actual_away_goals INT, -- 赛后回填 actual_home_goals INT, actual_away_goals INT, -- 赛后回填
settled BOOLEAN DEFAULT FALSE settled BOOLEAN DEFAULT FALSE
); );
``` ```
其余 7 张表(DDL 略,详见 `src/db/models.py` 与 alembic 迁移):
| 表 | 状态 | 用途 |
|---|---|---|
| `standings` | 已启用 | 联赛积分榜快照,按 `(league_id, season, team_id)` upsert,同联赛同赛季只保留最新快照;含排名/战绩/进失球/积分/分区(zone) |
| `app_settings` | 已启用 | 后台运行时设置(如数据源 API Key),读取时优先于 `.env` 默认值 |
| `schedules` | 已启用 | 定时采集任务配置(task/cron/leagues/enabled),供内置调度器执行 |
| `raw_events` | 预留未启用 | Bronze 层原始事件存档;规划中用于重放与审计 |
| `ingest_failures` | 已启用 | 采集失败死信:bzzoiro 三条管线(events/standings/stats)抓取失败时写入,admin 后台可查看与重试 |
| `data_quality_checks` | 预留未启用 | 数据质量检查结果;规划中定时检查比赛/统计/积分榜完整性 |
| `data_lineage` | 预留未启用 | ETL 血缘追踪;规划中记录源记录到目标表的映射 |
### 关键设计点 ### 关键设计点
1. **`match_date_date`(天级日期)**: 用于天级去重。bzzoiro 返回的时间带时分秒,精确匹配不可靠,故拆出 `DATE` 列做唯一键。 1. **`match_date_date`(天级日期)**: 用于天级去重。bzzoiro 返回的时间带时分秒,精确匹配不可靠,故拆出 `DATE` 列做唯一键。
@@ -170,7 +178,9 @@ CREATE TABLE predictions (
- 状态:只允许单向升级(`scheduled` → `finished`),防止完赛行被覆盖成赛程 - 状态:只允许单向升级(`scheduled` → `finished`),防止完赛行被覆盖成赛程
- stats:只补空(`home_xg` 已有值时不覆盖) - stats:只补空(`home_xg` 已有值时不覆盖)
`ingest_understat` 只回填 xG(也只补空),不创建比赛。 `task=stats` 只回填统计(xG/射门/控球等,也只补空),不创建比赛。
`task=standings` 按 `(league_id, season, team_id)` upsert 积分榜快照,同一联赛同一赛季只保留最新一份。
## 采集建议 ## 采集建议
@@ -183,9 +193,11 @@ curl -X POST /api/v1/ingest/bzzoiro \
curl -X POST /api/v1/ingest/bzzoiro \ curl -X POST /api/v1/ingest/bzzoiro \
-d '{"leagues":["E0"],"date_from":"2026-09-01","date_to":"2026-09-08"}' -d '{"leagues":["E0"],"date_from":"2026-09-01","date_to":"2026-09-08"}'
# 3. xG 回填(可选,提升 xg agent 质量) # 3. 积分榜 + 统计回填(xG/射门/控球,提升 stats/standings 专家质量)
curl -X POST /api/v1/ingest/understat -d '{"league":"E0","season":2025}' curl -X POST /api/v1/ingest/bzzoiro -d '{"task":"standings"}'
curl -X POST /api/v1/ingest/understat -d '{"league":"E0","season":2026}' curl -X POST /api/v1/ingest/bzzoiro -d '{"task":"stats","leagues":["E0"],"limit":300}'
# 注:采集端点需管理员凭据(Cookie 会话或 X-API-Key 头),下同
``` ```
建议用外部 cron(如系统 crontab)定时触发,不引入 worker/redis。 建议用外部 cron(如系统 crontab)定时触发,不引入 worker/redis。
+1 -2
View File
@@ -81,8 +81,7 @@ cd frontend && npm install && npm run dev
| `LLM_TIMEOUT` | ❌ | `60` | 单次调用超时(秒) | | `LLM_TIMEOUT` | ❌ | `60` | 单次调用超时(秒) |
| `LLM_SPECIALIST_MODEL` | ❌ | — | 专家模型(回落 `LLM_MODEL`) | | `LLM_SPECIALIST_MODEL` | ❌ | — | 专家模型(回落 `LLM_MODEL`) |
| `LLM_AGGREGATOR_MODEL` | ❌ | — | 终裁模型(回落 `LLM_MODEL`) | | `LLM_AGGREGATOR_MODEL` | ❌ | — | 终裁模型(回落 `LLM_MODEL`) |
| `BZZOIRO_KEY` | ✅ | — | bzzoiro 数据源 Key | | `BZZOIRO_KEY` | ✅ | — | bzzoiro 数据源 Key(唯一数据源) |
| `API_FOOTBALL_KEY` | ❌ | — | 伤停数据源 Key |
| `CORS_ORIGINS` | ❌ | `http://localhost:5173,...` | 允许的跨域来源 | | `CORS_ORIGINS` | ❌ | `http://localhost:5173,...` | 允许的跨域来源 |
| `SECRET_KEY` | ❌ | — | 加密主密钥(生产环境必填) | | `SECRET_KEY` | ❌ | — | 加密主密钥(生产环境必填) |
| `ADMIN_PASSWORD` | ❌ | — | 管理后台密码(留空=不启用) | | `ADMIN_PASSWORD` | ❌ | — | 管理后台密码(留空=不启用) |
+11 -7
View File
@@ -83,16 +83,16 @@ Profeto/
│ │ └── app.py # FastAPI 工厂 │ │ └── app.py # FastAPI 工厂
│ ├── db/ │ ├── db/
│ │ ├── base.py # SQLAlchemy async engine + session │ │ ├── base.py # SQLAlchemy async engine + session
│ │ ├── models.py # 6 张表 ORM │ │ ├── models.py # 12 张表 ORM
│ │ ├── repositories.py # 仓储层(查询封装) │ │ ├── repositories.py # 仓储层(查询封装)
│ │ └── unit_of_work.py # 事务边界 │ │ └── unit_of_work.py # 事务边界
│ ├── data/ │ ├── data/
│ │ ├── bzzoiro.py # bzzoiro 采集 + 入库 │ │ ├── bzzoiro.py # bzzoiro 采集 + 入库(唯一数据源)
│ │ ├── understat.py # understat xG 回填
│ │ ├── injuries.py # 伤停采集
│ │ ├── normalize.py # 数据清洗契约 │ │ ├── normalize.py # 数据清洗契约
│ │ ├── team_names.py # 队名归一化映射 │ │ ├── team_names.py # 队名归一化映射
│ │ ├── team_names_zh.py # 队名中文名映射
│ │ ├── sources.py # 数据源注册表 │ │ ├── sources.py # 数据源注册表
│ │ ├── key_ring.py # 数据源 Key 读取(DB 设置优先于 env)
│ │ └── config.py # 联赛映射常量 │ │ └── config.py # 联赛映射常量
│ ├── llm/ │ ├── llm/
│ │ ├── provider.py # LLM 提供商抽象(OpenAI-compatible) │ │ ├── provider.py # LLM 提供商抽象(OpenAI-compatible)
@@ -110,13 +110,17 @@ Profeto/
│ │ ├── form_v1.md │ │ ├── form_v1.md
│ │ ├── stats_v1.md │ │ ├── stats_v1.md
│ │ ├── home_away_v1.md │ │ ├── home_away_v1.md
│ │ ├── injuries_v1.md │ │ ├── standings_v1.md
│ │ ├── h2h_v1.md │ │ ├── h2h_v1.md
│ │ └── aggregator_v1.md │ │ └── aggregator_v1.md
│ └── core/ │ └── core/
│ ├── config.py # pydantic-settings 配置 │ ├── config.py # pydantic-settings 配置
│ ├── http_client.py # 共享 httpx 客户端 │ ├── http_client.py # 共享 httpx 客户端
── retry.py # 重试工具 ── crypto.py # 对称加密(Fernet)与密码哈希
│ ├── log_buffer.py # 内存日志缓冲(admin「系统日志」页)
│ ├── runtime_config.py # 运行时配置(数据库优先,回落 .env)
│ ├── scheduler.py # 定时任务调度器(cron 触发采集)
│ └── security_check.py # 生产启动安全校验(缺配置拒绝启动)
├── frontend/ # React 单页前端 ├── frontend/ # React 单页前端
├── alembic/ # 数据库迁移 ├── alembic/ # 数据库迁移
│ └── versions/ │ └── versions/
@@ -194,7 +198,7 @@ cp src/llm/prompts/agents/h2h_v1.md src/llm/prompts/agents/h2h_v2.md
### 3. 新增数据源 ### 3. 新增数据源
1.`src/data/` 写采集模块(参考 `understat.py`) 1.`src/data/` 写采集模块(参考 `bzzoiro.py`)
2.`normalize.py` 加清洗函数 2.`normalize.py` 加清洗函数
3.`context_builder.py` 加切片函数 3.`context_builder.py` 加切片函数
4.`api/routes/ingest.py` 加端点 4.`api/routes/ingest.py` 加端点
+6
View File
@@ -1,3 +1,9 @@
> ⚠️ **历史文档(已过时)**:伤停(injuries)数据源与 api-football 集成已移除,
> 数据源收敛为 bzzoiro 唯一来源。本文仅作历史决策记录保留,
> 现状请见 [05-data.md](05-data.md) 与 [01-architecture.md](01-architecture.md)。
---
"""检查 injuries 数据源 api-football 的响应结构。""" """检查 injuries 数据源 api-football 的响应结构。"""
API_FOOTBALL_INJURY_RESPONSE_EXAMPLE = """ API_FOOTBALL_INJURY_RESPONSE_EXAMPLE = """
{ {