修复关键位
This commit is contained in:
@@ -7649,6 +7649,12 @@ def _hub_meta_bundle():
|
||||
|
||||
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
from hub_bridge import install_on_app
|
||||
|
||||
install_on_app(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const REPO_ROOT = path.join(ROOT, "..");
|
||||
const PY = path.join(ROOT, ".venv", "bin", "python");
|
||||
|
||||
module.exports = {
|
||||
@@ -27,7 +28,7 @@ module.exports = {
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "800M",
|
||||
// app.py 从项目根目录 .env 加载(由 .env.example 复制而来,勿提交 Git)
|
||||
env: { PYTHONPATH: REPO_ROOT },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -7684,6 +7684,12 @@ def _hub_meta_bundle():
|
||||
|
||||
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
from hub_bridge import install_on_app
|
||||
|
||||
install_on_app(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const REPO_ROOT = path.join(ROOT, "..");
|
||||
const PY = path.join(ROOT, ".venv", "bin", "python");
|
||||
|
||||
module.exports = {
|
||||
@@ -27,7 +28,7 @@ module.exports = {
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "800M",
|
||||
// app.py 从项目根目录 .env 加载(由 .env.example 复制而来,勿提交 Git)
|
||||
env: { PYTHONPATH: REPO_ROOT },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -7313,6 +7313,12 @@ def _hub_meta_bundle():
|
||||
|
||||
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
from hub_bridge import install_on_app
|
||||
|
||||
install_on_app(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const REPO_ROOT = path.join(ROOT, "..");
|
||||
const PY = path.join(ROOT, ".venv", "bin", "python");
|
||||
|
||||
module.exports = {
|
||||
@@ -27,7 +28,7 @@ module.exports = {
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "800M",
|
||||
// app.py 从项目根目录 .env 加载(由 .env.example 复制而来,勿提交 Git)
|
||||
env: { PYTHONPATH: REPO_ROOT },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5925,6 +5925,12 @@ def _hub_meta_bundle():
|
||||
|
||||
|
||||
try:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
from hub_bridge import install_on_app
|
||||
|
||||
install_on_app(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const REPO_ROOT = path.join(ROOT, "..");
|
||||
const PY = path.join(ROOT, ".venv", "bin", "python");
|
||||
|
||||
module.exports = {
|
||||
@@ -27,7 +28,7 @@ module.exports = {
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "800M",
|
||||
// app.py 从项目根目录 .env 加载(由 .env.example 复制而来,勿提交 Git)
|
||||
env: { PYTHONPATH: REPO_ROOT },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -20,6 +20,12 @@ HUB_DISABLED_IDS=1
|
||||
# true=允许 RFC1918 私网访问中控页面;false=仅 127.0.0.1
|
||||
HUB_TRUST_LAN=true
|
||||
|
||||
# 浏览器打开的复盘/实例链接:把 127.0.0.1 换成 Ubuntu 内网 IP 或域名(中控本机调 API 仍用 127.0.0.1)
|
||||
# 例:用手机/另一台电脑访问中控时必填,否则「交易复盘」会指向你自己电脑的 localhost
|
||||
# HUB_PUBLIC_ORIGIN=http://192.168.1.100
|
||||
# 或只写主机名:HUB_PUBLIC_HOST=192.168.1.100
|
||||
# HUB_PUBLIC_SCHEME=http
|
||||
|
||||
# --- 子代理 agent.py(在 crypto_monitor_* 目录启动时另设 EXCHANGE / PORT)---
|
||||
# 与 HUB_BRIDGE_TOKEN 一致时可只设其一;agent 校验请求头 X-Control-Token
|
||||
# CONTROL_TOKEN=your-long-random-token
|
||||
|
||||
@@ -20,6 +20,7 @@ from settings_store import (
|
||||
load_settings,
|
||||
save_settings,
|
||||
)
|
||||
from url_public import browser_url, default_review_url, public_origin
|
||||
|
||||
HUB_HOST = os.getenv("HUB_HOST", "0.0.0.0")
|
||||
HUB_PORT = int(os.getenv("HUB_PORT", "5100"))
|
||||
@@ -137,10 +138,17 @@ def api_save_settings(body: SettingsBody):
|
||||
|
||||
@app.get("/api/settings/meta")
|
||||
def api_settings_meta():
|
||||
po = public_origin()
|
||||
return {
|
||||
"env_disabled_ids": sorted(env_force_disabled_ids()),
|
||||
"hub_bridge_token_set": bool(HUB_BRIDGE_TOKEN),
|
||||
"capability_options": ["order", "key", "trend"],
|
||||
"public_origin": f"{po[0]}://{po[1]}" if po else None,
|
||||
"public_origin_hint": (
|
||||
"未设置 HUB_PUBLIC_ORIGIN 时,复盘链接若为 127.0.0.1,仅服务器本机浏览器可打开"
|
||||
if not po
|
||||
else "复盘/展示链接已替换为对外地址"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -209,15 +217,30 @@ async def api_monitor_board():
|
||||
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 (str(hub_mon.get("text") or "")[:200] or None)
|
||||
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,
|
||||
"review_url": ex.get("review_url") or "",
|
||||
"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,
|
||||
|
||||
@@ -133,7 +133,13 @@
|
||||
if ((row.capabilities || []).includes("key")) {
|
||||
if (!flaskOk) {
|
||||
inner += `<div style="margin-top:8px;font-size:12px;color:#f85149">关键位/机器人:策略 Flask 未连通</div>`;
|
||||
inner += `<div class="rule-tip">${esc(row.flask_error || hm.msg || "请确认实例 app 已启动,且 HUB_BRIDGE_TOKEN 与实例一致或 APP_AUTH_DISABLED=true")}</div>`;
|
||||
const fe = row.flask_error || hm.msg || hm.error || "";
|
||||
const short =
|
||||
fe ||
|
||||
(hm.status === 404
|
||||
? "HTTP 404:请 git pull 并重启各 crypto_* Flask(hub_bridge 路由未注册)"
|
||||
: "请确认实例已启动,且 HUB_BRIDGE_TOKEN 与实例一致或 APP_AUTH_DISABLED=true");
|
||||
inner += `<div class="rule-tip">${esc(short)}</div>`;
|
||||
} else if (!keys.length) {
|
||||
inner += `<div style="margin-top:8px;color:var(--muted);font-size:12px">关键位:当前无记录(在下单区或实例首页添加)</div>`;
|
||||
} else {
|
||||
@@ -163,7 +169,7 @@
|
||||
: "";
|
||||
return `<div class="card">
|
||||
<div class="card-head">
|
||||
<div><strong>${esc(row.name)}</strong><div class="rule-tip">${esc(row.flask_url || "")}</div></div>
|
||||
<div><strong>${esc(row.name)}</strong><div class="rule-tip">${esc(row.flask_url_browser || row.flask_url || "")}</div></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
${review}
|
||||
<button type="button" class="danger btn-close-ex" data-id="${esc(row.id)}">该户全平</button>
|
||||
@@ -349,6 +355,8 @@
|
||||
const parts = [];
|
||||
if (m.hub_bridge_token_set) parts.push("中控已配置 HUB_BRIDGE_TOKEN");
|
||||
else parts.push("中控未设 HUB_BRIDGE_TOKEN(实例需 APP_AUTH_DISABLED 或同令牌)");
|
||||
if (m.public_origin) parts.push("复盘外链: " + m.public_origin);
|
||||
else parts.push("未设 HUB_PUBLIC_ORIGIN(复盘 127.0.0.1 仅服务器本机可开)");
|
||||
if ((m.env_disabled_ids || []).length)
|
||||
parts.push("环境强制关闭 id: " + m.env_disabled_ids.join(", "));
|
||||
el.textContent = parts.join(" · ");
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<h1>监控区</h1>
|
||||
<p class="rule-tip" style="margin-top:0">
|
||||
持仓/余额来自子代理;关键位、机器人单来自各实例 Flask(须 PM2 跑着 crypto_*)。
|
||||
卡片右上角「交易复盘」= 打开该所交易记录页,不在中控里做复盘。
|
||||
「交易复盘」在新标签打开 /records。其它电脑访问中控时,请在 hub 的 .env 设置
|
||||
HUB_PUBLIC_ORIGIN=http://Ubuntu内网IP,否则会跳到 127.0.0.1。
|
||||
</p>
|
||||
<div class="toolbar">
|
||||
<button type="button" id="btn-monitor-refresh">立即刷新</button>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""将 127.0.0.1 服务地址转为浏览器可访问的外链(内网 IP 或域名)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
_LOCAL_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
||||
|
||||
|
||||
def public_origin() -> tuple[str, str] | None:
|
||||
"""
|
||||
从环境变量读取对外 Origin。
|
||||
HUB_PUBLIC_ORIGIN=http://192.168.1.10 或 HUB_PUBLIC_HOST=192.168.1.10
|
||||
"""
|
||||
raw = (os.getenv("HUB_PUBLIC_ORIGIN") or os.getenv("HUB_PUBLIC_HOST") or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
if "://" in raw:
|
||||
p = urlparse(raw)
|
||||
scheme = (p.scheme or "http").strip()
|
||||
host = (p.hostname or "").strip()
|
||||
if not host:
|
||||
return None
|
||||
return scheme, host
|
||||
scheme = (os.getenv("HUB_PUBLIC_SCHEME") or "http").strip() or "http"
|
||||
host = raw.split("/")[0].split(":")[0].strip()
|
||||
return (scheme, host) if host else None
|
||||
|
||||
|
||||
def browser_url(internal_url: str | None) -> str:
|
||||
"""
|
||||
中控本机请求仍用 internal_url;返回给前端、复盘链接用本函数。
|
||||
若未配置 HUB_PUBLIC_* 或原 URL 已是非本机地址,则原样返回。
|
||||
"""
|
||||
if not internal_url or not str(internal_url).strip():
|
||||
return ""
|
||||
u = str(internal_url).strip()
|
||||
origin = public_origin()
|
||||
if not origin:
|
||||
return u
|
||||
scheme_pub, host_pub = origin
|
||||
try:
|
||||
p = urlparse(u)
|
||||
except Exception:
|
||||
return u
|
||||
if not p.scheme or not p.netloc:
|
||||
return u
|
||||
host = (p.hostname or "").lower()
|
||||
if host not in _LOCAL_HOSTS and not host.startswith("::ffff:127.0.0.1"):
|
||||
return u
|
||||
port = p.port
|
||||
netloc = f"{host_pub}:{port}" if port else host_pub
|
||||
return urlunparse((scheme_pub, netloc, p.path or "", p.params, p.query, p.fragment))
|
||||
|
||||
|
||||
def default_review_url(flask_url: str | None) -> str:
|
||||
base = browser_url((flask_url or "").rstrip("/"))
|
||||
if not base:
|
||||
return ""
|
||||
return f"{base}/records"
|
||||
@@ -154,7 +154,7 @@ python hub.py
|
||||
| **机器人持仓** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active) |
|
||||
| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) |
|
||||
| **趋势计划** | 仅 Gate 趋势户;`trend_pullback_plans` active |
|
||||
| **交易复盘** | 新标签打开该户 Flask 的 `/records`(交易记录、笔记、导出 CSV);**中控不做复盘**,仅跳转 |
|
||||
| **交易复盘** | 新标签打开该户 `/records`;**中控不做复盘**。链接默认由 `flask_url` 生成;若配置 **`HUB_PUBLIC_ORIGIN`**(如 `http://192.168.x.x`),会把 `127.0.0.1` 换成 Ubuntu 内网 IP,方便局域网其它设备打开 |
|
||||
| **关键位** | 来自实例 `/api/hub/monitor` + `/api/price_snapshot`(须 Flask 已启动);无记录或 Flask 未连通时卡片会提示原因;**Gate 趋势户**无关键位 |
|
||||
| **该户全平** | `POST` 子代理 `/emergency/close-all`,仅平该 API Key 仓位 |
|
||||
| **全局紧急全平** | 对所有已启用户依次全平(不含 `HUB_DISABLED_IDS` 强制关闭的 id) |
|
||||
|
||||
@@ -177,7 +177,7 @@ curl -s http://127.0.0.1:15200/health
|
||||
2. `hub_settings.json` 中 Flask/Agent 保持 **`http://127.0.0.1:...`**。
|
||||
3. 四实例 **`APP_AUTH_DISABLED=false`** + 与中控相同 **`HUB_BRIDGE_TOKEN`**(见 `.env.example` 注释)。
|
||||
4. 子代理 **`HOST=127.0.0.1`**,防火墙勿放行 `15200`~`15203`、各 `APP_PORT`。
|
||||
5. 监控页「复盘」链到本机 Flask,公网浏览器通常打不开,需 VPN/内网访问实例。
|
||||
5. **交易复盘**:`manual_trading_hub/.env` 设 `HUB_PUBLIC_ORIGIN=http://<Ubuntu局域网IP>`;内网其它设备才能打开复盘;须能访问各实例端口(5000/5001/5002)或单独反代。
|
||||
|
||||
---
|
||||
|
||||
@@ -189,6 +189,7 @@ curl -s http://127.0.0.1:15200/health
|
||||
| `HUB_PORT` | `5100` | 端口 |
|
||||
| `HUB_DISABLED_IDS` | `1` | 强制关闭的账户 id(OKX) |
|
||||
| `HUB_TRUST_LAN` | `true` | 私网可访问;仅本机可 `false` |
|
||||
| `HUB_PUBLIC_ORIGIN` | 空 | 浏览器用复盘链接;如 `http://192.168.1.100`(**内网其它电脑访问中控时建议设置**) |
|
||||
| `HUB_BRIDGE_TOKEN` | 空 | 公网/开登录时建议配置 |
|
||||
|
||||
本地联调、实例 `APP_AUTH_DISABLED=true` 时可不配 `HUB_BRIDGE_TOKEN`。
|
||||
|
||||
Reference in New Issue
Block a user