Files
Profeto/docs/07-development.md
T
WorkBuddy 983363dab7 feat(ingest): 采集任务状态跟踪(ingest_jobs),解决 fire-and-forget 不可观测
- 新表 ingest_jobs(迁移 0019): id UUID/task/params JSONB/
  status(pending|running|success|failed,CheckConstraint)/
  result JSONB(统计摘要)/error/created_at/started_at/finished_at
- POST /ingest/bzzoiro: 启动后台前创建 pending job,响应返回 job_id;
  仍 require_admin。后台 _run_bzzoiro 流转 running→success/failed,
  result 按子任务(events/standings/stats)记录摘要(errors 截断 10 条)
- _update_job 尽力而为: 状态更新失败只记日志,绝不拖垮采集主流程;
  与 IngestFailure 死信独立(行级 vs 任务级,可同时存在)
- 新增 admin 端点(挂 /api/v1/admin 路由,路由级 require_admin):
  GET /admin/ingest/jobs/{job_id} 与 GET /admin/ingest/jobs?limit&status
- 前端采集页: 提交后凭 job_id 3 秒轮询,终态展示结果摘要/失败原因;
  无 job_id 时回退旧的 30 秒盲等 + 系统日志提示
- 测试 11 项: 建 job+job_id 契约、非法 task 422、成功/失败/all 流转、
  update 失败不拖垮采集、部分失败仍 success、admin 端点 200/404/列表、
  结构守护(job 路由在 admin 路由且带 require_admin)
- 禁止项确认: 未动分批 UoW、BzzoiroSource、死信与 Bronze 写入;
  docs(01/03/05/07/README)同步 13 张表与端点说明
2026-09-21 21:19:21 +08:00

266 lines
9.1 KiB
Markdown

# 07 · 开发指南
## 本地开发环境搭建
### 锁定依赖策略
项目使用 [pip-tools](https://github.com/jazzband/pip-tools) 锁定依赖版本,确保本地、CI、Docker 三端一致:
| 文件 | 用途 | 生成命令 |
|---|---|---|
| `requirements.txt` | 生产依赖锁定(含 SHA256 哈希) | `pip-compile pyproject.toml --generate-hashes` |
| `requirements-dev.txt` | 开发+CI 依赖锁定(含哈希) | `pip-compile pyproject.toml --extra dev --generate-hashes` |
### 安装步骤
```bash
# 1. 克隆并进入项目
cd Profeto
# 2. 创建虚拟环境
python3.11 -m venv .venv
source .venv/bin/activate
# 3. 安装 pip-tools(用于同步锁定依赖)
pip install pip-tools
# 4. 同步生产+开发依赖到当前环境(严格按 lock 文件版本,含哈希校验)
pip-sync requirements-dev.txt
# 5. 启动 PostgreSQL(或在 .env 配置外部库)
docker run -d --name profeto-pg \
-e POSTGRES_USER=football -e POSTGRES_PASSWORD=football -e POSTGRES_DB=football \
-p 5432:5432 postgres:16-alpine
# 6. 配置环境变量
cp .env.example .env
# 编辑 .env 填 LLM_API_KEY / BZZOIRO_KEY
# 7. 建表
alembic upgrade head
# 8. 启动 API(热重载)
uvicorn src.api.app:app --reload
# 9. 启动前端(另一个终端)
cd frontend && npm install && npm run dev
```
> **注意**:不要用 `pip install -e ".[dev]"` 直接安装——它按 pyproject 下界约束解析,版本可能与 lock 文件不一致。统一用 `pip-sync requirements-dev.txt` 保证三端一致。
### 变更依赖时
```bash
# 1. 编辑 pyproject.toml(调整依赖或版本约束)
# 2. 重新生成 lock 文件(含哈希)
pip-compile pyproject.toml --generate-hashes --output-file=requirements.txt --index-url=https://pypi.tuna.tsinghua.edu.cn/simple
pip-compile pyproject.toml --extra dev --generate-hashes --output-file=requirements-dev.txt --index-url=https://pypi.tuna.tsinghua.edu.cn/simple
# 3. 同步到本地环境
pip-sync requirements-dev.txt
# 4. 提交 lock 文件
git add requirements.txt requirements-dev.txt pyproject.toml
```
> **禁止无故大升级主版本依赖**:仅升级真正需要的包,并重新跑全量测试。
## 项目结构
```
Profeto/
├── src/ # 后端源码
│ ├── api/
│ │ ├── routes/ # FastAPI 路由
│ │ │ ├── matches.py # 联赛/比赛查询
│ │ │ ├── predict.py # 预测入口
│ │ │ ├── ingest.py # 数据采集
│ │ │ ├── eval.py # 评估回填
│ │ │ └── backtest.py # 历史回测
│ │ ├── deps.py # 依赖:管理接口鉴权(X-API-Key)
│ │ ├── schemas.py # Pydantic 模型
│ │ └── app.py # FastAPI 工厂
│ ├── db/
│ │ ├── base.py # SQLAlchemy async engine + session
│ │ ├── models.py # 12 张表 ORM
│ │ ├── repositories.py # 仓储层(查询封装)
│ │ └── unit_of_work.py # 事务边界
│ ├── data/
│ │ ├── bzzoiro.py # bzzoiro 采集 + 入库(唯一数据源)
│ │ ├── normalize.py # 数据清洗契约
│ │ ├── team_names.py # 队名归一化映射
│ │ ├── team_names_zh.py # 队名中文名映射
│ │ ├── sources.py # 数据源注册表
│ │ ├── key_ring.py # 数据源 Key 读取(DB 设置优先于 env)
│ │ └── config.py # 联赛映射常量
│ ├── llm/
│ │ ├── provider.py # LLM 提供商抽象(OpenAI-compatible)
│ │ ├── context_builder.py # 数据切片 + 拼接
│ │ ├── predict.py # 预测入口(单/多模式分派)
│ │ ├── eval.py # 评估统计
│ │ ├── validation.py # LLM 输出严格校验
│ │ ├── backtest.py # 回测执行
│ │ ├── agents/
│ │ │ ├── base.py # AgentSpec + run_agent
│ │ │ └── orchestrator.py # 多 agent 编排
│ │ └── prompts/
│ │ ├── match_prediction_v1.md # 单 agent prompt
│ │ └── agents/ # 多 agent prompt
│ │ ├── form_v1.md
│ │ ├── stats_v1.md
│ │ ├── home_away_v1.md
│ │ ├── standings_v1.md
│ │ ├── h2h_v1.md
│ │ └── aggregator_v1.md
│ └── core/
│ ├── config.py # pydantic-settings 配置
│ ├── http_client.py # 共享 httpx 客户端
│ ├── crypto.py # 对称加密(Fernet)与密码哈希
│ ├── log_buffer.py # 内存日志缓冲(admin「系统日志」页)
│ ├── runtime_config.py # 运行时配置(数据库优先,回落 .env)
│ ├── scheduler.py # 定时任务调度器(cron 触发采集)
│ └── security_check.py # 生产启动安全校验(缺配置拒绝启动)
├── frontend/ # React 单页前端
├── alembic/ # 数据库迁移
│ └── versions/
│ ├── 0001_initial.py
│ ├── 0002_agent_outputs.py
│ ├── 0003_injuries.py
│ ├── 0004_snapshot_and_constraints.py
│ ├── 0005_prediction_status_and_stats_provenance.py
│ ├── 0006_schema_model_drift_cleanup.py
│ └── …… (共 19 个迁移,最新 0019_ingest_jobs)
├── tests/ # 测试
│ ├── test_core.py # 核心逻辑测试
│ └── test_agents.py # 多 agent 测试
├── docs/ # 本文档
├── pyproject.toml # 依赖 + 构建配置
├── alembic.ini # Alembic 配置
├── Dockerfile # 生产镜像
├── docker-compose.yml # 本地/生产编排
└── .env.example # 环境变量模板
```
## 测试
```bash
# 跑全部测试
pytest
# 详细输出
pytest -v
# 只跑 agent 测试
pytest tests/test_agents.py -v
# 覆盖率
pytest --cov=src --cov-report=term-missing
```
当前测试覆盖:
- `test_core.py`(13 项):数据清洗、队名归一、赛季标签、LLM 解析
- `test_agents.py`(20 项):no_data 门控、fail-open、prompt 加载、报告解析、终裁渲染
### 写新测试的模式
mock LLM 提供商(避免真实调用):
```python
class MockProvider:
model = "test-model"
async def chat(self, system, user, **kw):
from src.llm.provider import LLMResponse
return LLMResponse(
content="{}",
parsed={"data_sufficiency": "high", "analysis": "ok",
"home_edge": 0.5, "subjective_confidence": 0.8,
"key_evidence": ["证据"]},
prompt_tokens=10, completion_tokens=5, latency_ms=100,
)
```
## 常见开发任务
### 1. 修改 Agent Prompt
直接编辑 `src/llm/prompts/agents/{name}_v1.md`,无需重启(有 `lru_cache`,改完清缓存或重启)。
迭代版本:
```bash
cp src/llm/prompts/agents/h2h_v1.md src/llm/prompts/agents/h2h_v2.md
# 编辑 h2h_v2.md
# 调用时传 prompt_version: "v2"
```
### 2. 新增 Agent
见 [04-agents.md § 如何新增一个专家 Agent](04-agents.md)。
### 3. 新增数据源
1.`src/data/` 写采集模块(参考 `bzzoiro.py`)
2.`normalize.py` 加清洗函数
3.`context_builder.py` 加切片函数
4.`api/routes/ingest.py` 加端点
5.`api/schemas.py` 加请求/响应模型
### 4. 新增联赛
编辑 `src/data/config.py`:
```python
BZZOIRO_LEAGUE_IDS["新代码"] = league_id
LEAGUE_NAMES["新代码"] = "联赛名"
LEAGUE_COUNTRIES["新代码"] = "国家"
```
### 5. 数据库 Schema 变更
```bash
# 1. 改 src/db/models.py
# 2. 生成迁移
alembic revision --autogenerate -m "描述"
# 3. 检查生成的迁移文件(自动推断不完美)
# 4. 应用
alembic upgrade head
```
## 前端开发
```bash
cd frontend
npm install
npm run dev # 开发(热重载,代理 /api → localhost:8000)
npm run build # 生产构建 → dist/
npm run preview # 预览生产构建
```
技术栈:React + TypeScript + Tailwind CSS + Vite。
主要页面:`src/pages/Matches.tsx`(比赛列表 + 预测 + agent 报告展示)。
## 编码约定
- **异步优先**:所有 IO 用 `async/await`,SQLAlchemy 用 async session
- **session 管理**:
- 路由读操作:依赖注入 `get_db_read`(不自动 commit)
- 路由写操作:依赖注入 `get_db`(自动 commit)
- 内部/ingest:直接用 `AsyncSessionLocal()` 自己管事务
- **错误处理**:领域层抛 `ValueError`/`RuntimeError`,路由层转 HTTP 状态码
- **日志**:用 `logging.getLogger(__name__)`,关键路径打 info/debug
- **类型提示**:全量标注,`Mapped[T]` + `mapped_column`
## 提交前检查清单
```bash
# 1. 测试全绿
pytest
# 2. 语法检查
python -m py_compile src/**/*.py
# 3. 确认 .env 不提交(已在 .gitignore)
# 4. 文档同步(改功能时更新 docs/)
```