refactor: 以 bzzoiro 为唯一数据源的全面重构

数据源统一为 bzzoiro,移除 Understat 与 injuries:
- 删除 src/data/understat.py / injuries.py 及相关测试
- 删除 injuries 模型与表;扩展 match_stats(xG 之外增加 big_chances/fouls)
- 新增 standings 表(联赛积分榜:位置/积分/xG/走势/分区)
- matches 表增加 source_event_id 血缘列,支撑统计回填

采集管线(bzzoiro 三条管线):
- events:赛程/比分(/events/),记录 source_event_id
- standings:积分榜快照(/leagues/{id}/standings/)
- stats:已完赛比赛详细统计回填(/events/{id}/stats/)

预测增强:
- standings_slice 替代 injuries_slice;积分榜专家替代阵容完整性专家
- AGENT_META runtime_config 同步更新

管理后台:
- ingest 路由重写为单一 bzzoiro 入口 + task 参数(events/standings/stats/all)
- 新增 /admin/data-completeness 数据完整性分析 API
- 数据源状态页简化为 bzzoiro 单源

前端:
- 采集页重构为任务驱动(比赛/积分榜/统计回填/全量)
- 新增「数据完整性」可视化页(覆盖率矩阵/字段完整率/健康摘要)
- 新增主站积分榜页(/standings)与比赛详情完整统计面板
- agent 名称同步更新(injuries→standings)

迁移 0015_bzzoiro_single_source 已在容器内验证通过,后端测试全部通过。

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
This commit is contained in:
shangfangjian
2026-09-20 19:13:07 +08:00
co-authored by new-provider/LongCat-2.0 <
parent ec8f36abb2
commit f05dc1ae15
41 changed files with 1603 additions and 1885 deletions
+5 -5
View File
@@ -22,7 +22,7 @@ class TestNoDataGate:
def test_stub_no_data_report(self):
from src.llm.agents.base import _stub_no_data
r = _stub_no_data("injuries")
r = _stub_no_data("standings")
assert r.status == "no_data"
assert r.data_sufficiency == "none"
assert r.home_edge is None
@@ -31,7 +31,7 @@ class TestNoDataGate:
class TestPromptLoading:
"""agent prompt 模板加载。"""
@pytest.mark.parametrize("name", ["form", "stats", "home_away", "injuries", "h2h", "aggregator"])
@pytest.mark.parametrize("name", ["form", "stats", "home_away", "standings", "h2h", "aggregator"])
def test_all_prompts_exist(self, name):
tpl = load_agent_prompt(name, "v1")
assert "{{context}}" in tpl or "{{agent_reports}}" in tpl
@@ -126,7 +126,7 @@ class TestRunAgent:
async def empty_slice(header, before=None):
return "── 伤停 ──\n 无数据"
spec = AgentSpec(name="injuries", system_prompt="s", slice_fn=empty_slice)
spec = AgentSpec(name="standings", system_prompt="s", slice_fn=empty_slice)
header = self._make_header()
class ExplodingProvider:
@@ -201,7 +201,7 @@ class TestOrchestratorAggregation:
reports = [
AgentReport(agent="h2h", status="ok", home_edge=0.5, subjective_confidence=0.8, analysis="a"),
AgentReport(agent="injuries", status="no_data", data_sufficiency="none"),
AgentReport(agent="standings", status="no_data", data_sufficiency="none"),
]
text = _reports_to_json(reports)
data = json.loads(text)
@@ -270,7 +270,7 @@ class TestAgentWeightsValidation:
w = validate_agent_weights({"form": 0.4, "h2h": 0.4, "bogus": 0.2, "stats": 0.4})
assert "bogus" not in w
assert set(w) <= {"form", "stats", "home_away", "injuries", "h2h"}
assert set(w) <= {"form", "stats", "home_away", "standings", "h2h"}
def test_out_of_range_clamped(self):
from src.llm.validation import validate_agent_weights