#!/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/homes/admin/.sanguo_projects/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 <名>(单模块) / --target (目标机器,默认 all) PUSH_MOD="" TARGET="all" while [[ $# -gt 0 ]]; do case "$1" in --module) [ -z "${2:-}" ] && { echo "ERROR: --module 需要模块名. 可用: ${ALL_MODS[*]}" >&2; exit 1; } PUSH_MOD="$2"; shift 2 [ ! -d "$PUSH_MOD" ] && { echo "ERROR: 本地目录不存在: $PUSH_MOD" >&2; exit 1; } ;; --target) case "${2:-}" in nas|vps|all) TARGET="$2"; shift 2;; *) echo "ERROR: --target 需 nas|vps|all" >&2; exit 1;; esac ;; *) echo "ERROR: 未知参数 $1 (支持 --module <名> / --target )" >&2; exit 1;; esac done # 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 "==============================================" if [[ "$TARGET" == "nas" || "$TARGET" == "all" ]]; then # ---- Step: Mac → NAS (rsync 代码镜像) ---- echo "" echo "[*] 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 fi if [[ "$TARGET" == "vps" || "$TARGET" == "all" ]]; then # ---- Step: Mac → VPS (scp 按模块/根文件推) ---- echo "" echo "[*] 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 fi # end target=vps|all echo "" echo "==============================================" echo " ✅ 晋升完成 $(date '+%F %T') [target=$TARGET]" echo " - NAS 代码镜像: $NAS:$NAS_CODE" echo " - VPS 生产代码: $VPS:$VPS_CODE" echo " - reload: VPS 无常驻 web,下次 schtask/回测启动自动生效" echo "=============================================="