Files
sanguo_vnpy_v2/docs/three-env-code-promote.md
T
claude_dev af2a379237
CI/CD / test (push) Successful in 46s
CI/CD / nas-deploy (push) Successful in 2s
CI/CD / nas-verify (push) Successful in 10s
docs(promote): 轮换流程坑注——CI checkout 无 gitignore 种子,backtest.yaml 须 Mac 手动 promote+容器重启(run638 实证) [nas]
Co-Authored-By: Claude Code <notify@anthropic.com>
2026-10-01 10:02:26 +08:00

310 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 三机代码晋升 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 全量晋升(所有模块 + 根文件)
改完一批代码后:
```bash
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
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):
```bash
ssh 49.232.102.198 'schtasks /end sanguo-bs-daily && schtasks /run sanguo-bs-daily'
```
## 5. 落盘验证
脚本 Step3 自动验证 VPS 第一个推送模块的 `.py` 文件 mtime。手动深度核查:
```bash
# 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
代码部署出问题需要回退:
```bash
# 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 的路径。
**手动重建命令要点**:
```bash
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)。
**可复现重建步骤(已实证)**:
```bash
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`](./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`)。
> **CI pytest 门禁范围(2026-10-01 扩至 8 目录)**:`tests/data_platform+portfolio+factor+api`(原四)+ `tests/trader+backtest+data+orchestrator`(10-01 架构审计 P0-5 补入——撮合/涨跌停/软限/持仓账本/shadow 等钱相关测试进强制门禁,本地实测 339 绿 7.6s)。门禁口径唯一维护处=`ci-cd.yml` 注释。
> **CI 凭据纪律(2026-10-01 起)**:nas-verify 登录口令走 Gitea secret `NAS_API_PASSWORD`(${{ secrets.* }} 引用),**明文口令禁入 workflow/文档/git**;auth 种子=`config/backtest.yaml`(gitignore 本地文件,bcrypt hash+jwt_secret)。**⚠️轮换流程(10-01 实战定型)**:①Gitea 建/改 secret → ②Mac 手动 `promote --target nas --module config`(**CI 的 nas-deploy 从 git checkout 跑 promote,checkout 里没有 gitignore 种子——yaml 只能从 Mac 工作树手动推**)→ ③重启 NAS 容器(auth 启动时读入)→ ④push CI 改动/触发重跑。VPS 侧=config 模块手动 `--target vps` + 重启 sanguo-api。反序或漏②③=nas-verify login 红(run638 实证)。
**判定钥匙**:维度是「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)**:
```bash
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 才生效。)