添加链式代理下规则适配

This commit is contained in:
Junkai W.
2025-09-22 18:16:17 +08:00
committed by GitHub
parent 620922f82e
commit 909c4028f1
12 changed files with 451 additions and 139 deletions

View File

@@ -37,9 +37,8 @@ pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String
Ok(Config::runtime().await.latest_ref().chain_logs.clone()) Ok(Config::runtime().await.latest_ref().chain_logs.clone())
} }
/// 读取运行时链式代理配置
#[tauri::command] #[tauri::command]
pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> { pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
let runtime = Config::runtime().await; let runtime = Config::runtime().await;
let runtime = runtime.latest_ref(); let runtime = runtime.latest_ref();
@@ -50,51 +49,36 @@ pub async fn get_runtime_proxy_chain_config() -> CmdResult<String> {
.ok_or(anyhow::anyhow!("failed to parse config to yaml file")) .ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
)?; )?;
if let ( if let Some(serde_yaml_ng::Value::Sequence(proxies)) = config.get("proxies") {
Some(serde_yaml_ng::Value::Sequence(proxies)), let mut proxy_name = Some(Some(proxy_chain_exit_node.as_str()));
Some(serde_yaml_ng::Value::Sequence(proxy_groups)),
) = (config.get("proxies"), config.get("proxy-groups"))
{
let mut proxy_name = None;
let mut proxies_chain = Vec::new(); let mut proxies_chain = Vec::new();
let proxy_chain_groups = proxy_groups
.iter()
.filter_map(
|proxy_group| match proxy_group.get("name").and_then(|n| n.as_str()) {
Some("proxy_chain") => {
if let Some(serde_yaml_ng::Value::Sequence(ps)) = proxy_group.get("proxies")
&& let Some(x) = ps.first()
{
proxy_name = Some(x); //插入出口节点名字
}
Some(proxy_group.to_owned())
}
_ => None,
},
)
.collect::<Vec<serde_yaml_ng::Value>>();
while let Some(proxy) = proxies.iter().find(|proxy| { while let Some(proxy) = proxies.iter().find(|proxy| {
if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy { if let serde_yaml_ng::Value::Mapping(proxy_map) = proxy {
proxy_map.get("name") == proxy_name && proxy_map.get("dialer-proxy").is_some() proxy_map.get("name").map(|x| x.as_str()) == proxy_name
&& proxy_map.get("dialer-proxy").is_some()
} else { } else {
false false
} }
}) { }) {
proxies_chain.push(proxy.to_owned()); proxies_chain.push(proxy.to_owned());
proxy_name = proxy.get("dialer-proxy"); proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
} }
if let Some(entry_proxy) = proxies.iter().find(|proxy| proxy.get("name") == proxy_name) { if let Some(entry_proxy) = proxies
.iter()
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
&& !proxies_chain.is_empty()
{
// 添加第一个节点
proxies_chain.push(entry_proxy.to_owned()); proxies_chain.push(entry_proxy.to_owned());
} }
proxies_chain.reverse(); proxies_chain.reverse();
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new(); let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
config.insert("proxies".to_string(), proxies_chain); config.insert("proxies".to_string(), proxies_chain);
config.insert("proxy-groups".to_string(), proxy_chain_groups);
wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed")) wrap_err!(serde_yaml_ng::to_string(&config).context("YAML generation failed"))
} else { } else {

View File

@@ -92,22 +92,6 @@ impl IRuntime {
/// 传入none 为删除 /// 传入none 为删除
pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) { pub fn update_proxy_chain_config(&mut self, proxy_chain_config: Option<Value>) {
if let Some(config) = self.config.as_mut() { if let Some(config) = self.config.as_mut() {
// 获取 默认第一的代理组的名字
let proxy_group_name =
if let Some(Value::Sequence(proxy_groups)) = config.get("proxy-groups") {
if let Some(Value::Mapping(proxy_group)) = proxy_groups.first() {
if let Some(Value::String(proxy_group_name)) = proxy_group.get("name") {
proxy_group_name.to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
} else {
"".to_string()
};
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") { if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
proxies.iter_mut().for_each(|proxy| { proxies.iter_mut().for_each(|proxy| {
if let Some(proxy) = proxy.as_mapping_mut() if let Some(proxy) = proxy.as_mapping_mut()
@@ -118,66 +102,19 @@ impl IRuntime {
}); });
} }
// 清除proxy_chain代理组 if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") { && let Some(Value::Sequence(proxies)) = config.get_mut("proxies")
proxy_groups.retain(|proxy_group| { {
!matches!(proxy_group.get("name").and_then(|n| n.as_str()), Some(name) if name== "proxy_chain") for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
}); if let Some(Value::Mapping(proxy)) = proxies
} .iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
// 清除rules && i != 0
if let Some(Value::Sequence(rules)) = config.get_mut("rules") { && let Some(dialer_proxy) = dialer_proxies.get(i - 1)
rules.retain(|rule| rule.as_str() != Some("MATCH,proxy_chain")); {
rules.push(Value::String(format!("MATCH,{}", proxy_group_name))); proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
// some 则写入新配置
// 给proxy添加dialer-proxy字段
// 第一个proxy不添加dialer-proxy
// 然后第二个开始dialer-proxy为上一个元素的name
if let Some(Value::Sequence(dialer_proxies)) = proxy_chain_config {
let mut proxy_chain_group = Mapping::new();
if let Some(Value::Sequence(proxies)) = config.get_mut("proxies") {
for (i, dialer_proxy) in dialer_proxies.iter().enumerate() {
if let Some(Value::Mapping(proxy)) = proxies
.iter_mut()
.find(|proxy| proxy.get("name") == Some(dialer_proxy))
{
if i != 0
&& let Some(dialer_proxy) = dialer_proxies.get(i - 1)
{
proxy.insert("dialer-proxy".into(), dialer_proxy.to_owned());
}
if i == dialer_proxies.len() - 1 {
// 添加proxy-groups
proxy_chain_group
.insert("name".into(), Value::String("proxy_chain".into()));
proxy_chain_group
.insert("type".into(), Value::String("select".into()));
proxy_chain_group.insert(
"proxies".into(),
Value::Sequence(vec![dialer_proxy.to_owned()]),
);
}
}
} }
} }
if let Some(Value::Sequence(proxy_groups)) = config.get_mut("proxy-groups") {
proxy_groups.push(Value::Mapping(proxy_chain_group));
}
// 添加rules
if let Some(Value::Sequence(rules)) = config.get_mut("rules")
&& let Ok(rule) = serde_yaml_ng::to_value("MATCH,proxy_chain")
{
rules.retain(|rule| {
rule.as_str() != Some(&format!("MATCH,{}", proxy_group_name))
});
rules.push(rule);
// *rules = vec![rule];
}
} }
} }
} }

View File

@@ -1034,9 +1034,9 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
return; return;
} }
if !is_in_lightweight_mode() { if !is_in_lightweight_mode() {
lightweight::entry_lightweight_mode().await; lightweight::entry_lightweight_mode().await; // Await async function
} else { } else {
lightweight::exit_lightweight_mode().await; lightweight::exit_lightweight_mode().await; // Await async function
} }
} }
"quit" => { "quit" => {

View File

@@ -276,9 +276,15 @@ impl IpcManager {
let payload = serde_json::json!({ let payload = serde_json::json!({
"name": proxy "name": proxy
}); });
// println!("group: {}, proxy: {}", group, proxy);
match self.send_request("PUT", &url, Some(&payload)).await { match self.send_request("PUT", &url, Some(&payload)).await {
Ok(_) => Ok(()), Ok(_) => {
// println!("updateProxy response: {:?}", response);
Ok(())
}
Err(e) => { Err(e) => {
// println!("updateProxy encountered error: {}", e);
logging!( logging!(
error, error,
crate::utils::logging::Type::Ipc, crate::utils::logging::Type::Ipc,

View File

@@ -63,6 +63,8 @@ interface ProxyChainProps {
onUpdateChain: (chain: ProxyChainItem[]) => void; onUpdateChain: (chain: ProxyChainItem[]) => void;
chainConfigData?: string | null; chainConfigData?: string | null;
onMarkUnsavedChanges?: () => void; onMarkUnsavedChanges?: () => void;
mode?: string;
selectedGroup?: string | null;
} }
interface SortableItemProps { interface SortableItemProps {
@@ -189,6 +191,8 @@ export const ProxyChain = ({
onUpdateChain, onUpdateChain,
chainConfigData, chainConfigData,
onMarkUnsavedChanges, onMarkUnsavedChanges,
mode,
selectedGroup,
}: ProxyChainProps) => { }: ProxyChainProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -215,25 +219,46 @@ export const ProxyChain = ({
return; 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]; const lastNode = proxyChain[proxyChain.length - 1];
// 检查当前选中的代理是否是配置的最后一个节点 // 根据模式确定要检查的代理组和当前选中的代理
if (proxyChainGroup.now === lastNode.name) { if (mode === "global") {
setIsConnected(true); // 全局模式:检查 global 对象
if (!currentProxies.global || !currentProxies.global.now) {
setIsConnected(false);
return;
}
// 检查当前选中的代理是否是配置的最后一个节点
if (currentProxies.global.now === lastNode.name) {
setIsConnected(true);
} else {
setIsConnected(false);
}
} else { } 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); const chainLengthRef = useRef(proxyChain.length);
@@ -334,14 +359,23 @@ export const ProxyChain = ({
// 第二步:连接到代理链的最后一个节点 // 第二步:连接到代理链的最后一个节点
const lastNode = proxyChain[proxyChain.length - 1]; const lastNode = proxyChain[proxyChain.length - 1];
console.log(`Connecting to proxy chain, last node: ${lastNode.name}`); 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(); mutateProxies();
// 清除未保存标记 // 清除未保存标记
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
console.log("Successfully connected to proxy chain"); console.log("Successfully connected to proxy chain");
} catch (error) { } catch (error) {
console.error("Failed to connect to proxy chain:", error); console.error("Failed to connect to proxy chain:", error);
@@ -349,7 +383,7 @@ export const ProxyChain = ({
} finally { } finally {
setIsConnecting(false); setIsConnecting(false);
} }
}, [proxyChain, isConnected, t, mutateProxies]); }, [proxyChain, isConnected, t, mutateProxies, mode, selectedGroup]);
const proxyChainRef = useRef(proxyChain); const proxyChainRef = useRef(proxyChain);
const onUpdateChainRef = useRef(onUpdateChain); const onUpdateChainRef = useRef(onUpdateChain);
@@ -504,7 +538,11 @@ export const ProxyChain = ({
variant="contained" variant="contained"
startIcon={isConnected ? <LinkOff /> : <Link />} startIcon={isConnected ? <LinkOff /> : <Link />}
onClick={handleConnect} onClick={handleConnect}
disabled={isConnecting || proxyChain.length < 2} disabled={
isConnecting ||
proxyChain.length < 2 ||
(mode !== "global" && !selectedGroup)
}
color={isConnected ? "error" : "success"} color={isConnected ? "error" : "success"}
sx={{ sx={{
minWidth: 90, minWidth: 90,

View File

@@ -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 { useLockFn } from "ahooks";
import { useRef, useState, useEffect, useCallback } from "react"; import { useRef, useState, useEffect, useCallback } from "react";
import useSWR from "swr";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge"; 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 delayManager from "@/services/delay";
import { BaseEmpty } from "../base"; import { BaseEmpty } from "../base";
@@ -33,18 +52,36 @@ export const ProxyGroups = (props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { mode, isChainMode = false, chainConfigData } = props; const { mode, isChainMode = false, chainConfigData } = props;
const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>([]); const [proxyChain, setProxyChain] = useState<ProxyChainItem[]>([]);
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
const [ruleMenuAnchor, setRuleMenuAnchor] = useState<null | HTMLElement>(
null,
);
const [duplicateWarning, setDuplicateWarning] = useState<{ const [duplicateWarning, setDuplicateWarning] = useState<{
open: boolean; open: boolean;
message: string; message: string;
}>({ open: false, message: "" }); }>({ 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( const { renderList, onProxies, onHeadState } = useRenderList(
mode, mode,
isChainMode, isChainMode,
selectedGroup,
); );
const { verge } = useVerge();
// 统代理选择 // 统代理选择
const { handleProxyGroupChange } = useProxySelection({ const { handleProxyGroupChange } = useProxySelection({
onSuccess: () => { onSuccess: () => {
@@ -142,6 +179,43 @@ export const ProxyGroups = (props: Props) => {
setDuplicateWarning({ open: false, message: "" }); 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( const handleChangeProxy = useCallback(
(group: IProxyGroupItem, proxy: IProxyItem) => { (group: IProxyGroupItem, proxy: IProxyItem) => {
if (isChainMode) { if (isChainMode) {
@@ -257,13 +331,89 @@ export const ProxyGroups = (props: Props) => {
} }
if (isChainMode) { if (isChainMode) {
// 获取所有代理组
const proxyGroups = proxiesData?.groups || [];
return ( return (
<> <>
<Box sx={{ display: "flex", height: "100%", gap: 2 }}> <Box sx={{ display: "flex", height: "100%", gap: 2 }}>
<Box sx={{ flex: 1, position: "relative" }}> <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 <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
style={{ height: "calc(100% - 14px)" }} style={{
height:
mode === "rule" && proxyGroups.length > 0
? "calc(100% - 80px)" // 只有标题的高度
: "calc(100% - 14px)",
}}
totalCount={renderList.length} totalCount={renderList.length}
increaseViewportBy={{ top: 200, bottom: 200 }} increaseViewportBy={{ top: 200, bottom: 200 }}
overscan={150} overscan={150}
@@ -297,6 +447,8 @@ export const ProxyGroups = (props: Props) => {
proxyChain={proxyChain} proxyChain={proxyChain}
onUpdateChain={setProxyChain} onUpdateChain={setProxyChain}
chainConfigData={chainConfigData} chainConfigData={chainConfigData}
mode={mode}
selectedGroup={selectedGroup}
/> />
</Box> </Box>
</Box> </Box>
@@ -315,6 +467,53 @@ export const ProxyGroups = (props: Props) => {
{duplicateWarning.message} {duplicateWarning.message}
</Alert> </Alert>
</Snackbar> </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>
</> </>
); );
} }

View File

@@ -93,7 +93,11 @@ const groupProxies = <T = any>(list: T[], size: number): T[][] => {
}, [] as 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 { proxies: proxiesData, refreshProxy } = useAppData();
const { verge } = useVerge(); const { verge } = useVerge();
@@ -180,7 +184,129 @@ export const useRenderList = (mode: string, isChainMode?: boolean) => {
const renderList: IRenderItem[] = useMemo(() => { const renderList: IRenderItem[] = useMemo(() => {
if (!proxiesData) return []; 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) { if (isChainMode && runtimeConfig) {
// 从运行时配置直接获取 proxies 列表 (需要类型断言) // 从运行时配置直接获取 proxies 列表 (需要类型断言)
const allProxies: IProxyItem[] = Object.values( const allProxies: IProxyItem[] = Object.values(
@@ -311,7 +437,15 @@ export const useRenderList = (mode: string, isChainMode?: boolean) => {
if (!useRule) return retList.slice(1); if (!useRule) return retList.slice(1);
return retList.filter((item: IRenderItem) => !item.group.hidden); return retList.filter((item: IRenderItem) => !item.group.hidden);
}, [headStates, proxiesData, mode, col, isChainMode, runtimeConfig]); }, [
headStates,
proxiesData,
mode,
col,
isChainMode,
runtimeConfig,
selectedGroup,
]);
return { return {
renderList, renderList,

View File

@@ -26,7 +26,7 @@
"Label-Settings": "Settings", "Label-Settings": "Settings",
"Proxies": "Proxies", "Proxies": "Proxies",
"Proxy Groups": "Proxy Groups", "Proxy Groups": "Proxy Groups",
"Node Pool": "Node Pool", "Proxy Chain Mode": "Proxy Chain Mode",
"Connect": "Connect", "Connect": "Connect",
"Connecting...": "Connecting...", "Connecting...": "Connecting...",
"Disconnect": "Disconnect", "Disconnect": "Disconnect",
@@ -40,6 +40,8 @@
"direct": "direct", "direct": "direct",
"Chain Proxy": "🔗 Chain Proxy", "Chain Proxy": "🔗 Chain Proxy",
"Chain Proxy Config": "Chain Proxy Config", "Chain Proxy Config": "Chain Proxy Config",
"Proxy Rules": "Proxy Rules",
"Select Rules": "Select Rules",
"Click nodes in order to add to proxy chain": "Click nodes in order to add to proxy chain", "Click nodes in order to add to proxy chain": "Click nodes in order to add to proxy chain",
"No proxy chain configured": "No proxy chain configured", "No proxy chain configured": "No proxy chain configured",
"Proxy Order": "Proxy Order", "Proxy Order": "Proxy Order",

View File

@@ -26,7 +26,7 @@
"Label-Settings": "设 置", "Label-Settings": "设 置",
"Proxies": "代理", "Proxies": "代理",
"Proxy Groups": "代理组", "Proxy Groups": "代理组",
"Node Pool": "节点池", "Proxy Chain Mode": "链式代理模式",
"Connect": "连接", "Connect": "连接",
"Connecting...": "连接中...", "Connecting...": "连接中...",
"Disconnect": "断开", "Disconnect": "断开",
@@ -40,6 +40,8 @@
"direct": "直连", "direct": "直连",
"Chain Proxy": "🔗 链式代理", "Chain Proxy": "🔗 链式代理",
"Chain Proxy Config": "代理链配置", "Chain Proxy Config": "代理链配置",
"Proxy Rules": "代理规则",
"Select Rules": "选择规则",
"Click nodes in order to add to proxy chain": "顺序点击节点添加到代理链中", "Click nodes in order to add to proxy chain": "顺序点击节点添加到代理链中",
"No proxy chain configured": "暂无代理链配置", "No proxy chain configured": "暂无代理链配置",
"Proxy Order": "代理顺序", "Proxy Order": "代理顺序",

View File

@@ -25,7 +25,7 @@
"Label-Unlock": "測 試", "Label-Unlock": "測 試",
"Label-Settings": "設 置", "Label-Settings": "設 置",
"Proxy Groups": "代理組", "Proxy Groups": "代理組",
"Node Pool": "節點池", "Proxy Chain Mode": "鏈式代理模式",
"Connect": "連接", "Connect": "連接",
"Connecting...": "連接中...", "Connecting...": "連接中...",
"Disconnect": "斷開", "Disconnect": "斷開",

View File

@@ -82,7 +82,15 @@ const ProxyPage = () => {
if (isChainMode) { if (isChainMode) {
const fetchChainConfig = async () => { const fetchChainConfig = async () => {
try { try {
const configData = await getRuntimeProxyChainConfig(); const exitNode = localStorage.getItem("proxy-chain-exit-node");
if (!exitNode) {
console.error("No proxy chain exit node found in localStorage");
setChainConfigData("");
return;
}
const configData = await getRuntimeProxyChainConfig(exitNode);
setChainConfigData(configData || ""); setChainConfigData(configData || "");
} catch (error) { } catch (error) {
console.error("Failed to get runtime proxy chain config:", error); console.error("Failed to get runtime proxy chain config:", error);
@@ -106,7 +114,7 @@ const ProxyPage = () => {
<BasePage <BasePage
full full
contentStyle={{ height: "101.5%" }} contentStyle={{ height: "101.5%" }}
title={isChainMode ? t("Node Pool") : t("Proxy Groups")} title={isChainMode ? t("Proxy Chain Mode") : t("Proxy Groups")}
header={ header={
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<ProviderButton /> <ProviderButton />

View File

@@ -87,8 +87,10 @@ export async function getRuntimeLogs() {
return invoke<Record<string, [string, string][]>>("get_runtime_logs"); return invoke<Record<string, [string, string][]>>("get_runtime_logs");
} }
export async function getRuntimeProxyChainConfig() { export async function getRuntimeProxyChainConfig(proxyChainExitNode: String) {
return invoke<string>("get_runtime_proxy_chain_config"); return invoke<string>("get_runtime_proxy_chain_config", {
proxyChainExitNode,
});
} }
export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) { export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) {