refactor: remove VLESS/Xray, Hy2-only stack
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-26
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user