refactor: remove VLESS/Xray, Hy2-only stack

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 00:26:18 +08:00
parent c2c8ae826d
commit 6a42f58f5b
27 changed files with 159 additions and 1322 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -26
View File
@@ -27,7 +27,6 @@ from stats import collect_node_stats
ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
SECRET_FILE = ROOT / "data" / ".panel_secret"
RENDER_SCRIPT = ROOT / "scripts" / "render-server.py"
RENDER_XRAY_SCRIPT = ROOT / "scripts" / "render-xray.py"
_apply_lock = threading.Lock()
@@ -114,24 +113,9 @@ def render_singbox_config() -> tuple[bool, str]:
return True, "ok"
def render_xray_config() -> tuple[bool, str]:
env = os.environ.copy()
env["JIEDIAN_ROOT"] = str(ROOT)
proc = subprocess.run(
["python3", str(RENDER_XRAY_SCRIPT)],
capture_output=True,
text=True,
env=env,
)
if proc.returncode != 0:
return False, proc.stderr or proc.stdout or "Xray 配置生成失败"
return True, "ok"
def restart_services_async() -> None:
"""后台重启 sing-box 与 Xray。"""
def restart_singbox_async() -> None:
subprocess.Popen(
["systemctl", "restart", "xray", "sing-box"],
["systemctl", "restart", "sing-box"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
@@ -141,17 +125,10 @@ def apply_singbox() -> tuple[bool, str]:
ok, msg = render_singbox_config()
if not ok:
return False, msg
ok, msg = render_xray_config()
if not ok:
return False, msg
restart_services_async()
restart_singbox_async()
return True, "ok"
def restart_singbox_async() -> None:
restart_services_async()
def apply_singbox_background(on_fail=None) -> None:
"""后台生成配置并重启 sing-box,避免阻塞 HTTP 请求导致 Nginx 503。"""
+4 -34
View File
@@ -1,4 +1,4 @@
"""分享链接生成。"""
"""分享链接生成Hysteria2"""
from __future__ import annotations
import os
@@ -21,44 +21,14 @@ def load_env() -> dict[str, str]:
return env
def _sni_qs(value: str) -> str:
"""SNI in share links — use plain domain; clients decode URL themselves."""
return value
def build_links(node: dict, env: dict | None = None) -> dict[str, str | dict[str, str]]:
def build_links(node: dict, env: dict | None = None) -> dict[str, str]:
env = env or load_env()
vps_ip = env["VPS_IP"]
domain = env["DOMAIN"]
reality_sni = env.get("REALITY_SERVER_NAME", "www.microsoft.com")
public_key = env["REALITY_PUBLIC_KEY"]
short_id = env["REALITY_SHORT_ID"]
name = quote(node["name"])
port = hy2_port(node, list_nodes())
# Parameter order follows Xray VLESS share-link convention; pbk/sid stay raw.
vless = (
f"vless://{node['uuid']}@{vps_ip}:443"
f"?type=tcp&security=reality&encryption=none&flow=xtls-rprx-vision"
f"&sni={_sni_qs(reality_sni)}&fp=chrome&pbk={public_key}&sid={short_id}"
f"&spx=%2F#{name}"
)
hy2 = (
f"hy2://{quote(node['hy2_password'], safe='')}@{domain}:{port}"
f"?sni={_sni_qs(domain)}#{name}-Hy2"
f"?sni={domain}#{name}-Hy2"
)
return {
"vless": vless,
"hy2": hy2,
"meta": {
"address": vps_ip,
"port": "443",
"uuid": node["uuid"],
"sni": reality_sni,
"pbk": public_key,
"sid": short_id,
"spx": "/",
"fp": "chrome",
"flow": "xtls-rprx-vision",
},
}
return {"hy2": hy2}
+2 -3
View File
@@ -72,15 +72,14 @@ document.querySelectorAll(".copy-row").forEach((row) => {
btn.addEventListener("click", async () => {
const card = row.closest(".node-card");
const nodeId = card?.dataset.id;
const kind = btn.dataset.linkKind || input.dataset.linkKind || "vless";
let text = input.value;
if (nodeId) {
try {
const nodes = await fetchNodeLinks();
const node = nodes.find((item) => String(item.id) === String(nodeId));
if (node?.links) {
text = kind === "hy2" ? node.links.hy2 : node.links.vless;
if (node?.links?.hy2) {
text = node.links.hy2;
}
} catch {
/* fall back to input.value */
+4 -50
View File
@@ -1,4 +1,4 @@
"""从 sing-box Clash API 采集节点连接与流量(官方预编译包不含 v2ray_api)"""
"""从 sing-box Clash API 采集节点连接与流量。"""
from __future__ import annotations
import json
@@ -18,15 +18,10 @@ ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
ENV_FILE = ROOT / ".env"
CLASH_ADDR = "127.0.0.1:9090"
_VLESS_INBOUND = "vless-reality-in"
_XRAY_ACCESS_LOG = Path("/var/log/xray/access.log")
_LOG_USER_RE = re.compile(
r"\[([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\]\s+inbound connection"
)
_LOG_INDEX_RE = re.compile(r"\[(\d+)\] inbound connection")
_LOG_XRAY_UUID_RE = re.compile(
r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"
)
_speed_cache: dict[int, tuple[float, int, int]] = {}
_conn_cache: dict[str, dict[str, int | str]] = {}
@@ -76,28 +71,8 @@ def fetch_clash_connections() -> tuple[list[dict], bool]:
return payload.get("connections") or [], True
def fetch_xray_access_uuids(nodes: list[dict]) -> set[str]:
"""VLESS Reality 由 Xray 承载,从 access.log 读取近期活跃 UUID。"""
if not _XRAY_ACCESS_LOG.exists():
return set()
try:
text = _XRAY_ACCESS_LOG.read_text(encoding="utf-8", errors="ignore")
except OSError:
return set()
known = {node["uuid"] for node in nodes}
active: set[str] = set()
for line in text.splitlines()[-400:]:
if "accepted" not in line:
continue
for match in _LOG_XRAY_UUID_RE.finditer(line):
uid = match.group(1)
if uid in known:
active.add(uid)
return active
def fetch_recent_log_uuids(nodes: list[dict]) -> set[str]:
"""sing-box Clash API 不导出 user 字段,VLESS 多用户需从近期日志补全在线 UUID。"""
"""sing-box Clash API 不导出 user 字段,多节点 Hy2 从近期日志补全在线 UUID。"""
try:
proc = subprocess.run(
[
@@ -134,7 +109,6 @@ def fetch_recent_log_uuids(nodes: list[dict]) -> set[str]:
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()
@@ -193,17 +167,6 @@ def _match_connection(conn: dict, node: dict, *, single_node: bool = False) -> b
return False
def _match_vless_connection(conn: dict, node: dict, log_active: set[str]) -> bool:
tag = _connection_inbound_tag(conn)
if tag != _VLESS_INBOUND:
return False
user = _connection_user(conn)
if user == node["uuid"]:
return True
# 共享 VLESS inbound 无法从 Clash API 区分用户;仅唯一活跃用户时归因
return node["uuid"] in log_active and len(log_active) == 1
def _connection_id(conn: dict) -> str:
return str(conn.get("id") or "")
@@ -265,9 +228,7 @@ def _sync_connections(
connections: list[dict],
nodes: list[dict],
uuid_to_node: dict[str, int],
log_active: set[str],
) -> dict[str, tuple[int, int]]:
"""同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。"""
seen: set[str] = set()
active: dict[str, tuple[int, int]] = {}
single_node = len(nodes) == 1
@@ -276,9 +237,7 @@ def _sync_connections(
for conn in connections:
matched_uuid = ""
for node in nodes:
if _match_connection(conn, node, single_node=single_node) or _match_vless_connection(
conn, node, log_active
):
if _match_connection(conn, node, single_node=single_node):
matched_uuid = node["uuid"]
break
if not matched_uuid and single_node and connections:
@@ -346,7 +305,6 @@ def _connections_for_node(
c
for c in connections
if _match_connection(c, node, single_node=single_node)
or _match_vless_connection(c, node, log_active)
]
if matched:
return matched
@@ -355,9 +313,6 @@ def _connections_for_node(
return connections
if node["uuid"] in log_active:
vless_hits = [c for c in connections if _connection_inbound_tag(c) == _VLESS_INBOUND]
if vless_hits:
return vless_hits
return [None]
return []
@@ -367,8 +322,7 @@ def collect_node_stats() -> dict:
uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes}
connections, clash_ok = fetch_clash_connections()
log_active = fetch_recent_log_uuids(nodes) if len(nodes) > 1 else set()
log_active |= fetch_xray_access_uuids(nodes)
active_by_uuid = _sync_connections(connections, nodes, uuid_to_node, log_active)
active_by_uuid = _sync_connections(connections, nodes, uuid_to_node)
single_node = len(nodes) == 1
has_connections = len(connections) > 0
global_up_speed, global_down_speed = _global_conn_speed(connections) if clash_ok else (0.0, 0.0)
+4 -13
View File
@@ -13,7 +13,7 @@
<section class="hero">
<div>
<h1>节点列表</h1>
<p class="muted">VPS {{ vps_ip }} · Reality 443 · Hysteria2 8443+</p>
<p class="muted">VPS {{ vps_ip }} · Hysteria2 8443+</p>
</div>
<button id="addBtn" class="btn primary">+ 添加节点</button>
</section>
@@ -72,22 +72,13 @@
</div>
</div>
</div>
<div class="field">
<label>VLESS + Reality</label>
<div class="copy-row">
<input class="copy-input" readonly value="{{ node.links.vless }}" data-link-kind="vless">
<button type="button" class="btn copy-btn" data-link-kind="vless">复制</button>
</div>
<p class="link-hint muted">
手动核对:SNI={{ node.links.meta.sni }} · pbk={{ node.links.meta.pbk[:20] }}… · sid={{ node.links.meta.sid }} · SpiderX=/
</p>
</div>
<div class="field">
<label>Hysteria2</label>
<div class="copy-row">
<input class="copy-input" readonly value="{{ node.links.hy2 }}" data-link-kind="hy2">
<button type="button" class="btn copy-btn" data-link-kind="hy2">复制</button>
<input class="copy-input" readonly value="{{ node.links.hy2 }}">
<button type="button" class="btn copy-btn">复制</button>
</div>
<p class="link-hint muted">一设备一节点;SNI 为 {{ domain }},端口随节点递增(8443、8444…)</p>
</div>
<div class="node-actions">
<button class="btn danger delete-btn" data-id="{{ node.id }}">删除</button>