添加链式代理下规则适配
This commit is contained in:
@@ -63,6 +63,8 @@ interface ProxyChainProps {
|
||||
onUpdateChain: (chain: ProxyChainItem[]) => void;
|
||||
chainConfigData?: string | null;
|
||||
onMarkUnsavedChanges?: () => void;
|
||||
mode?: string;
|
||||
selectedGroup?: string | null;
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
@@ -189,6 +191,8 @@ export const ProxyChain = ({
|
||||
onUpdateChain,
|
||||
chainConfigData,
|
||||
onMarkUnsavedChanges,
|
||||
mode,
|
||||
selectedGroup,
|
||||
}: ProxyChainProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -215,25 +219,46 @@ export const ProxyChain = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找 proxy_chain 代理组
|
||||
const proxyChainGroup = currentProxies.groups.find(
|
||||
(group) => group.name === "proxy_chain",
|
||||
);
|
||||
if (!proxyChainGroup || !proxyChainGroup.now) {
|
||||
setIsConnected(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户配置的最后一个节点
|
||||
const lastNode = proxyChain[proxyChain.length - 1];
|
||||
|
||||
// 检查当前选中的代理是否是配置的最后一个节点
|
||||
if (proxyChainGroup.now === lastNode.name) {
|
||||
setIsConnected(true);
|
||||
// 根据模式确定要检查的代理组和当前选中的代理
|
||||
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 {
|
||||
setIsConnected(false);
|
||||
// 规则模式:检查指定的代理组
|
||||
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]);
|
||||
}, [currentProxies, proxyChain, mode, selectedGroup]);
|
||||
|
||||
// 监听链的变化,但排除从配置加载的情况
|
||||
const chainLengthRef = useRef(proxyChain.length);
|
||||
@@ -334,14 +359,23 @@ export const ProxyChain = ({
|
||||
// 第二步:连接到代理链的最后一个节点
|
||||
const lastNode = proxyChain[proxyChain.length - 1];
|
||||
console.log(`Connecting to proxy chain, last node: ${lastNode.name}`);
|
||||
await updateProxyAndSync("proxy_chain", 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);
|
||||
@@ -349,7 +383,7 @@ export const ProxyChain = ({
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
}, [proxyChain, isConnected, t, mutateProxies]);
|
||||
}, [proxyChain, isConnected, t, mutateProxies, mode, selectedGroup]);
|
||||
|
||||
const proxyChainRef = useRef(proxyChain);
|
||||
const onUpdateChainRef = useRef(onUpdateChain);
|
||||
@@ -504,7 +538,11 @@ export const ProxyChain = ({
|
||||
variant="contained"
|
||||
startIcon={isConnected ? <LinkOff /> : <Link />}
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || proxyChain.length < 2}
|
||||
disabled={
|
||||
isConnecting ||
|
||||
proxyChain.length < 2 ||
|
||||
(mode !== "global" && !selectedGroup)
|
||||
}
|
||||
color={isConnected ? "error" : "success"}
|
||||
sx={{
|
||||
minWidth: 90,
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import { Box, Snackbar, Alert } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Chip,
|
||||
Stack,
|
||||
Typography,
|
||||
IconButton,
|
||||
Collapse,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { ArchiveOutlined, ExpandMoreRounded } from "@mui/icons-material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
|
||||
import { useProxySelection } from "@/hooks/use-proxy-selection";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { providerHealthCheck, getGroupProxyDelays } from "@/services/cmds";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import {
|
||||
providerHealthCheck,
|
||||
getGroupProxyDelays,
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
import delayManager from "@/services/delay";
|
||||
|
||||
import { BaseEmpty } from "../base";
|
||||
@@ -33,18 +52,36 @@ export const ProxyGroups = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mode, isChainMode = false, chainConfigData } = props;
|
||||
const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
||||
const [ruleMenuAnchor, setRuleMenuAnchor] = useState<null | HTMLElement>(
|
||||
null,
|
||||
);
|
||||
const [duplicateWarning, setDuplicateWarning] = useState<{
|
||||
open: boolean;
|
||||
message: string;
|
||||
}>({ open: false, message: "" });
|
||||
|
||||
const { verge } = useVerge();
|
||||
const { proxies: proxiesData } = useAppData();
|
||||
|
||||
// 当链式代理模式且规则模式下,如果没有选择代理组,默认选择第一个
|
||||
useEffect(() => {
|
||||
if (
|
||||
isChainMode &&
|
||||
mode === "rule" &&
|
||||
!selectedGroup &&
|
||||
proxiesData?.groups?.length > 0
|
||||
) {
|
||||
setSelectedGroup(proxiesData.groups[0].name);
|
||||
}
|
||||
}, [isChainMode, mode, selectedGroup, proxiesData]);
|
||||
|
||||
const { renderList, onProxies, onHeadState } = useRenderList(
|
||||
mode,
|
||||
isChainMode,
|
||||
selectedGroup,
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
// 统代理选择
|
||||
const { handleProxyGroupChange } = useProxySelection({
|
||||
onSuccess: () => {
|
||||
@@ -142,6 +179,43 @@ export const ProxyGroups = (props: Props) => {
|
||||
setDuplicateWarning({ open: false, message: "" });
|
||||
}, []);
|
||||
|
||||
// 获取当前选中的代理组信息
|
||||
const getCurrentGroup = useCallback(() => {
|
||||
if (!selectedGroup || !proxiesData?.groups) return null;
|
||||
return proxiesData.groups.find(
|
||||
(group: any) => group.name === selectedGroup,
|
||||
);
|
||||
}, [selectedGroup, proxiesData]);
|
||||
|
||||
// 获取可用的代理组列表
|
||||
const getAvailableGroups = useCallback(() => {
|
||||
return proxiesData?.groups || [];
|
||||
}, [proxiesData]);
|
||||
|
||||
// 处理代理组选择菜单
|
||||
const handleGroupMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setRuleMenuAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleGroupMenuClose = () => {
|
||||
setRuleMenuAnchor(null);
|
||||
};
|
||||
|
||||
const handleGroupSelect = (groupName: string) => {
|
||||
setSelectedGroup(groupName);
|
||||
handleGroupMenuClose();
|
||||
|
||||
// 在链式代理模式的规则模式下,切换代理组时清空链式代理配置
|
||||
if (isChainMode && mode === "rule") {
|
||||
updateProxyChainConfigInRuntime(null);
|
||||
// 同时清空右侧链式代理配置
|
||||
setProxyChain([]);
|
||||
}
|
||||
};
|
||||
|
||||
const currentGroup = getCurrentGroup();
|
||||
const availableGroups = getAvailableGroups();
|
||||
|
||||
const handleChangeProxy = useCallback(
|
||||
(group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||
if (isChainMode) {
|
||||
@@ -257,13 +331,89 @@ export const ProxyGroups = (props: Props) => {
|
||||
}
|
||||
|
||||
if (isChainMode) {
|
||||
// 获取所有代理组
|
||||
const proxyGroups = proxiesData?.groups || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: "flex", height: "100%", gap: 2 }}>
|
||||
<Box sx={{ flex: 1, position: "relative" }}>
|
||||
{/* 代理规则标题和代理组按钮栏 */}
|
||||
{mode === "rule" && proxyGroups.length > 0 && (
|
||||
<Box sx={{ borderBottom: "1px solid", borderColor: "divider" }}>
|
||||
{/* 代理规则标题 */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1.5,
|
||||
borderBottom: "1px solid",
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontWeight: 600, fontSize: "16px" }}
|
||||
>
|
||||
{t("Proxy Rules")}
|
||||
</Typography>
|
||||
{currentGroup && (
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", gap: 1 }}
|
||||
>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${currentGroup.name} (${currentGroup.type})`}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
maxWidth: "200px",
|
||||
"& .MuiChip-label": {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{availableGroups.length > 0 && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleGroupMenuOpen}
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
borderRadius: "4px",
|
||||
padding: "4px 8px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mr: 0.5, fontSize: "12px" }}
|
||||
>
|
||||
{t("Select Rules")}
|
||||
</Typography>
|
||||
<ExpandMoreRounded fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "calc(100% - 14px)" }}
|
||||
style={{
|
||||
height:
|
||||
mode === "rule" && proxyGroups.length > 0
|
||||
? "calc(100% - 80px)" // 只有标题的高度
|
||||
: "calc(100% - 14px)",
|
||||
}}
|
||||
totalCount={renderList.length}
|
||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
||||
overscan={150}
|
||||
@@ -297,6 +447,8 @@ export const ProxyGroups = (props: Props) => {
|
||||
proxyChain={proxyChain}
|
||||
onUpdateChain={setProxyChain}
|
||||
chainConfigData={chainConfigData}
|
||||
mode={mode}
|
||||
selectedGroup={selectedGroup}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -315,6 +467,53 @@ export const ProxyGroups = (props: Props) => {
|
||||
{duplicateWarning.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
{/* 代理组选择菜单 */}
|
||||
<Menu
|
||||
anchorEl={ruleMenuAnchor}
|
||||
open={Boolean(ruleMenuAnchor)}
|
||||
onClose={handleGroupMenuClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
maxHeight: 300,
|
||||
minWidth: 200,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{availableGroups.map((group: any, index: number) => (
|
||||
<MenuItem
|
||||
key={group.name}
|
||||
onClick={() => handleGroupSelect(group.name)}
|
||||
selected={selectedGroup === group.name}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{group.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{group.type} · {group.all.length} 节点
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
{availableGroups.length === 0 && (
|
||||
<MenuItem disabled>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
暂无可用代理组
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,11 @@ const groupProxies = <T = any>(list: T[], size: number): T[][] => {
|
||||
}, [] as T[][]);
|
||||
};
|
||||
|
||||
export const useRenderList = (mode: string, isChainMode?: boolean) => {
|
||||
export const useRenderList = (
|
||||
mode: string,
|
||||
isChainMode?: boolean,
|
||||
selectedGroup?: string | null,
|
||||
) => {
|
||||
// 使用全局数据提供者
|
||||
const { proxies: proxiesData, refreshProxy } = useAppData();
|
||||
const { verge } = useVerge();
|
||||
@@ -180,7 +184,129 @@ export const useRenderList = (mode: string, isChainMode?: boolean) => {
|
||||
const renderList: IRenderItem[] = useMemo(() => {
|
||||
if (!proxiesData) return [];
|
||||
|
||||
// 链式代理模式下,从运行时配置读取所有 proxies
|
||||
// 链式代理模式下,显示代理组和其节点
|
||||
if (isChainMode && runtimeConfig && mode === "rule") {
|
||||
// 使用正常的规则模式代理组
|
||||
const allGroups = proxiesData.groups.length
|
||||
? proxiesData.groups
|
||||
: [proxiesData.global!];
|
||||
|
||||
// 如果选择了特定代理组,只显示该组的节点
|
||||
if (selectedGroup) {
|
||||
const targetGroup = allGroups.find(
|
||||
(g: any) => g.name === selectedGroup,
|
||||
);
|
||||
if (targetGroup) {
|
||||
const proxies = filterSort(targetGroup.all, targetGroup.name, "", 0);
|
||||
|
||||
if (col > 1) {
|
||||
return groupProxies(proxies, col).map((proxyCol, colIndex) => ({
|
||||
type: 4,
|
||||
key: `chain-col-${selectedGroup}-${colIndex}`,
|
||||
group: targetGroup,
|
||||
headState: DEFAULT_STATE,
|
||||
col,
|
||||
proxyCol,
|
||||
provider: proxyCol[0]?.provider,
|
||||
}));
|
||||
} else {
|
||||
return proxies.map((proxy) => ({
|
||||
type: 2,
|
||||
key: `chain-${selectedGroup}-${proxy!.name}`,
|
||||
group: targetGroup,
|
||||
proxy,
|
||||
headState: DEFAULT_STATE,
|
||||
provider: proxy.provider,
|
||||
}));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果没有选择特定组,显示第一个组的节点(如果有组的话)
|
||||
if (allGroups.length > 0) {
|
||||
const firstGroup = allGroups[0];
|
||||
const proxies = filterSort(firstGroup.all, firstGroup.name, "", 0);
|
||||
|
||||
if (col > 1) {
|
||||
return groupProxies(proxies, col).map((proxyCol, colIndex) => ({
|
||||
type: 4,
|
||||
key: `chain-col-first-${colIndex}`,
|
||||
group: firstGroup,
|
||||
headState: DEFAULT_STATE,
|
||||
col,
|
||||
proxyCol,
|
||||
provider: proxyCol[0]?.provider,
|
||||
}));
|
||||
} else {
|
||||
return proxies.map((proxy) => ({
|
||||
type: 2,
|
||||
key: `chain-first-${proxy!.name}`,
|
||||
group: firstGroup,
|
||||
proxy,
|
||||
headState: DEFAULT_STATE,
|
||||
provider: proxy.provider,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有组,显示所有节点
|
||||
const allProxies: IProxyItem[] = allGroups.flatMap(
|
||||
(group: any) => group.all,
|
||||
);
|
||||
|
||||
// 为每个节点获取延迟信息
|
||||
const proxiesWithDelay = allProxies.map((proxy) => {
|
||||
const delay = delayManager.getDelay(proxy.name, "chain-mode");
|
||||
return {
|
||||
...proxy,
|
||||
// 如果delayManager有延迟数据,更新history
|
||||
history:
|
||||
delay >= 0
|
||||
? [{ time: new Date().toISOString(), delay }]
|
||||
: proxy.history || [],
|
||||
};
|
||||
});
|
||||
|
||||
// 创建一个虚拟的组来容纳所有节点
|
||||
const virtualGroup: ProxyGroup = {
|
||||
name: "All Proxies",
|
||||
type: "Selector",
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
history: [],
|
||||
now: "",
|
||||
all: proxiesWithDelay,
|
||||
};
|
||||
|
||||
if (col > 1) {
|
||||
return groupProxies(proxiesWithDelay, col).map(
|
||||
(proxyCol, colIndex) => ({
|
||||
type: 4,
|
||||
key: `chain-col-all-${colIndex}`,
|
||||
group: virtualGroup,
|
||||
headState: DEFAULT_STATE,
|
||||
col,
|
||||
proxyCol,
|
||||
provider: proxyCol[0]?.provider,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return proxiesWithDelay.map((proxy) => ({
|
||||
type: 2,
|
||||
key: `chain-all-${proxy.name}`,
|
||||
group: virtualGroup,
|
||||
proxy,
|
||||
headState: DEFAULT_STATE,
|
||||
provider: proxy.provider,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 链式代理模式下的其他模式(如global)仍显示所有节点
|
||||
if (isChainMode && runtimeConfig) {
|
||||
// 从运行时配置直接获取 proxies 列表 (需要类型断言)
|
||||
const allProxies: IProxyItem[] = Object.values(
|
||||
@@ -311,7 +437,15 @@ export const useRenderList = (mode: string, isChainMode?: boolean) => {
|
||||
|
||||
if (!useRule) return retList.slice(1);
|
||||
return retList.filter((item: IRenderItem) => !item.group.hidden);
|
||||
}, [headStates, proxiesData, mode, col, isChainMode, runtimeConfig]);
|
||||
}, [
|
||||
headStates,
|
||||
proxiesData,
|
||||
mode,
|
||||
col,
|
||||
isChainMode,
|
||||
runtimeConfig,
|
||||
selectedGroup,
|
||||
]);
|
||||
|
||||
return {
|
||||
renderList,
|
||||
|
||||
Reference in New Issue
Block a user