docs(deploy): VPS生产runbook+三机环境矩阵+NAS ops脚本+修rsync危险命令
vps-production-runbook(VPS运维一站式,已验证状态+拓扑+发布流水线+人类闸口+排障); env-version-matrix(三机Python/deps矩阵+Lock建议); scripts/ops(NAS bridge探针-容器无curl-+VPS→NAS备份脚本); nas-deploy-plan§3(修rsync --exclude语法,原会删万级staging parquet+破坏entrypoint启动).
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
# scripts/ops — NAS 运维脚本
|
||||
|
||||
NAS(测试 + 备份角色)的运维脚本。仅用标准库 / `set -euo pipefail` bash,
|
||||
不引入第三方依赖,不硬编码密钥。
|
||||
|
||||
| 脚本 | 用途 | 运行位置 |
|
||||
|------|------|----------|
|
||||
| `nas_bridge_probe.py` | QMT bridge 连通性 / 健康探针 | NAS 容器内(容器无 curl,只能 python) |
|
||||
| `nas_backup_vps.sh` | 生产 VPS → NAS 每日快照备份(模板) | NAS(ssh 免密拉取生产) |
|
||||
|
||||
---
|
||||
|
||||
## 1. nas_bridge_probe.py — bridge 健康探针
|
||||
|
||||
**为何存在**:容器镜像没装 curl/wget,bridge 连通性只能用 python urllib 探。
|
||||
走 Mac tunnel `http://192.168.2.101:8765`(华为光猫拦 NAS→VPS 公网 80/443)。
|
||||
|
||||
**接口**:
|
||||
- `GET /health`(免鉴权)→ `{"status":"ok","miniqmt_connected":true}`
|
||||
- `GET /account`(需 `X-Bridge-Token`)→ `{"ok":true,"cash":..,"total":..}`
|
||||
|
||||
**退出码**:`0`=健康(可达 + miniQMT 连接),`1`=不健康。
|
||||
|
||||
### 用法(容器内)
|
||||
|
||||
```bash
|
||||
# 进容器(Docker 不在 NAS PATH,须全路径)
|
||||
/var/packages/Docker/target/usr/bin/docker exec -it sanguo_vnpy_v2 \
|
||||
python /app/scripts/ops/nas_bridge_probe.py
|
||||
|
||||
# 额外探 /account(从环境读 token,绝不硬编码)
|
||||
/var/packages/Docker/target/usr/bin/docker exec -it -e BRIDGE_TOKEN=xxx \
|
||||
sanguo_vnpy_v2 python /app/scripts/ops/nas_bridge_probe.py
|
||||
|
||||
# 覆盖 bridge URL(换 tunnel 端点时)
|
||||
BRIDGE_URL=http://192.168.2.101:8765 BRIDGE_TOKEN=xxx \
|
||||
python /app/scripts/ops/nas_bridge_probe.py
|
||||
```
|
||||
|
||||
**环境变量**:
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `BRIDGE_URL` | `http://192.168.2.101:8765` | bridge 基址(Mac tunnel 端点) |
|
||||
| `BRIDGE_TOKEN` | — | 设了才探 `/account`;从环境读,绝不硬编码 |
|
||||
| `BRIDGE_TIMEOUT` | `8` | 单请求超时秒数 |
|
||||
|
||||
---
|
||||
|
||||
## 2. nas_backup_vps.sh — 生产→NAS 每日快照备份
|
||||
|
||||
**方向(铁律)**:生产 VPS → NAS 单向,**绝不反向写**(NAS 是测试/备份角色,
|
||||
回灌会污染生产库)。
|
||||
|
||||
**策略**:每日 `YYYYMMDD/` 快照 + `--link-dest` 硬链接去重(7 份近乎 1 份体积)
|
||||
+ 保留最近 N 份自动清旧。**模板**:源路径随生产拓扑填。
|
||||
|
||||
### 用法
|
||||
|
||||
```bash
|
||||
# 生产拓扑就绪后(占位示例)
|
||||
./nas_backup_vps.sh \
|
||||
--host sanguo-vps \
|
||||
--paths /var/lib/sanguo/data,/opt/sanguo/config \
|
||||
--dest /volume1/stock/backup/sanguo_vps \
|
||||
--keep 7
|
||||
|
||||
# 先 dry-run 看会做什么(不传输/不删除)
|
||||
./nas_backup_vps.sh --host sanguo-vps --paths /a,/b --dest /volume1/.../x --dry-run
|
||||
```
|
||||
|
||||
**参数**:
|
||||
| 参数 | 必填 | 默认 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--host` | 是 | — | 生产源 ssh 别名(`~/.ssh/config`) |
|
||||
| `--paths` | 是 | — | 生产侧目录列表(逗号分隔,绝对路径) |
|
||||
| `--dest` | 是 | — | NAS 备份根目录(快照建在其下 `YYYYMMDD/`) |
|
||||
| `--keep` | 否 | `7` | 保留最近 N 份快照 |
|
||||
| `--dry-run` | 否 | — | 只打印,不传输/不删除 |
|
||||
|
||||
**前置**:生产 host 别名加进 NAS 的 `~/.ssh/config` + key 免密。
|
||||
别名不存在时 preflight 会清晰报错并给 `~/.ssh/config` 占位片段。
|
||||
|
||||
---
|
||||
|
||||
## 3. dev→test→prod 流水线命令清单
|
||||
|
||||
### dev(Mac Mini 开发目录)
|
||||
|
||||
```bash
|
||||
SRC=~/.openclaw/sanguo_projects/sanguo_vnpy_v2/
|
||||
DEST=sanguo-nas:/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2/
|
||||
DOCKER="/var/packages/Docker/target/usr/bin/docker"
|
||||
|
||||
# (1) 同步代码到 NAS(test/staging)—— 注意每个 pattern 单独 --exclude
|
||||
# ⚠️ 务必排除 data_cache/(见下方"已知坑"),否则 --delete 会删 NAS 上
|
||||
# 万级 staging parquet
|
||||
rsync -avz --delete \
|
||||
--exclude='.git' --exclude='__pycache__' --exclude='*.pyc' --exclude='.pytest_cache' \
|
||||
--exclude='data/' --exclude='data_cache/' --exclude='logs/' --exclude='temp/' \
|
||||
--exclude='*.log' --exclude='.DS_Store' \
|
||||
--exclude='.venv/' --exclude='venv/' --exclude='venv311/' \
|
||||
--exclude='node_modules/' --exclude='config/' --exclude='.superpowers/' \
|
||||
--exclude='entrypoint.sh' \
|
||||
-e ssh "$SRC" "$DEST"
|
||||
|
||||
# (2) 重启容器(bind-mount,改代码即生效,重启重载进程)
|
||||
ssh sanguo-nas "$DOCKER restart sanguo_vnpy_v2"
|
||||
|
||||
# (3) 冒烟:看日志 + 探健康
|
||||
sleep 8
|
||||
ssh sanguo-nas "$DOCKER logs --tail 30 sanguo_vnpy_v2"
|
||||
```
|
||||
|
||||
### test(NAS 容器内)
|
||||
|
||||
```bash
|
||||
# bridge 连通性(容器无 curl,用 python 探针)
|
||||
/var/packages/Docker/target/usr/bin/docker exec -it sanguo_vnpy_v2 \
|
||||
python /app/scripts/ops/nas_bridge_probe.py
|
||||
```
|
||||
|
||||
### prod(生产 VPS,部署时按 `docs/deployment/nas-deploy-plan.md` 第五节,
|
||||
不要动 frpc/socat/Caddy 外网链路)
|
||||
|
||||
> 生产方向数据备份:`nas_backup_vps.sh` 从 VPS 拉 → NAS,单向。
|
||||
|
||||
---
|
||||
|
||||
## 已知坑
|
||||
|
||||
1. **rsync `--exclude` 语法**:`--exclude='a' 'b' 'c'`(一个 `--exclude` 后跟多个
|
||||
bare pattern)是**错的**——rsync 会把 `b`/`c` 当源路径(报 `lstat` 错,排除不生效)。
|
||||
正确写法:每个 pattern 单独一个 `--exclude=PATTERN`。`nas-deploy-plan.md` 第三节的
|
||||
命令需修。
|
||||
|
||||
2. **`data_cache/` 必须排除**:dev↔NAS 同步是**代码**迭代,`data_cache/` 是本机下载缓存
|
||||
(dev 59k 文件 / NAS 69k 文件,差异万级 staging parquet)。带 `--delete` 不排除会删 NAS
|
||||
上大量 staging 文件。deploy-plan 排除列表漏了它,已在上文命令补 `--exclude='data_cache/'`。
|
||||
|
||||
3. **`venv/`(无点)必须排除**:dev 机有 `venv/`(Python 3.14)和 `venv311/`(Python 3.11),
|
||||
deploy-plan 只排了 `.venv/`(带点)。不排会把 509 个 venv 文件(含 182MB 的
|
||||
`_polars_runtime.abi3.so`)推到 NAS 容器(容器是 Python 3.10,这堆二进制毫无用处)。
|
||||
已在上文命令补 `--exclude='venv/' --exclude='venv311/'`。
|
||||
|
||||
4. **`entrypoint.sh` 位置不一致(会炸容器,已确认)**:容器 `Entrypoint=[/app/entrypoint.sh]`
|
||||
(`docker inspect` 实证),但 dev 侧根目录已无 `entrypoint.sh`(移到 `docker/entrypoint.sh`)。
|
||||
直接跑带 `--delete` 的 rsync 会删 NAS 的 `/app/entrypoint.sh` → 下次 `docker restart`
|
||||
启动失败(entrypoint not found)。**已在上文命令加 `--exclude='entrypoint.sh'` 保护**。
|
||||
根治方案(二选一,部署时定):① dev 根目录恢复 `entrypoint.sh`(从 `docker/` 软链或拷贝);
|
||||
② 更新容器 entrypoint 指向 `/app/docker/entrypoint.sh`(需 `docker rm` + 重 `run`)。
|
||||
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
# nas_backup_vps.sh — 从生产 VPS 拉取状态到 NAS 做每日快照备份(模板)。
|
||||
#
|
||||
# 方向(铁律):生产 VPS → NAS 单向;**绝不反向写**(NAS 是测试/备份角色,
|
||||
# 不向生产回灌数据,防止测试库污染生产库)。
|
||||
#
|
||||
# 这是**模板**:源侧(VPS)的具体路径随生产拓扑填(--paths),host 别名随
|
||||
# ~/.ssh/config 配。脚本不硬编码任何 host / 路径 / 密钥。
|
||||
#
|
||||
# 策略:
|
||||
# - rsync over ssh,幂等(同一快照目录重复跑结果一致)
|
||||
# - 每日快照子目录 $DEST/YYYYMMDD/,用 --link-dest 指向上一份做硬链接去重
|
||||
# (未变文件零额外占盘,7 份快照近乎 1 份体积)
|
||||
# - 保留最近 N 份(--keep,默认 7),旧的自动删
|
||||
# - --dry-run 只看不动;preflight 检查源 host 可达
|
||||
#
|
||||
# 用法:
|
||||
# nas_backup_vps.sh \
|
||||
# --host sanguo-vps \
|
||||
# --paths /var/lib/sanguo/data,/opt/sanguo/config \
|
||||
# --dest /volume1/stock/backup/sanguo_vps \
|
||||
# --keep 7
|
||||
# nas_backup_vps.sh ... --dry-run # 只打印会做什么
|
||||
#
|
||||
# 前置(生产拓扑就绪后做一次):
|
||||
# 1) 在 NAS 的 ~/.ssh/config 加生产 host 别名 + key(免密)
|
||||
# (现在别名可能还没有——preflight 会清晰报错并给出提示)
|
||||
# 2) 确保 NAS 上 $DEST 根目录已创建并可写
|
||||
set -euo pipefail
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 参数解析
|
||||
# ----------------------------------------------------------------------------
|
||||
HOST=""
|
||||
PATHS=""
|
||||
DEST=""
|
||||
KEEP=7
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法: nas_backup_vps.sh --host <ssh-alias> --paths <a,b,c> --dest <dir> [--keep N] [--dry-run]
|
||||
|
||||
必填:
|
||||
--host <alias> 生产源 host 的 ssh 别名(~/.ssh/config)
|
||||
--paths <a,b,...> 生产侧要备份的目录列表(逗号分隔,绝对路径)
|
||||
--dest <dir> NAS 上备份根目录(快照建在其下 YYYYMMDD/)
|
||||
可选:
|
||||
--keep N 保留最近 N 份快照(默认 7)
|
||||
--dry-run 只打印会做什么,不实际传输/删除
|
||||
-h, --help 显示本帮助
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--host) HOST="$2"; shift 2 ;;
|
||||
--paths) PATHS="$2"; shift 2 ;;
|
||||
--dest) DEST="$2"; shift 2 ;;
|
||||
--keep) KEEP="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "未知参数: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 必填校验
|
||||
MISSING=""
|
||||
[ -z "$HOST" ] && MISSING="$MISSING --host"
|
||||
[ -z "$PATHS" ] && MISSING="$MISSING --paths"
|
||||
[ -z "$DEST" ] && MISSING="$MISSING --dest"
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "[FAIL] 缺少必填参数:$MISSING" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# KEEP 数值校验
|
||||
if ! [[ "$KEEP" =~ ^[0-9]+$ ]] || [ "$KEEP" -lt 1 ]; then
|
||||
echo "[FAIL] --keep 必须是 >=1 的整数,得到: $KEEP" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
RSYNC_FLAGS=(-a --delete --info=stats1)
|
||||
LINK_DEST_OPT=""
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
RSYNC_FLAGS+=(-n)
|
||||
echo "[MODE] DRY-RUN(不实际传输/删除)"
|
||||
fi
|
||||
|
||||
TODAY="$(date +%Y%m%d)"
|
||||
SNAP="$DEST/$TODAY"
|
||||
PREV="$(ls -1 "$DEST" 2>/dev/null | grep -E '^[0-9]{8}$' | sort | tail -1 || true)"
|
||||
|
||||
echo "=== VPS→NAS 备份 ==="
|
||||
echo " host = $HOST"
|
||||
echo " paths = $PATHS"
|
||||
echo " dest = $DEST"
|
||||
echo " snap = $SNAP"
|
||||
echo " keep = $KEEP"
|
||||
echo " prev = ${PREV:-(无,首份快照)}"
|
||||
echo " dryrun = $DRY_RUN"
|
||||
echo
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Preflight:源 host 可达性
|
||||
# host 别名可能还不存在(生产拓扑未就绪)——给清晰提示而非晦涩的 ssh 报错。
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[preflight] 检查源 host 可达: $HOST"
|
||||
if ! ssh -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new \
|
||||
"$HOST" 'echo OK' >/dev/null 2>&1; then
|
||||
cat >&2 <<EOF
|
||||
[FAIL] 源 host 不可达: $HOST
|
||||
|
||||
排查:
|
||||
1) ~/.ssh/config 是否有该 host 别名?若无,加一段(占位示例):
|
||||
Host $HOST
|
||||
HostName <生产 VPS IP 或 域名>
|
||||
User <ssh 用户>
|
||||
IdentityFile ~/.ssh/<生产 key>
|
||||
2) key 是否免密?本脚本用 BatchMode(绝不交互输密码),须 key 认证。
|
||||
3) 网络/防火墙是否放通 ssh 端口。
|
||||
4) 先手动跑一次确认: ssh $HOST echo OK
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
echo "[preflight] OK: $HOST 可达"
|
||||
echo
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 备份根目录
|
||||
# ----------------------------------------------------------------------------
|
||||
mkdir -p "$SNAP"
|
||||
|
||||
# --link-dest 指向上份快照(硬链接未变文件,省盘)。dry-run 下也安全。
|
||||
if [ -n "$PREV" ] && [ -d "$DEST/$PREV" ]; then
|
||||
LINK_DEST_OPT="--link-dest=$DEST/$PREV"
|
||||
echo "[link-dest] 硬链接基准: $DEST/$PREV"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 逐路径 rsync 拉取(生产 → NAS 快照)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 把逗号分隔的 paths 转成数组(去空段)
|
||||
IFS=',' read -ra PATH_ARR <<< "$PATHS"
|
||||
for p in "${PATH_ARR[@]}"; do
|
||||
p="${p%/}" # 去尾斜杠,保证语义一致
|
||||
[ -z "$p" ] && continue
|
||||
name="$(basename "$p")"
|
||||
target="$SNAP/$name"
|
||||
mkdir -p "$target"
|
||||
echo "[rsync] $HOST:$p/ → $target/"
|
||||
# shellcheck disable=SC2086 # LINK_DEST_OPT 需要按词拆分
|
||||
rsync "${RSYNC_FLAGS[@]}" $LINK_DEST_OPT \
|
||||
"$HOST:$p/" "$target/" \
|
||||
|| { echo "[FAIL] rsync 失败: $HOST:$p" >&2; exit 1; }
|
||||
echo "[rsync] done: $name"
|
||||
echo
|
||||
done
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 保留策略:留最近 N 份,旧的删
|
||||
# 只识别 YYYYMMDD 命名的子目录,绝不碰其他名字(防止误删 dest 下非快照内容)。
|
||||
# ----------------------------------------------------------------------------
|
||||
echo "[retention] 保留最近 $KEEP 份快照"
|
||||
mapfile -t SNAPSHOTS < <(ls -1 "$DEST" 2>/dev/null | grep -E '^[0-9]{8}$' | sort -r || true)
|
||||
DELETED=0
|
||||
idx=0
|
||||
for s in "${SNAPSHOTS[@]}"; do
|
||||
idx=$((idx + 1))
|
||||
if [ "$idx" -le "$KEEP" ]; then
|
||||
echo " [keep] $s"
|
||||
else
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo " [would-delete] $s"
|
||||
else
|
||||
rm -rf "${DEST:?}/$s" && echo " [deleted] $s"
|
||||
fi
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
done
|
||||
echo "[retention] 本次删除/拟删: $DELETED"
|
||||
|
||||
echo
|
||||
echo "=== 备份完成 ==="
|
||||
echo " 快照: $SNAP"
|
||||
[ "$DRY_RUN" -eq 1 ] && echo " (dry-run,未实际写入/删除)"
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NAS 容器内 QMT bridge 健康探针(纯标准库 urllib)。
|
||||
|
||||
为何用 python 不用 curl:
|
||||
sanguo_vnpy_v2 容器镜像(Python 3.10)**没有装 curl/wget**,NAS 上做
|
||||
bridge 连通性探测只能用 python urllib(标准库自带,零第三方依赖)。
|
||||
|
||||
为何走 Mac tunnel(默认 http://192.168.2.101:8765)不走 VPS 公网:
|
||||
华为光猫拦截 NAS→VPS 公网 80/443,bridge 只能通过 Mac Mini tunnel
|
||||
反向暴露给 NAS。默认探 http://192.168.2.101:8765/health。
|
||||
|
||||
接口契约(见 sanguo_qmt_bridge/README.md):
|
||||
GET /health 免鉴权 → {"status":"ok","miniqmt_connected": true|false}
|
||||
GET /account 需 header X-Bridge-Token →
|
||||
{"ok":true,"cash":..,"frozen":..,"market_value":..,"total":..}
|
||||
|
||||
退出码:0=健康(bridge 可达 且 status=ok 且 miniqmt_connected=true)
|
||||
1=不健康(bridge 不可达 / status 异常 / miniQMT 断开)
|
||||
|
||||
容错风格:网络/解析失败不抛异常,记 warning 后视情况降级(与
|
||||
sanguo_trader.bridge_client 一致——探针失败不应崩溃,只报状态退出)。
|
||||
|
||||
用法(容器内执行):
|
||||
python /app/scripts/ops/nas_bridge_probe.py
|
||||
BRIDGE_TOKEN=xxx python /app/scripts/ops/nas_bridge_probe.py # 额外探 /account
|
||||
BRIDGE_URL=http://192.168.2.101:8765 python .../nas_bridge_probe.py # 覆盖 URL
|
||||
|
||||
环境变量:
|
||||
BRIDGE_URL bridge 基址(默认 http://192.168.2.101:8765)
|
||||
BRIDGE_TOKEN 鉴权 token;设了才探 /account(绝不硬编码,从环境读)
|
||||
BRIDGE_TIMEOUT 单请求超时秒数(默认 8)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_BRIDGE_URL = "http://192.168.2.101:8765"
|
||||
DEFAULT_TIMEOUT = 8
|
||||
|
||||
|
||||
def _get_json(url: str, token: str | None, timeout: float) -> tuple[dict | None, str | None]:
|
||||
"""GET JSON;失败返回 (None, 错误描述),绝不抛异常。
|
||||
|
||||
与 bridge_client._get 同风格:URLError/Timeout/JSON/OSError 统一吞掉记 warning。
|
||||
"""
|
||||
headers: dict[str, str] = {"Accept": "application/json"}
|
||||
if token:
|
||||
headers["X-Bridge-Token"] = token
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
return json.loads(body), None
|
||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as e:
|
||||
return None, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def _fmt_money(val: Any) -> str:
|
||||
"""资金字段友好打印;非数字原样 str。"""
|
||||
try:
|
||||
return f"{float(val):,.2f}"
|
||||
except (TypeError, ValueError):
|
||||
return str(val)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
base = os.environ.get("BRIDGE_URL", DEFAULT_BRIDGE_URL).rstrip("/")
|
||||
token = os.environ.get("BRIDGE_TOKEN") or None
|
||||
timeout = float(os.environ.get("BRIDGE_TIMEOUT", str(DEFAULT_TIMEOUT)))
|
||||
|
||||
print(f"bridge_url = {base}")
|
||||
print(f"timeout = {timeout}s")
|
||||
print(f"token = {'(set, will probe /account)' if token else '(not set, skip /account)'}")
|
||||
print("-" * 48)
|
||||
|
||||
# 1. /health(免鉴权)
|
||||
health, err = _get_json(f"{base}/health", token=None, timeout=timeout)
|
||||
if health is None:
|
||||
print(f"[DOWN] bridge 不可达:{err}")
|
||||
print(" 排查:Mac tunnel 是否在线 / 192.168.2.101:8765 是否监听 / 光猫拦截")
|
||||
return 1
|
||||
|
||||
status = str(health.get("status", ""))
|
||||
mini_connected = bool(health.get("miniqmt_connected", False))
|
||||
print(f"[/health] status={status or '?'} miniqmt_connected={mini_connected}")
|
||||
|
||||
if status != "ok":
|
||||
print(f"[WARN] bridge 可达但 status 异常: {status!r}(可能是降级态)")
|
||||
return 1
|
||||
|
||||
if not mini_connected:
|
||||
print("[DOWN] bridge UP 但 miniQMT 未连接(disconnected)——无法下单/查账户")
|
||||
return 1
|
||||
|
||||
print("[UP] bridge 健康,miniQMT 已连接")
|
||||
|
||||
# 2. /account(需 token;token 缺省则跳过,不报错)
|
||||
if not token:
|
||||
print("[SKIP] /account(未设 BRIDGE_TOKEN)")
|
||||
return 0
|
||||
|
||||
acct, err = _get_json(f"{base}/account", token=token, timeout=timeout)
|
||||
if acct is None:
|
||||
print(f"[WARN] /account 探测失败:{err}(bridge 本身健康,鉴权/查询侧异常)")
|
||||
# bridge 已确认健康,账户查询失败不改变整体健康判定
|
||||
return 0
|
||||
if not acct.get("ok"):
|
||||
print(f"[/account] ok=false error={acct.get('error', '?')}")
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"[/account] ok=true"
|
||||
f" cash={_fmt_money(acct.get('cash'))}"
|
||||
f" frozen={_fmt_money(acct.get('frozen'))}"
|
||||
f" market_value={_fmt_money(acct.get('market_value'))}"
|
||||
f" total={_fmt_money(acct.get('total'))}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user