Pārlūkot izejas kodu

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
dongchengjie 1 gadu atpakaļ
vecāks
revīzija
213d417481

+ 13 - 11
src/components/layout/layout-traffic.tsx

@@ -29,21 +29,23 @@ export const LayoutTraffic = () => {
   // setup log ws during layout
   useLogSetup();
 
-  const { connect, disconnect } = useWebsocket((event) => {
-    const data = JSON.parse(event.data) as ITrafficItem;
-    trafficRef.current?.appendData(data);
-    setTraffic(data);
-  });
+  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(() => {

+ 7 - 2
src/components/proxy/proxy-groups.tsx

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

+ 20 - 10
src/components/proxy/proxy-item-mini.tsx

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

+ 7 - 7
src/components/proxy/proxy-item.tsx

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

+ 2 - 2
src/components/proxy/proxy-render.tsx

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

+ 11 - 4
src/hooks/use-websocket.ts

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

+ 24 - 2
src/services/api.ts

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

+ 1 - 0
src/services/types.d.ts

@@ -64,6 +64,7 @@ interface IProxyItem {
   hidden?: boolean;
   icon?: string;
   provider?: string; // 记录是否来自provider
+  fixed?: string; // 记录固定(优先)的节点
 }
 
 type IProxyGroupItem = Omit<IProxyItem, "all"> & {