import { AccessTimeRounded, ChevronRight, NetworkCheckRounded, WifiOff as SignalError, SignalWifi3Bar as SignalGood, SignalWifi2Bar as SignalMedium, SignalWifi0Bar as SignalNone, SignalWifi4Bar as SignalStrong, SignalWifi1Bar as SignalWeak, SortByAlphaRounded, SortRounded, } from "@mui/icons-material"; import { Box, Button, Chip, FormControl, IconButton, InputLabel, MenuItem, Select, SelectChangeEvent, Tooltip, Typography, alpha, useTheme, } from "@mui/material"; import { useLockFn } from "ahooks"; import React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { EnhancedCard } from "@/components/home/enhanced-card"; import { useProfiles } from "@/hooks/use-profiles"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; import delayManager from "@/services/delay"; import { debugLog } from "@/utils/debug"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; const STORAGE_KEY_PROXY = "clash-verge-selected-proxy"; const STORAGE_KEY_SORT_TYPE = "clash-verge-proxy-sort-type"; const AUTO_CHECK_INITIAL_DELAY_MS = 1500; const AUTO_CHECK_DEFAULT_INTERVAL_MINUTES = 5; // 代理节点信息接口 interface ProxyOption { name: string; } // 排序类型: 默认 | 按延迟 | 按字母 type ProxySortType = 0 | 1 | 2; function convertDelayColor( delayValue: number, ): "success" | "warning" | "error" | "primary" | "default" { const colorStr = delayManager.formatDelayColor(delayValue); if (!colorStr) return "default"; const mainColor = colorStr.split(".")[0]; switch (mainColor) { case "success": return "success"; case "warning": return "warning"; case "error": return "error"; case "primary": return "primary"; default: return "default"; } } function getSignalIcon(delay: number): { icon: React.ReactElement; text: string; color: string; } { if (delay < 0) return { icon: , text: "未测试", color: "text.secondary" }; if (delay >= 10000) return { icon: , text: "超时", color: "error.main" }; if (delay >= 500) return { icon: , text: "延迟较高", color: "error.main" }; if (delay >= 300) return { icon: , text: "延迟中等", color: "warning.main" }; if (delay >= 200) return { icon: , text: "延迟良好", color: "info.main" }; return { icon: , text: "延迟极佳", color: "success.main" }; } export const CurrentProxyCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const theme = useTheme(); const { proxies, clashConfig, refreshProxy, rules } = useAppData(); const { verge } = useVerge(); const { current: currentProfile } = useProfiles(); const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false; const defaultLatencyTimeout = verge?.default_latency_timeout; const autoDelayIntervalMs = useMemo(() => { const rawInterval = verge?.auto_delay_detection_interval_minutes; const intervalMinutes = typeof rawInterval === "number" && rawInterval > 0 ? rawInterval : AUTO_CHECK_DEFAULT_INTERVAL_MINUTES; return Math.max(1, Math.round(intervalMinutes)) * 60 * 1000; }, [verge?.auto_delay_detection_interval_minutes]); const currentProfileId = currentProfile?.uid || null; const getProfileStorageKey = useCallback( (baseKey: string) => currentProfileId ? `${baseKey}:${currentProfileId}` : baseKey, [currentProfileId], ); const readProfileScopedItem = useCallback( (baseKey: string) => { if (typeof window === "undefined") return null; const profileKey = getProfileStorageKey(baseKey); const profileValue = localStorage.getItem(profileKey); if (profileValue != null) { return profileValue; } if (profileKey !== baseKey) { const legacyValue = localStorage.getItem(baseKey); if (legacyValue != null) { localStorage.removeItem(baseKey); localStorage.setItem(profileKey, legacyValue); return legacyValue; } } return null; }, [getProfileStorageKey], ); const writeProfileScopedItem = useCallback( (baseKey: string, value: string) => { if (typeof window === "undefined") return; const profileKey = getProfileStorageKey(baseKey); localStorage.setItem(profileKey, value); if (profileKey !== baseKey) { localStorage.removeItem(baseKey); } }, [getProfileStorageKey], ); // 统一代理选择器 const { handleSelectChange } = useProxySelection({ onSuccess: () => { refreshProxy(); }, onError: (error) => { console.error("代理切换失败", error); refreshProxy(); }, }); // 判断模式 const mode = clashConfig?.mode?.toLowerCase() || "rule"; const isGlobalMode = mode === "global"; const isDirectMode = mode === "direct"; // Sorting type state const [sortType, setSortType] = useState(() => { const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE); return savedSortType ? (Number(savedSortType) as ProxySortType) : 0; }); const [delaySortRefresh, setDelaySortRefresh] = useState(0); const normalizePolicyName = useCallback( (value?: string | null) => (typeof value === "string" ? value.trim() : ""), [], ); const matchPolicyName = useMemo(() => { if (!Array.isArray(rules)) return ""; for (let index = rules.length - 1; index >= 0; index -= 1) { const rule = rules[index]; if (!rule) continue; if ( typeof rule?.type === "string" && rule.type.toUpperCase() === "MATCH" ) { const policy = normalizePolicyName(rule.proxy); if (policy) { return policy; } } } return ""; }, [rules, normalizePolicyName]); type ProxyGroupOption = { name: string; now: string; all: string[]; type?: string; }; type ProxyState = { proxyData: { groups: ProxyGroupOption[]; records: Record; }; selection: { group: string; proxy: string; }; displayProxy: any; }; const [state, setState] = useState({ proxyData: { groups: [], records: {}, }, selection: { group: "", proxy: "", }, displayProxy: null, }); const autoCheckInProgressRef = useRef(false); const latestTimeoutRef = useRef( verge?.default_latency_timeout || 10000, ); const latestProxyRecordRef = useRef(null); useEffect(() => { latestTimeoutRef.current = verge?.default_latency_timeout || 10000; }, [verge?.default_latency_timeout]); useEffect(() => { if (!state.selection.proxy) { latestProxyRecordRef.current = null; return; } latestProxyRecordRef.current = state.proxyData.records?.[state.selection.proxy] || null; }, [state.selection.proxy, state.proxyData.records]); // 初始化选择的组 useEffect(() => { if (!proxies) return; const getPrimaryGroupName = () => { if (!proxies?.groups?.length) return ""; const primaryKeywords = [ "auto", "select", "proxy", "节点选择", "自动选择", ]; const primaryGroup = proxies.groups.find((group: { name: string }) => primaryKeywords.some((keyword) => group.name.toLowerCase().includes(keyword.toLowerCase()), ), ) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0]; return primaryGroup?.name || ""; }; const primaryGroupName = getPrimaryGroupName(); // 根据模式确定初始组 if (isGlobalMode) { // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { ...prev.selection, group: "GLOBAL", }, })); } else if (isDirectMode) { // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { ...prev.selection, group: "DIRECT", }, })); } else { const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP); // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => ({ ...prev, selection: { ...prev.selection, group: savedGroup || primaryGroupName || "", }, })); } }, [isGlobalMode, isDirectMode, proxies, readProfileScopedItem]); // 监听代理数据变化,更新状态 useEffect(() => { if (!proxies) return; // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect setState((prev) => { const groupsMap = new Map(); const registerGroup = (group: any, fallbackName?: string) => { if (!group && !fallbackName) return; const rawName = typeof group?.name === "string" && group.name.length > 0 ? group.name : fallbackName; const name = normalizePolicyName(rawName); if (!name || groupsMap.has(name)) return; const rawAll = ( Array.isArray(group?.all) ? (group.all as Array) : [] ) as Array; const allNames = rawAll .map((item) => typeof item === "string" ? normalizePolicyName(item) : normalizePolicyName(item?.name), ) .filter((value): value is string => value.length > 0); const uniqueAll = Array.from(new Set(allNames)); if (uniqueAll.length === 0) return; groupsMap.set(name, { name, now: normalizePolicyName(group?.now), all: uniqueAll, type: group?.type, }); }; if (matchPolicyName) { const matchGroup = proxies.groups?.find( (g: { name: string }) => g.name === matchPolicyName, ) || (proxies.global?.name === matchPolicyName ? proxies.global : null) || proxies.records?.[matchPolicyName]; registerGroup(matchGroup, matchPolicyName); } (proxies.groups || []) .filter((g: { type?: string }) => g?.type === "Selector") .forEach((selectorGroup: any) => registerGroup(selectorGroup)); const filteredGroups = Array.from(groupsMap.values()); let newProxy = ""; let newDisplayProxy = null; let newGroup = prev.selection.group; if (isDirectMode) { newGroup = "DIRECT"; newProxy = "DIRECT"; newDisplayProxy = proxies.records?.DIRECT || { name: "DIRECT" }; } else if (isGlobalMode && proxies.global) { newGroup = "GLOBAL"; newProxy = proxies.global.now || ""; newDisplayProxy = proxies.records?.[newProxy] || null; } else { const currentGroup = filteredGroups.find( (g: { name: string }) => g.name === prev.selection.group, ); if (!currentGroup && filteredGroups.length > 0) { const firstGroup = filteredGroups[0]; if (firstGroup) { newGroup = firstGroup.name; newProxy = firstGroup.now || firstGroup.all[0] || ""; newDisplayProxy = proxies.records?.[newProxy] || null; if (!isGlobalMode && !isDirectMode) { writeProfileScopedItem(STORAGE_KEY_GROUP, newGroup); if (newProxy) { writeProfileScopedItem(STORAGE_KEY_PROXY, newProxy); } } } } else if (currentGroup) { newProxy = currentGroup.now || currentGroup.all[0] || ""; newDisplayProxy = proxies.records?.[newProxy] || null; } } return { proxyData: { groups: filteredGroups, records: proxies.records || {}, }, selection: { group: newGroup, proxy: newProxy, }, displayProxy: newDisplayProxy, }; }); }, [ proxies, isGlobalMode, isDirectMode, writeProfileScopedItem, normalizePolicyName, matchPolicyName, ]); // 使用防抖包装状态更新 const timeoutRef = React.useRef | null>(null); const debouncedSetState = useCallback( (updateFn: (prev: ProxyState) => ProxyState) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setState(updateFn); }, 300); }, [setState], ); // 处理代理组变更 const handleGroupChange = useCallback( (event: SelectChangeEvent) => { if (isGlobalMode || isDirectMode) return; const newGroup = event.target.value; writeProfileScopedItem(STORAGE_KEY_GROUP, newGroup); setState((prev) => { const group = prev.proxyData.groups.find( (g: { name: string }) => g.name === newGroup, ); if (group) { return { ...prev, selection: { group: newGroup, proxy: group.now, }, displayProxy: prev.proxyData.records[group.now] || null, }; } return { ...prev, selection: { ...prev.selection, group: newGroup, }, }; }); }, [isGlobalMode, isDirectMode, writeProfileScopedItem], ); // 处理代理节点变更 const handleProxyChange = useCallback( (event: SelectChangeEvent) => { if (isDirectMode) return; const newProxy = event.target.value; const currentGroup = state.selection.group; const previousProxy = state.selection.proxy; debouncedSetState((prev: ProxyState) => ({ ...prev, selection: { ...prev.selection, proxy: newProxy, }, displayProxy: prev.proxyData.records[newProxy] || null, })); if (!isGlobalMode && !isDirectMode) { writeProfileScopedItem(STORAGE_KEY_PROXY, newProxy); } const skipConfigSave = isGlobalMode || isDirectMode; handleSelectChange(currentGroup, previousProxy, skipConfigSave)(event); }, [ isDirectMode, isGlobalMode, state.selection, debouncedSetState, handleSelectChange, writeProfileScopedItem, ], ); // 导航到代理页面 const goToProxies = useCallback(() => { navigate("/proxies"); }, [navigate]); // 获取要显示的代理节点 const currentProxy = useMemo(() => { return state.displayProxy; }, [state.displayProxy]); // 获取当前节点的延迟(增加非空校验) const currentDelay = currentProxy && state.selection.group ? delayManager.getDelayFix(currentProxy, state.selection.group) : -1; // 信号图标(增加非空校验) const signalInfo = currentProxy && state.selection.group ? getSignalIcon(currentDelay) : { icon: , text: "未初始化", color: "text.secondary" }; const checkCurrentProxyDelay = useCallback(async () => { if (autoCheckInProgressRef.current) return; if (isDirectMode) return; const groupName = state.selection.group; const proxyName = state.selection.proxy; if (!groupName || !proxyName) return; const proxyRecord = latestProxyRecordRef.current; if (!proxyRecord) { debugLog( `[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`, ); return; } autoCheckInProgressRef.current = true; const timeout = latestTimeoutRef.current || 10000; try { debugLog( `[CurrentProxyCard] 自动检测当前节点延迟,组: ${groupName}, 节点: ${proxyName}`, ); if (proxyRecord.provider) { await healthcheckProxyProvider(proxyRecord.provider); } else { await delayManager.checkDelay(proxyName, groupName, timeout); } } catch (error) { console.error( `[CurrentProxyCard] 自动检测当前节点延迟失败,组: ${groupName}, 节点: ${proxyName}`, error, ); } finally { autoCheckInProgressRef.current = false; refreshProxy(); if (sortType === 1) { setDelaySortRefresh((prev) => prev + 1); } } }, [ isDirectMode, refreshProxy, state.selection.group, state.selection.proxy, sortType, setDelaySortRefresh, ]); useEffect(() => { if (isDirectMode) return; if (!autoDelayEnabled) return; if (!state.selection.group || !state.selection.proxy) return; let disposed = false; let intervalTimer: ReturnType | null = null; let initialTimer: ReturnType | null = null; const runAndSchedule = async () => { if (disposed) return; await checkCurrentProxyDelay(); if (disposed) return; intervalTimer = setTimeout(runAndSchedule, autoDelayIntervalMs); }; initialTimer = setTimeout(async () => { await checkCurrentProxyDelay(); if (disposed) return; intervalTimer = setTimeout(runAndSchedule, autoDelayIntervalMs); }, AUTO_CHECK_INITIAL_DELAY_MS); return () => { disposed = true; if (initialTimer) clearTimeout(initialTimer); if (intervalTimer) clearTimeout(intervalTimer); }; }, [ checkCurrentProxyDelay, autoDelayIntervalMs, isDirectMode, state.selection.group, state.selection.proxy, autoDelayEnabled, ]); // 自定义渲染选择框中的值 const renderProxyValue = (selected: string) => { if (!selected || !state.proxyData.records[selected]) return selected; const delayValue = delayManager.getDelayFix( state.proxyData.records[selected], state.selection.group, ); return ( {selected} ); }; // 排序类型变更 const handleSortTypeChange = useCallback(() => { const newSortType = ((sortType + 1) % 3) as ProxySortType; setSortType(newSortType); localStorage.setItem(STORAGE_KEY_SORT_TYPE, newSortType.toString()); }, [sortType]); // 延迟测试 const handleCheckDelay = useLockFn(async () => { const groupName = state.selection.group; if (!groupName || isDirectMode) return; debugLog(`[CurrentProxyCard] 开始测试所有延迟,组: ${groupName}`); const timeout = verge?.default_latency_timeout || 10000; // 获取当前组的所有代理 const proxyNames: string[] = []; const providers: Set = new Set(); if (isGlobalMode && proxies?.global) { // 全局模式 const allProxies = proxies.global.all .filter((p: any) => { const name = typeof p === "string" ? p : p.name; return name !== "DIRECT" && name !== "REJECT"; }) .map((p: any) => (typeof p === "string" ? p : p.name)); allProxies.forEach((name: string) => { const proxy = state.proxyData.records[name]; if (proxy?.provider) { providers.add(proxy.provider); } else { proxyNames.push(name); } }); } else { // 规则模式 const group = state.proxyData.groups.find((g) => g.name === groupName); if (group) { group.all.forEach((name: string) => { const proxy = state.proxyData.records[name]; if (proxy?.provider) { providers.add(proxy.provider); } else { proxyNames.push(name); } }); } } debugLog( `[CurrentProxyCard] 找到代理数量: ${proxyNames.length}, 提供者数量: ${providers.size}`, ); // 测试提供者的节点 if (providers.size > 0) { debugLog(`[CurrentProxyCard] 开始测试提供者节点`); await Promise.allSettled( [...providers].map((p) => healthcheckProxyProvider(p)), ); } // 测试非提供者的节点 if (proxyNames.length > 0) { const url = delayManager.getUrl(groupName); debugLog(`[CurrentProxyCard] 测试URL: ${url}, 超时: ${timeout}ms`); try { await Promise.race([ delayManager.checkListDelay(proxyNames, groupName, timeout), delayGroup(groupName, url, timeout), ]); debugLog(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`); } catch (error) { console.error( `[CurrentProxyCard] 延迟测试出错,组: ${groupName}`, error, ); } } refreshProxy(); if (sortType === 1) { setDelaySortRefresh((prev) => prev + 1); } }); // 计算要显示的代理选项(增加非空校验) const proxyOptions = useMemo(() => { const sortWithLatency = (proxiesToSort: ProxyOption[]) => { if (!proxiesToSort || sortType === 0) return proxiesToSort; if (!state.proxyData.records || !state.selection.group) { return proxiesToSort; } const list = [...proxiesToSort]; if (sortType === 1) { const refreshTick = delaySortRefresh; const effectiveTimeout = typeof defaultLatencyTimeout === "number" && defaultLatencyTimeout > 0 ? defaultLatencyTimeout : 10000; const categorizeDelay = (delay: number): [number, number] => { if (!Number.isFinite(delay)) return [5, Number.MAX_SAFE_INTEGER]; if (delay > 1e5) return [4, delay]; if (delay === 0 || (delay >= effectiveTimeout && delay <= 1e5)) { return [3, delay || effectiveTimeout]; } if (delay < 0) return [5, Number.MAX_SAFE_INTEGER]; return [0, delay]; }; list.sort((a, b) => { const recordA = state.proxyData.records[a.name]; const recordB = state.proxyData.records[b.name]; const [ar, av] = recordA ? categorizeDelay( delayManager.getDelayFix(recordA, state.selection.group), ) : [6, Number.MAX_SAFE_INTEGER]; const [br, bv] = recordB ? categorizeDelay( delayManager.getDelayFix(recordB, state.selection.group), ) : [6, Number.MAX_SAFE_INTEGER]; if (ar !== br) return ar - br; if (av !== bv) return av - bv; return refreshTick >= 0 ? a.name.localeCompare(b.name) : 0; }); } else { list.sort((a, b) => a.name.localeCompare(b.name)); } return list; }; if (isDirectMode) { return [{ name: "DIRECT" }]; } if (isGlobalMode && proxies?.global) { const options = proxies.global.all .filter((p: any) => { const name = typeof p === "string" ? p : p.name; return name !== "DIRECT" && name !== "REJECT"; }) .map((p: any) => ({ name: typeof p === "string" ? p : p.name, })); return sortWithLatency(options); } // 规则模式 const group = state.selection.group ? state.proxyData.groups.find((g) => g.name === state.selection.group) : null; if (group) { const options = group.all.map((name) => ({ name })); return sortWithLatency(options); } return []; }, [ isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortType, delaySortRefresh, defaultLatencyTimeout, ]); // 获取排序图标 const getSortIcon = (): React.ReactElement => { switch (sortType) { case 1: return ; case 2: return ; default: return ; } }; // 获取排序提示文本 const getSortTooltip = (): string => { switch (sortType) { case 0: return t("proxies.page.tooltips.sortDefault"); case 1: return t("proxies.page.tooltips.sortDelay"); case 2: return t("proxies.page.tooltips.sortName"); default: return ""; } }; return ( {currentProxy ? signalInfo.icon : } } iconColor={currentProxy ? "primary" : undefined} action={ {getSortIcon()} } > {currentProxy ? ( {/* 代理节点信息显示 */} {currentProxy.name} {currentProxy.type} {isGlobalMode && ( )} {isDirectMode && ( )} {/* 节点特性 */} {currentProxy.udp && ( )} {currentProxy.tfo && ( )} {currentProxy.xudp && ( )} {currentProxy.mptcp && ( )} {currentProxy.smux && ( )} {/* 显示延迟 */} {currentProxy && !isDirectMode && ( )} {/* 代理组选择器 */} {t("home.components.currentProxy.labels.group")} {/* 代理节点选择器 */} {t("home.components.currentProxy.labels.proxy")} ) : ( {t("home.components.currentProxy.labels.noActiveNode")} )} ); };