import { forwardRef, useImperativeHandle, useState, useMemo, useEffect, ReactNode, } from "react"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; import useSWR, { mutate } from "swr"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; // Новые импорты import { DialogRef, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-provider"; import { getClashConfig } from "@/services/api"; import { getAutotemProxy, getNetworkInterfacesInfo, getSystemHostname, getSystemProxy, patchVergeConfig, } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Check, ChevronsUpDown, Edit, Loader2 } from "lucide-react"; import { cn } from "@root/lib/utils"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; // --- Вся ваша оригинальная логика, константы и хелперы --- const DEFAULT_PAC = `function FindProxyForURL(url, host) { return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; }`; const ipv4_part = String.raw`\d{1,3}`; const rDomainSimple = String.raw`(?:[a-z0-9\-\*]+\.|\*)*` + String.raw`(?:\w{2,64}\*?|\*)`; const ipv6_part = "(?:[a-fA-F0-9:])+"; const rLocal = `localhost||localdomain`; const getValidReg = (isWindows: boolean) => { const rIPv4Unix = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}(?:\/\d{1,2})?`; const rIPv4Windows = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`; const rIPv6Unix = String.raw`(?:${ipv6_part}:+)+${ipv6_part}(?:\/\d{1,3})?`; const rIPv6Windows = String.raw`(?:${ipv6_part}:+)+${ipv6_part}`; const rValidPart = `${rDomainSimple}|${isWindows ? rIPv4Windows : rIPv4Unix}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`; const separator = isWindows ? ";" : ","; const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`; return new RegExp(rValid); }; // --- Компонент Combobox для замены Autocomplete --- const Combobox = ({ options, value, onValueChange, placeholder, }: { options: string[]; value: string; onValueChange: (value: string) => void; placeholder?: string; }) => { const [open, setOpen] = useState(false); return ( No results found. {options.map((option) => ( { onValueChange( options.find((opt) => opt.toLowerCase() === currentValue) || "", ); setOpen(false); }} > {option} ))} ); }; // --- Наш переиспользуемый компонент для строки настроек --- const SettingRow = ({ label, children, }: { label: React.ReactNode; children?: React.ReactNode; }) => (
{children}
); export const SysproxyViewer = forwardRef((props, ref) => { const { t } = useTranslation(); const isWindows = getSystem() === "windows"; const validReg = useMemo(() => getValidReg(isWindows), [isWindows]); const [open, setOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false); const [saving, setSaving] = useState(false); const { verge, patchVerge, mutateVerge } = useVerge(); const [hostOptions, setHostOptions] = useState([]); type SysProxy = Awaited>; const [sysproxy, setSysproxy] = useState(); type AutoProxy = Awaited>; const [autoproxy, setAutoproxy] = useState(); const { enable_system_proxy: enabled, proxy_auto_config, pac_file_content, enable_proxy_guard, use_default_bypass, system_proxy_bypass, proxy_guard_duration, proxy_host, } = verge ?? {}; const [value, setValue] = useState({ guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10, use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC, proxy_host: proxy_host ?? "127.0.0.1", }); const defaultBypass = () => { if (isWindows) return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;"; if (getSystem() === "linux") return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1"; return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; }; const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, { revalidateOnFocus: false, revalidateIfStale: true, dedupingInterval: 1000, errorRetryInterval: 5000, }); const [prevMixedPort, setPrevMixedPort] = useState( clashConfig?.["mixed-port"], ); useEffect(() => { if ( clashConfig?.["mixed-port"] && clashConfig?.["mixed-port"] !== prevMixedPort ) { setPrevMixedPort(clashConfig?.["mixed-port"]); resetSystemProxy(); } }, [clashConfig?.["mixed-port"]]); const resetSystemProxy = async () => { try { const currentSysProxy = await getSystemProxy(); const currentAutoProxy = await getAutotemProxy(); if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) { await patchVergeConfig({ enable_system_proxy: false }); await new Promise((resolve) => setTimeout(resolve, 200)); await patchVergeConfig({ enable_system_proxy: true }); await Promise.all([ mutate("getSystemProxy"), mutate("getAutotemProxy"), ]); } } catch (err: any) { showNotice("error", err.toString()); } }; const { systemProxyAddress } = useAppData(); const getSystemProxyAddress = useMemo(() => { if (!clashConfig) return "-"; const isPacMode = value.pac ?? false; if (isPacMode) { const host = value.proxy_host || "127.0.0.1"; const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897; return `${host}:${port}`; } else { return systemProxyAddress; } }, [ value.pac, value.proxy_host, verge?.verge_mixed_port, clashConfig, systemProxyAddress, ]); const getCurrentPacUrl = useMemo(() => { const host = value.proxy_host || "127.0.0.1"; const port = import.meta.env.DEV ? 11233 : 33331; return `http://${host}:${port}/commands/pac`; }, [value.proxy_host]); const fetchNetworkInterfaces = async () => { try { const interfaces = await getNetworkInterfacesInfo(); const ipAddresses: string[] = []; interfaces.forEach((iface) => { iface.addr.forEach((address) => { if (address.V4 && address.V4.ip) ipAddresses.push(address.V4.ip); if (address.V6 && address.V6.ip) ipAddresses.push(address.V6.ip); }); }); let hostname = ""; try { hostname = await getSystemHostname(); if (hostname && hostname !== "localhost" && hostname !== "127.0.0.1") { hostname = hostname + ".local"; } } catch (err) { console.error("Failed to get hostname:", err); } const options = ["127.0.0.1", "localhost"]; if (hostname) options.push(hostname); options.push(...ipAddresses); setHostOptions(Array.from(new Set(options))); } catch (error) { console.error("Failed to get network interfaces:", error); setHostOptions(["127.0.0.1", "localhost"]); } }; useImperativeHandle(ref, () => ({ open: () => { setOpen(true); setValue({ guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10, use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC, proxy_host: proxy_host ?? "127.0.0.1", }); getSystemProxy().then(setSysproxy); getAutotemProxy().then(setAutoproxy); fetchNetworkInterfaces(); }, close: () => setOpen(false), })); const onSave = useLockFn(async () => { if (value.duration < 1) { showNotice( "error", t("Proxy Daemon Duration Cannot be Less than 1 Second"), ); return; } if (value.bypass && !validReg.test(value.bypass)) { showNotice("error", t("Invalid Bypass Format")); return; } // 修改验证规则,允许IP和主机名 const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; const hostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; if ( !ipv4Regex.test(value.proxy_host) && !ipv6Regex.test(value.proxy_host) && !hostnameRegex.test(value.proxy_host) ) { showNotice("error", t("Invalid Proxy Host Format")); return; } setSaving(true); setOpen(false); setSaving(false); const patch: Partial = {}; if (value.guard !== enable_proxy_guard) { patch.enable_proxy_guard = value.guard; } if (value.duration !== proxy_guard_duration) { patch.proxy_guard_duration = value.duration; } if (value.bypass !== system_proxy_bypass) { patch.system_proxy_bypass = value.bypass; } if (value.pac !== proxy_auto_config) { patch.proxy_auto_config = value.pac; } if (value.use_default !== use_default_bypass) { patch.use_default_bypass = value.use_default; } let pacContent = value.pac_content; if (pacContent) { pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host); // 将 mixed-port 转换为字符串 const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString(); pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr); } if (pacContent !== pac_file_content) { patch.pac_file_content = pacContent; } // 处理IPv6地址,如果是IPv6地址但没有被方括号包围,则添加方括号 let proxyHost = value.proxy_host; if ( ipv6Regex.test(proxyHost) && !proxyHost.startsWith("[") && !proxyHost.endsWith("]") ) { proxyHost = `[${proxyHost}]`; } if (proxyHost !== proxy_host) { patch.proxy_host = proxyHost; } // 判断是否需要重置系统代理 const needResetProxy = value.pac !== proxy_auto_config || proxyHost !== proxy_host || pacContent !== pac_file_content || value.bypass !== system_proxy_bypass || value.use_default !== use_default_bypass; Promise.resolve().then(async () => { try { // 乐观更新本地状态 if (Object.keys(patch).length > 0) { mutateVerge({ ...verge, ...patch }, false); } if (Object.keys(patch).length > 0) { await patchVerge(patch); } setTimeout(async () => { try { await Promise.all([ mutate("getSystemProxy"), mutate("getAutotemProxy"), ]); // 如果需要重置代理且代理当前启用 if (needResetProxy && enabled) { const [currentSysProxy, currentAutoProxy] = await Promise.all([ getSystemProxy(), getAutotemProxy(), ]); const isProxyActive = value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable; if (isProxyActive) { await patchVergeConfig({ enable_system_proxy: false }); await new Promise((resolve) => setTimeout(resolve, 50)); await patchVergeConfig({ enable_system_proxy: true }); await Promise.all([ mutate("getSystemProxy"), mutate("getAutotemProxy"), ]); } } } catch (err) { console.warn("Proxy status update failed:", err); } }, 50); } catch (err: any) { console.error("Configuration save failed:", err); mutateVerge(); showNotice("error", err.toString()); // setOpen(true); } }); }); return ( <> {t("System Proxy Setting")}
{t("Enable status")} {value.pac ? autoproxy?.enable ? t("Enabled") : t("Disabled") : sysproxy?.enable ? t("Enabled") : t("Disabled")}
{!value.pac && (
{t("Server Addr")} {getSystemProxyAddress}
)} {value.pac && (
{t("PAC URL")} {getCurrentPacUrl || "-"}
)}
setValue((v) => ({ ...v, proxy_host: val })) } placeholder="127.0.0.1" /> setValue((v) => ({ ...v, pac: e }))} /> {t("Proxy Guard")}{" "} } > setValue((v) => ({ ...v, guard: e }))} />
setValue((v) => ({ ...v, duration: +e.target.value.replace(/\D/, ""), })) } /> s
{!value.pac && ( setValue((v) => ({ ...v, use_default: e, bypass: !e && !v.bypass ? defaultBypass() : v.bypass, })) } /> )} {!value.pac && !value.use_default && (