迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
92 lines
3.6 KiB
Python
Executable File
92 lines
3.6 KiB
Python
Executable File
#!/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.")
|