修复中控

This commit is contained in:
dekun
2026-05-25 08:02:06 +08:00
parent e89708726f
commit 5f4f33cc10
7 changed files with 154 additions and 54 deletions
+6
View File
@@ -36,6 +36,12 @@ HUB_TRUST_LAN=true
# 或只写主机名:HUB_PUBLIC_HOST=192.168.1.100
# HUB_PUBLIC_SCHEME=http
# 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10
# HUB_AGENT_TIMEOUT=8
# HUB_FLASK_TIMEOUT=10
# 为 false 时不拉各实例 /api/price_snapshot(关键位门控简化为「-」,首屏明显更快)
# HUB_BOARD_KEY_PRICES=true
# --- 子代理 agent.py(在 crypto_monitor_* 目录启动时另设 EXCHANGE / PORT---
# 与 HUB_BRIDGE_TOKEN 一致时可只设其一;agent 校验请求头 X-Control-Token
# CONTROL_TOKEN=your-long-random-token
+83 -48
View File
@@ -43,7 +43,11 @@ 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-mobile"
HUB_BUILD = "20260525-perf"
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()
HUB_BOARD_KEY_PRICES = _board_key_prices_raw in ("1", "true", "yes", "on")
def _is_local(host: str | None) -> bool:
@@ -261,7 +265,7 @@ def api_settings_meta():
async def _fetch_agent_status(client: httpx.AsyncClient, ex: dict) -> dict:
url = f"{ex['agent_url'].rstrip('/')}/status"
try:
r = await client.get(url, headers=_agent_headers(), timeout=12.0)
r = await client.get(url, headers=_agent_headers(), timeout=HUB_AGENT_TIMEOUT)
body = r.json() if r.content else {}
return {
"id": ex["id"],
@@ -317,7 +321,7 @@ async def _fetch_flask_json(
return None
try:
if method == "GET":
r = await client.get(f"{base}{path}", headers=_hub_headers(), timeout=15.0)
r = await client.get(f"{base}{path}", headers=_hub_headers(), timeout=HUB_FLASK_TIMEOUT)
else:
r = await client.post(f"{base}{path}", headers=_hub_headers(), data=data, timeout=120.0)
if r.status_code >= 400:
@@ -330,55 +334,86 @@ async def _fetch_flask_json(
return {"ok": False, "error": str(e)}
def _flask_error_from_hub_mon(hub_mon: dict | None) -> str | None:
if not isinstance(hub_mon, dict) or hub_mon.get("ok") is not False:
return None
st = hub_mon.get("status")
if st == 404:
return (
"HTTP 404:该 Flask 未注册 /api/hub/*hub_bridge 未加载)。"
"请在仓库根目录 git pull 后 pm2 restart crypto_binance crypto_gate crypto_gate_bot"
"并查看启动日志是否含 [hub_bridge] ImportError"
)
return (
hub_mon.get("msg")
or hub_mon.get("error")
or (f"HTTP {st}" if st else None)
or (str(hub_mon.get("text") or "")[:120] or None)
)
async def _fetch_exchange_flask_bundle(
client: httpx.AsyncClient, ex: dict
) -> tuple[dict | None, dict | None, list | None]:
"""单所 Flaskmonitor / meta /(可选)price_snapshot 并行拉取。"""
caps = ex.get("capabilities") or []
tasks = [
_fetch_flask_json(client, ex, "/api/hub/monitor"),
_fetch_flask_json(client, ex, "/api/hub/meta"),
]
want_prices = HUB_BOARD_KEY_PRICES and "key" in caps
if want_prices:
tasks.append(_fetch_flask_json(client, ex, "/api/price_snapshot"))
results = await asyncio.gather(*tasks)
hub_mon = results[0]
meta = results[1]
key_prices = None
if want_prices and len(results) > 2:
snap = results[2]
if isinstance(snap, dict):
key_prices = snap.get("key_prices")
return hub_mon, meta, key_prices
async def _assemble_board_row(
client: httpx.AsyncClient, ex: dict, agent_row: dict
) -> dict:
hub_mon, meta, key_prices = await _fetch_exchange_flask_bundle(client, ex)
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
raw_review = (ex.get("review_url") or "").strip()
review_link = browser_url(raw_review) if raw_review else default_review_url(
ex.get("flask_url")
)
return {
**agent_row,
"flask_url": ex.get("flask_url") or "",
"flask_url_browser": browser_url(ex.get("flask_url")),
"review_url": review_link,
"hub_monitor": hub_mon,
"flask_ok": flask_ok,
"flask_error": _flask_error_from_hub_mon(hub_mon if isinstance(hub_mon, dict) else None),
"meta": (meta or {}).get("meta") if isinstance(meta, dict) else meta,
"key_prices": key_prices,
}
@app.get("/api/monitor/board")
async def api_monitor_board():
exchanges = enabled_exchanges()
async with httpx.AsyncClient() as client:
agent_rows = await asyncio.gather(*[_fetch_agent_status(client, ex) for ex in exchanges])
out = []
for ex, agent_row in zip(exchanges, agent_rows):
hub_mon = await _fetch_flask_json(client, ex, "/api/hub/monitor")
meta = await _fetch_flask_json(client, ex, "/api/hub/meta")
key_prices = None
if "key" in (ex.get("capabilities") or []):
snap = await _fetch_flask_json(client, ex, "/api/price_snapshot")
if isinstance(snap, dict):
key_prices = snap.get("key_prices")
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
flask_err = None
if isinstance(hub_mon, dict) and hub_mon.get("ok") is False:
st = hub_mon.get("status")
if st == 404:
flask_err = (
"HTTP 404:该 Flask 未注册 /api/hub/*hub_bridge 未加载)。"
"请在仓库根目录 git pull 后 pm2 restart crypto_binance crypto_gate crypto_gate_bot"
"并查看启动日志是否含 [hub_bridge] ImportError"
)
else:
flask_err = (
hub_mon.get("msg")
or hub_mon.get("error")
or (f"HTTP {st}" if st else None)
or (str(hub_mon.get("text") or "")[:120] or None)
)
raw_review = (ex.get("review_url") or "").strip()
review_link = browser_url(raw_review) if raw_review else default_review_url(
ex.get("flask_url")
)
out.append(
{
**agent_row,
"flask_url": ex.get("flask_url") or "",
"flask_url_browser": browser_url(ex.get("flask_url")),
"review_url": review_link,
"hub_monitor": hub_mon,
"flask_ok": flask_ok,
"flask_error": flask_err,
"meta": (meta or {}).get("meta") if isinstance(meta, dict) else meta,
"key_prices": key_prices,
}
)
return {"rows": out, "updated_at": __import__("datetime").datetime.now().isoformat(timespec="seconds")}
agent_rows = await asyncio.gather(
*[_fetch_agent_status(client, ex) for ex in exchanges]
)
out = await asyncio.gather(
*[
_assemble_board_row(client, ex, agent_row)
for ex, agent_row in zip(exchanges, agent_rows)
]
)
return {
"rows": list(out),
"updated_at": __import__("datetime").datetime.now().isoformat(timespec="seconds"),
}
class CloseAllBody(BaseModel):
+30
View File
@@ -1238,6 +1238,36 @@ button.btn-sm {
padding: 8px 0;
}
.board-loading {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
min-height: 120px;
padding: 24px;
color: var(--muted);
font-size: 13px;
border: 1px dashed var(--border-soft);
border-radius: var(--radius);
background: rgba(0, 0, 0, 0.25);
}
.board-loading-spin {
width: 18px;
height: 18px;
border: 2px solid var(--border-soft);
border-top-color: var(--accent);
border-radius: 50%;
animation: hub-spin 0.8s linear infinite;
}
@keyframes hub-spin {
to {
transform: rotate(360deg);
}
}
.pnl-pos {
color: var(--green);
text-shadow: 0 0 12px rgba(0, 255, 157, 0.3);
+8 -1
View File
@@ -198,6 +198,11 @@
async function loadMonitorBoard() {
const box = document.getElementById("monitor-grid");
const showLoading = !lastMonitorRows.length;
if (showLoading && box) {
box.innerHTML =
'<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在聚合四所数据…</div>';
}
try {
const r = await apiFetch("/api/monitor/board");
const data = await r.json();
@@ -1125,8 +1130,10 @@
initAuth().then((ok) => {
if (!ok) return;
loadSettings().catch(() => {});
setActiveNav();
if (currentPage() === "settings") {
loadSettings().catch(() => {});
}
window.addEventListener("popstate", setActiveNav);
});
})();
+4 -3
View File
@@ -6,8 +6,9 @@
<title>复盘系统中控</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/assets/app.css?v=20260525-mobile" />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260525-perf" />
</head>
<body>
<div class="app-bg" aria-hidden="true"></div>
@@ -108,6 +109,6 @@
</div>
<div id="toast"></div>
<script src="/assets/app.js?v=20260525-mobile"></script>
<script src="/assets/app.js?v=20260525-perf"></script>
</body>
</html>
+3 -2
View File
@@ -6,8 +6,9 @@
<title>登录 · 复盘系统中控</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/assets/app.css?v=20260525-mobile" />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260525-perf" />
</head>
<body class="login-page">
<div class="login-bg" aria-hidden="true"></div>
+20
View File
@@ -150,6 +150,26 @@ cd manual_trading_hub && pm2 restart manual-agent-gate manual-agent-gate-bot
curl -s -H "X-Hub-Token:你的令牌" http://127.0.0.1:5000/api/hub/ping
```
### 3.3 中控监控区打开慢、一直转圈
**原因(常见)**
1. 首屏要等 **`/api/monitor/board`**:向 4 个子代理拉持仓/余额,并向 4 个 Flask 拉监控与(默认)关键位行情;任一实例慢或超时都会拖住整页。
2. 旧版 hub 对每所 Flask **串行**请求,4 所 × 3 接口容易累计到十几秒;新版已改为**并行**`git pull``pm2 restart manual-trading-hub`)。
3. 各实例 **`/api/price_snapshot`** 会调交易所接口(含全量持仓),最耗时;内网访问 Google 字体也会拖首屏渲染。
4. 子代理 `/status``fetch_balance` / `fetch_positions` / 挂单列表走交易所 API,网络差时单次可达数秒。
**加快办法**
```env
# manual_trading_hub/.env
HUB_BOARD_KEY_PRICES=false # 不拉 price_snapshot,关键位门控显示为「-」,首屏明显更快
HUB_AGENT_TIMEOUT=6
HUB_FLASK_TIMEOUT=8
```
并确认四所 `crypto_*``manual-agent-*` 均为 **online**,避免等满超时。浏览器 **Ctrl+F5** 强刷静态资源(版本号含 `20260525-perf`)。
---
## 四、复盘链接与公网反代