diff --git a/hub_auth.py b/hub_auth.py index 2837b59..bb928d6 100644 --- a/hub_auth.py +++ b/hub_auth.py @@ -1,11 +1,24 @@ -"""中控调用实例 API 时的鉴权辅助(各 crypto_monitor_* 的 login_required 共用)。""" - +"""中控调用实例 API 时的鉴权;实例浏览器 SSO(复用 HUB_BRIDGE_TOKEN)。""" from __future__ import annotations +import base64 +import hashlib +import hmac +import json import os +import secrets +import threading +import time +from typing import Any from flask import request +# 中控打开实例链接有效期(秒),默认 2 小时 +HUB_SSO_TTL_SEC = int(os.getenv("HUB_SSO_TTL_SEC", "7200")) + +_used_nonces: dict[str, float] = {} +_nonce_lock = threading.Lock() + def hub_bridge_token() -> str: return (os.getenv("HUB_BRIDGE_TOKEN") or "").strip() @@ -18,3 +31,94 @@ def request_allowed(session_logged_in: bool, auth_disabled: bool) -> bool: if tok and request.headers.get("X-Hub-Token") == tok: return True return False + + +def safe_next_path(raw: str | None) -> str: + """仅允许站内相对路径,防止开放重定向。""" + p = (raw or "/").strip() + if not p.startswith("/") or p.startswith("//"): + return "/" + if "://" in p: + return "/" + return p + + +def _sso_secret() -> str: + return hub_bridge_token() + + +def _b64url_encode(data: bytes) -> str: + return base64.urlsafe_b64encode(data).decode().rstrip("=") + + +def _b64url_decode(data: str) -> bytes: + pad = "=" * (-len(data) % 4) + return base64.urlsafe_b64decode(data + pad) + + +def _prune_used_nonces() -> None: + now = time.time() + with _nonce_lock: + dead = [k for k, exp in _used_nonces.items() if exp <= now] + for k in dead: + del _used_nonces[k] + + +def mint_hub_sso_token(exchange_key: str, next_path: str = "/") -> str | None: + """签发实例浏览器 SSO token(exchange_key 与 hub_bridge install_on_app 的 exchange 一致)。""" + secret = _sso_secret() + ex = (exchange_key or "").strip().lower() + if not secret or not ex: + return None + payload = { + "ex": ex, + "exp": int(time.time()) + max(60, HUB_SSO_TTL_SEC), + "nonce": secrets.token_urlsafe(16), + "next": safe_next_path(next_path), + } + body = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode()) + sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest() + return f"{body}.{sig}" + + +def verify_hub_sso_token( + token: str | None, expected_exchange: str +) -> tuple[bool, str, str | None]: + """ + 校验 SSO token。成功返回 (True, next_path, None);失败返回 (False, '/', 原因)。 + 单次使用:nonce 用过后在 exp 之前不可复用。 + """ + secret = _sso_secret() + expected = (expected_exchange or "").strip().lower() + if not secret or not expected: + return False, "/", "未配置 HUB_BRIDGE_TOKEN" + raw = (token or "").strip() + if "." not in raw: + return False, "/", "token 无效" + body, sig = raw.rsplit(".", 1) + try: + expect_sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest() + if not hmac.compare_digest(expect_sig, sig): + return False, "/", "签名校验失败" + payload = json.loads(_b64url_decode(body).decode()) + except Exception: + return False, "/", "token 解析失败" + if not isinstance(payload, dict): + return False, "/", "payload 无效" + if str(payload.get("ex") or "").lower() != expected: + return False, "/", "实例不匹配" + try: + exp = int(payload.get("exp") or 0) + except (TypeError, ValueError): + return False, "/", "exp 无效" + if exp < int(time.time()): + return False, "/", "链接已过期" + nonce = str(payload.get("nonce") or "") + if not nonce: + return False, "/", "nonce 缺失" + _prune_used_nonces() + with _nonce_lock: + if nonce in _used_nonces: + return False, "/", "链接已使用" + _used_nonces[nonce] = float(exp) + return True, safe_next_path(str(payload.get("next") or "/")), None diff --git a/hub_bridge.py b/hub_bridge.py index 9bdaf07..2dbc37a 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -9,9 +9,9 @@ import json import time from functools import wraps -from flask import current_app, get_flashed_messages, jsonify, request, session +from flask import current_app, get_flashed_messages, jsonify, redirect, request, session -from hub_auth import request_allowed +from hub_auth import request_allowed, safe_next_path, verify_hub_sso_token def _hub_auth_required(f): @@ -244,6 +244,22 @@ def register_hub_routes(app): return jsonify({"ok": False, "msg": "预览不存在或已过期"}), 404 return jsonify({"ok": True, "preview": preview}) + @app.route("/hub-sso") + def hub_sso_login(): + """中控签发的临时链接:写入 session 后跳转,直链访问仍走 /login。""" + auth_disabled = bool(current_app.config.get("HUB_AUTH_DISABLED")) + next_arg = request.args.get("next") + if auth_disabled: + session["logged_in"] = True + return redirect(safe_next_path(next_arg)) + ex = str((_ctx().get("exchange") or "")).strip().lower() + token = (request.args.get("token") or "").strip() + ok, next_path, _err = verify_hub_sso_token(token, ex) + if ok: + session["logged_in"] = True + return redirect(next_path) + return redirect("/login") + def _latest_preview_id(): get_db = _ctx().get("get_db") diff --git a/manual_trading_hub/.env.example b/manual_trading_hub/.env.example index 2bb46d9..68835c2 100644 --- a/manual_trading_hub/.env.example +++ b/manual_trading_hub/.env.example @@ -11,7 +11,9 @@ HUB_PORT=5100 # 与四实例 .env 中 HUB_BRIDGE_TOKEN 相同的长随机串 # 中控 → 各 Flask:请求头 X-Hub-Token # 中控 → 各子代理:请求头 X-Control-Token(可与子代理 CONTROL_TOKEN 同值,hub 会用 HUB_BRIDGE_TOKEN 转发) +# 中控「打开实例」SSO 链接也复用此令牌签名(默认 2 小时内有效、单次使用) # HUB_BRIDGE_TOKEN=your-long-random-token +# HUB_SSO_TTL_SEC=7200 # 逗号分隔的账户 id,强制关闭(不参与监控/全局全平;设置页对应行勾选框灰掉) # 留空 = 不强制关闭;仅不想用 OKX 时可设 HUB_DISABLED_IDS=1 @@ -30,12 +32,16 @@ HUB_TRUST_LAN=true # 登录保持天数(默认 7) # HUB_SESSION_DAYS=7 -# 浏览器打开的复盘/实例链接:把 127.0.0.1 换成 Ubuntu 内网 IP 或域名(中控本机调 API 仍用 127.0.0.1) -# 例:用手机/另一台电脑访问中控时必填,否则「交易复盘」会指向你自己电脑的 localhost +# 浏览器打开的实例/复盘链接(hub_settings 里 flask_url 为 127.0.0.1 时替换为对外地址) +# 局域网:填内网 IP,见《局域网与反代部署说明.md》 # HUB_PUBLIC_ORIGIN=http://192.168.1.100 -# 或只写主机名:HUB_PUBLIC_HOST=192.168.1.100 +# 反代:各实例 flask_url 建议直接写 https 域名,可不设此项 +# HUB_PUBLIC_HOST=192.168.1.100 # HUB_PUBLIC_SCHEME=http +# 四实例网页登录(直链反代/IP:端口 访问时输入;中控点「打开实例」免输) +# 各 crypto_monitor_*/.env 统一:APP_USERNAME=... APP_PASSWORD=... + # 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10) # HUB_AGENT_TIMEOUT=8 # HUB_FLASK_TIMEOUT=10 diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 619e06a..9805fbf 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -35,7 +35,9 @@ from hub_web_auth import ( expected_username, verify_credentials, ) +from hub_auth import HUB_SSO_TTL_SEC, mint_hub_sso_token, safe_next_path from url_public import browser_url, default_review_url, public_origin +from urllib.parse import urlencode try: from exchange_orders import symbols_match as _symbols_match @@ -52,7 +54,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") _trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower() HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off") DIR = Path(__file__).resolve().parent -HUB_BUILD = "20260525-okx-tpsl2" +HUB_BUILD = "20260525-hub-sso" HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8")) HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10")) _board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower() @@ -523,6 +525,40 @@ async def api_monitor_board(): } +def _require_hub_logged_in(request: Request) -> None: + if password_required() and not validate_session_token(request.cookies.get(SESSION_COOKIE)): + raise HTTPException(status_code=401, detail="未登录中控") + + +@app.get("/api/instance/open-url") +def api_instance_open_url(request: Request, exchange_id: str, next: str = "/"): + """已登录中控时生成实例 SSO 打开链接(2h 有效、单次使用,复用 HUB_BRIDGE_TOKEN)。""" + _require_hub_logged_in(request) + if not HUB_BRIDGE_TOKEN: + raise HTTPException(status_code=503, detail="未配置 HUB_BRIDGE_TOKEN,无法签发实例打开链接") + ex = _find_exchange(exchange_id) + if not ex: + raise HTTPException(status_code=404, detail="未知交易所 id") + base = browser_url((ex.get("flask_url") or "").strip()).rstrip("/") + if not base: + raise HTTPException(status_code=400, detail="该账户未配置 flask_url") + ex_key = (ex.get("key") or "").strip().lower() + if not ex_key: + raise HTTPException(status_code=400, detail="该账户缺少 key(用于 SSO 校验)") + nxt = safe_next_path(next) + token = mint_hub_sso_token(ex_key, nxt) + if not token: + raise HTTPException(status_code=503, detail="签发 SSO 失败") + q = urlencode({"token": token, "next": nxt}) + return { + "ok": True, + "url": f"{base}/hub-sso?{q}", + "expires_in": HUB_SSO_TTL_SEC, + "exchange_id": exchange_id, + "exchange_key": ex_key, + } + + class CloseAllBody(BaseModel): exclude_ids: list[str] = Field(default_factory=list) diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 2dcd5f7..7d495b8 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -17,6 +17,22 @@ return r; } + async function openInstanceInBrowser(exchangeId, nextPath) { + const next = nextPath || "/"; + try { + const q = new URLSearchParams({ exchange_id: String(exchangeId), next }); + const r = await apiFetch("/api/instance/open-url?" + q.toString()); + const j = await r.json(); + if (j.ok && j.url) { + window.open(j.url, "_blank", "noopener"); + return; + } + showToast(j.detail || "无法生成打开链接", true); + } catch (e) { + showToast(String(e), true); + } + } + async function initAuth() { try { const r = await fetch("/api/auth/status"); @@ -312,6 +328,13 @@ } function bindMonitorInteractions(box) { + box.querySelectorAll(".btn-open-instance").forEach((btn) => { + btn.onclick = (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + openInstanceInBrowser(btn.dataset.exId, btn.dataset.next || "/"); + }; + }); box.querySelectorAll(".btn-close-ex").forEach((btn) => { btn.onclick = () => closeOne(btn.dataset.id); }); @@ -703,7 +726,6 @@ kmap[k.id] = k; }); const flaskOpen = row.flask_url_browser || row.flask_url; - const strategyUrl = flaskOpen ? esc(flaskOpen.replace(/\/$/, "") + "/strategy") : ""; let html = `

${esc(row.name)}

@@ -711,8 +733,8 @@
- ${flaskOpen ? `打开实例` : ""} - ${strategyUrl ? `策略交易` : ""} + ${flaskOpen ? `打开实例` : ""} + ${flaskOpen ? `策略交易` : ""}
`; @@ -937,12 +959,12 @@ const online = row.http_ok && agOk; const cardCls = online ? "card-online" : "card-offline"; const dotCls = online ? "ok" : "bad"; - const review = row.review_url - ? `复盘` - : ""; const flaskOpen = row.flask_url_browser || row.flask_url; const openFlask = flaskOpen - ? `实例` + ? `实例` + : ""; + const openReview = flaskOpen + ? `复盘` : ""; return `
@@ -955,7 +977,7 @@
${openFlask} - ${review} + ${openReview}
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 0966276..575f39e 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -109,6 +109,6 @@
- + diff --git a/manual_trading_hub/使用说明.md b/manual_trading_hub/使用说明.md index 7b08a5b..3629463 100644 --- a/manual_trading_hub/使用说明.md +++ b/manual_trading_hub/使用说明.md @@ -186,7 +186,7 @@ curl -s http://127.0.0.1:5100/api/ping | **机器人单** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active),为本地监控计划,**不等于**交易所条件单 | | **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) | | **趋势计划** | 仅当该户勾选 **监控趋势计划** 时展示 `trend_pullback_plans`(active) | -| **实例 / 复盘** | 「实例」→ 该户 Flask(**实盘下单、关键位、策略交易 `/strategy`、复盘**);「复盘」→ `/records`。若配置 **`HUB_PUBLIC_ORIGIN`**,外链替换 `127.0.0.1` | +| **实例 / 复盘** | 「实例」「策略交易」「复盘」经中控签发 **SSO 链接**(默认 2h、单次)打开,**免输**实例 `APP_USERNAME/PASSWORD`;直链实例 IP/域名仍走 `/login`。局域网与反代配置见 **[局域网与反代部署说明.md](./局域网与反代部署说明.md)** | | **关键位列表** | 来自 `/api/hub/monitor` + `/api/price_snapshot`;Flask 未连通时卡片提示原因;**Gate 趋势户**无关键位块 | | **该户全平** | `POST` 子代理 `/emergency/close-all`,仅平该 API Key 仓位 | | **全局紧急全平** | 对所有已启用户依次全平(不含 `HUB_DISABLED_IDS` 强制关闭的 id) | diff --git a/manual_trading_hub/局域网与反代部署说明.md b/manual_trading_hub/局域网与反代部署说明.md new file mode 100644 index 0000000..41ce1e6 --- /dev/null +++ b/manual_trading_hub/局域网与反代部署说明.md @@ -0,0 +1,226 @@ +# 中控 · 局域网与反代部署说明 + +本文说明在 **局域网(IP + 端口)** 与 **宝塔/Nginx 反代(域名)** 两种场景下,如何配置中控与各实例,并实现: + +- **从中控** 点「实例 / 策略交易 / 复盘」→ **免输入** 实例网页密码(SSO 临时链接,默认 **2 小时** 内有效、**单次使用**) +- **浏览器直链** 实例地址(反代域名或 `http://IP:端口`)→ 进入 **`/login`**,输入统一 **`APP_USERNAME` / `APP_PASSWORD`** + +SSO 签名复用 **`HUB_BRIDGE_TOKEN`**(与中控调实例 API 相同,四所 `.env` 与 `manual_trading_hub/.env` 保持一致)。 + +--- + +## 一、两种访问方式对照 + +| 项目 | 局域网 | 反代(域名) | +|------|--------|----------------| +| 中控地址 | `http://内网IP:5100` | `https://hub.你的域名.com` | +| 实例地址(浏览器) | `http://内网IP:5004` 等 | `https://okx.你的域名.com` 等 | +| `hub_settings` 里 `flask_url` | 建议写 **`http://内网IP:端口`** | 建议写 **`https://该实例域名`**(与浏览器一致) | +| 中控本机调实例 API | 可与浏览器相同;同机也可用 `http://127.0.0.1:端口` + `HUB_PUBLIC_ORIGIN` | 同机可用 `127.0.0.1:端口` 或域名(需 Nginx 转发 `X-Hub-Token`) | +| `HUB_PUBLIC_ORIGIN` | 若 `flask_url` 填 `127.0.0.1`,**必填** `http://内网IP` | 若 `flask_url` 已是完整域名,**可不设** | +| 宝塔 | 可不装反代,直连端口 | 每实例一个站点 + SSL;中控单独站点 | +| 直链登录 | 实例 `/login` | 实例 `/login` | +| 从中控打开 | `/hub-sso?token=...` 自动登录 | 同上 | + +--- + +## 二、共用环境变量(必配) + +### 2.1 中控 `manual_trading_hub/.env` + +```bash +HUB_BRIDGE_TOKEN=请填一长串随机字符 +HUB_USERNAME=admin # 中控登录(建议设置) +HUB_PASSWORD=你的中控密码 +HUB_SSO_TTL_SEC=7200 # 可选,默认 7200 = 2 小时 +``` + +### 2.2 四个实例 `crypto_monitor_*/.env` + +每个目录相同(**直链**时用这套登录实例网页): + +```bash +HUB_BRIDGE_TOKEN=与中控完全相同 +APP_USERNAME=统一用户名 +APP_PASSWORD=统一密码 +# 云上切勿 APP_AUTH_DISABLED=true +``` + +### 2.3 子代理 + +`CONTROL_TOKEN` 可与 `HUB_BRIDGE_TOKEN` 相同;子代理只监听 `127.0.0.1`,**不要**对公网暴露 `15200`~`15203`。 + +--- + +## 三、局域网部署(IP + 端口) + +适用:家里/办公室内网,例如服务器 `192.168.8.6`。 + +### 3.1 端口约定(示例,以你实际为准) + +| 服务 | 端口 | +|------|------| +| 中控 hub | 5100 | +| OKX Flask | 5004 | +| 币安 Flask | 5001 | +| Gate 训练 | 5000 | +| Gate 趋势 | 5002 | +| agent | 15200~15203(仅本机) | + +### 3.2 系统设置 `hub_settings.json`(网页「系统设置」保存) + +浏览器里你会打开的地址,应使用 **内网 IP**,不要用 `127.0.0.1`(否则别的电脑上的浏览器会连到你本机): + +```json +{ + "flask_url": "http://192.168.8.6:5004", + "agent_url": "http://127.0.0.1:15201" +} +``` + +说明: + +- **`flask_url`**:给浏览器用的实例页地址 → 写 **`http://192.168.8.6:端口`** +- **`agent_url`**:仅中控服务器访问 → 写 **`http://127.0.0.1:1520x`** + +各账户按上表改端口即可。 + +### 3.3 可选:`flask_url` 仍写 127.0.0.1 时 + +若坚持 `flask_url` 为 `http://127.0.0.1:5004`(仅 hub 与本机 Flask 同机),在中控 `.env` 增加: + +```bash +HUB_PUBLIC_ORIGIN=http://192.168.8.6 +``` + +中控会把返回给前端的链接从 `127.0.0.1` 替换为 `192.168.8.6`(端口保留)。 + +### 3.4 访问方式 + +1. 中控:`http://192.168.8.6:5100` → 登录中控 → 点「实例」→ 新标签进入 OKX,**无需**再输实例密码。 +2. 直链:`http://192.168.8.6:5004` → 出现登录页 → 输入 `APP_USERNAME` / `APP_PASSWORD`。 + +### 3.5 防火墙 + +内网自用:放行 `5100`、各 `APP_PORT`;**不要**对公网开放 agent 端口。 + +--- + +## 四、反代部署(域名 + 宝塔) + +适用:云服务器,对外用 HTTPS 域名。 + +### 4.1 域名规划(示例) + +| 站点 | 反代到 | +|------|--------| +| `hub.example.com` | `127.0.0.1:5100` | +| `okx.example.com` | `127.0.0.1:5004` | +| `binance.example.com` | `127.0.0.1:5001` | +| `gate.example.com` | `127.0.0.1:5000` | +| `gate-bot.example.com` | `127.0.0.1:5002` | + +Flask / hub 进程仍只监听 **127.0.0.1** 或 `0.0.0.0` 本机端口,由 Nginx 对外提供 HTTPS。 + +### 4.2 宝塔操作要点 + +1. 每个域名 → **反向代理** → 目标 `http://127.0.0.1:对应端口`。 +2. 申请 **SSL**(Let’s Encrypt)。 +3. **不要**再给实例站加一层宝塔「访问密码」(避免与 Flask `/login` 重复);直链鉴权用 **`APP_USERNAME` / `APP_PASSWORD`** 即可。 +4. 自定义 Nginx 配置中保留 WebSocket/大 body 如需;确保代理头: + +```nginx +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +``` + +中控请求实例 API 时会带 **`X-Hub-Token`**,Nginx 默认会转发请求头,一般无需额外配置。 + +### 4.3 `hub_settings` 示例(反代) + +```json +{ + "flask_url": "https://okx.example.com", + "agent_url": "http://127.0.0.1:15201" +} +``` + +- 浏览器与 SSO 链接使用 **`https://okx.example.com`**。 +- 中控服务器拉 `/api/hub/*` 仍走本机 `agent_url`;`flask_url` 用域名时,hub 会请求 `https://okx.example.com/api/...`(同机可通即可)。 + +同机部署时也可: + +- `flask_url`: `http://127.0.0.1:5004` +- `HUB_PUBLIC_ORIGIN`: `https://okx.example.com` + +仅当**所有实例共用一个对外 IP、靠端口区分**时才适合用 `HUB_PUBLIC_ORIGIN`;**每实例独立域名**时,请直接在 `flask_url` 写该实例域名。 + +### 4.4 中控 `.env`(反代建议) + +```bash +HUB_BRIDGE_TOKEN=... +HUB_USERNAME=... +HUB_PASSWORD=... +HUB_COOKIE_SECURE=true # 中控为 HTTPS 时建议开启 +``` + +### 4.5 访问方式 + +1. `https://hub.example.com` 登录中控 → 点「打开实例」→ `https://okx.example.com/hub-sso?...` → 进入系统。 +2. 地址栏直接输入 `https://okx.example.com` → `/login` → 实例账号密码。 + +--- + +## 五、SSO 行为说明(2 小时) + +| 项 | 说明 | +|----|------| +| 有效期 | 默认 **7200 秒(2 小时)**,`HUB_SSO_TTL_SEC` 可改 | +| 单次使用 | 同一链接成功登录后 **不能再用**;需在中控重新点「打开实例」 | +| 密钥 | 复用 **`HUB_BRIDGE_TOKEN`** | +| 直链 | 无 token → 正常 **`/login`** | + +--- + +## 六、部署与重启顺序 + +```bash +cd /opt/crypto_monitor +# 各实例 +pm2 restart crypto_okx crypto_binance crypto_gate crypto_gate_bot # 名称以你为准 + +cd manual_trading_hub +pm2 restart manual-trading-hub manual-agent-binance manual-agent-okx manual-agent-gate manual-agent-gate-bot +``` + +改 `hub_settings` 或 `.env` 后重启 **hub + 对应实例 Flask**(`hub_bridge` 与 `/hub-sso` 在实例进程内)。 + +--- + +## 七、验收清单 + +- [ ] 四实例 `.env` 与中控 `HUB_BRIDGE_TOKEN` 一致 +- [ ] 四实例 `APP_USERNAME` / `APP_PASSWORD` 一致 +- [ ] 局域网:`flask_url` 为 `http://IP:端口`;反代:`flask_url` 为 `https://域名` +- [ ] 已登录中控 → 点「实例」→ **无**实例登录页 +- [ ] 隐身窗口直链实例域名/IP → **有** `/login` +- [ ] 复制「打开实例」完整 URL,用过一次后再开 → 失效并回到登录页 + +--- + +## 八、常见问题 + +**Q:从中控打开仍要登录?** +- 检查实例是否已 `git pull` 并重启(需有 `/hub-sso`)。 +- `HUB_BRIDGE_TOKEN` 是否四所一致。 +- `hub_settings` 里该账户 `key` 是否与 `install_on_app(exchange=...)` 一致(如 `okx`、`binance`、`gate`、`gate_bot`)。 + +**Q:直链也要登录中控?** +- 不应。直链只走实例 `/login`。若跳到中控,检查是否点错链接或 Nginx 配错站点。 + +**Q:链接多久失效?** +- 签发后 **2 小时**内且 **未使用过**;过期或已用需在中控重新点打开。 + +更多故障见 [常见问题.md](./常见问题.md)、[部署文档.md](./部署文档.md)。 diff --git a/manual_trading_hub/常见问题.md b/manual_trading_hub/常见问题.md index 8065289..0a6edf0 100644 --- a/manual_trading_hub/常见问题.md +++ b/manual_trading_hub/常见问题.md @@ -192,6 +192,19 @@ HUB_PUBLIC_ORIGIN=http://192.168.8.6 **可以**。中控聚合监控与全平;复盘、下单、关键位维护进各实例网页。实例 Flask/agent 建议 `127.0.0.1` + 与中控相同的 `HUB_BRIDGE_TOKEN`。 +### 4.3 从中控「打开实例」仍要输密码 + +**完整说明**:[局域网与反代部署说明.md](./局域网与反代部署说明.md) + +**常见原因**: + +1. 四实例未重启,`/hub-sso` 未加载(启动日志勿长期 `[hub_bridge] ImportError`)。 +2. `HUB_BRIDGE_TOKEN` 与四实例 `.env` 不一致。 +3. `hub_settings` 里该户 `key` 与实例 `install_on_app(exchange=...)` 不一致(如 `okx`、`gate_bot`)。 +4. 浏览器仍用旧书签直链首页,未从中控点「实例」(直链本来就要登录)。 + +**直链**:`http://IP:端口` 或 `https://实例域名` → 使用各实例 **`APP_USERNAME` / `APP_PASSWORD`**(四所建议统一)。 + --- ## 五、Gate 趋势 / 复盘相关(实例侧) diff --git a/manual_trading_hub/部署文档.md b/manual_trading_hub/部署文档.md index 3f8105f..53c2312 100644 --- a/manual_trading_hub/部署文档.md +++ b/manual_trading_hub/部署文档.md @@ -3,6 +3,7 @@ 本文档说明在 **Ubuntu / Linux** 上部署 **manual_trading_hub**(复盘系统中控:监控区、系统设置、登录保护)的推荐步骤。 - 功能与界面:[使用说明.md](./使用说明.md) +- **局域网 IP:端口 / 反代域名、中控打开实例免登录**:[局域网与反代部署说明.md](./局域网与反代部署说明.md) - 故障实录:[常见问题.md](./常见问题.md) - 环境变量模板:[.env.example](./.env.example)