mirror of
https://github.com/clash-verge-rev/clash-verge-rev
synced 2025-05-05 06:53:44 +08:00
feat: url-test支持手动选择、节点组fixed节点使用角标展示 (#840)
* feat: allow manual selection of url-test group * feat: fixed proxy indicator * fix: try to fix traffic websocket no longer updating * fixup: group delay test use defined url
This commit is contained in:
parent
c0f650d7dc
commit
4f7e8116cb
@ -29,21 +29,23 @@ export const LayoutTraffic = () => {
|
|||||||
// setup log ws during layout
|
// setup log ws during layout
|
||||||
useLogSetup();
|
useLogSetup();
|
||||||
|
|
||||||
const { connect, disconnect } = useWebsocket((event) => {
|
const trafficWs = useWebsocket(
|
||||||
const data = JSON.parse(event.data) as ITrafficItem;
|
(event) => {
|
||||||
trafficRef.current?.appendData(data);
|
const data = JSON.parse(event.data) as ITrafficItem;
|
||||||
setTraffic(data);
|
trafficRef.current?.appendData(data);
|
||||||
});
|
setTraffic(data);
|
||||||
|
},
|
||||||
|
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!clashInfo || !pageVisible) return;
|
if (!clashInfo || !pageVisible) return;
|
||||||
|
|
||||||
const { server = "", secret = "" } = clashInfo;
|
const { server = "", secret = "" } = clashInfo;
|
||||||
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
|
trafficWs.connect(
|
||||||
|
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
|
||||||
return () => {
|
);
|
||||||
disconnect();
|
return () => trafficWs.disconnect();
|
||||||
};
|
|
||||||
}, [clashInfo, pageVisible]);
|
}, [clashInfo, pageVisible]);
|
||||||
|
|
||||||
/* --------- meta memory information --------- */
|
/* --------- meta memory information --------- */
|
||||||
@ -54,7 +56,7 @@ export const LayoutTraffic = () => {
|
|||||||
(event) => {
|
(event) => {
|
||||||
setMemory(JSON.parse(event.data));
|
setMemory(JSON.parse(event.data));
|
||||||
},
|
},
|
||||||
{ onError: () => setMemory({ inuse: 0 }) }
|
{ onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
providerHealthCheck,
|
providerHealthCheck,
|
||||||
updateProxy,
|
updateProxy,
|
||||||
deleteConnection,
|
deleteConnection,
|
||||||
|
getGroupProxyDelays,
|
||||||
} from "@/services/api";
|
} from "@/services/api";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { useProfiles } from "@/hooks/use-profiles";
|
import { useProfiles } from "@/hooks/use-profiles";
|
||||||
@ -33,7 +34,7 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
// 切换分组的节点代理
|
// 切换分组的节点代理
|
||||||
const handleChangeProxy = useLockFn(
|
const handleChangeProxy = useLockFn(
|
||||||
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||||
if (group.type !== "Selector" && group.type !== "Fallback") return;
|
if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
|
||||||
|
|
||||||
const { name, now } = group;
|
const { name, now } = group;
|
||||||
await updateProxy(name, proxy.name);
|
await updateProxy(name, proxy.name);
|
||||||
@ -85,7 +86,11 @@ export const ProxyGroups = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
|
||||||
await delayManager.checkListDelay(names, groupName, timeout);
|
|
||||||
|
await Promise.race([
|
||||||
|
delayManager.checkListDelay(names, groupName, timeout),
|
||||||
|
getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果)
|
||||||
|
]);
|
||||||
|
|
||||||
onProxies();
|
onProxies();
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import delayManager from "@/services/delay";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
group: IProxyGroupItem;
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
showType?: boolean;
|
showType?: boolean;
|
||||||
@ -16,7 +16,7 @@ interface Props {
|
|||||||
|
|
||||||
// 多列布局
|
// 多列布局
|
||||||
export const ProxyItemMini = (props: Props) => {
|
export const ProxyItemMini = (props: Props) => {
|
||||||
const { groupName, proxy, selected, showType = true, onClick } = props;
|
const { group, proxy, selected, showType = true, onClick } = props;
|
||||||
|
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
@ -25,21 +25,21 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, groupName);
|
delayManager.removeListener(proxy.name, group.name);
|
||||||
};
|
};
|
||||||
}, [proxy.name, groupName]);
|
}, [proxy.name, group.name]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, groupName));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [proxy]);
|
}, [proxy]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -65,6 +65,12 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
|
||||||
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
|
||||||
"&:hover .the-icon": { display: "none" },
|
"&:hover .the-icon": { display: "none" },
|
||||||
|
"& .the-pin, & .the-unpin": {
|
||||||
|
position: "absolute",
|
||||||
|
top: "-8px",
|
||||||
|
right: "-8px",
|
||||||
|
},
|
||||||
|
"& .the-unpin": { filter: "grayscale(1)" },
|
||||||
"&.Mui-selected": {
|
"&.Mui-selected": {
|
||||||
width: `calc(100% + 3px)`,
|
width: `calc(100% + 3px)`,
|
||||||
marginLeft: `-3px`,
|
marginLeft: `-3px`,
|
||||||
@ -147,14 +153,12 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ ml: 0.5, color: "primary.main" }}>
|
<Box sx={{ ml: 0.5, color: "primary.main" }}>
|
||||||
{delay === -2 && (
|
{delay === -2 && (
|
||||||
<Widget>
|
<Widget>
|
||||||
<BaseLoading />
|
<BaseLoading />
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!proxy.provider && delay !== -2 && (
|
{!proxy.provider && delay !== -2 && (
|
||||||
// provider的节点不支持检测
|
// provider的节点不支持检测
|
||||||
<Widget
|
<Widget
|
||||||
@ -193,7 +197,6 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
{delayManager.formatDelay(delay, timeout)}
|
{delayManager.formatDelay(delay, timeout)}
|
||||||
</Widget>
|
</Widget>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{delay !== -2 && delay <= 0 && selected && (
|
{delay !== -2 && delay <= 0 && selected && (
|
||||||
// 展示已选择的icon
|
// 展示已选择的icon
|
||||||
<CheckCircleOutlineRounded
|
<CheckCircleOutlineRounded
|
||||||
@ -202,6 +205,13 @@ export const ProxyItemMini = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{group.fixed && group.fixed === proxy.name && (
|
||||||
|
// 展示fixed状态
|
||||||
|
<span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
|
||||||
|
📌
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ import delayManager from "@/services/delay";
|
|||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
groupName: string;
|
group: IProxyGroupItem;
|
||||||
proxy: IProxyItem;
|
proxy: IProxyItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
showType?: boolean;
|
showType?: boolean;
|
||||||
@ -44,7 +44,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProxyItem = (props: Props) => {
|
export const ProxyItem = (props: Props) => {
|
||||||
const { groupName, proxy, selected, showType = true, sx, onClick } = props;
|
const { group, proxy, selected, showType = true, sx, onClick } = props;
|
||||||
|
|
||||||
// -1/<=0 为 不显示
|
// -1/<=0 为 不显示
|
||||||
// -2 为 loading
|
// -2 为 loading
|
||||||
@ -52,21 +52,21 @@ export const ProxyItem = (props: Props) => {
|
|||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const timeout = verge?.default_latency_timeout || 10000;
|
const timeout = verge?.default_latency_timeout || 10000;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
delayManager.setListener(proxy.name, groupName, setDelay);
|
delayManager.setListener(proxy.name, group.name, setDelay);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delayManager.removeListener(proxy.name, groupName);
|
delayManager.removeListener(proxy.name, group.name);
|
||||||
};
|
};
|
||||||
}, [proxy.name, groupName]);
|
}, [proxy.name, group.name]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
setDelay(delayManager.getDelayFix(proxy, groupName));
|
setDelay(delayManager.getDelayFix(proxy, group.name));
|
||||||
}, [proxy]);
|
}, [proxy]);
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
setDelay(-2);
|
setDelay(-2);
|
||||||
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
|
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -142,7 +142,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
if (type === 2 && !group.hidden) {
|
if (type === 2 && !group.hidden) {
|
||||||
return (
|
return (
|
||||||
<ProxyItem
|
<ProxyItem
|
||||||
groupName={group.name}
|
group={group}
|
||||||
proxy={proxy!}
|
proxy={proxy!}
|
||||||
selected={group.now === proxy?.name}
|
selected={group.now === proxy?.name}
|
||||||
showType={headState?.showType}
|
showType={headState?.showType}
|
||||||
@ -186,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||||||
{proxyCol?.map((proxy) => (
|
{proxyCol?.map((proxy) => (
|
||||||
<ProxyItemMini
|
<ProxyItemMini
|
||||||
key={item.key + proxy.name}
|
key={item.key + proxy.name}
|
||||||
groupName={group.name}
|
group={group}
|
||||||
proxy={proxy!}
|
proxy={proxy!}
|
||||||
selected={group.now === proxy.name}
|
selected={group.now === proxy.name}
|
||||||
showType={headState?.showType}
|
showType={headState?.showType}
|
||||||
|
@ -5,7 +5,8 @@ export type WsMsgFn = (event: MessageEvent<any>) => void;
|
|||||||
export interface WsOptions {
|
export interface WsOptions {
|
||||||
errorCount?: number; // default is 5
|
errorCount?: number; // default is 5
|
||||||
retryInterval?: number; // default is 2500
|
retryInterval?: number; // default is 2500
|
||||||
onError?: () => void;
|
onError?: (event: Event) => void;
|
||||||
|
onClose?: (event: CloseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
||||||
@ -33,17 +34,23 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
|||||||
const ws = new WebSocket(url);
|
const ws = new WebSocket(url);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.addEventListener("message", onMessage);
|
ws.addEventListener("message", (event) => {
|
||||||
ws.addEventListener("error", () => {
|
errorCount = 0; // reset counter
|
||||||
|
onMessage(event);
|
||||||
|
});
|
||||||
|
ws.addEventListener("error", (event) => {
|
||||||
errorCount -= 1;
|
errorCount -= 1;
|
||||||
|
|
||||||
if (errorCount >= 0) {
|
if (errorCount >= 0) {
|
||||||
timerRef.current = setTimeout(connectHelper, 2500);
|
timerRef.current = setTimeout(connectHelper, 2500);
|
||||||
} else {
|
} else {
|
||||||
disconnect();
|
disconnect();
|
||||||
options?.onError?.();
|
options?.onError?.(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ws.addEventListener("close", (event) => {
|
||||||
|
options?.onClose?.(event);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
connectHelper();
|
connectHelper();
|
||||||
|
@ -75,9 +75,13 @@ export const getRules = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Get Proxy delay
|
/// Get Proxy delay
|
||||||
export const getProxyDelay = async (name: string, url?: string) => {
|
export const getProxyDelay = async (
|
||||||
|
name: string,
|
||||||
|
url?: string,
|
||||||
|
timeout?: number
|
||||||
|
) => {
|
||||||
const params = {
|
const params = {
|
||||||
timeout: 10000,
|
timeout: timeout || 10000,
|
||||||
url: url || "http://1.1.1.1",
|
url: url || "http://1.1.1.1",
|
||||||
};
|
};
|
||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
@ -237,3 +241,21 @@ export const closeAllConnections = async () => {
|
|||||||
const instance = await getAxios();
|
const instance = await getAxios();
|
||||||
await instance.delete<any, any>(`/connections`);
|
await instance.delete<any, any>(`/connections`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get Group Proxy Delays
|
||||||
|
export const getGroupProxyDelays = async (
|
||||||
|
groupName: string,
|
||||||
|
url?: string,
|
||||||
|
timeout?: number
|
||||||
|
) => {
|
||||||
|
const params = {
|
||||||
|
timeout: timeout || 10000,
|
||||||
|
url: url || "http://1.1.1.1",
|
||||||
|
};
|
||||||
|
const instance = await getAxios();
|
||||||
|
const result = await instance.get(
|
||||||
|
`/group/${encodeURIComponent(groupName)}/delay`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return result as any as Record<string, number>;
|
||||||
|
};
|
||||||
|
1
src/services/types.d.ts
vendored
1
src/services/types.d.ts
vendored
@ -64,6 +64,7 @@ interface IProxyItem {
|
|||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
provider?: string; // 记录是否来自provider
|
provider?: string; // 记录是否来自provider
|
||||||
|
fixed?: string; // 记录固定(优先)的节点
|
||||||
}
|
}
|
||||||
|
|
||||||
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user