import { useState, useEffect } from "react"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { detectV2raynDbPath, getActiveV2raynNode, scanV2raynNodes, buildShareLink, syncWslProxy, getWslProxyStatus, clearWslProxy, testWslProxyIp, testWinProxyIp, getSettings, updateSettings, CONFIG_TYPE_LABEL, type ActiveV2raynNode, type V2raynNode, type WslProxyStatus, } from "../../lib/commands"; import { useUIStore } from "../../store/ui"; const inputCls = "flex-1 border border-gray-200 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:ring-2 focus:ring-blue-200"; // ── Win 代理配置卡片 ────────────────────────────────────────────────────────── function WinProxyCard({ onPathSaved }: { onPathSaved: (path: string) => void }) { const showToast = useUIStore((s) => s.showToast); const qc = useQueryClient(); const { data: settings } = useQuery({ queryKey: ["settings", undefined], queryFn: () => getSettings(undefined), staleTime: 30000, }); const [dbPath, setDbPath] = useState(""); const [detecting, setDetecting] = useState(false); const [loadingNode, setLoadingNode] = useState(false); const [activeNode, setActiveNode] = useState(null); const saved = settings?.v2rayn_db_path ?? ""; useEffect(() => { if (saved) { setDbPath(saved); onPathSaved(saved); } }, [saved]); const handleDetect = async () => { setDetecting(true); try { const found = await detectV2raynDbPath(); if (found) { setDbPath(found); showToast(`已找到:${found}`); } else { showToast("未自动找到,请手动浏览选择文件"); } } finally { setDetecting(false); } }; const handleBrowse = async () => { const selected = await openDialog({ multiple: false, filters: [{ name: "v2rayN 数据库", extensions: ["db"] }], }); if (selected) setDbPath(selected as string); }; const handleSave = async () => { if (!dbPath.trim()) { showToast("❌ 路径不能为空"); return; } await updateSettings({ v2rayn_db_path: dbPath.trim() }, undefined); qc.invalidateQueries({ queryKey: ["settings", undefined] }); onPathSaved(dbPath.trim()); showToast("已保存 ✓"); }; const handleReadNode = async () => { const path = dbPath.trim() || saved; if (!path) { showToast("❌ 请先配置 v2rayN 路径"); return; } setLoadingNode(true); try { const node = await getActiveV2raynNode(path); setActiveNode(node); } catch (e) { showToast(`❌ ${e}`); } finally { setLoadingNode(false); } }; const isChanged = dbPath.trim() !== saved; return (
🪟

Windows 代理(v2rayN)

{/* 路径配置 */}
setDbPath(e.target.value)} placeholder="v2rayN guiNDB.db 路径" className={inputCls} />

通常位于:%APPDATA%\v2rayN\guiConfigs\guiNDB.db

{saved && (
已配置:{saved}
)}
{/* 当前节点 */}
{activeNode ? (
{CONFIG_TYPE_LABEL[activeNode.config_type] ?? "?"} {activeNode.remarks} {activeNode.address}:{activeNode.port}
) : (

点击读取 v2rayN 当前激活节点

)}
); } // ── WSL 代理同步卡片 ────────────────────────────────────────────────────────── function WslSyncCard({ dbPath }: { dbPath: string }) { const showToast = useUIStore((s) => s.showToast); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [syncing, setSyncing] = useState(false); const [clearing, setClearing] = useState(false); const refreshStatus = async () => { setLoading(true); try { const s = await getWslProxyStatus(); setStatus(s); } catch { setStatus(null); } finally { setLoading(false); } }; // 不再自动刷新 — WSL 状态检测较慢,由用户手动点击「刷新状态」触发 const handleSync = async () => { setSyncing(true); try { const msg = await syncWslProxy(dbPath || undefined); showToast(`✓ ${msg}`); await refreshStatus(); } catch (e) { showToast(`❌ ${e}`); } finally { setSyncing(false); } }; const handleClear = async () => { setClearing(true); try { await clearWslProxy(); showToast("WSL 代理配置已清除"); await refreshStatus(); } catch (e) { showToast(`❌ ${e}`); } finally { setClearing(false); } }; return (
🐧

WSL 代理同步

{/* 状态指示 */} {status === null ? (
WSL 状态未知(点击刷新状态)
) : status.configured ? (
{/* 多个警告可并存,由上到下优先级降低 */} {status.stale && (
⚠ IP 已变更,需重新同步 {status.configured_ip ?? "?"} → {status.current_ip ?? "?"}
)} {status.bashrc_ok === false && (
⚠ 非登录 Shell 代理链接已断开 /etc/bash.bashrc source 行缺失,请重新同步
)} {!status.stale && status.bashrc_ok !== false && (
已配置 {status.port && ( http_proxy=http://{status.current_ip ?? "win-host"}:{status.port} )}
)}
) : (
未配置,点击「同步到 WSL」完成设置
)} {/* 操作按钮 */}
{status?.configured && ( )}
{!dbPath && (

⚠ 请先完成上方「Win 代理配置」并保存路径

)} {/* 说明 */}

同步后的效果

写入 /etc/profile.d/v2rayn-proxy.sh,新开终端自动生效

当前终端立即生效:source /etc/profile.d/v2rayn-proxy.sh

适用于 curl、git、npm、pip 等命令行工具

); } // ── 代理一致性测试卡片 ──────────────────────────────────────────────────────── type IpState = { status: "idle" } | { status: "loading" } | { status: "ok"; ip: string } | { status: "error"; msg: string }; function ProxyTestCard() { const [winState, setWinState] = useState({ status: "idle" }); const [wslState, setWslState] = useState({ status: "idle" }); const [testing, setTesting] = useState(false); const runTest = async () => { setTesting(true); setWinState({ status: "loading" }); setWslState({ status: "loading" }); const [winResult, wslResult] = await Promise.allSettled([ testWinProxyIp(), testWslProxyIp(), ]); setWinState( winResult.status === "fulfilled" ? { status: "ok", ip: winResult.value } : { status: "error", msg: String(winResult.reason) }, ); setWslState( wslResult.status === "fulfilled" ? { status: "ok", ip: wslResult.value } : { status: "error", msg: String(wslResult.reason) }, ); setTesting(false); }; const winIp = winState.status === "ok" ? winState.ip : null; const wslIp = wslState.status === "ok" ? wslState.ip : null; const bothDone = winState.status !== "idle" && winState.status !== "loading" && wslState.status !== "idle" && wslState.status !== "loading"; const consistent = bothDone && winIp !== null && wslIp !== null && winIp === wslIp; return (
🔍

代理一致性测试

{/* 结果行 */}
{( [ { label: "🪟 Windows", state: winState }, { label: "🐧 WSL", state: wslState }, ] as const ).map(({ label, state }) => (
{label} {state.status === "idle" && ( 点击测试 )} {state.status === "loading" && ( 请求中… )} {state.status === "ok" && ( {state.ip} )} {state.status === "error" && ( {state.msg} )}
))}
{/* 对比结果 */} {bothDone && (
{consistent ? ( <> 出口 IP 一致,代理效果相同 ) : winIp && wslIp ? ( <> 出口 IP 不一致,请重新同步代理 ) : ( <> 部分测试失败,请检查错误信息 )}
)}

两侧均通过 api.ipify.org 获取出口 IP,测试约需 5-15 秒

); } // ── 节点列表(可选,用于查看/复制)──────────────────────────────────────────── const TYPE_COLOR: Record = { 1: "bg-blue-100 text-blue-700", 3: "bg-purple-100 text-purple-700", 5: "bg-green-100 text-green-700", 6: "bg-orange-100 text-orange-700", 7: "bg-pink-100 text-pink-700", }; function NodeListCard({ dbPath }: { dbPath: string }) { const showToast = useUIStore((s) => s.showToast); const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(false); const [expanded, setExpanded] = useState(false); const handleScan = async () => { if (!dbPath) { showToast("❌ 请先设置 v2rayN 路径"); return; } setLoading(true); try { const result = await scanV2raynNodes(dbPath); setNodes(result); setExpanded(true); } catch (e) { showToast(`❌ ${e}`); } finally { setLoading(false); } }; const copyLink = async (node: V2raynNode) => { const link = buildShareLink(node); if (!link) { showToast("❌ 暂不支持此协议"); return; } await navigator.clipboard.writeText(link).then( () => showToast(`已复制 ${node.remarks} ✓`), () => showToast("❌ 复制失败"), ); }; return (

节点列表

{nodes.length > 0 && ( )}

可查看所有节点并复制 share link

{expanded && nodes.length > 0 && (
{nodes.map((node) => (
{node.remarks} {CONFIG_TYPE_LABEL[node.config_type] ?? "Unknown"}

{node.address}:{node.port}

))}
)}
); } // ── 交互教程 ────────────────────────────────────────────────────────────────── function CodeBlock({ code }: { code: string }) { const showToast = useUIStore((s) => s.showToast); return (
        {code}
      
); } type Section = { id: string; icon: string; title: string; tag?: string; content: React.ReactNode; }; const GUIDE_SECTIONS: Section[] = [ { id: "concept", icon: "💡", title: "核心概念:出口 IP 才是判断标准", content: (

WSL 和 Windows 各有自己的 IP,但这些都是"内部地址",不重要。

你的机器v2rayN (Win)36.50.x.x(出口 IP) → 目标网站

WSL{" "}→ v2rayN (Win)36.50.x.x(出口 IP) → 目标网站

两边出口 IP 相同 = 代理效果一致。用本应用的「代理一致性测试」验证。

), }, { id: "why-simple", icon: "🔗", title: "为什么不需要在 WSL 装 v2rayA", tag: "经验教训", content: (

v2rayA 方案很复杂:需要安装、配置账号、对接 REST API,还有各种坑。

其实 v2rayN 已经开了 AllowLAN = true,WSL 可以直接通过 Windows 宿主机 IP 访问它:

结论:WSL 只需要设置环境变量,不需要安装任何代理软件。

), }, { id: "quickpath", icon: "⚡", title: "最佳捷径:三步完成配置", tag: "推荐流程", content: (

① 确认 v2rayN 开启了 AllowLAN

v2rayN → 参数设置 → Core 类型设置 → 确认「允许来自局域网的连接」已勾选

② 本应用一键同步

「Win 代理配置」设好路径 → 点「同步到 WSL」→ 自动写入代理配置

③ 重开终端或手动 source

重装后恢复:装好 v2rayN → 装好本应用 → 点「同步到 WSL」,完成。

), }, { id: "pitfall-binary", icon: "⚠️", title: "坑:工具是 Windows 程序,WSL 代理对它无效", tag: "常见陷阱", content: (

在 WSL 里敲命令,不代表它就是 Linux 程序。路径以 /mnt/c/ 开头的都是 Windows 程序,走 Windows 网络栈,WSL 的 http_proxy 对它完全无效。

快速诊断:

解决方案:安装 Linux 原生版本

), }, { id: "pitfall-shell", icon: "⚠️", title: "坑:代理变量只在部分 Shell 类型生效", tag: "常见陷阱", content: (
{[ ["登录 Shell(Ubuntu 终端图标)", "/etc/profile.d/", "✓ 自动"], ["非登录交互 Shell(VSCode 终端)", "~/.bashrc", "✓ 自动(同步时已写入)"], ["非交互 Shell(脚本、子进程)", "不加载任何配置", "需继承父进程环境"], ].map(([type, source, result]) => ( ))}
Shell 类型 加载来源 代理是否自动生效
{type} {source} {result}

本应用同步时会同时写入 /etc/profile.d//etc/bash.bashrc~/.bashrc,覆盖所有场景。

如果当前终端没生效(旧会话):

), }, { id: "diagnose", icon: "🔍", title: "快速诊断清单", content: (

遇到 WSL 里无法联网时,按顺序排查:

), }, ]; function ProxyGuideCard() { const [open, setOpen] = useState(false); const [activeId, setActiveId] = useState(null); return (
{/* 标题栏 */} {open && (
{/* 章节导航 */}
{GUIDE_SECTIONS.map((s) => ( ))}
{/* 内容区 */}
{GUIDE_SECTIONS.map((s) => (
{activeId === s.id && (
{s.content}
)}
))}
)}
); } // ── 主页面 ──────────────────────────────────────────────────────────────────── export function ProxyPage() { const [savedDbPath, setSavedDbPath] = useState(""); return (

代理管理

读取 Windows v2rayN 代理配置,一键同步到 WSL 环境变量

); }