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:
dongchengjie 2024-04-09 13:15:45 +08:00 committed by GitHub
parent c0f650d7dc
commit 4f7e8116cb
8 changed files with 85 additions and 38 deletions

View File

@ -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(() => {

View File

@ -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();
}); });

View File

@ -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>
); );
}; };

View File

@ -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 (

View File

@ -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}

View File

@ -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();

View File

@ -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>;
};

View File

@ -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"> & {