diff --git a/panel/links.py b/panel/links.py index f688d0d..3c8b30f 100644 --- a/panel/links.py +++ b/panel/links.py @@ -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", + }, + } diff --git a/panel/static/app.js b/panel/static/app.js index 8c43a02..f9a19dc 100644 --- a/panel/static/app.js +++ b/panel/static/app.js @@ -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"); } }); diff --git a/panel/static/style.css b/panel/static/style.css index 6e4c74d..446075d 100644 --- a/panel/static/style.css +++ b/panel/static/style.css @@ -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; diff --git a/panel/templates/dashboard.html b/panel/templates/dashboard.html index b74ee5f..b0ce9ad 100644 --- a/panel/templates/dashboard.html +++ b/panel/templates/dashboard.html @@ -75,15 +75,18 @@
+ 手动核对:SNI={{ node.links.meta.sni }} · pbk={{ node.links.meta.pbk[:20] }}… · sid={{ node.links.meta.sid }} · SpiderX=/ +