From 851d7d49b7d7fd0d9cee920eee3ef7a8ac98e66a Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 29 Jul 2026 21:47:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Phase4=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=99=8B=E5=8D=87=E8=84=9A=E6=9C=AC=20promote.sh=20+=20runbook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mac→NAS(rsync代码镜像)+Mac→VPS(scp生产部署)单向晋升管线。 - 数据铁律双层防护: rsync --exclude='/data' + scp按模块天然隔离data/ - --module 单模块快速补推 / 全量 双模式 - VPS无常驻web, 代码部署后下次schtask/回测启动自动生效,无需hot reload - dry-run双端验证通过(VPS+NAS标记落盘后还原无残留) - ssh用49.232.102.198(严禁ssh vps,fake-ip劫持198.18.1.254) Co-Authored-By: Claude --- docs/three-env-code-promote.md | 136 +++++++++++++++++++++++++++++++ scripts/nas_sync/promote.sh | 144 +++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 docs/three-env-code-promote.md create mode 100755 scripts/nas_sync/promote.sh diff --git a/docs/three-env-code-promote.md b/docs/three-env-code-promote.md new file mode 100644 index 0000000..e7b18b3 --- /dev/null +++ b/docs/three-env-code-promote.md @@ -0,0 +1,136 @@ +# 三机代码晋升 Runbook (Phase4) + +> Mac(dev 源头) → NAS(test 镜像) → VPS(prod 生产) 单向代码晋升。 +> 本文档只管 **代码**,不管数据(数据铁律见末节)。 + +## 1. 角色与流向 + +| 机器 | 角色 | 代码路径 | 工具 | +|------|------|----------|------| +| **Mac** | dev 源头(改代码) | `~/.openclaw/sanguo_projects/sanguo_vnpy_v2` | rsync/scp | +| **NAS** | test 镜像(只读副本) | `/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 +``` + +推送内容: +- 模块目录(13 个): `sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor sanguo_live sanguo_orchestrator sanguo_portfolio sanguo_qmt_bridge sanguo_research sanguo_trader sanguo_web scripts config tests` +- 根文件(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 进程模式是 **一次性任务**(回测 runner_backtest + 采集 schtask bs_eod/akshare),**无常驻 web 服务**。 + +| 场景 | reload 动作 | +|------|-------------| +| 采集 schtask(定时) | **无需操作**。下次 schtask 触发时自动加载新代码 | +| 回测任务 | **无需操作**。下次启动回测时自动加载新代码 | +| 立即让采集生效 | `schtasks /end sanguo-bs-eod && schtasks /run sanguo-bs-eod` | + +> 一句话: 代码部署后,**下次启动自动生效**,不用 hot reload、不用重启服务。 + +立即生效命令(在 VPS 上执行,通过 ssh): + +```bash +ssh 49.232.102.198 'schtasks /end sanguo-bs-eod' +ssh 49.232.102.198 'schtasks /run sanguo-bs-eod' +``` + +## 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/stock/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 -- 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` + 空数组 | diff --git a/scripts/nas_sync/promote.sh b/scripts/nas_sync/promote.sh new file mode 100755 index 0000000..4415213 --- /dev/null +++ b/scripts/nas_sync/promote.sh @@ -0,0 +1,144 @@ +#!/bin/bash +# Phase4: Mac→NAS(代码镜像) + Mac→VPS(生产部署) 单向代码晋升 +# +# 用法: +# 全量晋升(所有模块+根文件): bash scripts/nas_sync/promote.sh +# 单模块晋升(快速/验证): bash scripts/nas_sync/promote.sh --module sanguo_portfolio +# +# 设计: +# - NAS 走 rsync(增量快),VPS 无 rsync 走 scp -r(按模块逐个推). +# - --module X: NAS+VPS 都只推该模块,不推根文件(用于快速补推/小范围验证). +# - 数据铁律: 严禁推 data/ (rsync --exclude '/data'; scp 按模块天然隔离). +# - reload: VPS 无常驻 web 服务,代码部署后下次 schtask/回测启动自动生效, +# 无需 hot reload. 如需立即生效见 runbook. +# - ssh 用 `49.232.102.198`(~/.ssh/config 已配 key),严禁 `ssh vps`(fake-ip 劫持). +set -eu + +VPS=49.232.102.198 +NAS=sanguo-nas +NAS_CODE=/volume1/stock/sanguo_vnpy_v2 +VPS_CODE='C:/sanguo_vnpy_v2' # scp 目标用正斜杠 + +ALL_MODS=(sanguo_api sanguo_backtest sanguo_common sanguo_data sanguo_factor + sanguo_live sanguo_orchestrator sanguo_portfolio sanguo_qmt_bridge + sanguo_research sanguo_trader sanguo_web scripts config tests) +ROOT_FILES=(pyproject.toml pytest.ini requirements-lock.txt run_web.py) + +# 解析 --module 参数 +PUSH_MOD="" +if [[ "${1:-}" == "--module" ]]; then + if [ -z "${2:-}" ]; then + echo "ERROR: --module 需要模块名参数" >&2 + echo "可用模块: ${ALL_MODS[*]}" >&2 + exit 1 + fi + PUSH_MOD="$2" + if [ ! -d "$PUSH_MOD" ]; then + echo "ERROR: 本地目录不存在: $PUSH_MOD" >&2 + exit 1 + fi +fi + +# rsync exclude: 前导 / = 仅根级匹配,不影响模块内同名子目录 +RSYNC_EXCLUDE=(--exclude='/data' --exclude='/data_backup' --exclude='/logs' + --exclude='__pycache__' --exclude='.git' --exclude='.pytest_cache' + --exclude='.mypy_cache' --exclude='.ruff_cache' + --exclude='/vnpy_v4.4.0' --exclude='/vnpy_qmt_v0.3.3' + --exclude='/htmlcov' --exclude='/docker' --exclude='/docs' + --exclude='/examples' --exclude='/venv310' --exclude='/venv' + --exclude='/requirements' --exclude='*.log' + --exclude='build*.log' --exclude='rebuild*.log' + --exclude='test.db' --exclude='*.pyc' --exclude='*.pyo' + --exclude='.coverage' --exclude='*.egg-info') + +echo "==============================================" +echo " Phase4 代码晋升 $(date '+%F %T')" +if [ -n "$PUSH_MOD" ]; then + echo " 模式: 单模块 [$PUSH_MOD] (NAS+VPS 只推该模块,不推根文件)" +else + echo " 模式: 全量 (所有模块+根文件)" +fi +echo "==============================================" + +# ---- Step 1/3: Mac → NAS (rsync 代码镜像) ---- +echo "" +echo "[1/3] rsync Mac → NAS ($NAS:$NAS_CODE) ..." +if [ -n "$PUSH_MOD" ]; then + rsync -az "${RSYNC_EXCLUDE[@]}" "$PUSH_MOD/" "$NAS:$NAS_CODE/$PUSH_MOD/" + echo " ✅ NAS 单模块镜像完成: $PUSH_MOD" +else + # 不用 --delete! NAS 有 data_backup 等独有目录,删了就丢数据 + rsync -avz "${RSYNC_EXCLUDE[@]}" ./ "$NAS:$NAS_CODE/" + echo " ✅ NAS 全量镜像完成" +fi + +# ---- Step 2/3: Mac → VPS (scp 按模块/根文件推) ---- +echo "" +echo "[2/3] scp Mac → VPS ($VPS:$VPS_CODE) ..." +if [ -n "$PUSH_MOD" ]; then + MODS=("$PUSH_MOD") + ROOT_PUSH=() # 单模块不推根文件 +else + MODS=("${ALL_MODS[@]}") + ROOT_PUSH=("${ROOT_FILES[@]}") +fi + +PUSHED=0 +FAILED=0 +for m in "${MODS[@]}"; do + if [ ! -d "$m" ]; then + echo " skip(本地缺失): $m" + continue + fi + if scp -r -C -o ConnectTimeout=20 "$m" "$VPS:$VPS_CODE/" 2>/dev/null; then + echo " pushed $m" + PUSHED=$((PUSHED+1)) + else + echo " FAIL $m (scp 退出非零)" + FAILED=$((FAILED+1)) + fi +done +# ROOT_PUSH 在 --module 模式下为空数组; macOS bash 3.2 + set -u 下 "${arr[@]}" 会报错, +# 用 ${#arr[@]} 计数守卫绕过(空数组长度=0, 跳过循环) +if [ "${#ROOT_PUSH[@]}" -gt 0 ]; then + for f in "${ROOT_PUSH[@]}"; do + if [ ! -f "$f" ]; then + echo " skip(本地缺失): $f" + continue + fi + if scp -C -o ConnectTimeout=20 "$f" "$VPS:$VPS_CODE/" 2>/dev/null; then + echo " pushed $f" + PUSHED=$((PUSHED+1)) + else + echo " FAIL $f" + FAILED=$((FAILED+1)) + fi + done +fi + +if [ "$FAILED" -gt 0 ]; then + echo "" + echo "❌ $FAILED 个文件推送失败,请检查上方日志。已成功 $PUSHED 个。" + exit 1 +fi +echo " 共推送 $PUSHED 个模块/文件" + +# ---- Step 3/3: VPS 落盘验证 ---- +echo "" +echo "[3/3] VPS 落盘验证 ..." +# 验证目标: 取第一个推送的模块,列其 .py 文件的 mtime +VERIFY_MOD="${MODS[0]}" +VERIFY_CMD="powershell -NoProfile -Command \"Get-ChildItem -Path 'C:\\sanguo_vnpy_v2\\${VERIFY_MOD}' -Filter '*.py' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 3 Name,Length,LastWriteTime | Format-Table -Auto | Out-String\"" +if ssh -o ConnectTimeout=20 "$VPS" "$VERIFY_CMD" 2>/dev/null | grep -q '\.py'; then + echo " ✅ VPS 落盘确认: $VERIFY_MOD/*.py 已就位" +else + echo " ⚠️ VPS 验证未取到 $VERIFY_MOD 的 .py 文件(可能目录为空或 ssh 超时,请手动核查)" +fi + +echo "" +echo "==============================================" +echo " ✅ 晋升完成 $(date '+%F %T')" +echo " - NAS 代码镜像: $NAS:$NAS_CODE" +echo " - VPS 生产代码: $VPS:$VPS_CODE" +echo " - reload: VPS 无常驻 web,下次 schtask/回测启动自动生效" +echo "=============================================="