From db251c39bf68b67501b02522622d3fbb5ca2d05f Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 16 Jun 2026 10:59:45 +0800 Subject: [PATCH] fix: stop panel stats from hanging on Clash /traffic WebSocket Co-authored-by: Cursor --- docs/troubleshooting.md | 18 +++++++++++++----- panel/stats.py | 39 ++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5f2f138..4e76507 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -112,9 +112,19 @@ python3 scripts/render-server.py systemctl restart sing-box jiedian-panel ``` -### 面板流量/在线状态显示「不可用」 +### 面板流量/在线状态显示「不可用」或一直「检测中」后超时 -sing-box Clash API 未就绪,按顺序检查: +常见原因是面板请求 sing-box **Clash API 的 `/traffic`**。该接口是 **WebSocket 实时流**,用普通 HTTP GET 会一直占用连接,直到 Nginx 60s 超时返回 **504**,前端 `/api/stats` 失败即显示「不可用」。 + +**处理**:更新代码并重启面板(已改为只读 `/connections`,不再阻塞请求): + +```bash +cd /opt/jiedian +git pull +systemctl restart jiedian-panel +``` + +若仍显示「不可用」,再检查 Clash API 是否启用: ```bash systemctl is-active sing-box @@ -141,10 +151,8 @@ systemctl restart sing-box jiedian-panel ```bash curl -s -H "Authorization: Bearer $(grep ^CLASH_API_SECRET= /opt/jiedian/.env | cut -d= -f2)" \ http://127.0.0.1:9090/connections | python3 -m json.tool | head -80 - curl -s -H "Authorization: Bearer $(grep ^CLASH_API_SECRET= /opt/jiedian/.env | cut -d= -f2)" \ - http://127.0.0.1:9090/traffic ``` - 若 `connections` 为空但 `traffic` 有速率,说明在传流量但连接详情未上报,单节点场景面板会用全局速率推断在线。 + 若 `connections` 为空但客户端确实在传流量,单节点场景面板会用连接汇总速率推断在线。 ### 面板打不开 / 404 diff --git a/panel/stats.py b/panel/stats.py index 9fa5c57..dce2e04 100644 --- a/panel/stats.py +++ b/panel/stats.py @@ -18,6 +18,7 @@ CLASH_ADDR = "127.0.0.1:9090" _speed_cache: dict[int, tuple[float, int, int]] = {} _conn_cache: dict[str, dict[str, int | str]] = {} +_global_bytes_cache: tuple[float, int, int] | None = None def _load_env() -> dict[str, str]: @@ -63,20 +64,21 @@ def fetch_clash_connections() -> tuple[list[dict], bool]: return payload.get("connections") or [], True -def fetch_clash_traffic() -> tuple[int, int, bool]: - """返回 (upload B/s, download B/s, ok)。""" - env = _load_env() - secret = env.get("CLASH_API_SECRET", "") - url = f"http://{CLASH_ADDR}/traffic" - req = urllib.request.Request(url) - if secret: - req.add_header("Authorization", f"Bearer {secret}") - try: - with urllib.request.urlopen(req, timeout=3) as resp: - payload = json.loads(resp.read().decode("utf-8")) - except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError): - return 0, 0, False - return int(payload.get("up") or 0), int(payload.get("down") or 0), True +def _global_conn_speed(connections: list[dict]) -> tuple[float, float]: + """从 /connections 汇总字节增量估算全局速率(/traffic 为 WebSocket 流,不能同步 HTTP 读)。""" + total_up = sum(int(c.get("upload") or 0) for c in connections) + total_down = sum(int(c.get("download") or 0) for c in connections) + now = time.time() + global _global_bytes_cache + prev = _global_bytes_cache + _global_bytes_cache = (now, total_up, total_down) + if not prev: + return 0.0, 0.0 + t0, u0, d0 = prev + dt = now - t0 + if dt <= 0: + return 0.0, 0.0 + return max(0.0, (total_up - u0) / dt), max(0.0, (total_down - d0) / dt) def _connection_user(conn: dict) -> str: @@ -254,12 +256,11 @@ def collect_node_stats() -> dict: nodes = list_nodes() uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes} connections, clash_ok = fetch_clash_connections() - traffic_up, traffic_down, traffic_ok = fetch_clash_traffic() active_by_uuid = _sync_connections(connections, nodes, uuid_to_node) - singbox_ok = clash_ok or traffic_ok single_node = len(nodes) == 1 has_connections = len(connections) > 0 - global_active = (traffic_up + traffic_down) > 512 + global_up_speed, global_down_speed = _global_conn_speed(connections) if clash_ok else (0.0, 0.0) + global_active = (global_up_speed + global_down_speed) > 512 result_nodes: dict[str, dict] = {} summary_online = 0 @@ -279,8 +280,8 @@ def collect_node_stats() -> dict: if not matched and single_node and has_connections: matched = connections if not matched and single_node and global_active: - up_speed = float(traffic_up) - down_speed = float(traffic_down) + up_speed = global_up_speed + down_speed = global_down_speed online = ( len(matched) > 0