Files
claude_dev 54f9ab4c4f 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启动).
2026-07-15 07:12:46 +08:00

128 lines
4.8 KiB
Python
Executable File
Raw Permalink 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.
#!/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/443bridge 只能通过 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(需 tokentoken 缺省则跳过,不报错)
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())