feat: 合并边缘节点运维并完善 Surge 配置安全

迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。

完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。

验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
2026-10-03 23:19:59 +08:00
parent 7358008936
commit 5326c572a2
93 changed files with 6780 additions and 320 deletions

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Run LOCALLY. Emits share links + QR codes + sing-box snippets for the QUIC plane
(Hysteria2 / TUIC x direct/WARP). Companion to 14-generate-links-and-qr.py, which
covers the six TCP inbounds.
QR codes are produced offline with the local `qrencode` binary -- these carry live
credentials and must never be pasted into an online QR generator.
"""
import json, os, re, subprocess, sys
from urllib.parse import quote
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "node-links")
def load_env():
env, path = {}, os.path.join(HERE, "config.env")
if not os.path.exists(path):
sys.exit("ERROR: config.env not found")
for line in open(path):
m = re.match(r'\s*([A-Z0-9_]+)\s*=\s*"?([^"#\n]*)"?', line)
if m:
env[m.group(1)] = m.group(2).strip()
return env
E = load_env()
COVER = "%s.%s" % (E["SUB_COVER"], E["DOMAIN_ROOT"])
NAME = E.get("NODE_NAME", "Edge Node")
PW = E["NODE_PASSWORD"]
UUID = E["XUI_UUID"]
OBFS = E["HY2_OBFS_PASSWORD"]
CC = E.get("TUIC_CC", "bbr")
os.makedirs(OUT, exist_ok=True)
def write(fname, content):
with open(os.path.join(OUT, fname), "w") as f:
f.write(content + "\n")
print(" wrote", fname)
def qr(fname, data):
try:
subprocess.run(["qrencode", "-o", os.path.join(OUT, fname), "-s", "6", data], check=True)
print(" wrote", fname)
except (FileNotFoundError, subprocess.CalledProcessError):
print(" (skipped %s -- install qrencode for QR codes)" % fname)
def p(name):
"""Empty/absent port -> protocol not deployed on this node; skip it."""
return (E.get(name) or "").strip()
# --- Hysteria2: hy2:// is the format sing-box / v2rayN / Shadowrocket accept ---
for suffix, port in (("direct", p("PORT_HY2_DIRECT")), ("warp", p("PORT_HY2_WARP"))):
if not port:
continue
tag = "%s-hy2-%s" % (NAME, suffix if suffix == "direct" else "warp-clean-exit")
link = ("hy2://%s@%s:%s/?sni=%s&alpn=h3&obfs=salamander&obfs-password=%s#%s"
% (quote(PW, safe=""), COVER, port, COVER, quote(OBFS, safe=""), quote(tag)))
write("hy2-%s.link" % suffix, link)
qr("hy2-%s-qr.png" % suffix, link)
write("hy2-%s-singbox.json" % suffix, json.dumps({
"type": "hysteria2", "tag": tag, "server": COVER, "server_port": int(port),
"password": PW,
"obfs": {"type": "salamander", "password": OBFS},
"up_mbps": int(E.get("HY2_UP_MBPS", 500)), "down_mbps": int(E.get("HY2_DOWN_MBPS", 500)),
"tls": {"enabled": True, "server_name": COVER, "alpn": ["h3"]},
}, indent=2))
# --- TUIC v5 ---
for suffix, port in (("direct", p("PORT_TUIC_DIRECT")), ("warp", p("PORT_TUIC_WARP"))):
if not port:
continue
tag = "%s-tuic-%s" % (NAME, suffix if suffix == "direct" else "warp-clean-exit")
link = ("tuic://%s:%s@%s:%s?sni=%s&alpn=h3&congestion_control=%s&udp_relay_mode=native#%s"
% (UUID, quote(PW, safe=""), COVER, port, COVER, CC, quote(tag)))
write("tuic-%s.link" % suffix, link)
qr("tuic-%s-qr.png" % suffix, link)
write("tuic-%s-singbox.json" % suffix, json.dumps({
"type": "tuic", "tag": tag, "server": COVER, "server_port": int(port),
"uuid": UUID, "password": PW, "congestion_control": CC,
"udp_relay_mode": "native", "zero_rtt_handshake": False, "heartbeat": "10s",
"tls": {"enabled": True, "server_name": COVER, "alpn": ["h3"]},
}, indent=2))
print("DONE — QUIC links + QR + sing-box snippets in", OUT)
print("NOTE: hy2:// and tuic:// share-link formats are client-dependent; the sing-box")
print(" JSON snippets are the unambiguous fallback if a client rejects a link.")