fix: improve VLESS link compatibility and add Reality repair script

Only encode SNI dots, keep pbk/sid raw, copy links via API, prefer xray keygen, and add repair-reality.sh for server-side fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-27 23:58:14 +08:00
parent 40d10a846f
commit 1fc3b8a89c
9 changed files with 127 additions and 28 deletions
+25 -12
View File
@@ -21,14 +21,12 @@ def load_env() -> dict[str, str]:
return env
def _qs(value: str) -> str:
"""URL-encode query / userinfo values for share links."""
# quote() leaves unreserved chars like '.' unencoded; some clients (v2rayNG)
# misparsed sni=www.microsoft.com — force dots encoded for compatibility.
return quote(value, safe="").replace(".", "%2E")
def _sni_qs(value: str) -> str:
"""Encode dots in SNI; some clients misparsed sni=www.microsoft.com."""
return value.replace(".", "%2E")
def build_links(node: dict, env: dict | None = None) -> dict[str, str]:
def build_links(node: dict, env: dict | None = None) -> dict[str, str | dict[str, str]]:
env = env or load_env()
vps_ip = env["VPS_IP"]
domain = env["DOMAIN"]
@@ -38,14 +36,29 @@ def build_links(node: dict, env: dict | None = None) -> dict[str, str]:
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"?encryption=none&flow=xtls-rprx-vision&security=reality"
f"&sni={_qs(reality_sni)}&fp=chrome&pbk={_qs(public_key)}&sid={_qs(short_id)}"
f"&spx=%2F&type=tcp#{name}"
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://{_qs(node['hy2_password'])}@{domain}:{port}"
f"?sni={_qs(domain)}#{name}-Hy2"
f"hy2://{quote(node['hy2_password'], safe='')}@{domain}:{port}"
f"?sni={_sni_qs(domain)}#{name}-Hy2"
)
return {"vless": vless, "hy2": 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",
},
}
+33 -3
View File
@@ -45,6 +45,20 @@ function copyText(text) {
});
}
let nodeLinksCache = null;
async function fetchNodeLinks() {
if (nodeLinksCache) {
return nodeLinksCache;
}
const res = await fetch(apiUrl("/api/nodes"), { credentials: "same-origin" });
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
nodeLinksCache = await res.json();
return nodeLinksCache;
}
document.querySelectorAll(".copy-row").forEach((row) => {
const input = row.querySelector(".copy-input");
const btn = row.querySelector(".copy-btn");
@@ -56,17 +70,33 @@ document.querySelectorAll(".copy-row").forEach((row) => {
});
btn.addEventListener("click", async () => {
const text = input.value;
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;
}
} catch {
/* fall back to input.value */
}
}
if (!text) {
toast("没有可复制的内容");
return;
}
try {
input.select();
input.setSelectionRange(0, text.length);
await copyText(text);
toast("已复制到剪贴板");
} catch {
input.select();
input.setSelectionRange(0, text.length);
toast("复制失败,请选中上方链接后 Ctrl+C");
}
});
+7
View File
@@ -85,6 +85,13 @@ input.copy-input {
font-size: 0.82rem;
}
.link-hint {
margin: 6px 0 0;
font-size: 0.78rem;
line-height: 1.45;
word-break: break-all;
}
.btn {
border: 1px solid var(--border);
background: #111827;
+7 -4
View File
@@ -75,15 +75,18 @@
<div class="field">
<label>VLESS + Reality</label>
<div class="copy-row">
<input class="copy-input" readonly value="{{ node.links.vless }}">
<button type="button" class="btn copy-btn">复制</button>
<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 }}">
<button type="button" class="btn copy-btn">复制</button>
<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>
</div>
</div>
<div class="node-actions">