按已通过样图(HEL-527,白栖知“按照这个试试吧”)重做 xiaobai-datahub
管理后台(8766/admin/),仅改 admin/index.html、admin/styles.css、
admin/app.js 三个文件,零新依赖、不改构建流程、不触碰 datahub/ 后端、
Docker/Compose、8765 主站与冻结区“问天”。
核心实现:
- 单个 76vh sticky 空间舞台(#stageWrap,position: sticky 钉在视口),
#track 六段 130vh 透明占位撑高文档、驱动滚动进度,舞台本体在滚动期间
保持不动,直到六幕滚完才随之离场。
- 一枚持续旋转的 3D 数据机芯(canvas 2D 手工透视投影,无 WebGL/新依赖),
六幕分别对应总览/数据源/调度任务/盘后发布/数据集/审计,切换幕时机芯
换面/爆炸展开/合拢,4 条数据流通道持续汇入机芯。
- A2 路由式 LINK/ACT 双灯:LINK 是稳态真实连通性(取自 /admin/api/sources
健康探测结果,仅 tushare/eastmoney/tencent/ifind 四路可流动,ths/xgb/
akshare 属永久预留源,不参与流动动画);ACT 严格由真实事件驱动——
tushare 靠 recent_calls 增量 diff,其余三源靠健康探测“探测动作本身
就是一次真实网络调用”,手动“探测一次”按钮同样触发真实后端请求
(已用真实浏览器验证会打到 /admin/api/sources/<provider>/probe)。
同源事件簇最多闪 3 次,全站闪烁令牌桶限流 ≤3 簇/秒,各源不共享时钟。
- 动效令牌统一:进出用强 ease-out,屏内位移用强 ease-in-out,持续流动用
linear;全文件禁止 ease-in、禁止 transition: all。
- prefers-reduced-motion:静态六幕面板(StaticShell)与动效版共用同一套
cabin/detail/bind 渲染函数,信息与交互完全对等,媒体查询变化时可不
刷新页面实时切换;系统级 CSS 兜底同样生效。
- 页面隐藏 / 断网即暂停所有轮询与 rAF 循环、清空未播闪烁队列,恢复时
只静默重建基线、不补播错过的事件。
- 保留原有登录/改密/登出、数据源探测、任务重跑、盘后发布二次确认
(密码+确认词)、回滚/补数等全部后端接口调用与危险操作确认流程。
自测(均在本地临时环境完成,未连接生产库/生产网络):
- `python -m unittest discover -s xiaobai-datahub/tests -v`:133 项全过。
- `node --check xiaobai-datahub/admin/app.js`:语法通过。
- `git diff --check`:无空白/换行问题;`git status`:仅上述 3 个文件改动。
- 起本地 datahub 服务 + Playwright 真实无头浏览器,22 项端到端断言全过:
画面渲染、六幕滚动到底/导航跳转、日夜切换、机芯点击开合详情、后台
切换(RAF 真停)、断网/恢复、reduced-motion 实时切换、探测按钮触发
真实后端调用等。过程中定位并修复两处真实缺陷:
1) #stageWrap 原为 position: relative,未真正钉住舞台,滚动时机芯会
随页面滚走——已改为 sticky,现验证滚动任意距离机芯位置不变。
2) 点击机芯打开详情硬编码成“数据源”,已改为按当前所在幕动态选择。
另外补上了此前遗漏的 #phase 幕序指示(如“3 / 6”),静态版切幕同步
更新顶部 crumb/phase。
未覆盖:未在 1440/1280/1024 三档做像素级视觉走查(仅验证 1024 无横向
溢出),未做真实弱网/高延迟环境下的手动观察,只做了断网模拟。
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
xiaobai-datahub
独立行情数据中枢(HEL-382 / P0)。与 xiaobai-review 同仓库、不同容器、不共享数据库文件。
本阶段不部署现网;只提供可本地运行、可自测的底座和盘后正式数据链路。
做什么
- SQLite WAL
datahub.db,容器名xiaobai-datahub,端口8766 - Tushare 盘后正式数据:交易日历、股票主档、daily、daily_basic、adj_factor、index_daily、moneyflow、stk_auction、limit_list_d、ths_hot/dc_hot、hm_detail、ths_daily/dc_index/sw_daily
- 盘中观察(provisional):东财/腾讯指数报价、个股最新价、全市场快照、分时点(
/v1/quotes/latest不传 codes 即全市场,/v1/indexes/quotes/v1/intraday/points);永不写入 eod_* 正式表 - 暂存 → 校验 → 整批原子发布 → 可回滚
/v1稳定接口(X-Datahub-Token)/admin/最小管理后台(总览 / 数据源 / 调度 / 发布 / 数据集 / 审计)- 同花顺/选股宝/AKShare/iFinD 适配器位仍预留;东财/腾讯已接入盘中观察
单位口径(相对现站)
现站 xiaobai-review 按 Tushare 原始单位入库、展示时再换算。中枢在归一化层一次换算:
| 字段 | Tushare / 现站 | 中枢 canonical |
|---|---|---|
daily.amount / index_daily.amount |
千元 | 元(×1000) |
daily.vol / index_daily.vol |
手 | 股(×100) |
moneyflow.*_amount |
万元 | 元(×1e4) |
daily_basic.total_mv / circ_mv |
万元 | 元(×1e4) |
stk_auction.amount |
元 | 元 |
差异为口径升级,golden 测试按上表对照,不为 0 的字段都有说明。
本地启动(不走 Docker)
cd xiaobai-datahub
python -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env
# 填入 DATAHUB_ENCRYPTION_KEY / DATAHUB_TOKEN / DATAHUB_ADMIN_PASSWORD / TUSHARE_TOKEN
# 生成 Fernet 密钥:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
.venv/bin/python server.py --host 127.0.0.1 --port 8766
- 管理后台:http://127.0.0.1:8766/admin/
- 存活检查:http://127.0.0.1:8766/livez (无需 token)
/v1/*必须带请求头X-Datahub-Token
Docker(独立 compose,不改现网 review 服务)
cd xiaobai-datahub
cp .env.example .env # 填密钥
mkdir -p data
docker compose build
docker compose up -d
仓库根目录另有 compose.datahub.yaml,供总工以后与现有 compose.yaml 叠加部署,本卡不执行现网 up。
自测
cd xiaobai-datahub
python -m unittest discover -s tests -v
不调用真实 Tushare;用内存/临时库和假适配器。
历史回补
交易日历默认从 20160101 拉到今天后 30 天;盘前 precheck 与手动回补都走同一 UPSERT,可重复执行。
网站实际使用的指数(上证、深成、创业板、沪深300)按交易日增量发布,默认覆盖 260 个交易日(大于现有 90 天窗口,并覆盖智能选股基准回看)。已发布日期默认跳过。
cd xiaobai-datahub
python -m datahub history-backfill
# 可选:--calendar-start 20160101 --index-days 260 --force
管理后台也可手动跑 history_backfill 任务,或 POST /admin/api/backfill 且 dataset=history、确认词 history:full。
区间接口在 meta.coverage / meta.incomplete 标明覆盖是否完整;网站只读接入把不完整区间视为不可用并回旧链路。个股日 K 的 90 天区间查询依赖已核实,本阶段不回补全市场历史。
估值字段级质量门
hub-quality.config.json 的 field_gates 按数据集配置关键字段:非空率下限(支持按字段覆盖,如 dv_ttm 合法高空值)、非有限值比例上限、以及相对上一已发布批次的非空率塌陷保护。字段大面积为空的批次会被拒绝发布、保留上一份正常正式数据,失败原因逐字段写入 batches.error / quality_json。被拒后数据集仍视为缺失,盘后自动重试(HEL-435 机制)会继续尝试直到成功或截止。配置对任意数据集生效,不写死单日或单字段。
整批原子发布(release group)
盘后发布/重发(eod_a、eod_retry、eod-refresh、跨数据集重发)不再逐数据集各自切换,而是走整批原子可见机制:
- 一致性边界:日 K、估值、资金流、竞价同属 A 组整批;指数日 K 为 B 组;当日股票主档快照随 A 组一同切换(主档
stock_master的 UPSERT 与快照发布同一事务,不会出现主档先行/滞后)。 - 流程:组内全部成员先在暂存表完成拉取、字段质量门、覆盖检查和跨数据集交叉校验(
cross_gates配置 ts_code 覆盖重叠率下限),全部达标后才在一个 SQLite 事务里复制正式表并翻转全部publications指针。 - 任一成员失败(拉取失败、质量门拒绝、交叉校验不过、切换事务中断)→ 整批不切换,对外继续提供上一份完整正式版本,失败原因写入
batches.error与audit_log(action=release-group),等待晚间自动重试。 - 读取侧任何时刻只会看到"旧完整版本"或"新完整版本":发布指针在单事务内统一翻转,容器重启/事务中断自动回滚,不暴露字段残缺或跨数据集混合版本。
- 幂等:仅当一致性边界内全部成员都已发布时才整组跳过;边界内任有缺失则整组重暂存后统一切换,避免旧批次与新批次混在同一次重发中。重复执行、并发重试不会在完整边界已就绪时生成重复批次(调度器另有 EOD 互斥锁)。
股票主档每日刷新与发布
交易日 20:00 与 23:10(stocks_refresh_times 可配)自动刷新股票主档并发布版本化快照(eod_stocks + publications.dataset='stocks'),覆盖当日新上市、证券简称变化和上市首日 N/C 前缀摘除;无变化则跳过,重复执行幂等。/v1/stocks 从最新已发布快照提供数据并带 batch_id / published_at;/v1/datasets/status 同步展示 stocks 状态。
cd xiaobai-datahub
python -m datahub stocks-refresh # 手动触发;--force 无变化也重发
资金流历史回补
网站会沿真实调用链查最近若干交易日的 moneyflow(个股详情任意日期点查 + 智能选股最近 5 个交易日),默认回补最近 60 个交易日(moneyflow_history_trading_days 可配,已发布日期自动跳过)。点查未覆盖的历史日期返回 DATASET_NOT_PUBLISHED 并附 available_from / available_to(低于下界时 reason=history_not_backfilled),网站据此明确回退旧链路,不会静默拿到半截数据。
cd xiaobai-datahub
python -m datahub moneyflow-backfill # --trading-days 60 --end-date --force 可选
盘后补跑与强制重发
cd xiaobai-datahub
python -m datahub eod-refresh --trade-date 20260904 # 补不完整的 A/B 边界
python -m datahub eod-refresh --trade-date 20260904 --force --dataset valuation
# --force 按一致性边界整组重发:valuation/daily/moneyflow/auction/stocks → A 组;
# index_daily → B 组。不可再单独切换某一个正式数据集。
管理后台「补数」对盘后正式数据集同样走 force_republish_boundary,不会绕过 A/B 整批边界。
估值发布后复核与自动追补
Tushare daily_basic 会在盘后继续改当日字段。HEL-423 在 2026-09-07 观察到:中枢 17:10 发布 003021.SZ turnover_rate=1.3565,21:05 上游/旧链路已是 1.3572;其余 7 类观察对象当日一致。日 K、资金流、竞价、指数没有同类晚间修订证据,股票主档已有 20:00/23:10 刷新,因此默认只复核估值,不盲目全量重拉。
窗口(可配):交易日 20:00–23:20,每 30 分钟一次轻量比对(对齐网站 21:00 / 23:30 观察)。只拉取 daily_basic,按网站真实请求字段精确比较,无误差豁免。
- 无变化:不产生新批次,状态「已追平」。
- 发现修订:重新走字段质量门、覆盖检查和 A 组整批原子发布;读者全程只能看到上一完整版本或新完整版本。
- 上游空 / 接口失败 / 不完整 / 质量门拒绝:保留上一完整版本,状态「复核失败」。
- 23:20 截止后停止当晚复核;下一自然日盘前对上一交易日再做一次安全追赶。
- 与
eod_a/eod_retry共用互斥锁;容器重启会在窗口内立即补一次。
备份
每日 00:40 任务把 datahub.db 备份到 data/backups/(保留 14 份)。也可手动:
python -c "from pathlib import Path; from datahub.db import HubDB; HubDB(Path('data/datahub.db')).backup_to(Path('data/backups/manual.db'))"
安全
- 密钥只以
configured / 末4位 / 更新时间出现在后台,不进日志、不进/v1 - HTTP 解析失败只记录“请求不是合法 JSON”,不把请求正文、密码或 Token 写入容器日志
- 回滚、补数需重新输入密码 + 确认词
- 容器非 root(uid 10002)、read_only、cap_drop ALL