From acbd9576bcf67615e1bd69762f8896dd9e45b424 Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 30 May 2026 12:08:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AD=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_okx/.env.example | 3 ++ hub_bridge.py | 29 +++++++++++ manual_trading_hub/.env.example | 6 ++- manual_trading_hub/static/app.css | 49 ++++++++++++++++++ manual_trading_hub/static/app.js | 77 ++++++++++++++++++++++++++-- manual_trading_hub/static/index.html | 19 +++++-- 6 files changed, 174 insertions(+), 9 deletions(-) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 3e1aa8d..e87c75a 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -30,6 +30,9 @@ APP_AUTH_DISABLED=true # 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致 # 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true # HUB_BRIDGE_TOKEN=your-long-random-token +# 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启) +# APP_ALLOW_HUB_EMBED=true +# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com # Flask 会话密钥(必须替换为长随机字符串) FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET diff --git a/hub_bridge.py b/hub_bridge.py index a7fbcc6..54458d3 100644 --- a/hub_bridge.py +++ b/hub_bridge.py @@ -108,9 +108,38 @@ def install_on_app( "meta_fn": meta_fn, "views": views, } + install_hub_embed_headers(app) register_hub_routes(app) +def install_hub_embed_headers(app): + """允许复盘中控 iframe 内嵌打开本实例(须与 hub 的 HUB_EMBED_ORIGINS 或域名一致)。""" + import os + + allowed = (os.getenv("APP_ALLOW_HUB_EMBED") or "true").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + if not allowed: + return + origins = ( + (os.getenv("HUB_EMBED_PARENT_ORIGINS") or os.getenv("HUB_EMBED_ORIGINS") or "*") + .strip() + ) + + @app.after_request + def _hub_embed_frame_headers(response): + if origins == "*": + response.headers["Content-Security-Policy"] = "frame-ancestors *" + else: + response.headers["Content-Security-Policy"] = ( + f"frame-ancestors 'self' {origins}" + ) + return response + + def register_hub_routes(app): auth_disabled = False try: diff --git a/manual_trading_hub/.env.example b/manual_trading_hub/.env.example index 135d8f3..b3abe7b 100644 --- a/manual_trading_hub/.env.example +++ b/manual_trading_hub/.env.example @@ -39,7 +39,11 @@ HUB_TRUST_LAN=true # 本地导航 / 门户 iframe 嵌入中控(默认 true) # HUB_ALLOW_EMBED=true # 限制可嵌入的父页来源(逗号分隔);默认 * 不限制 -# HUB_EMBED_ORIGINS=http://192.168.8.6:5070 +# HUB_EMBED_ORIGINS=http://192.168.8.6:5070,https://hub.example.com + +# 四实例允许被中控 iframe 内嵌(各 crypto_monitor_*/.env,与 hub 同步部署) +# APP_ALLOW_HUB_EMBED=true +# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com # 浏览器打开的实例/复盘链接(hub_settings 里 flask_url 为 127.0.0.1 时替换为对外地址) # 局域网:填内网 IP,见《局域网与反代部署说明.md》 diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 12575af..59167f0 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -505,6 +505,55 @@ body.hub-fullscreen-open { overflow: hidden; } +body.hub-instance-frame-open { + overflow: hidden; +} + +.instance-frame-shell { + position: fixed; + inset: 0; + z-index: 200; + display: flex; + flex-direction: column; + background: var(--bg, #0a0e14); +} + +.instance-frame-shell.hidden { + display: none !important; +} + +.instance-frame-toolbar { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + border-bottom: 1px solid var(--border-soft, #2a3150); + background: rgba(10, 14, 20, 0.98); +} + +.instance-frame-title { + flex: 1; + font-weight: 600; + color: var(--text, #dbe4ff); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.instance-frame-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} + +.instance-frame { + flex: 1 1 auto; + width: 100%; + border: none; + background: #0f1216; +} + .exchange-fullscreen { position: fixed; inset: 0; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 939646c..5b44af4 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -17,22 +17,65 @@ return r; } - async function openInstanceInBrowser(exchangeId, nextPath) { + let instanceFrameUrl = ""; + + async function openInstance(exchangeId, nextPath, opts) { + const options = opts || {}; + const newTab = !!options.newTab; 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) { + if (!j.ok || !j.url) { + showToast(j.detail || "无法生成打开链接", true); + return; + } + if (newTab) { window.open(j.url, "_blank", "noopener"); return; } - showToast(j.detail || "无法生成打开链接", true); + const row = lastMonitorRows.find((x) => String(x.id) === String(exchangeId)); + openInstanceFrame(j.url, row ? row.name : exchangeId); } catch (e) { showToast(String(e), true); } } + function openInstanceFrame(url, title) { + const shell = document.getElementById("instance-frame-shell"); + const frame = document.getElementById("instance-frame"); + const titleEl = document.getElementById("instance-frame-title"); + if (!shell || !frame) { + window.open(url, "_blank", "noopener"); + return; + } + closeExchangeFullscreen(); + instanceFrameUrl = url; + if (titleEl) titleEl.textContent = title || "实例"; + frame.src = url; + shell.classList.remove("hidden"); + shell.setAttribute("aria-hidden", "false"); + document.body.classList.add("hub-instance-frame-open"); + } + + function closeInstanceFrame() { + const shell = document.getElementById("instance-frame-shell"); + const frame = document.getElementById("instance-frame"); + instanceFrameUrl = ""; + if (frame) frame.src = "about:blank"; + if (shell) { + shell.classList.add("hidden"); + shell.setAttribute("aria-hidden", "true"); + } + document.body.classList.remove("hub-instance-frame-open"); + } + + /** @deprecated use openInstance */ + async function openInstanceInBrowser(exchangeId, nextPath) { + return openInstance(exchangeId, nextPath, { newTab: false }); + } + async function initAuth() { try { const r = await fetch("/api/auth/status"); @@ -379,7 +422,9 @@ btn.onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); - openInstanceInBrowser(btn.dataset.exId, btn.dataset.next || "/"); + openInstance(btn.dataset.exId, btn.dataset.next || "/", { + newTab: ev.ctrlKey || ev.metaKey, + }); }; }); box.querySelectorAll(".btn-close-ex").forEach((btn) => { @@ -928,6 +973,24 @@ } } + function initInstanceFrame() { + const back = document.getElementById("instance-frame-back"); + const refresh = document.getElementById("instance-frame-refresh"); + const newTab = document.getElementById("instance-frame-newtab"); + const frame = document.getElementById("instance-frame"); + if (back) back.onclick = () => closeInstanceFrame(); + if (refresh && frame) { + refresh.onclick = () => { + if (instanceFrameUrl) frame.src = instanceFrameUrl; + }; + } + if (newTab) { + newTab.onclick = () => { + if (instanceFrameUrl) window.open(instanceFrameUrl, "_blank", "noopener"); + }; + } + } + function initFullscreen() { const backdrop = document.getElementById("exchange-fullscreen-backdrop"); if (backdrop) { @@ -953,6 +1016,11 @@ document.addEventListener("keydown", (ev) => { if (ev.key === "Escape") { closeTpslModal(); + const shell = document.getElementById("instance-frame-shell"); + if (shell && !shell.classList.contains("hidden")) { + closeInstanceFrame(); + return; + } if (expandedExchangeId) { closeExchangeFullscreen(); renderMonitorGrid(lastMonitorRows); @@ -1261,6 +1329,7 @@ }; initTpslModal(); + initInstanceFrame(); initFullscreen(); initMobileLayout(); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index f5becad..0253c9f 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -8,7 +8,7 @@ - + @@ -41,8 +41,7 @@
持仓与余额来自子代理;关键位、机器人单、趋势计划来自各实例 Flask(须 PM2 运行 crypto_*)。
人工下单、添加关键位、趋势回调请在各实例网页操作;中控可监控、单仓平仓与账户全平。
- 「交易复盘」在新标签打开该实例 /records。其它电脑访问中控时,请在 hub 的 .env 设置 - HUB_PUBLIC_ORIGIN=http://服务器内网IP。 + 点「实例 / 策略交易 / 复盘」在本页内嵌打开(SSO 免密);按住 Ctrl 点击可在新标签打开。
@@ -57,6 +56,18 @@
+ +