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 @@
- - + +
+
- - + +
diff --git a/scripts/generate-keys.sh b/scripts/generate-keys.sh index acf2d00..c4cf109 100644 --- a/scripts/generate-keys.sh +++ b/scripts/generate-keys.sh @@ -22,9 +22,15 @@ else SB="sing-box" fi -KEYPAIR="$("$SB" generate reality-keypair)" -REALITY_PRIVATE_KEY="$(echo "$KEYPAIR" | grep 'PrivateKey:' | awk '{print $2}')" -REALITY_PUBLIC_KEY="$(echo "$KEYPAIR" | grep 'PublicKey:' | awk '{print $2}')" +if command -v xray &>/dev/null; then + KEYPAIR="$(xray x25519)" + REALITY_PRIVATE_KEY="$(echo "$KEYPAIR" | awk '/Private key/ {print $3; exit} /PrivateKey/ {print $3; exit}')" + REALITY_PUBLIC_KEY="$(echo "$KEYPAIR" | awk '/Public key/ {print $3; exit} /Password/ {print $3; exit}')" +else + KEYPAIR="$("$SB" generate reality-keypair)" + REALITY_PRIVATE_KEY="$(echo "$KEYPAIR" | grep 'PrivateKey:' | awk '{print $2}')" + REALITY_PUBLIC_KEY="$(echo "$KEYPAIR" | grep 'PublicKey:' | awk '{print $2}')" +fi REALITY_SHORT_ID="$("$SB" generate rand --hex 8)" GENERATE_PANEL_PASSWORD=1 diff --git a/scripts/render-client.sh b/scripts/render-client.sh index f7e9cf2..b056390 100644 --- a/scripts/render-client.sh +++ b/scripts/render-client.sh @@ -18,13 +18,13 @@ done mkdir -p "$OUT_DIR" urlencode() { - python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe='').replace('.', '%2E'))" "$1" + python3 -c "import sys; print(sys.argv[1].replace('.', '%2E'))" "$1" } REALITY_SNI_ENC="$(urlencode "$REALITY_SERVER_NAME")" -REALITY_PBK_ENC="$(urlencode "$REALITY_PUBLIC_KEY")" -REALITY_SID_ENC="$(urlencode "$REALITY_SHORT_ID")" -HY2_PASSWORD_ENC="$(urlencode "$HY2_PASSWORD")" +REALITY_PBK_ENC="$REALITY_PUBLIC_KEY" +REALITY_SID_ENC="$REALITY_SHORT_ID" +HY2_PASSWORD_ENC="$(python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=''))" "$HY2_PASSWORD")" DOMAIN_SNI_ENC="$(urlencode "$DOMAIN")" sed -e "s|\${VPS_IP}|${VPS_IP}|g" \ @@ -38,7 +38,7 @@ sed -e "s|\${VPS_IP}|${VPS_IP}|g" \ cat > "$OUT_DIR/share-links.txt" < dict: }, "inbounds": [ { + "tag": "vless-reality-in", "listen": "0.0.0.0", "port": 443, "protocol": "vless", diff --git a/scripts/repair-reality.sh b/scripts/repair-reality.sh new file mode 100644 index 0000000..77f2001 --- /dev/null +++ b/scripts/repair-reality.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# 一键修复 VLESS Reality:拉代码、重渲染配置、重启服务、诊断 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +[[ $EUID -eq 0 ]] || { echo "请使用 root 运行: sudo bash scripts/repair-reality.sh"; exit 1; } + +export JIEDIAN_ROOT="$ROOT" + +echo "[1/5] 更新代码 ..." +if [[ -d "$ROOT/.git" ]]; then + git -C "$ROOT" pull --ff-only || echo "(git pull 跳过,请手动同步代码)" +fi + +echo "[2/5] 检查 443 监听进程 ..." +if ss -tlnp 2>/dev/null | grep -q ':443'; then + ss -tlnp | grep ':443' || true +else + echo " 443 端口未监听" +fi + +if [[ -f /etc/sing-box/config.json ]] && grep -q vless-reality /etc/sing-box/config.json 2>/dev/null; then + echo "[!] sing-box 仍含 VLESS Reality,执行迁移 ..." + bash "$ROOT/scripts/migrate-xray-reality.sh" +else + echo "[3/5] 渲染 Xray + sing-box 配置 ..." + python3 "$ROOT/scripts/render-xray.py" + python3 "$ROOT/scripts/render-server.py" + echo "[4/5] 重启服务 ..." + systemctl restart xray sing-box jiedian-panel +fi + +echo "[5/5] 诊断 ..." +bash "$ROOT/scripts/verify-reality.sh" + +echo "" +echo "完成。请在面板重新复制 VLESS 链接,删除 v2rayNG 旧节点后重新导入。" +echo "若仍失败,请执行: journalctl -u xray -n 50 --no-pager" diff --git a/scripts/verify-reality.sh b/scripts/verify-reality.sh index 1703586..5641d72 100644 --- a/scripts/verify-reality.sh +++ b/scripts/verify-reality.sh @@ -57,7 +57,7 @@ fi if command -v xray &>/dev/null && [[ -n "${REALITY_PRIVATE_KEY:-}" ]]; then echo "" echo "========== 公钥配对 ==========" - DERIVED="$(xray x25519 -i "$REALITY_PRIVATE_KEY" 2>/dev/null | awk '/Public key/ {print $3}')" + DERIVED="$(xray x25519 -i "$REALITY_PRIVATE_KEY" 2>/dev/null | awk '/Public key/ {print $3; exit} /Password/ {print $3; exit}')" if [[ -n "$DERIVED" ]]; then if [[ "$DERIVED" == "${REALITY_PUBLIC_KEY:-}" ]]; then echo "公钥与私钥配对: 是"