# VPS 生产运维 Runbook > **sanguo QMT bridge 生产环境运维手册。** VPS 已部署并在运行,本文档是"运维 + 已验证状态记录",不是从零搭建指南。 > > 相关文档: > - bridge 接口契约:[`sanguo_qmt_bridge/README.md`](../../sanguo_qmt_bridge/README.md) > - 首次部署参考(历史):[`d-phase-windows-deploy.md`](./d-phase-windows-deploy.md) / [`windows-bridge-setup.md`](./windows-bridge-setup.md) > - NAS 容器运维:[`nas-deploy-plan.md`](./nas-deploy-plan.md) > > **安全约定**:本文档进 git,**不硬编码 BRIDGE_TOKEN**。所有命令中 `` 为占位符,真实值见 Claude memory `windows-vps-access.md` 或 VPS 系统环境变量 `BRIDGE_TOKEN`。 --- ## 1. 已验证生产状态(2026-07-14 巡检) > 以下事实经实地验证,直接采纳。下次巡检时更新日期并重新核实。 | 项目 | 值 | 备注 | |------|-----|------| | **VPS** | `49.232.102.198` | 腾讯云轻量,Win Server 2022,4C/16G/180G SSD | | **SSH** | `ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198` | Mac ~/.ssh/id_ed25519 免密 | | **磁盘** | C: 24G 用 / 156G 空闲 | 充裕 | | **Python** | `C:\Python310\python.exe` (3.10.11) | xtquant import OK | | **miniQMT** | `C:\国金QMT交易端\` userdata `userdata_mini` | 模拟账户 `66639661` 已登录 | | **bridge 代码** | `C:\sanguo_qmt_bridge\` | FastAPI + uvicorn :8765 | | **bridge 启动脚本** | `C:\run_bridge.ps1` | 设环境变量 + BRIDGE_SESSION_ID + uvicorn | | **Caddy** | `C:\caddy\` + `C:\run_caddy.ps1` | :443 → :8765 反代,Let's Encrypt 自动续期 | | **schtasks** | `sanguo-bridge` + `sanguo-caddy` 均 Running | SYSTEM 账户 / onstart 自启 | ### 进程状态 | 进程 | 映像名 | 角色 | |------|--------|------| | XtMiniQmt | `XtMiniQmt.exe` | miniQMT 客户端,已登录模拟账户 | | python | `python.exe` | uvicorn bridge :8765 | | caddy | `caddy.exe` | HTTPS 反代 :443 → :8765 | ### bridge 端点验证(VPS 本地 127.0.0.1:8765) | 端点 | 响应 | |------|------| | `GET /health` | `{"status":"ok","miniqmt_connected":true}` | | `GET /account` | `{"ok":true,"cash":9997075.51,"frozen":9001769.0,"market_value":2901.0,"total":9999978.51}` | | `GET /positions` | `{"ok":true,"positions":[...sh600000(200股), sz000001(100股)...]}` | ### 三条访问路径 | # | 路径 | 地址 | 场景 | |---|------|------|------| | ① | VPS 本地 | `http://127.0.0.1:8765` | 运维 RDP/SSH 内验证 | | ② | 公网 HTTPS | `https://bridge.mysanguo.top` | 外部客户端(Mac 浏览器等)| | ③ | NAS 容器经 Mac 隧道 | `http://192.168.2.101:8765` | NAS sanguo 容器调 bridge | --- ## 2. 拓扑图 ``` ┌───────────────────────────────────────────────────┐ │ Windows VPS · 49.232.102.198 │ │ Win Server 2022 · 4C/16G/180G SSD │ │ │ │ ┌───────────┐ xtquant ┌──────────────┐ │ │ │ miniQMT │◄──────────│ bridge │ │ │ │ 66639661 │ │ :8765 │ │ │ └───────────┘ └──────┬───────┘ │ │ │ 反代 │ │ ┌──────▼───────┐ │ │ │ Caddy │ │ │ │ :443 │ │ │ │ Let's Encr. │ │ │ └──────┬───────┘ │ └──────────────────────────────────┼────────────────┘ │ ┌────────────────────────┼───────────────────┐ │ │ │ ① VPS 本地 ② 公网 HTTPS ③ NAS→Mac→VPS http://127.0.0.1:8765 https://bridge. http://192.168.2.101 (运维 RDP/SSH) mysanguo.top :8765 │ │ │ ▼ ▼ ▼ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ VPS 终端 │ │ 外部客户端 │ │ Mac Mini │ └──────────┘ │(Mac 浏览器等) │ │ 192.168.2.101│ └──────────────┘ │ 开发 + 隧道 │ │ │ │ ssh -N -L │ │ 0.0.0.0:8765:│──SSH:22──► VPS │ 127.0.0.1:8765│ │ │ │ caffeinate │ │ -i -s 防睡眠 │ └──────┬───────┘ │ ┌──────▼───────┐ │ NAS 容器 │ │ 192.168.2.154│ │ sanguo_vnpy │ │ _v2 (测试+备) │ └──────────────┘ ⚠ 华为光猫拦截 NAS→VPS 的 80/443 → NAS 不能走路径② → NAS 容器走路径③:HTTP 到 Mac :8765 → Mac SSH 隧道 → VPS :8765 → Mac 需常驻 SSH tunnel + caffeinate -i -s 防睡眠 ``` ### 三机分工 | 机器 | IP | 角色 | 能做什么 | 不能做什么 | |------|-----|------|----------|-----------| | **VPS** | 49.232.102.198 | 生产 | miniQMT + bridge + Caddy 全链路 | 无开发环境 | | **Mac Mini** | 192.168.2.101 | 开发 + 隧道中继 | 写代码、git、scp 部署、SSH 隧道 | 跑不了 xtquant(Windows only) | | **NAS** | 192.168.2.154 | 测试 + 备份 | Docker 容器跑集成测试、回测、每日备份 | 无法直连 VPS 80/443(光猫拦截) | --- ## 3. 日常运维操作 > 以下命令均从 **Mac Mini** 执行,通过 SSH 远程操控 VPS。 ### 3.1 健康巡检(一条命令验三端点) **快速健康(无需 token):** ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "curl -s http://127.0.0.1:8765/health" ``` 期望:`{"status":"ok","miniqmt_connected":true}` **完整三端点验证(含 account + positions,需 token):** 复杂引号场景用 stdin 喂 PowerShell(见 [§8 Windows 坑](#8-windows-跑命令的坑)): ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1' Write-Host "=== health ===" curl.exe -s http://127.0.0.1:8765/health Write-Host "`n=== account ===" curl.exe -s -H "X-Bridge-Token: " http://127.0.0.1:8765/account Write-Host "`n=== positions ===" curl.exe -s -H "X-Bridge-Token: " http://127.0.0.1:8765/positions PS1 ``` **检查 schtasks 状态 + 进程 + 磁盘:** ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1' Write-Host "=== schtasks ===" schtasks /query /tn "sanguo-bridge" /fo list | Select-String "Status" schtasks /query /tn "sanguo-caddy" /fo list | Select-String "Status" Write-Host "=== processes ===" Get-Process python,caddy,XtMiniQmt -ErrorAction SilentlyContinue | Format-Table Name,Id,CPU -Auto Write-Host "=== disk ===" Get-PSDrive C | Format-Table Used,Free -Auto PS1 ``` ### 3.2 bridge 重启(必须换 BRIDGE_SESSION_ID) > **关键**:xtquant 的 `connect()` 复用旧 session 会返回 -1。每次重启 bridge 前必须换 `BRIDGE_SESSION_ID`,否则 bridge 起来但 miniQMT 连不上。 **步骤:** 1. **改 session_id** — RDP 或 SSH 编辑 `C:\run_bridge.ps1`,把 `$env:BRIDGE_SESSION_ID` 改为新值(如日期递增 `20260714` → `20260715` 或加后缀 `20260714b`): ```bash # SSH 在线编辑(PowerShell 替换) ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1' $path = "C:\run_bridge.ps1" $content = Get-Content $path -Raw # 把旧 session_id 替换为今天的(按实际值调整正则) $newId = Get-Date -Format "yyyyMMddHHmm" $content = $content -replace 'BRIDGE_SESSION_ID\s*=\s*"\d+"', "BRIDGE_SESSION_ID = `"$newId`"" Set-Content $path -Value $content -Encoding UTF8 Write-Host "session_id updated to $newId" # 确认 Select-String -Path $path -Pattern "BRIDGE_SESSION_ID" PS1 ``` 2. **重启 schtask:** ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "schtasks /end /tn \"sanguo-bridge\" & schtasks /run /tn \"sanguo-bridge\"" ``` 3. **等 5 秒后验证健康**(同 §3.1 快速健康命令)。 ### 3.3 Caddy 重启 Caddy 无 session 状态问题,直接重启: ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "schtasks /end /tn \"sanguo-caddy\" & schtasks /run /tn \"sanguo-caddy\"" ``` 验证公网: ```bash curl -s https://bridge.mysanguo.top/health ``` ### 3.4 miniQMT 崩溃恢复(human-gated) > miniQMT 是券商客户端 GUI 程序,崩溃后需要人工 RDP 登录。**无法自动化。** **症状**:`/health` 返回 `{"miniqmt_connected": false}`,或 bridge 日志 xtquant connect 报错。 **恢复步骤(人工操作):** 1. RDP 登录 VPS(远程桌面 `49.232.102.198`) 2. 手动启动 miniQMT 客户端 → 登录模拟账户 `66639661` 3. 确认客户端进入极简模式/独立交易界面 4. **换 BRIDGE_SESSION_ID 后重启 bridge**(同 §3.2) 5. 验证 `/health` → `miniqmt_connected: true` --- ## 4. dev → test → prod 发布流水线 ``` Mac Mini (开发) NAS (测试) VPS (生产) ───────────── ────────── ────────── 写代码 git pull scp bridge 代码 sanguo_qmt_bridge/ → 容器跑集成测试 → schtasks 重启 本地 unit test 验证通过 → 健康巡检 │ │ │ ▼ ▼ ▼ git push ──────────► gitea ──────► NAS pull ────► VPS deploy (git.mysanguo.top) (Docker 容器) (scp + restart) │ 每日 VPS 状态 备份 → NAS ``` | 阶段 | 机器 | 动作 | 验证 | |------|------|------|------| | dev | Mac Mini | 改 `sanguo_qmt_bridge/` 代码 | 本地 unit test(`pytest tests/`)| | push | Mac Mini | `git push origin master` | gitea 仓库更新 | | test | NAS | 容器 `git pull` + 跑集成测试 | bridge_client 测试通过、数据管道 OK | | prod | VPS | scp 变更文件 + 重启 bridge | `/health` + `/account` + `/positions` 三通 | | backup | NAS | 每日 VPS 状态备份到 NAS | bridge 代码 + config 快照 | > **注意**:NAS 测试只能验 bridge_client 端(发 HTTP 请求到 bridge),无法验 xtquant/miniQMT 端(Windows only)。bridge 服务端 + xtquant + miniQMT 的端到端验证只能在 VPS 上做。 --- ## 5. 代码部署到 VPS(Mac → VPS scp + 重启) ### 5.1 scp 变更文件 bridge 代码在 Mac 的 `sanguo_qmt_bridge/` 目录,改完后 scp 到 VPS: ```bash # 只传变更的 .py 文件(快速迭代) scp -i ~/.ssh/id_ed25519 \ sanguo_qmt_bridge/bridge.py \ sanguo_qmt_bridge/xt_gateway.py \ sanguo_qmt_bridge/auth.py \ Administrator@49.232.102.198:C:/sanguo_qmt_bridge/ # 或整目录同步(含 requirements.txt 等) scp -i ~/.ssh/id_ed25519 sanguo_qmt_bridge/*.py \ Administrator@49.232.102.198:C:/sanguo_qmt_bridge/ ``` > **Windows scp 路径用正斜杠**:`C:/sanguo_qmt_bridge/`(不是反斜杠)。 ### 5.2 换 session_id + 重启 ```bash # 1. 换 BRIDGE_SESSION_ID(同 §3.2 步骤 1) ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1' $path = "C:\run_bridge.ps1" $content = Get-Content $path -Raw $newId = Get-Date -Format "yyyyMMddHHmm" $content = $content -replace 'BRIDGE_SESSION_ID\s*=\s*"\d+"', "BRIDGE_SESSION_ID = `"$newId`"" Set-Content $path -Value $content -Encoding UTF8 Write-Host "session_id → $newId" PS1 # 2. 重启 schtask ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "schtasks /end /tn \"sanguo-bridge\" & schtasks /run /tn \"sanguo-bridge\"" # 3. 等 5 秒,验证 sleep 5 ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "curl -s http://127.0.0.1:8765/health" ``` ### 5.3 如改了 requirements.txt(依赖变更) ```bash ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 \ "cd C:\sanguo_qmt_bridge && C:\Python310\python.exe -m pip install -r requirements.txt" # 然后同 5.2 重启 ``` --- ## 6. 人类闸口清单 > 以下操作无法自动化,必须人工执行。自动化脚本碰到这些步骤应明确报"需人工干预"。 | 操作 | 为什么只能人做 | 频率 | |------|---------------|------| | **VPS 开通 / 重置密码** | 云厂商控制台操作 | 极低(首次/安全事件) | | **miniQMT 券商客户端登录** | GUI 程序 + 可能需验证码/密码 | 崩溃后 / VPS 重启后 | | **BRIDGE_TOKEN 轮换** | 需同时在 VPS + NAS + Mac 三处同步更新 | 定期(安全策略) | | **Let's Encrypt 证书异常处理** | Caddy 自动续期,但 Rate Limit / DNS 异常需人工介入 | 极低(自动续期正常时无需干预) | | **华为光猫 / 网络配置变更** | 运营商设备,SSH 碰不到 | 极低 | | **VPS 计划任务创建/修改** | 首次配置 `schtasks /create` 需 RDP | 低(配置变更时) | ### BRIDGE_TOKEN 轮换流程(人工) 1. 生成新 token:`python -c "import secrets; print(secrets.token_urlsafe(32))"` 2. VPS:更新 `C:\run_bridge.ps1` 里的 BRIDGE_TOKEN(或系统环境变量) 3. NAS:更新 sanguo 容器环境变量 + `docker restart sanguo_vnpy_v2` 4. Mac:更新 `sanguo_trader/bridge_client.py` 引用的配置或环境变量 5. VPS:换 session_id + 重启 bridge(§3.2) 6. 全链路验证三端点 --- ## 7. 排障表 | 现象 | 根因 | 排查 / 修复 | |------|------|-------------| | `/health` → `miniqmt_connected: false` | miniQMT 未登录 / 崩溃 / userdata 路径错 | RDP 检查 miniQMT 客户端状态(§3.4),确认 `MINIQMT_USERDATA` 指向正确 userdata_mini | | bridge 启动 xtquant connect 返回 -1 | **BRIDGE_SESSION_ID 与旧 session 冲突** | 换 session_id 后重启(§3.2)——这是最常见坑 | | 401 token 无效 | VPS 与 NAS/Mac 的 `BRIDGE_TOKEN` 不一致 | 核对三处 token 值一致 | | 公网 `https://bridge.mysanguo.top` 502 | Caddy 没启 或 bridge 没启 | 先验 VPS 本地 `curl 127.0.0.1:8765/health`;本地通=bridge OK→查 Caddy(§3.3);本地不通→查 bridge | | 公网 DNS 解析到错误 IP(如 198.18.1.244) | **Mac 本地代理(Clash/Surge)DNS 劫持到 fake-IP**。公网 DNS(8.8.8.8)解析正确到 49.232.102.198,但代理层拦截 | 修复①:`/etc/hosts` 加 `49.232.102.198 bridge.mysanguo.top`;修复②:代理规则里该域名直连(bypass proxy) | | NAS 容器访问 `bridge.mysanguo.top` 超时 | **华为光猫拦截 NAS→VPS 的 80/443** | 走路径③:Mac SSH 隧道 `http://192.168.2.101:8765`(Mac 需常驻 tunnel + caffeinate) | | Mac SSH 隧道断了 → NAS 容器连不上 bridge | Mac 睡眠 / SSH 进程退出 | Mac 跑 `caffeinate -i -s &` 防睡眠;用 autossh 或 launchd 守护 SSH tunnel | | 下单报 `[120141][证券交易未初始化]` | **非交易日**(miniQMT 交易日才初始化交易通道)| 等交易日。这是 miniQMT 设计,不是 bug | | 影子下单未触发 | `live.enabled` 未开 / 当日无成交 / BRIDGE_TOKEN 未设 / bridge_url 不对 | 逐项检查 NAS `config/data_platform.yaml` + 环境变量 | | 重复下单 | — | 不会。`paper_shadow_orders` 表 `UNIQUE(account_id, trade_id)` 幂等去重 | | cmd 中文/emoji 乱码 | cmd 默认 GBK 编码 | 用 PowerShell + `chcp 65001`,或 python 加 `-X utf8`(§8) | | scp 中文路径失败 | Windows 中文目录 + SSH 编码 | 用正斜杠路径 + ASCII 变量名,中文路径用搜索代替字面量 | --- ## 8. Windows 跑命令的坑 > Windows SSH 远程跑命令有三个经典坑:编码、引号、中文路径。 ### 8.1 编码(GBK → UTF8) cmd 默认 GBK,中文输出和 emoji 会乱码。 ```bash # PowerShell 设 UTF8(代码页 65001) ssh ... "powershell -Command \"[Console]::OutputEncoding = [Text.Encoding]::UTF8; chcp 65001; <你的命令>\"" # Python 加 -X utf8 ssh ... "C:\Python310\python.exe -X utf8 script.py" ``` ### 8.2 引号嵌套 → 用 stdin SSH → cmd → PowerShell 三层引号极易出错。复杂脚本用 stdin 喂: ```bash # powershell -Command - 从 stdin 读脚本 ssh -i ~/.ssh/id_ed25519 Administrator@49.232.102.198 "powershell -Command -" <<'PS1' # 这里写 PowerShell,引号无需转义 curl.exe -s -H "X-Bridge-Token: " http://127.0.0.1:8765/account PS1 ``` `<<'PS1'` 的单引号防止 Mac shell 展开变量,PowerShell 在 VPS 侧原样执行。 ### 8.3 中文路径 `C:\国金QMT交易端\` 等中文路径在 SSH 传输中可能因编码错乱。 - scp 目标用正斜杠:`C:/sanguo_qmt_bridge/`(ASCII) - 需引用中文路径时,在 PowerShell 内用变量拼接或 `Get-ChildItem` 搜索,不写字面量: ```powershell # 不写 "C:\国金QMT交易端\...",用搜索 $qmt = Get-ChildItem C:\ -Directory | Where-Object Name -like "*QMT*" ``` ### 8.4 curl vs Invoke-WebRequest PowerShell 里 `curl` 默认是 `Invoke-WebRequest` 的别名(参数语法不同)。要用的标准 curl: ```powershell curl.exe -s http://... # 显式 .exe 绕过别名 ``` cmd 里 `curl` 直接就是 `curl.exe`,无此问题。