fix: improve online detection when Clash API omits user metadata
Match connections by multiple auth keys, fall back to global traffic for single-node setups, and document offline-status troubleshooting. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+17
-2
@@ -78,8 +78,23 @@ systemctl restart sing-box jiedian-panel
|
|||||||
|
|
||||||
### 在线状态始终离线但客户端能连
|
### 在线状态始终离线但客户端能连
|
||||||
|
|
||||||
- 有流量时面板会按速率推断在线;若仍显示离线,执行 `curl -s -H "Authorization: Bearer $(grep CLASH_API_SECRET /opt/jiedian/.env | cut -d= -f2)" http://127.0.0.1:9090/connections | head` 查看连接 metadata 是否含 user 字段
|
面板通过 sing-box **Clash API** 判断在线。统计服务显示「正常」只代表 API 能访问,不代表能识别到连接。
|
||||||
- 修改节点或升级后需重新渲染配置:`python3 /opt/jiedian/scripts/render-server.py && systemctl restart sing-box`
|
|
||||||
|
常见原因:
|
||||||
|
|
||||||
|
1. **Clash API 未带 user 字段**(Hysteria2 旧配置未设置 `name`)→ 重新生成配置:
|
||||||
|
```bash
|
||||||
|
python3 /opt/jiedian/scripts/render-server.py && systemctl restart sing-box
|
||||||
|
```
|
||||||
|
2. **仅有一个节点时**:新版面板会把所有活跃连接归到该节点;多节点需确保 hy2 用户 `name` 与 VLESS 的 UUID 一致。
|
||||||
|
3. **客户端测速有延迟但面板仍离线**:在 VPS 上查看 Clash 连接列表:
|
||||||
|
```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` 有速率,说明在传流量但连接详情未上报,单节点场景面板会用全局速率推断在线。
|
||||||
|
|
||||||
### 面板打不开 / 404
|
### 面板打不开 / 404
|
||||||
|
|
||||||
|
|||||||
+97
-12
@@ -63,9 +63,63 @@ def fetch_clash_connections() -> tuple[list[dict], bool]:
|
|||||||
return payload.get("connections") or [], True
|
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 _connection_user(conn: dict) -> str:
|
def _connection_user(conn: dict) -> str:
|
||||||
meta = conn.get("metadata") or {}
|
meta = conn.get("metadata") or {}
|
||||||
return str(meta.get("user") or meta.get("uid") or "")
|
for key in ("user", "uid", "auth_user", "auth", "username"):
|
||||||
|
val = meta.get(key)
|
||||||
|
if val:
|
||||||
|
return str(val)
|
||||||
|
for key in ("user", "uid"):
|
||||||
|
val = conn.get(key)
|
||||||
|
if val:
|
||||||
|
return str(val)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _connection_meta(conn: dict) -> str:
|
||||||
|
meta = conn.get("metadata") or {}
|
||||||
|
parts = [
|
||||||
|
str(meta.get("type") or ""),
|
||||||
|
str(meta.get("network") or ""),
|
||||||
|
str(meta.get("inbound") or meta.get("inboundTag") or ""),
|
||||||
|
str(meta.get("inboundType") or ""),
|
||||||
|
]
|
||||||
|
return " ".join(parts).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _node_auth_keys(node: dict) -> set[str]:
|
||||||
|
keys = {node["uuid"]}
|
||||||
|
if node.get("hy2_password"):
|
||||||
|
keys.add(node["hy2_password"])
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def _match_connection(conn: dict, node: dict) -> bool:
|
||||||
|
user = _connection_user(conn)
|
||||||
|
if user and user in _node_auth_keys(node):
|
||||||
|
return True
|
||||||
|
meta = _connection_meta(conn)
|
||||||
|
if "hysteria" in meta and node.get("hy2_password"):
|
||||||
|
# 旧配置未设置 hy2 name 时,Clash API 可能不带 user
|
||||||
|
if user == node["hy2_password"]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _connection_id(conn: dict) -> str:
|
def _connection_id(conn: dict) -> str:
|
||||||
@@ -126,16 +180,28 @@ def _get_stored_totals(node_id: int) -> tuple[int, int]:
|
|||||||
|
|
||||||
|
|
||||||
def _sync_connections(
|
def _sync_connections(
|
||||||
connections: list[dict], uuid_to_node: dict[str, int]
|
connections: list[dict],
|
||||||
|
nodes: list[dict],
|
||||||
|
uuid_to_node: dict[str, int],
|
||||||
) -> dict[str, tuple[int, int]]:
|
) -> dict[str, tuple[int, int]]:
|
||||||
"""同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。"""
|
"""同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。"""
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
active: dict[str, tuple[int, int]] = {}
|
active: dict[str, tuple[int, int]] = {}
|
||||||
|
single_node = len(nodes) == 1
|
||||||
|
only_uuid = nodes[0]["uuid"] if single_node else ""
|
||||||
|
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
user = _connection_user(conn)
|
matched_uuid = ""
|
||||||
if user not in uuid_to_node:
|
for node in nodes:
|
||||||
|
if _match_connection(conn, node):
|
||||||
|
matched_uuid = node["uuid"]
|
||||||
|
break
|
||||||
|
if not matched_uuid and single_node and connections:
|
||||||
|
matched_uuid = only_uuid
|
||||||
|
|
||||||
|
if matched_uuid not in uuid_to_node:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cid = _connection_id(conn)
|
cid = _connection_id(conn)
|
||||||
if not cid:
|
if not cid:
|
||||||
continue
|
continue
|
||||||
@@ -145,15 +211,19 @@ def _sync_connections(
|
|||||||
seen.add(cid)
|
seen.add(cid)
|
||||||
|
|
||||||
prev = _conn_cache.get(cid)
|
prev = _conn_cache.get(cid)
|
||||||
if prev and prev["uuid"] == user:
|
if prev and prev["uuid"] == matched_uuid:
|
||||||
prev_up = int(prev["upload"])
|
prev_up = int(prev["upload"])
|
||||||
prev_down = int(prev["download"])
|
prev_down = int(prev["download"])
|
||||||
if upload < prev_up or download < prev_down:
|
if upload < prev_up or download < prev_down:
|
||||||
_add_closed_traffic(uuid_to_node[user], prev_up, prev_down)
|
_add_closed_traffic(uuid_to_node[matched_uuid], prev_up, prev_down)
|
||||||
_conn_cache[cid] = {"uuid": user, "upload": upload, "download": download}
|
_conn_cache[cid] = {
|
||||||
|
"uuid": matched_uuid,
|
||||||
|
"upload": upload,
|
||||||
|
"download": download,
|
||||||
|
}
|
||||||
|
|
||||||
cur_up, cur_down = active.get(user, (0, 0))
|
cur_up, cur_down = active.get(matched_uuid, (0, 0))
|
||||||
active[user] = (cur_up + upload, cur_down + download)
|
active[matched_uuid] = (cur_up + upload, cur_down + download)
|
||||||
|
|
||||||
for cid in list(_conn_cache.keys()):
|
for cid in list(_conn_cache.keys()):
|
||||||
if cid in seen:
|
if cid in seen:
|
||||||
@@ -184,7 +254,12 @@ def collect_node_stats() -> dict:
|
|||||||
nodes = list_nodes()
|
nodes = list_nodes()
|
||||||
uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes}
|
uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes}
|
||||||
connections, clash_ok = fetch_clash_connections()
|
connections, clash_ok = fetch_clash_connections()
|
||||||
active_by_uuid = _sync_connections(connections, uuid_to_node)
|
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
|
||||||
|
|
||||||
result_nodes: dict[str, dict] = {}
|
result_nodes: dict[str, dict] = {}
|
||||||
summary_online = 0
|
summary_online = 0
|
||||||
@@ -200,8 +275,18 @@ def collect_node_stats() -> dict:
|
|||||||
display_down = stored_down + session_down
|
display_down = stored_down + session_down
|
||||||
up_speed, down_speed = _calc_speed(node_id, display_up, display_down)
|
up_speed, down_speed = _calc_speed(node_id, display_up, display_down)
|
||||||
|
|
||||||
matched = [c for c in connections if _connection_user(c) == uid]
|
matched = [c for c in connections if _match_connection(c, node)]
|
||||||
online = len(matched) > 0 or (up_speed + down_speed) > 512
|
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)
|
||||||
|
|
||||||
|
online = (
|
||||||
|
len(matched) > 0
|
||||||
|
or (up_speed + down_speed) > 512
|
||||||
|
or (single_node and global_active)
|
||||||
|
)
|
||||||
|
|
||||||
if online:
|
if online:
|
||||||
summary_online += 1
|
summary_online += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user