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
useLogSetup();
const { connect, disconnect } = useWebsocket((event) => {
const trafficWs = useWebsocket(
(event) => {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
setTraffic(data);
});
},
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
);
useEffect(() => {
if (!clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
return () => {
disconnect();
};
trafficWs.connect(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
);
return () => trafficWs.disconnect();
}, [clashInfo, pageVisible]);
/* --------- meta memory information --------- */
@ -54,7 +56,7 @@ export const LayoutTraffic = () => {
(event) => {
setMemory(JSON.parse(event.data));
},
{ onError: () => setMemory({ inuse: 0 }) }
{ onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
);
useEffect(() => {

View File

@ -6,6 +6,7 @@ import {
providerHealthCheck,
updateProxy,
deleteConnection,
getGroupProxyDelays,
} from "@/services/api";
import { Box } from "@mui/material";
import { useProfiles } from "@/hooks/use-profiles";
@ -33,7 +34,7 @@ export const ProxyGroups = (props: Props) => {
// 切换分组的节点代理
const handleChangeProxy = useLockFn(
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;
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);
await delayManager.checkListDelay(names, groupName, timeout);
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果)
]);
onProxies();
});

View File

@ -7,7 +7,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
group: IProxyGroupItem;
proxy: IProxyItem;
selected: boolean;
showType?: boolean;
@ -16,7 +16,7 @@ interface Props {
// 多列布局
export const ProxyItemMini = (props: Props) => {
const { groupName, proxy, selected, showType = true, onClick } = props;
const { group, proxy, selected, showType = true, onClick } = props;
// -1/<=0 为 不显示
// -2 为 loading
@ -25,21 +25,21 @@ export const ProxyItemMini = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, groupName]);
}, [proxy.name, group.name]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName));
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
});
return (
@ -65,6 +65,12 @@ export const ProxyItemMini = (props: Props) => {
"&:hover .the-check": { display: !showDelay ? "block" : "none" },
"&:hover .the-delay": { display: showDelay ? "block" : "none" },
"&:hover .the-icon": { display: "none" },
"& .the-pin, & .the-unpin": {
position: "absolute",
top: "-8px",
right: "-8px",
},
"& .the-unpin": { filter: "grayscale(1)" },
"&.Mui-selected": {
width: `calc(100% + 3px)`,
marginLeft: `-3px`,
@ -147,14 +153,12 @@ export const ProxyItemMini = (props: Props) => {
</Box>
)}
</Box>
<Box sx={{ ml: 0.5, color: "primary.main" }}>
{delay === -2 && (
<Widget>
<BaseLoading />
</Widget>
)}
{!proxy.provider && delay !== -2 && (
// provider的节点不支持检测
<Widget
@ -193,7 +197,6 @@ export const ProxyItemMini = (props: Props) => {
{delayManager.formatDelay(delay, timeout)}
</Widget>
)}
{delay !== -2 && delay <= 0 && selected && (
// 展示已选择的icon
<CheckCircleOutlineRounded
@ -202,6 +205,13 @@ export const ProxyItemMini = (props: Props) => {
/>
)}
</Box>
{group.fixed && group.fixed === proxy.name && (
// 展示fixed状态
<span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
📌
</span>
)}
</ListItemButton>
);
};

View File

@ -17,7 +17,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge";
interface Props {
groupName: string;
group: IProxyGroupItem;
proxy: IProxyItem;
selected: boolean;
showType?: boolean;
@ -44,7 +44,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
}));
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 为 不显示
// -2 为 loading
@ -52,21 +52,21 @@ export const ProxyItem = (props: Props) => {
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay);
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
delayManager.removeListener(proxy.name, groupName);
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, groupName]);
}, [proxy.name, group.name]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName));
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout));
setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
});
return (

View File

@ -142,7 +142,7 @@ export const ProxyRender = (props: RenderProps) => {
if (type === 2 && !group.hidden) {
return (
<ProxyItem
groupName={group.name}
group={group}
proxy={proxy!}
selected={group.now === proxy?.name}
showType={headState?.showType}
@ -186,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
{proxyCol?.map((proxy) => (
<ProxyItemMini
key={item.key + proxy.name}
groupName={group.name}
group={group}
proxy={proxy!}
selected={group.now === proxy.name}
showType={headState?.showType}

View File

@ -5,7 +5,8 @@ export type WsMsgFn = (event: MessageEvent<any>) => void;
export interface WsOptions {
errorCount?: number; // default is 5
retryInterval?: number; // default is 2500
onError?: () => void;
onError?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
}
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
@ -33,17 +34,23 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
const ws = new WebSocket(url);
wsRef.current = ws;
ws.addEventListener("message", onMessage);
ws.addEventListener("error", () => {
ws.addEventListener("message", (event) => {
errorCount = 0; // reset counter
onMessage(event);
});
ws.addEventListener("error", (event) => {
errorCount -= 1;
if (errorCount >= 0) {
timerRef.current = setTimeout(connectHelper, 2500);
} else {
disconnect();
options?.onError?.();
options?.onError?.(event);
}
});
ws.addEventListener("close", (event) => {
options?.onClose?.(event);
});
};
connectHelper();

View File

@ -75,9 +75,13 @@ export const getRules = async () => {
};
/// Get Proxy delay
export const getProxyDelay = async (name: string, url?: string) => {
export const getProxyDelay = async (
name: string,
url?: string,
timeout?: number
) => {
const params = {
timeout: 10000,
timeout: timeout || 10000,
url: url || "http://1.1.1.1",
};
const instance = await getAxios();
@ -237,3 +241,21 @@ export const closeAllConnections = async () => {
const instance = await getAxios();
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;
icon?: string;
provider?: string; // 记录是否来自provider
fixed?: string; // 记录固定(优先)的节点
}
type IProxyGroupItem = Omit<IProxyItem, "all"> & {