Files
sanguo_vnpy_v2/docs/three-env-code-promote.md
T
claude_dev bf57aed17a retire(web): sanguo_web legacy 平行后端归档——用户拍板「代码保留归档,服务删」 [nas]
- git mv sanguo_web → _retired/sanguo_web/(git 历史保全+墓碑 README:
  归档原因/拍板记录/现役=sanguo_api)
- 退出启动面三处: promote.sh ALL_MODS 白名单(15→14 模块)+docker/Dockerfile:63
  COPY+docker/Dockerfile.nas:16 COPY——不可再被部署或启动为服务
- 背景(架构审计 P0-3): 硬编码 SECRET_KEY/admin123 默认口令/CORS 全开/可下
  真实订单/零测试/与 sanguo_api 路由冲突; 生产从未部署为服务, 纯误启动风险
  (README 旧「生产模式 uvicorn sanguo_web」指引已于 f6ca85e 勘误)
- three-env §3.1 模块清单+§12 矩阵行同步; session-guide §5 角色表前端行
  勘误(旧表把 sanguo_web 当前端模块推——实为 legacy 后端, 前端=frontend/)
- 零外部 import(runuweb/tests/packages 全 grep 实证), pytest testpaths=tests
  不受影响

Co-Authored-By: Claude Code <notify@anthropic.com>
2026-10-01 09:46:21 +08:00

20 KiB
Raw Blame History

三机代码晋升 Runbook (Phase4)

Mac(dev 源头) → NAS(test 镜像) → VPS(prod 生产) 单向代码晋升。 本文档只管 代码,不管数据(数据铁律见末节)。

1. 角色与流向

机器 角色 代码路径 工具
Mac dev 源头(改代码) ~/.openclaw/sanguo_projects/sanguo_vnpy_v2 rsync/scp
NAS test 镜像(只读副本) /volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2(容器挂载源;⚠️/volume1/stock/sanguo_vnpy_v2=数据备份区非代码位) rsync(LAN 快)
VPS prod 生产(实跑) C:\sanguo_vnpy_v2 scp(无 rsync)
   改代码          rsync (Step1)
  Mac dev ──────────────────► NAS test (镜像)
      │
      └─────── scp (Step2) ─────► VPS prod (生产)

两路都从 Mac 出发,NAS 是只读镜像(供回归),VPS 是生产实跑。NAS 不是中转,VPS 代码不经 NAS。

晋升是 单向(Mac → 外),NAS/VPS 永不回推 Mac。NAS 是镜像备份,VPS 是生产实跑。

2. 前置条件

  • Mac SSH key 连 VPS: ~/.ssh/config 已配 Host 49.232.102.198 + key ~/.ssh/id_ed25519。
    • 验证: ssh 49.232.102.198 'echo VPS_OK' 应直接通(免密)。
    • 严禁 ssh vps: 本机 config 无此别名,会被代理 fake-ip 劫持到 198.18.1.254 报 Connection reset。
  • NAS SSH: ssh sanguo-nas(LAN 免密)。
  • 工作目录: 在 Mac 代码根 ~/.openclaw/sanguo_projects/sanguo_vnpy_v2 下执行。

3. 使用

3.1 全量晋升(所有模块 + 根文件)

改完一批代码后:

cd ~/.openclaw/sanguo_projects/sanguo_vnpy_v2
bash scripts/nas_sync/promote.sh

推送内容:

  • 模块目录(14 个): sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor sanguo_live sanguo_orchestrator sanguo_portfolio sanguo_qmt_bridge sanguo_research sanguo_trader scripts config tests(2026-10-01 起 sanguo_web 移出——legacy 平行后端归档至 _retired/sanguo_web/,退出 promote 白名单与 docker COPY 面)
  • 根文件(4 个): pyproject.toml pytest.ini requirements-lock.txt run_web.py

3.2 单模块快速补推

只想推刚改的一个模块(如 sanguo_portfolio):

bash scripts/nas_sync/promote.sh --module sanguo_portfolio

--module 模式: NAS+VPS 只推该模块,不推根文件(根文件改动需走全量)。

可用模块名见上方列表。脚本会在本地缺失时报错退出。

4. Reload 机制(重要)

VPS 进程分两类:常驻(sanguo-api 生产控制台 / live-supervisor / shadow-desk,开机自启)+ 一次性任务(回测 runner_backtest + 采集 schtask)。⚠️ 2026-10-01 勘误:旧文「无常驻 web 服务」过时(见 §12 勘误注);旧任务名 sanguo-bs-eod 已于 2026-08-20 并入 sanguo-bs-daily。

场景 reload 动作
采集 schtask(定时) 无需操作。下次 schtask 触发时自动加载新代码
回测任务 无需操作。下次启动回测时自动加载新代码
sanguo-api(常驻控制台) schtasks /end sanguo-api && schtasks /run sanguo-api(改 sanguo_api/orchestrator/前端后必须)
立即让采集生效 schtasks /end sanguo-bs-daily && schtasks /run sanguo-bs-daily

一句话: 代码部署后,下次启动自动生效;常驻进程(api/supervisor)须显式重启。

立即生效命令(在 VPS 上执行,通过 ssh):

ssh 49.232.102.198 'schtasks /end sanguo-bs-daily && schtasks /run sanguo-bs-daily'

5. 落盘验证

脚本 Step3 自动验证 VPS 第一个推送模块的 .py 文件 mtime。手动深度核查:

# VPS: 查某模块文件列表+mtime
ssh 49.232.102.198 'powershell -NoProfile -Command "Get-ChildItem C:\sanguo_vnpy_v2\sanguo_portfolio -Filter *.py | Select Name,Length,LastWriteTime | Format-Table -Auto"'

# VPS: 查某文件是否含新代码标记
ssh 49.232.102.198 'powershell -NoProfile -Command "Select-String -Path C:\sanguo_vnpy_v2\sanguo_common\__init__.py -Pattern SOME_TOKEN"'

# NAS: 查某文件
ssh sanguo-nas "ls -la /volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2/sanguo_portfolio/"

教训(memory: commit≠部署VPS): 改完代码必须晋升,否则 VPS 跑旧代码。用 Select-String/grep 验证新代码落盘。

6. Rollback

代码部署出问题需要回退:

# 1. Mac 本地回退到上一个好版本
cd ~/.openclaw/sanguo_projects/sanguo_vnpy_v2
git log --oneline -5              # 找到好版本
git checkout <good_commit> -- sanguo_portfolio/   # 或整个目录

# 2. 重新晋升回退后的代码
bash scripts/nas_sync/promote.sh --module sanguo_portfolio

NAS/VPS 没有 git,rollback = Mac git 回退 + 重新 promote。所以 Mac git 历史是唯一真相源。

7. 数据铁律(边界)

只推代码,严禁推 data/。

允许推 严禁推
sanguo_*/ 代码模块 data/(42GB 数据库 + parquet)
scripts/、config/、tests/ logs/、vnpy_v4.4.0/(VPS 已有)
根配置文件 vnpy_qmt_v0.3.3/、docker/、.git/
__pycache__/、*.pyc、htmlcov/、*.log

脚本已通过两层防护:

  1. rsync(NAS): --exclude='/data' --exclude='__pycache__' ... 白名单式排除。
  2. scp(VPS): 按模块逐个推,天然隔离 data/(不在模块列表里)。

8. 常见坑

坑 解法
ssh vps 连不上(fake-ip) 用 ssh 49.232.102.198,config 无 vps 别名
scp 路径反斜杠报错 VPS 目标用正斜杠: 49.232.102.198:"C:/sanguo_vnpy_v2/"
PowerShell 中文乱码 用 -NoProfile,查文件用 Get-ChildItem/Select-String,避免中文路径
scp 某模块卡住 >60s Ctrl-C 记录,先验证已通的模块,单独重推失败的
部署后 VPS 行为没变 代码是下次启动才加载;采集 schtask 需 /end && /run 立即生效
macOS bash 3.2 空数组报错 脚本已用 ${#arr[@]} -gt 0 守卫;如改脚本注意 set -u + 空数组

9. NAS 容器 --init 铁律(防后台回测被清)

NAS test 容器 sanguo_vnpy_v2 必须 docker run --init 创建(tini 作 PID1)。

根因:无 --init 时 PID1=uvicorn(非 init/tini)。docker exec ... nohup python & 启动的后台回测进程,在 docker exec 会话(bash)结束后成孤儿,docker 清理该后台进程组 → 长任务读盘中途被清(02 small_cap day0 选股 5128 只 fundamentals 需 ~4min 超会话存活被清,log 停 day0 无 error;03 纯量价 day0 秒级没被清)。一度误判 OOM,被 dmesg 空 + 前台跑过 day0 推翻。

何时会丢 --init(复发风险):

  • ci-cd nas-deploy 只 docker restart(不重建,--init 保留 ✅)。
  • ⚠️ 任何手动 docker stop && rm && run 必须带 --init —— 这是唯一丢 --init 的路径。

手动重建命令要点:

D=/var/packages/Docker/target/usr/bin/docker   # Synology docker 不在 PATH
# 1. 先 commit 当前容器(备份 + 含运行时手动装的 pip 包如 jwt)
$D commit sanguo_vnpy_v2 sanguo_vnpy_v2:pre-init-rebuild
# 2. stop && rm
$D stop sanguo_vnpy_v2 && $D rm sanguo_vnpy_v2
# 3. run --init 原参数(挂载/端口/env/network 原样保留)
$D run -d --init --name sanguo_vnpy_v2 \
  -v /volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2:/app \
  -v /volume1/stock:/volume1/stock -p 8000:8000 \
  --restart unless-stopped sanguo_vnpy_v2:pre-init-rebuild
  • ⚠️ 启动 image 用 pre-init-rebuild(含 jwt 等运行时包)。勿用 base with-sqlite —— 缺 jwt,web 起 ModuleNotFoundError。(Dockerfile.nas 零 pip install,依赖全靠 base,故 jwt 只在 commit 层,重建从 base 会丢。)
  • 验证:$D inspect sanguo_vnpy_v2 --format '{{.HostConfig.Init}}' = true;ps -o comm -p $(...PID) = docker-init(tini)。

正式 nas-verify(重建后必跑,同 ci-cd 口径):login(token 字段) + /api/v1/strategy/list 200 非空 + dbbardata 600519 SSE >0。

10. NAS 镜像可复现构建(避免 commit 链依赖丢失)

现状:NAS 运行的 sanguo_vnpy_v2:pre-init-rebuild 是 docker commit 快照链(with-sqlite → 手动 pip 装 bullet-trade/jwt/… → commit),非从 Dockerfile 构建。手动装的包只活在容器层,任何从 base 重建都会丢。实测 pre-init-rebuild 比 with-sqlite 多 64 个包(含 jwt/bullet-trade/baostock/jqdatasdk/vnpy_ctastrategy/pytest/整个 jupyter 栈)。

构建走 docker/Dockerfile(单阶段自包含,2026-08-01 已验证+换装):FROM python:3.10-slim + 编译 TA-Lib + pip install -r requirements-docker.txt + COPY 代码。⚠️不是 Dockerfile.nas(它 FROM sanguo_vnpy:base 是从未建成的 2-stage 残留,别用)。NAS 容器 bind-mount /app 提供运行时代码,镜像只需 Python+依赖层。

已修(2026-08-01,均在 git):

  • requirements-docker.txt 补缺失顶层运行时依赖:bullet-trade==0.9.2/jqdatasdk==1.9.8(bullet_trade import 链)/vnpy_ctastrategy==1.4.1/vnpy_sqlite==1.1.3。
  • polars[rtcompat]==1.42.1:NAS CPU(Synology Celeron)无 AVX2,plain polars import 即 SIGILL(exit132);[rtcompat] 拉 polars-runtime-compat 兜底。勿放回 >= 松约束(会拉到要 AVX2 的新版崩溃)。
  • Dockerfile pip+apt 都补清华镜像(原 pypi.org/deb.debian.org 国内不稳);加 .dockerignore(排 data_cache/frontend/venv 等,上下文 1.9G→~50MB)。

可复现重建步骤(已实证):

D=/var/packages/Docker/target/usr/bin/docker
CTX=/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2
# 1. 单阶段构建(setsid/nohup 防 ssh 杀;log 落盘)
ssh sanguo-nas "cd $CTX && setsid $D build -f docker/Dockerfile -t sanguo_vnpy_v2:reproducible . > /tmp/repro_build.log 2>&1 &"
# 2. 起测试容器带 --init(见 §9),新名+8001 端口不占 sanguo_vnpy_v2
$D run -d --init --name sanguo_repro_test -v $CTX:/app -v /volume1/stock:/volume1/stock -p 8001:8000 --network sanguo_vnpy_network -e DATA_DIR=$CTX sanguo_vnpy_v2:reproducible
# 3. 验证:nas-verify(8001 login+strategy/list+dbbardata) + 03 短测(bullet_trade 链路)
# 4. 全过才换主容器(见下);pre-init-rebuild image 留回滚
# 5. 进程检测用 /proc/<pid>/cmdline,别用 ps(此容器 ps 看不到 python,会误判)

换主容器:$D stop sanguo_vnpy_v2 && $D rm sanguo_vnpy_v2 → $D run -d --init --name sanguo_vnpy_v2(同 §9 挂载/端口/network/env) sanguo_vnpy_v2:reproducible → 8000 nas-verify。回滚:同命令换 image 为 sanguo_vnpy_v2:pre-init-rebuild。

验证结果(2026-08-01):03 momentum_timing 短测 total_return 15.62%/sharpe 1.59/6 持仓;web/login/dbbardata 全过;reproducible 镜像已换上主容器(Init=true)。⚠️02/01 未单独实测(用同栈,理论同),策略 session 首跑若异常即回滚。

11. 发布关联性检查清单(每次推 VPS 必过,2026-09-14 起)

背景:09-08 切大QMT 桥后,每次发布都伴随数据/策略 session 的连锁问题(09-08 断链/09-11 停摆/09-14 shadow 树死)。 本节是事后复盘的沉淀:发布动作本身只管代码落盘,而生产系统的其余半边(schtask/引擎长进程/桥契约/非代码资产)全在代码流之外。 生产栈架构与运维详见 deployment/vps-production-runbook.md。

11.0 铁律:VPS 引擎是长进程,promote ≠ 生效

12 引擎(live×6+shadow×6)常驻运行,promote 只落盘不重启进程——存活引擎继续跑旧代码, 下次启动的进程(schtask/收割重启)用新代码 = 混版运行。涉引擎模块(sanguo_portfolio/sanguo_live/sanguo_trader) 发布必须与策略 session 约定收割重启窗口(工序见 production-runbook §5,先 /end desk 再收割)。 引擎重启会重置 run_monthly force 计数与锚语义 → 月度开火日(value 等)前禁重启,窗口由策略域报。

11.1 发布时机窗

窗口 禁止动作 原因
09:15–15:10 推涉引擎/桥/schtask 的变更 交易窗:探针开火/引擎下单/哨兵巡检全在忙
21:00–22:00 推涉数据腿的变更 xt_eod 夜班占桥
盘前 07:50–09:25 重启桥/引擎 relogin→晨查→探针→闸门链会被打断
月度开火日盘前 重启引擎 锚/force 计数重置=开火缺席(09-09 value 教训)

11.2 影响矩阵(改了什么 → 谁中招 → 必做动作)

改动面 波及 发布时必做
sanguo_portfolio/ sanguo_live/ sanguo_trader/ 策略域 12 引擎 提前通知策略 session 安排收割重启;重启后核台账锚点+基线
sanguo_data/ scripts/data_platform/ 数据域采集 schtask(xt_eod/bs/ak) 通知数据 session;21:00 前完成让其当晚验锚
scripts/qmt_relogin/(哨兵/探针/relogin/gate) 三层防御本体 手动 scp 到 C:\sanguo_bigqmt\(promote 不推此目录);观察哨兵下一跳
xtquant_bridge shim / 桥 server(bigqmt_signal_trader) 桥契约双侧(引擎+xt_eod) shim 与 server 补丁同步部署;relogin 生效;日历探针断言 int 毫秒
provider/取数层 引擎+回测+xt_eod verify_unified_e2e.py 必跑
sanguo_backtest/ 仅回测 无窗口约束,常规流程

11.3 非代码资产(本流程推不到,清单见 production-runbook §4)

promote.sh 只推 git 模块。桥工具箱(C:\sanguo_bigqmt)、glue shim、QMT 安装树内 server 补丁、 wrapper ps1、redis、DPAPI 凭证、schtask 注册全部在代码流之外——repo 改了 ≠ VPS 生效, 须手动 scp;QMT 客户端升级会重装安装树=server 补丁丢失须重打。

11.4 发布后验证序列(涉 🔴 面必跑)

  1. 落盘:Select-String 验新代码标记(commit≠部署)
  2. verify_unified_e2e.py(provider/数据面改动)
  3. bridge_switch_ops.py trade-probe → GREEN cash 基线零漂(涉桥/引擎)
  4. 哨兵下一跳(:10/:40)GREEN(涉防御/桥)
  5. 交易日 09:15 探针 status=50(涉交易链)
  6. 策略域恒等式(涉引擎,由其执行)

11.5 已知部署陷阱(事故实锤,复发先查)

陷阱 实锤
pip 装 xtquant 相关包覆盖官方 xtquant 09-08 事故:wheel src/ 同名直覆盖 site-packages
桥 server 补丁打错部署位 09-11:安装树同名包多拷贝,首打错到源码副本
wrapper ps1 带中文注释 PS5.1 GBK 解析吃掉语句行,PYTHONPATH 静默失效(两次踩中)
杀 auto-respawn 子进程不先 /end desk 09-14:重拉赛跑,新树 8 秒全灭
裸跑 python xt_eod.py 无 wrapper PYTHONPATH 回落旧腿,09-18 后必死
长断桥后以为引擎会自动重连 36h+ 重试环会死,桥活也不重连,须收割

12. Commit 环境标签与 VPS 触及面判定(2026-09-14 自 vps-impact-map.md 收编)

CI enforce-label 强制:本次 push 的 commit 必须带 [vps](VPS 运行时也触发,待推 prod)或 [nas](NAS/docker 专属),无标签 CI 直接 fail(merge commit 豁免)。 [vps] 触发链:nas-verify 绿后 CI 自动开/更新 [待推VPS] issue(open=待推);推 vps-deploy.yml 成功 → 打 vps-deployed tag + 自动关 issue。 查 VPS 未部署:git log vps-deployed..HEAD --grep='\[vps\]'(版本真相=移动 tag vps-deployed)。

判定钥匙:维度是「VPS 运行时是否触发」,不是「文件改了没」。

归属 模块/路径 VPS 运行时场景
🔴 VPS 触及(=[vps],要考虑推) sanguo_data/ schtask 每日采集(bs/xt/ak)
🔴 sanguo_backtest/ CTA 回测入口(load_data/metrics/引擎)
🔴 sanguo_portfolio/runner_backtest.py VPS 直接调 run_backtest(不走 pool)
🔴 sanguo_portfolio/runner_live.py sanguo_live/ sanguo_trader/(shadow) sanguo_qmt_bridge/ 实盘引擎 / 大QMT 桥栈
🔴 sanguo_common/ 被上述引用
🔴 scripts/data_platform/*.py VPS schtask 跑的采集/merge/verify
🔴 scripts/qmt_relogin/ 三层防御本体;⚠️promote 不部署它——须手动 scp 到 C:\sanguo_bigqmt\(§11.3)
🔴 sanguo_api/(routes/app/uvicorn) VPS 常驻 sanguo-api(run_api.ps1→run_web.py→:8000=生产控制台 vnpy.mysanguo.top,runbook §7.1);改后须 schtasks /end /tn sanguo-api && schtasks /run /tn sanguo-api
🔴 frontend/(Vue 源码) VPS 挂 frontend/dist(公网前端);vps-deploy.yml 每次自动构建+推送+重启 sanguo-api(§13)
🟡 弱触及 sanguo_orchestrator/ 随 sanguo_api 常驻加载(run_web.py 单进程内存态,app.py 直接 import);VPS 控制台动作可触发,改后同样须重启 sanguo-api
🟡 弱触及 requirements-lock.txt VPS 手动 pip 装,lock 仅参考
🟢 sanguo_web/(legacy 平行后端,不是前端) 已退役归档 _retired/sanguo_web/(2026-10-01 用户拍板:代码保留、退出 promote 白名单+docker COPY,不可再启动为服务);前端=frontend/
🟢 docker/ Dockerfile entrypoint.sh .gitea/ VPS 无 docker
🟢 config/(vnpy_db 等容器 config) VPS 用 SANGUO_DB_PATH env 覆盖(backtest.yaml 仍作种子直推)

⚠️ 2026-10-01 勘误(audit/20261001_docs_audit/ P0-2):旧矩阵曾把 sanguo_api/+前端整行标 🟢「VPS 无常驻 web」——错。VPS 一直常驻 sanguo-api(runbook §7.6 09-25 勘误原文:「本就常驻跑纯 API」),vps-deploy.yml 每次部署都构建前端 dist 推 VPS 并重启 sanguo-api。api/前端改动按上表现判 🔴 [vps]。同病原文当时还复制在 .claude/CLAUDE.md、session-environment-guide.md、vps-deploy-pending.md 三处,本轮一并修正。

  • 混合模块(sanguo_portfolio/ 若只改 orchestrator 调度、sanguo_api/ 若只改 routes)看 diff 具体文件对照上表,不整模块一刀切。
  • 判定 3 步:①看 commit 标签 → ②对照触及面表 → ③问「VPS 跑这段代码吗」。
  • 物理层天然隔离:promote.sh 白名单推送,docker/、.gitea/ 等进不了 VPS。

12.1 设计文档同步闸门(enforce design-doc sync,2026-09-14 起)

三条线各有一份唯一活文档,CI 强制「档随码走」:push 触及某线代码域而未同 push 更新其设计文档(多档任一)→ CI 直接 fail;确属纯实现/bug 修复不改设计 → commit 标题结尾打 [no-doc] 声明豁免(显式可审计,与 [vps]/[nas] 同款肌肉记忆)。

线 代码域 唯一设计文档(任一更新即可)
因子 sanguo_factor/、frontend/src/views/factor/、sanguo_portfolio/strategies/factor_topn.py docs/factor_research/factor-system-design.md(禁新增独立 md,收编/日期节追加)
数据 sanguo_data/ docs/superpowers/specs/2026-07-21-data-source-fusion-design.md
基建 scripts/nas_sync/、scripts/qmt_relogin/、scripts/bridge_switch_ops.py、.gitea/workflows/、docker/、Dockerfile docs/three-env-code-promote.md 或 docs/deployment/vps-production-runbook.md

设计哲学(双层):指令层(CLAUDE.md 三线立牌)管「会做」,机械层(本闸门)管「不忘」;豁免是显式声明而非静默跳过,只有遗忘才会被拦。fail-open:闸门脚本自身异常一律放行,绝不因闸门 bug 卡全团队 push。

09-14 上线当日加固四处(评审实锤):①[no-doc] 匹配锚定标题行尾——原子串匹配被闸门自身 commit(描述里提及 [no-doc] 字样)误豁免;②闸门自我 fetch BEFORE/AFTER——原先依赖上一步 enforce-label 的 fetch 副作用,步骤重排即静默 no-op;③基建域补 scripts/qmt_relogin/ + scripts/bridge_switch_ops.py(runbook §4 钦定的桥工具箱资产,当日桥工具修复曾零义务过闸);④步骤名含冒号必须加引号——首版 name: 里裸冒号使整个 workflow YAML 解析失败,Gitea 静默不创建任何 run(CI 全瘫而非单步红,0141f4e 起三笔 push 零 run 才暴露);改 workflow 必须本地 yaml.safe_load 验过再推。

13. 前端构建与发布

  • NAS(自动):ci-cd.yml 的 nas-deploy 自动 npm ci + vue-tsc + build + rsync dist(dist 不进 git、不在 promote.sh)。
  • VPS(手动,mysanguo.online 北京直 serve):
    cd frontend && npm run build
    scp -r frontend/dist/* Administrator@49.232.102.198:C:/sanguo_vnpy_v2/frontend/dist/
    ssh 49.232.102.198 "schtasks /end /tn sanguo-api && schtasks /run /tn sanguo-api"
    
    (sanguo_api 的 ProcessPool worker 缓存旧代码,改后端必须重启 sanguo-api 才生效。)