mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 08:45:41 +08:00
添加链式代理下规则适配
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "代理顺序",
|
||||||
|
|||||||
@@ -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": "斷開",
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user