import { closestCenter, DndContext, DragEndEvent, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Delete as DeleteIcon, DragIndicator, Link, LinkOff, } from "@mui/icons-material"; import { Alert, Box, Button, Chip, IconButton, Paper, Typography, useTheme, } from "@mui/material"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; import { useAppData } from "@/providers/app-data-provider"; import { closeAllConnections, getProxies, updateProxyAndSync, updateProxyChainConfigInRuntime, } from "@/services/cmds"; interface ProxyChainItem { id: string; name: string; type?: string; delay?: number; } interface ParsedChainConfig { proxies?: Array<{ name: string; type: string; [key: string]: any; }>; } interface ProxyChainProps { proxyChain: ProxyChainItem[]; onUpdateChain: (chain: ProxyChainItem[]) => void; chainConfigData?: string | null; onMarkUnsavedChanges?: () => void; mode?: string; selectedGroup?: string | null; } interface SortableItemProps { proxy: ProxyChainItem; index: number; onRemove: (id: string) => void; } const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => { const theme = useTheme(); const { t } = useTranslation(); const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: proxy.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return ( {proxy.name} {proxy.type && ( )} {proxy.delay !== undefined && ( 0 ? `${proxy.delay}ms` : t("timeout") || "超时"} size="small" color={ proxy.delay > 0 && proxy.delay < 200 ? "success" : proxy.delay > 0 && proxy.delay < 800 ? "warning" : "error" } sx={{ mr: 1, fontSize: "0.7rem", minWidth: 50 }} /> )} onRemove(proxy.id)} sx={{ color: theme.palette.error.main, "&:hover": { backgroundColor: theme.palette.error.light + "20", }, }} > ); }; export const ProxyChain = ({ proxyChain, onUpdateChain, chainConfigData, onMarkUnsavedChanges, mode, selectedGroup, }: ProxyChainProps) => { const theme = useTheme(); const { t } = useTranslation(); const { proxies } = useAppData(); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isConnected, setIsConnected] = useState(false); // 获取当前代理信息以检查连接状态 const { data: currentProxies, mutate: mutateProxies } = useSWR( "getProxies", getProxies, { revalidateOnFocus: true, revalidateIfStale: true, refreshInterval: 5000, // 每5秒刷新一次 }, ); // 检查连接状态 useEffect(() => { if (!currentProxies || proxyChain.length < 2) { setIsConnected(false); return; } // 获取用户配置的最后一个节点 const lastNode = proxyChain[proxyChain.length - 1]; // 根据模式确定要检查的代理组和当前选中的代理 if (mode === "global") { // 全局模式:检查 global 对象 if (!currentProxies.global || !currentProxies.global.now) { setIsConnected(false); return; } // 检查当前选中的代理是否是配置的最后一个节点 if (currentProxies.global.now === lastNode.name) { setIsConnected(true); } else { setIsConnected(false); } } else { // 规则模式:检查指定的代理组 if (!selectedGroup) { setIsConnected(false); return; } const proxyChainGroup = currentProxies.groups.find( (group) => group.name === selectedGroup, ); if (!proxyChainGroup || !proxyChainGroup.now) { setIsConnected(false); return; } // 检查当前选中的代理是否是配置的最后一个节点 if (proxyChainGroup.now === lastNode.name) { setIsConnected(true); } else { setIsConnected(false); } } }, [currentProxies, proxyChain, mode, selectedGroup]); // 监听链的变化,但排除从配置加载的情况 const chainLengthRef = useRef(proxyChain.length); useEffect(() => { // 只有当链长度发生变化且不是初始加载时,才标记为未保存 if ( chainLengthRef.current !== proxyChain.length && chainLengthRef.current !== 0 ) { setHasUnsavedChanges(true); } chainLengthRef.current = proxyChain.length; }, [proxyChain.length]); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (active.id !== over?.id) { const oldIndex = proxyChain.findIndex((item) => item.id === active.id); const newIndex = proxyChain.findIndex((item) => item.id === over?.id); onUpdateChain(arrayMove(proxyChain, oldIndex, newIndex)); setHasUnsavedChanges(true); } }, [proxyChain, onUpdateChain], ); const handleRemoveProxy = useCallback( (id: string) => { const newChain = proxyChain.filter((item) => item.id !== id); onUpdateChain(newChain); setHasUnsavedChanges(true); }, [proxyChain, onUpdateChain], ); const handleClearAll = useCallback(() => { onUpdateChain([]); setHasUnsavedChanges(true); }, [onUpdateChain]); const handleConnect = useCallback(async () => { if (isConnected) { // 如果已连接,则断开连接 setIsConnecting(true); try { // 清空链式代理配置 await updateProxyChainConfigInRuntime(null); // 切换到 DIRECT 模式断开代理连接 // await updateProxyAndSync("GLOBAL", "DIRECT"); // 关闭所有连接 await closeAllConnections(); // 刷新代理信息以更新连接状态 mutateProxies(); // 清空链式代理配置UI // onUpdateChain([]); // setHasUnsavedChanges(false); // 强制更新连接状态 setIsConnected(false); } catch (error) { console.error("Failed to disconnect from proxy chain:", error); alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败"); } finally { setIsConnecting(false); } return; } if (proxyChain.length < 2) { alert( t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点", ); return; } setIsConnecting(true); try { // 第一步:保存链式代理配置 const chainProxies = proxyChain.map((node) => node.name); console.log("Saving chain config:", chainProxies); await updateProxyChainConfigInRuntime(chainProxies); console.log("Chain configuration saved successfully"); // 第二步:连接到代理链的最后一个节点 const lastNode = proxyChain[proxyChain.length - 1]; console.log(`Connecting to proxy chain, last node: ${lastNode.name}`); // 根据模式确定使用的代理组名称 if (mode !== "global" && !selectedGroup) { throw new Error("规则模式下必须选择代理组"); } const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup; await updateProxyAndSync(targetGroup || "GLOBAL", lastNode.name); localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL"); localStorage.setItem("proxy-chain-exit-node", lastNode.name); // 刷新代理信息以更新连接状态 mutateProxies(); // 清除未保存标记 setHasUnsavedChanges(false); console.log("Successfully connected to proxy chain"); } catch (error) { console.error("Failed to connect to proxy chain:", error); alert(t("Failed to connect to proxy chain") || "连接链式代理失败"); } finally { setIsConnecting(false); } }, [proxyChain, isConnected, t, mutateProxies, mode, selectedGroup]); const proxyChainRef = useRef(proxyChain); const onUpdateChainRef = useRef(onUpdateChain); useEffect(() => { proxyChainRef.current = proxyChain; onUpdateChainRef.current = onUpdateChain; }, [proxyChain, onUpdateChain]); // 处理链式代理配置数据 useEffect(() => { if (chainConfigData) { try { // Try to parse as YAML using dynamic import import("js-yaml") .then((yaml) => { try { const parsedConfig = yaml.load( chainConfigData, ) as ParsedChainConfig; const chainItems = parsedConfig?.proxies?.map((proxy, index: number) => ({ id: `${proxy.name}_${Date.now()}_${index}`, name: proxy.name, type: proxy.type, delay: undefined, })) || []; onUpdateChain(chainItems); setHasUnsavedChanges(false); } catch (parseError) { console.error("Failed to parse YAML:", parseError); onUpdateChain([]); } }) .catch((importError) => { // Fallback: try to parse as JSON if YAML is not available console.warn( "js-yaml not available, trying JSON parse:", importError, ); try { const parsedConfig = JSON.parse( chainConfigData, ) as ParsedChainConfig; const chainItems = parsedConfig?.proxies?.map((proxy, index: number) => ({ id: `${proxy.name}_${Date.now()}_${index}`, name: proxy.name, type: proxy.type, delay: undefined, })) || []; onUpdateChain(chainItems); setHasUnsavedChanges(false); } catch (jsonError) { console.error("Failed to parse as JSON either:", jsonError); onUpdateChain([]); } }); } catch (error) { console.error("Failed to process chain config data:", error); onUpdateChain([]); } } else if (chainConfigData === "") { // Empty string means no proxies available, show empty state onUpdateChain([]); setHasUnsavedChanges(false); } }, [chainConfigData, onUpdateChain]); // 定时更新延迟数据 useEffect(() => { if (!proxies?.records) return; const updateDelays = () => { const currentChain = proxyChainRef.current; if (currentChain.length === 0) return; const updatedChain = currentChain.map((item) => { const proxyRecord = proxies.records[item.name]; if ( proxyRecord && proxyRecord.history && proxyRecord.history.length > 0 ) { const latestDelay = proxyRecord.history[proxyRecord.history.length - 1].delay; return { ...item, delay: latestDelay }; } return item; }); // 只有在延迟数据确实发生变化时才更新 const hasChanged = updatedChain.some( (item, index) => item.delay !== currentChain[index]?.delay, ); if (hasChanged) { onUpdateChainRef.current(updatedChain); } }; // 立即更新一次延迟 updateDelays(); // 设置定时器,每5秒更新一次延迟 const interval = setInterval(updateDelays, 5000); return () => clearInterval(interval); }, [proxies?.records]); // 只依赖proxies.records return ( {t("Chain Proxy Config")} {proxyChain.length > 0 && ( { updateProxyChainConfigInRuntime(null); onUpdateChain([]); setHasUnsavedChanges(false); }} sx={{ color: theme.palette.error.main, "&:hover": { backgroundColor: theme.palette.error.light + "20", }, }} title={t("Delete Chain Config") || "删除链式配置"} > )} {proxyChain.length === 1 ? t( "Chain proxy requires at least 2 nodes. Please add one more node.", ) || "链式代理至少需要2个节点,请再添加一个节点。" : t("Click nodes in order to add to proxy chain") || "按顺序点击节点添加到代理链中"} {proxyChain.length === 0 ? ( {t("No proxy chain configured")} ) : ( proxy.id)} strategy={verticalListSortingStrategy} > {proxyChain.map((proxy, index) => ( ))} )} ); };