Commit Graph
133 Commits
Author SHA1 Message Date
shangfangjian 64ae8e663a fix(P0-03): Prediction 幂等指纹——只追加,不覆盖
_upsert_prediction 改为 _insert_or_find_by_fingerprint:
- 同 input_hash → 返回已有行(绝不 UPDATE pred_/reasoning/agent_outputs)
- 不同 input_hash → INSERT 新行

input_hash 升级为规范 JSON SHA-256,捕获:match_id, cutoff, prompt_version,
prompt_hash, system_prompt_hash, provider, model, mode, run_type, temperature,
context_hash, agent_ids。移除旧 (match, provider, model, mode, run_type) 唯一约束,
改为 partial unique index(WHERE input_hash IS NOT NULL,兼容旧 NULL 数据)。

三条路径(single/multi/baseline)统一传足指纹字段。
迁移 0024 + 测试 test_p0_prediction_fingerprint(10/10);全量 295 通过。
2026-09-22 03:13:32 +08:00
shangfangjian 49d78136a1 fix(P0-02): 积分榜改为追加快照(append-only) + available_at cutoff
去掉 uq_standings_league_season_team,改为 (league, season, team, available_at) 唯一;
每次采集 INSERT 新行(available_at=now),ON CONFLICT DO NOTHING,不覆盖旧行。

standings_slice(before):DISTINCT ON (team_id) WHERE available_at<=cutoff
ORDER available_at DESC;before=None → cutoff=now()。
公开 list_standings 取每队最新可用快照(子查询 max available_at)。

迁移 0023 + 切片/路由/docs 同步;测试 test_p0_standings_cutoff(5/5)。
284 测试全绿。
2026-09-22 02:45:50 +08:00
shangfangjian 63caa6736c fix(P0-01): missing score 不得变 0:0 — score_status + CHECK 约束
新增 matches.score_status(known/missing/unknown):
- 替换 ck_matches_finished_has_score 为 ck_matches_score_integrity:
  known → 必须有分; missing/unknown → goals 必须 NULL(不伪造 0:0)
- normalize: 完赛缺分不再静默降级为 scheduled,改设 score_status=missing
- events ingest: 创建/更新 Match 同步 score_status(比分由缺变 known / 确认缺分 missing)
- slices(form/h2h/home_away)/backtest: 显式加 score_status='known' 过滤完赛样本
- 迁移 0022 回填现有数据(绝不 UPDATE goals=0)

测试 tests/test_p0_score_status.py(9/9):约束存在性/Match 构造/normalize 行为。
54 相关测试全绿。
2026-09-22 02:32:53 +08:00
shangfangjian f563d5cc99 fix(P0-00): HTTP client 必须传递 method/body,仅 body 存在时设 Content-Type
request() 此前忽略 options.method(默认 GET)与 options.body,导致 POST/PUT/DELETE
全部以 GET 空 body 发出;且无条件设置 Content-Type 误污染无 body 请求。

修复: 显式传递 method(默认 GET) 与 body; 仅当 body 非空时加 Content-Type。

测试 frontend/src/lib/http.test.ts(5/5):GET/POST/PUT/DELETE 的 method/body/headers。
运行: node --experimental-transform-types frontend/src/lib/http.test.ts
2026-09-22 02:24:50 +08:00
shangfangjian 7d2eabf750 chore: 死代码与重复逻辑清理
删除未引用/未调用符号:
- PredictionRepository(无引用)
- is_correct_1x2(无调用者)
- LeagueOut(路由用 list[dict])
- IngestResponse(IngestBzzoiroResponse 已替代)
- SecurityCheckError(从未 raise,assert_security_on_startup 用 sys.exit)
- short_write(仅自引用,全仓库无外部调用)
- fetchIngestJobs(列表函数无页面使用,单数 fetchIngestJob 仍保留)
- clear_prompt_cache(无入口)

去重:
- eval._actual_1x2 改为委托 utils.actual_1x2(单一权威源)

全量测试 270 通过,业务行为不变。
2026-09-22 01:55:52 +08:00
shangfangjian a00364d4a7 perf+unify: matches 列表查询优化 + Standings 统一数据源
matches 列表:selectinload 加 load_only 限定列(League.code/Team.name,name_zh/
MatchStats.home_xg,away_xg),补 stats 加载消除 N+1(m.stats 此前懒加载)。
详情接口保持完整 options;游标分页/响应字段/空值语义不变。

Standings 改用 useLeagues() 统一数据源(API 优先,失败回退本地常量),
LEAGUES 常量扩展 CL/EL;无数据联赛显示虚线 tab + 空态(非隐藏)。
2026-09-22 01:35:06 +08:00
shangfangjian 92f50fa5d4 feat: 采集任务状态跟踪 ingest_jobs
新增 ingest_jobs 表(UUID/task/params/status/result/error/时间戳),
POST /ingest/bzzoiro 启动前插入 job(pending)→ 后台 running → success/failed,
响应新增 job_id(兼容原 message)。

Admin GET /admin/ingest/jobs/{id} 与 /admin/ingest/jobs?limit= 只读查询;
Collection 页提交后轮询 job 至终态,展示真实 result/error 汇总。
迁移 0019_ingest_jobs + 分批 get_uow/BzzoiroSource/IngestFailure 不变。

全量测试 270 通过。
2026-09-22 01:34:55 +08:00
shangfangjian 44816794d3 refactor: context_builder 按 slice 拆到 src/llm/slices/ 包
单文件拆分(仅搬迁无逻辑修改):
- common.py    共享类型/头信息/_outcome/_is_stats_available
- form.py      form_slice + _get_form
- h2h.py       h2h_slice + _get_h2h
- stats.py     stats_slice(复用 form._get_form)
- home_away.py home_away_slice + _get_home_away
- standings.py standings_slice
- aggregate.py build_context

context_builder.py 改为纯 re-export 门面,公开签名不变。
同步修复测试 patch 目标(p0_home_away/h2h_perspective/multi_agent_cutoff)
与 regressions 源码断言(读 slices/*.py)。

全量测试 270 通过。
2026-09-22 01:33:14 +08:00
shangfangjian f1016b610a docs: predict.py docstring 补落库层级表格
明确 single/multi/baseline 均在服务层(_upsert_prediction)落库,
路由层永不写入 predictions,仅做 result → PredictOut 映射。
2026-09-22 01:19:19 +08:00
shangfangjian 03727bda00 refactor: 管线写入依赖解耦——pipeline_write 直接 import
bzzoiro_events / bzzoiro_standings / bzzoiro_stats 直接 import
src.data.pipeline_write(_write_raw_event/_write_lineage/_safe_write_ingest_failure),
删除 bzzoiro.py 门面中的 pipeline_write 转发胶水。

保留 fetch_*/_fetch_json_async/REQUEST_INTERVAL 经 bz. 门面调用(测试 monkeypatch 入口);
测试 best-effort 改为 patch 管线模块自身命名空间(from-import 绑定语义)。
函数语义与「失败不拖垮主流程」不变;source_record_id/transform_name 约定不变。

全量测试 270 通过。
2026-09-22 01:15:45 +08:00
shangfangjian 3a56ee17e1 feat: 预测缓存可选 Redis 后端(PREDICT_CACHE_URL)
PREDICT_CACHE_URL 为空=进程内 LRU+TTL dict(默认,行为不变);
填 redis:// 启用 Redis,失败自动降级内存并 warning,不中断预测。

- _CacheBackend 接口 + _MemoryCache/_RedisCache 两个后端
- 同键格式(predict:{match}:{provider}:{model}:{version}:{tpl_hash[:12}])
- 同 TTL(300s);Redis 用 pickle 序列化
- redis 包未安装/连接失败 → 降级内存;不强制依赖 redis 启动
- clear_prompt_cache 同步清空内存预测缓存

全量测试 270 通过;缓存后端单测 4/4。
2026-09-22 00:53:27 +08:00
shangfangjian 7593b99e39 docs+feat: matches 唯一键语义明确化 + source_event_id partial unique
docs/05-data.md:业务唯一=同联赛同主客同自然天;
source_event_id 用于统计回填与血缘,新增部分唯一索引说明与 upsert 查找顺序。

新增 ix_matches_source_event_id_unique(WHERE IS NOT NULL),
兼容存量空值历史行;MatchRepository.find_by_source_event_id;
events upsert 优先按 event_id 定位,回退自然键。
迁移 0021 + 回归测试修复(find_by_source_event_id 方法调用误判)。
2026-09-22 00:41:54 +08:00
shangfangjian e15b554ba3 feat: 球队实体一致性 — 归一化咽喉 + team_aliases 别名机制
events/standings 创建 Team 前均经 team_names.normalize(已有,确认),
TeamRepository.get_or_create 收敛为归一化唯一咽喉 + info 日志。

新增 team_aliases 表(NFKD 归一别名 → teams.id FK CASCADE),
定位三步链:normalize(name) → teams.name → team_aliases → insert。
不自动合并历史重复队;提供 POST /api/v1/admin/teams/aliases 显式添加。

迁移 0020_team_aliases + Admin 别名管理端点(admin_teams.py)。
全量测试 270 通过。
2026-09-22 00:27:53 +08:00
shangfangjian 4b0d6ee58a chore(P3): baseline 落库下沉 + MatchPredictPanel 拆分 + 多 worker/CSRF 文档
P3-2 baseline 落库从路由下沉到服务层(predict_baseline 内直接落库),
删除路由层 _persist_baseline,三种模式统一 result.prediction_id,对外 JSON 不变。

P3-1 MatchPredictPanel.PredictionPanel 拆为 OutcomePanel/AgentsPanel/ReasoningPanel
三个子组件,本文件保留 PredictModal/PredictProgress/Spinner,对外导出路径不变。

P3-3 docs 加 ⚠️ 多 worker 陷阱红字 + STRICT_SINGLE_WORKER 环境变量(启动期强制拒绝多 worker)。
P3-4 docs 新增「同站部署 vs 跨站 CSRF」节。
2026-09-21 23:29:09 +08:00
shangfangjian 45497d2112 refactor: admin_settings.py 按职责拆分为 4 个模块
681 行单文件拆为(保留 admin_settings 为 include_router 聚合入口):
- admin_datasources  数据源列表/连通性测试/KeyRing/ingest status (5 路由)
- admin_config       settings CRUD + 运行日志 (4 路由)
- admin_llm          LLM agents/models/ping (3 路由)
- admin_quality      stats/data-completeness/data-quality (4 路由)

所有路由仍挂 /api/v1/admin 且带 dependencies=[Depends(require_admin)]。
app.py 注册方式不变(仍 import admin_settings.router)。
2026-09-21 23:28:58 +08:00
shangfangjian 66f0844798 feat: standings Bronze 血缘补齐 + 队名归一 MVP
standings 成功 upsert 后写入 RawEvent(source_record_id=standings:{league}:{season})
+ DataLineage(target_table=standings),与 events/stats 管线对称。

队名归一化收敛到 TeamRepository.get_or_create 唯一咽喉点,
创建新 Team 时 info 日志打出原始名与归一后名;
Admin 新增 GET /api/v1/admin/team-name-duplicates 只读接口,
启发式列出近似重名候选(大小写变体/子串/前缀碰撞),不做自动合并。
2026-09-21 23:28:46 +08:00
shangfangjian 317a5e338a refactor: bzzoiro.py 按管线拆分为 5 个模块
单文件 852 行按职责拆分,保持 BzzoiroSource 与 get_source("bzzoiro") 行为不变:
- bzzoiro_common  HTTP 抓取(多 key 轮换) + 字段转换原语
- bzzoiro_events   fetch_bzzoiro_events + BzzoiroSource.ingest + Bronze 补写
- bzzoiro_standings  standings 管线
- bzzoiro_stats     stats 回填
- pipeline_write    RawEvent/IngestFailure/DataLineage 写入助手

子模块运行期经聚合门面 src.data.bzzoiro 解析可替换协作者,
单文件时代的 bz.* monkeypatch 语义完全保留。
路由 import 已指向新模块(ingest.py / schedules.py)。
2026-09-21 23:27:48 +08:00
shangfangjian 7a5c695b89 Merge pull request '公开只读 API+ 全套文档对齐(P1-1)' (#11) from docs-public-api-fixes into main
Reviewed-on: #11
2026-09-21 21:38:58 +08:00
WorkBuddy 6bdb1f8ae6 docs(deploy): 补充生产上线检查清单(README + docs/06)
- docs/06 新增「生产上线检查清单」10 项:APP_ENV / SECRET_KEY 强随机 /
  管理鉴权二选一 / 数据库强密码 / HTTPS+Cookie Secure /
  TRUST_PROXY_HEADERS+X-Forwarded-For / Nginx limit_req 限流前置
  (应用内限流与 KeyRing 单进程限制) / uvicorn 单 worker /
  /health 与 /health/ready / alembic 迁移已内置
- README 在部署说明后加同名精简清单,链接到 docs/06 详情
- 全部条目均对齐真实实现(security_check 弱值黑名单、auth.py
  secure=production、deps.py TRUST_PROXY 解析、app.py 双健康检查、
  compose/Dockerfile 内置迁移);纯文档,未改业务逻辑
2026-09-21 21:02:16 +08:00
WorkBuddy 80616cf459 docs: 全套文档与代码对齐(P1-1,数据源收敛 bzzoiro 后的存量漂移清理)
- 数据源:全部文档统一为 bzzoiro 唯一来源,删除 understat/api-football
  作为现行数据源的表述(仅保留历史注记);.env.example/docs/06 移除
  失效的 API_FOOTBALL_KEY
- 多 Agent:injuries 阵容完整性 → standings 联赛排名
  (README/01/04 的专家表、数据流图、no_data 示例、agent_weights、
  prompt 清单全部对齐 form/stats/home_away/standings/h2h)
- 数据库:6 张表 → 12 张表,补 standings/app_settings/schedules 与
  4 张治理表说明(标注 raw_events/data_quality_checks/data_lineage
  为预留未启用,ingest_failures 已启用),predictions 补 agent_weights 列
- API 文档(03):新增鉴权模型三档表;context/standings/leagues 公开端点
  补全;predictions/eval/backtest 标注需管理员;ingest/bzzoiro 补
  task/limit/season 参数契约,删除 understat/injuries 端点小节
- 目录树(README/01/07):与真实 src/、frontend/src 结构一致
  (core 7 文件、data 7 文件、移除不存在的 retry.py)
- 采集命令(02/05):understat 回填 → task=stats/standings;
  补管理员凭据提示;docs/08 文首标注历史/过时
2026-09-21 20:55:48 +08:00
WorkBuddy a24017eb69 feat(api): 比赛上下文与联赛列表改公开只读(P1-2/P1-3)
- GET /matches/{id}/context 移除 require_admin:公开站详情页「近况/交锋」
  数据来源,匿名 401 会导致前端静默空态;响应结构不变,不触发 LLM
- GET /leagues 改公开只读:公开站联赛筛选动态加载;仅返回
  id/code/name/country 四个展示字段,不含配置或密钥信息
- 前端新增 useLeagues hook:优先请求 /api/v1/leagues,失败/空回退
  本地五大联赛常量(已知代码保留中文标签,新联赛按 API 名称追加)
- 新增 tests/test_public_readonly_api.py(6 项):匿名 200/404、响应形状、
  路由依赖声明检查(matches 路由无 require_admin)+ 判别力守卫
  (ingest 路由必须检出 require_admin,防检测器恒真)
2026-09-21 20:55:26 +08:00
shangfangjian 3056a95ef4 Merge pull request '工程债清理(Bronze 血缘/预测类型统一/Matches 拆分/Repository 治理/导航单源等)'
Reviewed-on: #10
2026-09-21 19:57:48 +08:00
WorkBuddy d9aeff2114 debt(D7): 限流/KeyRing 单进程限制说明 + 生产启动一次性告警
- app.py lifespan:APP_ENV=production 时 logger.warning 提醒
  进程内 rate-limit/KeyRing 仅单进程有效(不 sys.exit、不引入 Redis)
- key_ring.py docstring 补充多 worker 不共享的后果说明
- deps.py 注释已具备(单进程有效,生产前置 Nginx),无需改动
2026-09-21 19:51:41 +08:00
WorkBuddy 17e99a4f9a debt(D5): agent_outputs 类型标注对齐真实 JSON 形状(list[dict]|dict|None)
- Mapped[list[dict] | dict | None]:multi 模式存专家报告列表,
  历史数据/兼容路径可能存 dict;加注释说明形状来源
- 仅类型标注修正,列类型(JSONB)与数据、迁移均不变
2026-09-21 19:51:38 +08:00
WorkBuddy 4514ef4e92 debt(D4): bzzoiro Team/League/Match 查找经 Repository 层,纠正空壳表述
- repositories.py 新增 MatchRepository.find_by_league_and_date_range /
  find_finished_with_stats(含 stats 预加载),TeamRepository.get_or_create
  支持 name_zh
- bzzoiro events/standings/stats 三条管线的联赛、球队、比赛查找与批量预载
  全部改走 Repository;事务提交仍由调用方 UnitOfWork 控制
- Standing/RawEvent/Lineage 管线内私有读写保留在本模块(不强行 Repository 化)
- 模块 docstring 纠正为准确表述,移除「已全面 Repository 化」的误导性声明
2026-09-21 19:48:56 +08:00
WorkBuddy 18c89111d8 debt(D6): Admin 导航单源 —— NAV_ITEMS 派生侧栏/命令面板/面包屑
- 新增 admin/nav.ts: 唯一配置源 NAV_ITEMS(to/label/group/icon/hideFromSidebar)
- COMMAND_PALETTE_PAGES / ROUTE_LABELS / NAV_SECTIONS 全部由其派生
- 删除 AdminLayout 中三份平行清单(已出现漂移: data-pipeline 不在侧栏、
  「系统」vs「系统设置」命名不一致、monitoring/eval 顺序两处相反)
- 已知微差: 命令面板「评估与监控」组内顺序统一为侧栏口径(eval 在前)
- 行为不变: 路由/标签/分组标题均保持现状;data-pipeline 保持仅面板可达
2026-09-21 19:34:31 +08:00
WorkBuddy 52d67863d6 debt(D3): 拆分 Matches.tsx(~1400 行)为 hooks + 组件 + 共享层
- pages/matches/types.ts: Match/Prediction/AgentReport + 常量(LEAGUES/STATUS_META/AGENT_LABELS 等)
- pages/matches/ui.tsx: Spinner/SkeletonRows/Switch/日期分组工具(Switch 自组件体内提升到模块级)
- pages/matches/hooks/useMatchesList.ts: 列表筛选/游标分页/进行中比赛,竞态防护语义不变
- pages/matches/hooks/useMatchPredict.ts: 预测状态机(连点/竞态/中止/超时)+ 可读错误文案
- pages/matches/components/MatchPredictPanel.tsx: 预测弹窗全套(过程可视化/结果/专家意见)
- pages/matches/components/MatchDetailSection.tsx: MatchRow(原内联行 JSX 抽出)+ 详情面板
- Matches.tsx 组装页 279 行(< 300);error state 共享行为与拆分前一致
- 验证: tsc + vite build 通过,渲染逻辑原样搬迁
2026-09-21 19:31:42 +08:00
WorkBuddy 5c7fdce0a3 debt(D2): 统一预测结果类型为 PredictResult,路由去 dict 分支
- PredictResult 扩展可选字段 mode/agent_outputs/agent_weights/prompt_tokens/completion_tokens
- MultiPredictResult 变为 PredictResult 别名(保留 R4 守卫标记与 re-export)
- baseline 改返回 PredictResult(修复 backtest 对 baseline AttributeError 的潜伏 bug)
- 预测路由单一属性映射,删除全部 isinstance(result, dict) 分支
- _persist_baseline 属性化,baseline upsert 语义不变(prompt_version/token/latency 同前)
- TDD: 5 新测试 + test_baseline.py 属性化;全量 257 passed
2026-09-21 19:23:50 +08:00
WorkBuddy 3a9f3f5a0e debt(D1): events 成功路径补写 Bronze 层(RawEvent + DataLineage)
- 新增 _events_record_id: 上游 id 缺失时用 (league:home:away:date) 合成稳定幂等键
- 新增 _write_events_bronze: best-effort 写 RawEvent(幂等) + Lineage(matches/events_ingest)
- ingest 插入与变更更新后触发;同批 seen 集合防重复;基础设施失败只 warning
- TDD: 6 测试(插入/合成键/幂等跳过/变更更新/无变化不写/失败不拖垮),双变异验证通过
2026-09-21 19:23:37 +08:00
shangfangjian f6c0145c32 Merge pull request 'fix: 死信表真正接线 + 前端 HTTP 收敛与状态修正' (#9) from fix-deadletter-frontend-cleanup into main
Reviewed-on: #9
2026-09-21 18:39:08 +08:00
WorkBuddy b6a251407d fix(pipeline): 死信表真正接线 + 前端 HTTP 收敛与状态修正
- bzzoiro events/standings/stats 抓取失败写入 IngestFailure 死信(尽力而为,
  写入失败不影响主流程);顺带修复 standings 失败路径 league_r 缺 errors 键
  的 KeyError —— 该路径此前从未跑通,一旦失败会顶掉原始异常
- admin/api.ts 收敛为 lib/http.ts 薄门面,消除第二套 HTTP 实现;
  UNAUTHORIZED_EVENT 定义移至共享层,断开 lib→admin 反向依赖
- STATUS_META 死键 live 改为 in_play(对齐 normalize.py 口径),
  补 paused/postponed/cancelled/suspended;移除无人消费的
  DashboardStats.total_matches(items.length 近似,上限 100)
- 新增 tests/test_ingest_deadletter.py(6 例,变异验证判别力)
2026-09-21 18:36:43 +08:00
shangfangjian b5f7e682f0 Merge pull request 'fix: 批量修复一些问题
Reviewed-on: #8
2026-09-21 18:04:54 +08:00
shangfangjian 06e973d34d fix(concurrency): 消除 model 覆盖的模块级变量中转(并发串味)
R4 首版实现用模块级 _ACTIVE_MODEL_OVERRIDE 中转 model override,
理由是不想改动 run_specialists 的签名(既有测试会 mock 它)。

但 backtest.py:225 会 asyncio.gather 并发 8 场预测(Semaphore(8)),
每场都调用 predict_match_multi —— 模块级变量会被并发调用互相覆盖,
导致 A 场的预测用上 B 场的模型。这是静默的正确性缺陷。

改为: run_specialists 新增 model_override 形参,一路显式下传;
删除模块级变量。同步更新 6 处 mock 签名。

新增 3 个守卫并做变异验证:
  - test_r4_no_module_level_model_override_global (源码级,可判别)
  - test_r4_run_specialists_accepts_model_override_parameter
  - test_r4_dispatch_passes_override_to_specialists
变异测试: 重新引入全局变量方案 → 两个守卫变红;还原 → 全绿。

注: 曾尝试写并发行为测试,但真实 run_specialists 会访问数据库,
测试环境下不稳定(ConnectionRefusedError),会是 flaky 的假证据,
故改用清晰的源码级判别守卫,并在注释中说明原因。

240 passed / 1 skipped; tsc 0 error; vite build 成功。
2026-09-21 17:52:45 +08:00
shangfangjian 50a6753ff8 test: 修正 selectin 守卫的假阳性(变异测试发现)
原断言对「关系声明到下一注解」的整段(含注释)做 lazy="selectin"
子串匹配;而 Match.league 声明正下方的注释里恰好也写着该字样,
导致移除真实 kwarg 后测试依旧通过(变异测试确认)。

改为只匹配 relationship(...) 括号内内容,并加注释说明该假阳性。
验证: 干净代码通过 → 移除 league 的 lazy 后失败 → 还原后又通过。
全套 238 passed / 1 skipped。
2026-09-21 17:36:07 +08:00
shangfangjian 951faf31df test: 修复 13 个腐化用例,套件恢复全绿且顺序无关
按「测试腐化(a)/ 源码缺陷(b)/ 测试污染(c)」逐项定性,仅改 tests/:

- 外机绝对路径(4): 迁移文件路径改为相对仓库根解析(沿用
  test_regressions._read 约定),断言内容保持不变。
- cutoff 用例(4+1): mock 未生效的真因是 _agent_provider 被换成同步
  lambda,await 抛 TypeError 被生产代码吞掉后 IndexError;改为 async
  mock 并按 run_specialists/load_match_header/_agent_provider 的真实契约
  打补丁。degraded 用例再加 _upsert_prediction 顶层 kwargs(model)采集。
- 陈旧断言(3): agent 键按现契约断言中文映射;h2h mock 改 async;
  Match.stats 按设计为 lazy="select",从 MATCH_RELATIONS 移出并单独
  固化该设计决定。
- P0-3 守卫(1): seg 越界扫到下游 stats 管线导致误报,改为按缩进收口;
  合法形状含经 raw 派生变量中转的写法,并补元测试确保守卫仍能抓到回归。
- 交叉污染(6): test_multi_agent_cutoff 用 patch.object 精确还原,消除
  裸赋值泄漏的同步 mock;现已验证顺序无关。
2026-09-21 17:31:36 +08:00
shangfangjian 11da4d3631 fix(critical): 修复评审确认的 R1-R5 五处缺陷
R1 429 key 轮换路径调用不存在的 _km → NameError(且位于凭证脱敏日志行):
   改用 src/data/key_ring._mask,全树不再有 _km 调用。
   [注: 该改动已随另一工作流提交 9ccda4b 一并入库]
R2 ingest_bzzoiro_standings 被截断(return 前无 upsert 逻辑,standings 表
   永不写入、total_upserted 恒为 0):从 f05dc1a 移植完整实现,按
   (league_id, season, team_id) upsert,保留逐联赛错误隔离,不加 db.commit()。
   [注: 同上,已随 9ccda4b 入库]
R3 src/llm/agents/orchestrator.py 完成日志把 list 喂给 %d → logging TypeError:
   改为 len(ok_reports),与同文件降级日志行写法一致。
R4 mode=multi 静默丢弃调用方传入的 model:predict_match_multi 新增
   model 关键字参数,经 _ACTIVE_MODEL_OVERRIDE 下传至 _agent_provider,
   显式 model 优先级最高;override 生效时跳过 provider 缓存读写以免串味,
   并在 predict.py 派发点透传。
R5 回测把字符串日期直接与 timestamptz 列比较:新增 _parse_date_bound 助手,
   支持 YYYY-MM-DD / 完整 ISO / datetime / None,裸日期按 UTC 锚定,
   结束日取当天末刻(闭区间,避免最后一天被静默排除),非法输入抛 ValueError。

新增 tests/test_review_required_fixes.py 覆盖 R1-R5(R2/R4 为行为测试),
20 项全通过。
2026-09-21 17:19:24 +08:00
shangfangjian b9d7b6423e test(c2): 补齐导入次序回归,注明环状导入为预期且可自愈
test_bzzoiro_source_registry.py 增至 11 项:
- 新增 TestImportOrderSelfHeal:用独立子进程验证两种真实导入次序
  (sources 先 / bzzoiro 先)下注册表均可用。该缺陷只在真实模块初始化
  时序下成立,mock 照不出来,故不 mock。
- 新增未知名字仍抛 ValueError 的契约守卫。

src/data/sources.py:补全 _load_sources 文档,说明首次装载失败属正常的
环状导入(bzzoiro 仍在初始化),失败时保持 _loaded=False 使后续
get_source()/list_sources() 调用可重试装载(自愈)。

验证:
- tests/test_bzzoiro_source_registry.py:11 passed
- 两种导入次序实测均输出 bzzoiro ['bzzoiro']
2026-09-21 17:13:57 +08:00
shangfangjian 9ccda4bf51 fix(critical): 恢复 bzzoiro events 采集管线,消除数据源注册表静默失效
C2 数据丢失修复。重构提交 6a49940 删除了 src/data/bzzoiro.py 中的
fetch_bzzoiro_events 与 BzzoiroSource 后未做替换,同时 sources.py 用
`try/except Exception: pass` 吞掉了 ImportError,导致:

  - _SOURCES 注册表恒为空
  - get_source("bzzoiro") 恒抛 ValueError("未知数据源: bzzoiro")
  - src/api/routes/ingest.py 与 schedules.py 运行时全线失效,
    且日志中无任何导入错误痕迹

修复内容:
1. 从 f05dc1a 恢复并移植 events 管线:
   - fetch_bzzoiro_events():分页抓取 /events/(纯异步,复用现有 _fetch_json_async)
   - @register class BzzoiroSource + ingest():保留原签名
     (db, *, leagues, date_from, date_to, status),保持「不 commit,
     事务由调用方 UnitOfWork 控制」的契约,并保留单联赛抓取失败的
     错误隔离(记录 error 后 continue)
   - 复用现有 _to_date / _to_int_or_none / _match_key,未重复定义
   - 血缘字段继续使用与 nm 配对的 raw(避免 P0-3 回归)
2. sources.py:导入失败改为 logger.exception 记录,新增 _loaded 标记
   保证 _load_sources() 至多执行一次;失败时不置位以便后续重试。
   get_source() 对真正未知的名字仍抛 ValueError。

验证:
   - 新增 tests/test_bzzoiro_source_registry.py(8 项):注册表非空、
     get_source/list_sources 可用、ingest 签名契约、不再静默吞异常
   - test_regressions.py:3 failed -> 2 failed
   - 全量:14 failed -> 13 failed,通过数 190 -> 199

注意事项:未改动任何 caller(routes/ingest.py、routes/schedules.py 原样
可用,证明签名保持正确)。
2026-09-21 17:10:17 +08:00
shangfangjian 8c87b9d024 fix(critical): 修复调度器静默失效、数据源缺失与前端类型错误
C1 定时任务静默失效(4 层缺陷):
  - scheduler.py: croniter(expr, datetime.now) 未调用 now(),
    导致 TypeError 被吞、next_run 恒为 None(最深一层)
  - scheduler.py: _calc_next 非法 cron 静默吞异常 -> 改为构造期抛 ValueError
  - scheduler.py: _run_loop 一次异常即永久停摆 -> 增加异常隔离
  - scheduler.py: sleep(min(wait,60)) 使运行期 cron 变更最长 60s 才生效
    -> 改为固定 SLEEP_TICK 轮询
  - app.py / schedules.py: 3 处 register() 第 2 参误传 task 类型名

C3 EvalPage.tsx: 引用未导入的 EmptyState -> 改为已导入的 EmptyText
  (tsc --noEmit 由 1 error 变为 0 error)

新增 tests/test_scheduler_registration.py: 9 项行为回归测试
2026-09-21 17:04:04 +08:00
Profeto Agent f04e0df9e8 预测结果展示增强:status 徽章 + 专家权重条形图
- types.ts: 添加 agent_weights / status 字段
- components.tsx: 添加 AgentWeightsBar 纯 CSS 权重分布条 + EmptyText 组件
- Predictions.tsx: 摘要行 degraded/failed 状态徽章、展开区 AgentWeightsBar、
  预测中防连点、中文错误映射
- EvalPage.tsx: 修复 EmptyText 导入

构建验证:tsc && vite build 通过
2026-09-21 05:56:03 +00:00
shangfangjianandnew-provider/LongCat-2.0 < 6a4994097a feat: 采集管线基础设施全面接线
后端:
- bzzoiro events 采集成功后写入 RawEvent(Bronze 层)
- 采集失败写入 IngestFailure(死信队列)
- 写入 DataLineage(ETL 血缘追踪)
- stats 回填成功后写入 RawEvent + DataLineage
- 新增 DataQualityScheduler(每小时自动质量检查)
- 新增 /api/v1/admin/data-quality 端点
- 新增 /api/v1/admin/ingest-failures 端点(失败记录 + 重试)
- 新增 /api/v1/admin/llm/ping 端点(LLM 连通性测试)
- lifespan 启动 quality_scheduler
- Match 表补充 CHECK 约束(完赛必须有比分、状态枚举、半场≤全场)
- Prediction FK 加 ondelete="CASCADE" 与迁移对齐
- PredictRequest mode 加 pattern 校验

前端:
- 新增「数据管线」管理页(/admin/data-pipeline)
- 采集失败记录列表(状态/重试次数/错误类型 + 重试按钮)
- 数据质量检查结果(通过/未通过/严重度)
- 手动触发质量检查按钮
- 预测历史:比赛信息内嵌(日期/队名/主客徽标/赛果自动填充)
- 导航统一为 React Router Link
- AdminStats 类型扩展(matches/stats/standings 真实计数)

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 10:28:02 +08:00
shangfangjianandnew-provider/LongCat-2.0 < ae89d0f04f feat: 数据采集管线基础设施接线 + 数据管线管理页
后端:
- bzzoiro 采集成功后写入 RawEvent(Bronze 层原始事件存档)
- 采集失败写入 IngestFailure(死信队列,支持重试)
- 写入 DataLineage(ETL 血缘追踪)
- 新增 DataQualityScheduler(每小时自动质量检查)
- 新增 /api/v1/admin/data-quality 端点(质量检查 + 手动触发)
- 新增 /api/v1/admin/ingest-failures 端点(失败记录 + 重试)
- 新增 /admin/llm/ping 端点(LLM 连通性测试,不依赖比赛)
- lifespan 启动 quality_scheduler

前端:
- 新增「数据管线」管理页(/admin/data-pipeline)
- 采集失败记录列表(状态/重试次数/错误类型)
- 数据质量检查结果(通过/未通过/严重度)
- 手动触发质量检查按钮
- 失败记录重试按钮
- 预测历史:比赛信息内嵌(日期/队名/主客徽标/赛果自动填充)
- 导航统一为 React Router Link
- AdminStats 类型扩展(matches/stats/standings 真实计数)

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 10:04:26 +08:00
shangfangjianandnew-provider/LongCat-2.0 < b6e367a640 Code Review 第二轮修复:前端 HTTP 统一 + 数据采集可靠性
F1: 统一前端 HTTP 客户端
- 新增 frontend/src/lib/http.ts(共享客户端,统一超时/错误/401 处理)
- Matches.tsx 的列表/分页/进行中/预测请求改用 http 客户端
- 公开站与 Admin 行为一致

D2: events 采集按联赛分批提交
- _run_bzzoiro 改为 per-league 独立事务,避免超长事务
- 单联赛失败不影响其他联赛

F3: 仪表盘真实计数
- admin_stats 补充 matches/stats/standings 真实聚合
- Dashboard 展示比赛总数/已完赛/统计行/积分榜

F4: LLM 连通性测试不依赖 match_id=1
- 新增 POST /admin/llm/ping 端点(只发一次 chat,不依赖比赛)
- testLLMConnection 优先调用 ping,失败回退旧方式

F5: 导航改用 React Router Link
- App.tsx 中 <a href> 全部替换为 <Link to>

D5: 删除 away_possession 死代码
- bzzoiro.py 中移除计算与注释

D1: 管线基础设施表文档化
- RawEvent/IngestFailure/DataQualityCheck/DataLineage 添加「预留未启用」注释

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 03:06:52 +08:00
shangfangjianandnew-provider/LongCat-2.0 < 675e176603 fix: 修复 Code Review 发现的 4 个 High + 1 个 Medium 问题
High-1/2: baseline mode 违反 DB CHECK 约束
- ck_mode_enum 扩展为 ('single','multi','baseline')
- baseline 的 run_type 从 'baseline' 改为 'live'(符合现有约束)
- 新增迁移 0017_mode_baseline

High-3: 限流日志 NameError: ip 未定义
- deps.py:190 的 logger.warning 中 ip → client_ip

High-4: 生产 Cookie 缺少 Secure 标志
- auth.py 登录时根据 APP_ENV 设置 secure=True(生产)

Medium-5: 降级日志参数类型错误
- orchestrator.py:266 ok_reports(list) → len(ok_reports)(int)

附加: PredictRequest mode 字段加 pattern 校验,与 DB 约束同源

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 02:44:32 +08:00
shangfangjian 0a6af0657a 预测历史:比赛信息内嵌到预测API + 实际比分自动填充
后端:
- PredictionOut 新增 match 字段(内嵌比赛信息)
- list_predictions 返回完整比赛数据(日期/队名/赛果/主客徽标)
- 新增 _match_dict() 辅助函数序列化比赛对象

前端:
- 移除独立的 fetchMatches 调用,直接使用 p.match
- 实际比分自动从比赛赛果填充
- 显示:比赛日(M/D) + 主客队徽标 + 队伍中文名
- 赛后一键结算(使用比赛实际赛果)

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
2026-09-21 01:54:30 +08:00
shangfangjian a9e85a5d62 修复定时任务更新接口:支持部分更新
PUT /schedules/{id} 原来要求完整 ScheduleIn(所有字段必传),
前端只传 {enabled} 时因缺少 task/cron 校验失败。
→ 新增 ScheduleUpdate schema(所有字段可选)
→ update_schedule 仅更新提供的字段
→ 使用最终值同步调度器

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
2026-09-21 01:45:19 +08:00
shangfangjianandnew-provider/LongCat-2.0 < 7a4e61ebc4 定时任务操作添加点击反馈和文案提示
- 启用/禁用/立即执行/删除按钮添加 loading 状态(Spinner)
- 操作成功后显示绿色提示(如「已启用任务 xxx」)
- 操作失败后显示红色错误提示
- 按钮禁用期间防止重复点击

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 01:42:17 +08:00
shangfangjianandnew-provider/LongCat-2.0 < 0a5a14cbbb 采集任务支持手动触发 + 定时调度(cron)
后端:
- 新增 Scheduler + ScheduledTask 核心模块(croniter 解析 cron 表达式)
- 新增 Schedule 数据模型(alembic 0016)
- 新增 /api/v1/admin/schedules CRUD 接口
- lifespan 启动时注册默认定时任务并加载数据库配置
- 默认任务:每日比赛/积分榜/30分钟统计回填(默认禁用)

前端:
- 设置页新增「定时任务」管理区块
- 支持:新建/启用禁用/立即执行/删除定时任务
- 显示:任务ID、类型、cron表达式、上次执行时间/状态

依赖:新增 croniter>=2.0

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
2026-09-21 01:36:54 +08:00
shangfangjian 1fc799a5b0 预测历史:实际比分自动填充 + 显示比赛日/队伍/主客徽标
- 实际比分自动从比赛赛果填充,无需手动输入
- 比赛列显示:比赛日(M/D) + 主客队徽标 + 队伍中文名
- 状态分三类:待赛果(灰)/可结算(蓝)/已结算(绿)
- 一键结算按钮:仅当比赛有赛果且未结算时显示
- 结算后1X2自动判断命中(绿✓/红✗)
- 顶部新增统计概览:已结算数/命中数/1X2准确率

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
2026-09-21 01:21:05 +08:00
shangfangjian f08fef8a22 预测管理改为预测历史页面
- 删除 Predictions.tsx,新建 PredictionHistory.tsx
- 不再支持触发预测(从主站比赛列表操作)
- 支持查看所有预测记录(全部/待标注/已结算筛选)
- 支持标注实际比分:行内编辑 + 保存,自动判断1X2是否命中
- 展开查看详情:预测比分/置信度/终裁意见/专家意见
- 侧边栏更名为「预测历史」,图标改为 logs

Co-Authored-By: new-provider/LongCat-2.0 <<EMAIL>>
)
2026-09-21 01:17:14 +08:00