fix: stop panel stats from hanging on Clash /traffic WebSocket
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+13
-5
@@ -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
|
||||
|
||||
|
||||
+20
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user