Files
gold_monitor/desktop/hub_client.py
T
2026-10-18 08:30:08 +08:00

160 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
"""与 gold_monitor 中控通信:激活、成交上报、K 线上报。"""
from __future__ import annotations
import json
import urllib.error
import urllib.request
from typing import Any, Optional
from device_store import load_device, save_device
class HubError(Exception):
pass
def _url(server: str, path: str) -> str:
base = (server or "").rstrip("/")
if not base:
raise HubError("未配置服务器地址")
if not path.startswith("/"):
path = "/" + path
return base + path
def _request(
method: str,
server: str,
path: str,
body: Optional[dict] = None,
token: str = "",
timeout: float = 15.0,
) -> dict[str, Any]:
data = None
headers = {"Accept": "application/json"}
if body is not None:
raw = json.dumps(body, ensure_ascii=False).encode("utf-8")
data = raw
headers["Content-Type"] = "application/json"
if token:
headers["X-Device-Token"] = token
req = urllib.request.Request(_url(server, path), data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
text = resp.read().decode("utf-8", errors="replace")
return json.loads(text) if text else {}
except urllib.error.HTTPError as exc:
try:
detail = exc.read().decode("utf-8", errors="replace")
payload = json.loads(detail) if detail else {}
raise HubError(payload.get("error") or detail or str(exc)) from exc
except HubError:
raise
except Exception:
raise HubError(f"HTTP {exc.code}") from exc
except urllib.error.URLError as exc:
raise HubError(f"无法连接服务器:{exc.reason}") from exc
def activate(server: str, code: str, fingerprint: str, machine_name: str) -> dict[str, Any]:
result = _request(
"POST",
server,
"/api/v1/activate",
{
"code": code.strip(),
"fingerprint": fingerprint,
"machine_name": machine_name,
},
)
if not result.get("ok"):
raise HubError(result.get("error") or "激活失败")
device = load_device()
device["server"] = server.rstrip("/")
device["token"] = result["token"]
device["account_id"] = result.get("account_id")
device["machine_id"] = result.get("machine_id")
device["fingerprint"] = fingerprint
device["machine_name"] = machine_name
save_device(device)
return result
def ping() -> bool:
d = load_device()
if not d.get("token") or not d.get("server"):
return False
try:
r = _request("GET", d["server"], "/api/v1/ping", token=d["token"], timeout=8)
return bool(r.get("ok"))
except HubError:
return False
def report_trade(payload: dict[str, Any]) -> Optional[int]:
d = load_device()
if not d.get("token") or not d.get("server"):
return None
try:
r = _request("POST", d["server"], "/api/v1/trades", body=payload, token=d["token"])
if r.get("ok"):
return int(r.get("id") or 0)
except HubError:
return None
return None
def report_bars(symbol: str, timeframe: str, bars: list[dict]) -> int:
d = load_device()
if not d.get("token") or not d.get("server") or not bars:
return 0
try:
r = _request(
"POST",
d["server"],
"/api/v1/bars",
body={"symbol": symbol, "timeframe": timeframe, "bars": bars},
token=d["token"],
timeout=30,
)
return int(r.get("count") or 0)
except HubError:
return 0
def open_review_url(next_path: str = "/records") -> str:
"""用本机设备 token 换免密复盘链接;旧服务端则退回 token 直链。"""
from urllib.parse import quote
d = load_device()
server = (d.get("server") or "").rstrip("/")
token = (d.get("token") or "").strip()
if not server or not token:
raise HubError("未激活:请先完成中控激活")
nxt = next_path if next_path.startswith("/") else "/records"
direct = f"{server}/auth/device-login?token={quote(token)}&next={quote(nxt)}"
try:
r = _request(
"POST",
server,
"/api/v1/web-sso",
body={"next": nxt},
token=token,
timeout=10,
)
if r.get("ok") and r.get("path"):
return server + str(r["path"])
if r.get("ok") and r.get("ticket"):
return (
f"{server}/auth/device-login?ticket={quote(str(r['ticket']))}"
f"&next={quote(nxt)}"
)
raise HubError(r.get("error") or "无法获取免密登录链接")
except HubError as exc:
msg = str(exc)
if "未绑定" in msg or "未激活" in msg:
raise
# 旧服务端无 /api/v1/web-sso 时走 token 直链
return direct