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:
+25
-12
@@ -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
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user