refactor: invock mihomo api by use tauri-plugin-mihomo

This commit is contained in:
oomeow
2025-10-01 09:30:28 +08:00
parent f905969ed8
commit 64545de4b6
65 changed files with 1982 additions and 1840 deletions
@@ -4,8 +4,8 @@ import dayjs from "dayjs";
import { t } from "i18next";
import { forwardRef, useImperativeHandle, useState } from "react";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { closeConnections } from "tauri-plugin-mihomo-api";
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void;
@@ -99,7 +99,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
];
const onDelete = useLockFn(async () => deleteConnection(data.id));
const onDelete = useLockFn(async () => closeConnections(data.id));
return (
<Box sx={{ userSelect: "text", color: theme.palette.text.secondary }}>
@@ -10,8 +10,8 @@ import {
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { deleteConnection } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { closeConnections } from "tauri-plugin-mihomo-api";
const Tag = styled("span")(({ theme }) => ({
fontSize: "10px",
@@ -34,7 +34,7 @@ export const ConnectionItem = (props: Props) => {
const { id, metadata, chains, start, curUpload, curDownload } = value;
const onDelete = useLockFn(async () => deleteConnection(id));
const onDelete = useLockFn(async () => closeConnections(id));
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
return (
+1 -1
View File
@@ -10,8 +10,8 @@ import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { patchClashMode } from "@/services/cmds";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
export const ClashModeCard = () => {
const { t } = useTranslation();
+25 -85
View File
@@ -13,25 +13,23 @@ import {
useTheme,
PaletteColor,
Grid,
Box,
} from "@mui/material";
import { useRef, useCallback, memo, useMemo } from "react";
import { useRef, memo, useMemo } from "react";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { isDebugEnabled, gc } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import {
EnhancedCanvasTrafficGraph,
type EnhancedCanvasTrafficGraphRef,
} from "./enhanced-canvas-traffic-graph";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useMemoryData } from "@/hooks/use-memory-data";
interface StatCardProps {
icon: ReactNode;
@@ -149,51 +147,33 @@ export const EnhancedTrafficStats = () => {
const trafficRef = useRef<EnhancedCanvasTrafficGraphRef>(null);
const pageVisible = useVisibility();
// 使用AppDataProvider
const { connections } = useAppData();
const {
response: { data: traffic },
} = useTrafficData();
// 使用增强版的统一流量数据Hook
const { traffic, memory, isLoading, isDataFresh, hasValidData } =
useTrafficDataEnhanced();
const {
response: { data: memory },
} = useMemoryData();
const {
response: { data: connections },
} = useConnectionData();
// 是否显示流量图表
const trafficGraph = verge?.traffic_graph ?? true;
// 检查是否支持调试
// TODO: merge this hook with layout-traffic.tsx
const { data: isDebug } = useSWR(
`clash-verge-rev-internal://isDebugEnabled`,
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
// 执行垃圾回收
const handleGarbageCollection = useCallback(async () => {
if (isDebug) {
try {
await gc();
console.log("[Debug] 垃圾回收已执行");
} catch (err) {
console.error("[Debug] 垃圾回收失败:", err);
}
}
}, [isDebug]);
// 使用useMemo计算解析后的流量数据
const parsedData = useMemo(() => {
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const [uploadTotal, uploadTotalUnit] = parseTraffic(
connections.uploadTotal,
connections?.uploadTotal,
);
const [downloadTotal, downloadTotalUnit] = parseTraffic(
connections.downloadTotal,
connections?.downloadTotal,
);
return {
@@ -207,7 +187,7 @@ export const EnhancedTrafficStats = () => {
uploadTotalUnit,
downloadTotal,
downloadTotalUnit,
connectionsCount: connections.count,
connectionsCount: connections?.connections.length,
};
}, [traffic, memory, connections]);
@@ -229,33 +209,10 @@ export const EnhancedTrafficStats = () => {
>
<div style={{ height: "100%", position: "relative" }}>
<EnhancedCanvasTrafficGraph ref={trafficRef} />
{isDebug && (
<div
style={{
position: "absolute",
top: "2px",
left: "2px",
zIndex: 10,
backgroundColor: "rgba(0,0,0,0.5)",
color: "white",
fontSize: "8px",
padding: "2px 4px",
borderRadius: "4px",
}}
>
DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"}
<br />
状态: {isDataFresh ? "active" : "inactive"}
<br />
数据新鲜度: {traffic?.is_fresh ? "Fresh" : "Stale"}
<br />
{new Date().toISOString().slice(11, 19)}
</div>
)}
</div>
</Paper>
);
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
}, [trafficGraph, pageVisible, theme.palette.divider]);
// 使用useMemo计算统计卡片配置
const statCards = useMemo(
@@ -301,10 +258,10 @@ export const EnhancedTrafficStats = () => {
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
onClick: isDebug ? handleGarbageCollection : undefined,
onClick: undefined,
},
],
[t, parsedData, isDebug, handleGarbageCollection],
[t, parsedData],
);
return (
@@ -321,28 +278,11 @@ export const EnhancedTrafficStats = () => {
</Grid>
)}
{/* 统计卡片区域 */}
{statCards.map((card, index) => (
<Grid key={index} size={4}>
{statCards.map((card, _index) => (
<Grid key={card.title} size={4}>
<CompactStatCard {...card} />
</Grid>
))}
{/* 数据状态指示器(调试用)*/}
{isDebug && (
<Grid size={12}>
<Box
sx={{
p: 1,
bgcolor: "action.hover",
borderRadius: 1,
fontSize: "0.75rem",
}}
>
数据状态: {isDataFresh ? "新鲜" : "过期"} | 有效数据:{" "}
{hasValidData ? "是" : "否"} | 加载中: {isLoading ? "是" : "否"}
</Box>
</Grid>
)}
</Grid>
</TrafficErrorBoundary>
);
+25 -56
View File
@@ -6,34 +6,19 @@ import {
import { Box, Typography } from "@mui/material";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import useSWR from "swr";
import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary";
import { useClashInfo } from "@/hooks/use-clash";
import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useMemoryData } from "@/hooks/use-memory-data";
// setup the traffic
export const LayoutTraffic = () => {
const { data: isDebug } = useSWR(
"clash-verge-rev-internal://isDebugEnabled",
() => isDebugEnabled(),
{
// default value before is fetched
fallbackData: false,
},
);
if (isDebug) {
console.debug("[Traffic][LayoutTraffic] 组件正在渲染");
}
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
// whether hide traffic graph
@@ -42,31 +27,19 @@ export const LayoutTraffic = () => {
const trafficRef = useRef<TrafficRef>(null);
const pageVisible = useVisibility();
// 使用增强版的统一流量数据Hook
const { traffic, memory } = useTrafficDataEnhanced();
// 启动流量服务
useEffect(() => {
console.log(
"[Traffic][LayoutTraffic] useEffect 触发,clashInfo:",
clashInfo,
"pageVisible:",
pageVisible,
);
// 简化条件,只要组件挂载就尝试启动服务
console.log("[Traffic][LayoutTraffic] 开始启动流量服务");
startTrafficService().catch((error) => {
console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error);
});
}, []); // 移除依赖,只在组件挂载时启动一次
const {
response: { data: traffic },
} = useTrafficData();
const {
response: { data: memory },
} = useMemoryData();
// 监听数据变化,为图表添加数据点
useEffect(() => {
if (traffic?.raw && trafficRef.current) {
if (trafficRef.current) {
trafficRef.current.appendData({
up: traffic.raw.up_rate || 0,
down: traffic.raw.down_rate || 0,
up: traffic?.up || 0,
down: traffic?.down || 0,
});
}
}, [traffic]);
@@ -75,9 +48,9 @@ export const LayoutTraffic = () => {
const displayMemory = verge?.enable_memory_usage ?? true;
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0);
const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0);
const [up, upUnit] = parseTraffic(traffic?.up || 0);
const [down, downUnit] = parseTraffic(traffic?.down || 0);
const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0);
const boxStyle: any = {
display: "flex",
@@ -114,18 +87,16 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={`${t("Upload Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Upload Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowUpwardRounded
{...iconStyle}
color={
(traffic?.raw?.up_rate || 0) > 0 ? "secondary" : "disabled"
}
color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"}
/>
<Typography {...valStyle} color="secondary">
{up}
@@ -134,18 +105,16 @@ export const LayoutTraffic = () => {
</Box>
<Box
title={`${t("Download Speed")} ${traffic?.is_fresh ? "" : "(Stale)"}`}
title={`${t("Download Speed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
opacity: traffic?.is_fresh ? 1 : 0.6,
// opacity: traffic?.is_fresh ? 1 : 0.6,
}}
>
<ArrowDownwardRounded
{...iconStyle}
color={
(traffic?.raw?.down_rate || 0) > 0 ? "primary" : "disabled"
}
color={(traffic?.down || 0) > 0 ? "primary" : "disabled"}
/>
<Typography {...valStyle} color="primary">
{down}
@@ -155,15 +124,15 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
title={`${t(isDebug ? "Memory Cleanup" : "Memory Usage")} ${memory?.is_fresh ? "" : "(Stale)"} ${"usage_percent" in (memory?.formatted || {}) && memory.formatted.usage_percent ? `(${memory.formatted.usage_percent.toFixed(1)}%)` : ""}`}
title={`${t("Memory Usage")} `}
{...boxStyle}
sx={{
cursor: isDebug ? "pointer" : "auto",
opacity: memory?.is_fresh ? 1 : 0.6,
cursor: "auto",
// opacity: memory?.is_fresh ? 1 : 0.6,
}}
color={isDebug ? "success.main" : "disabled"}
color={"disabled"}
onClick={async () => {
isDebug && (await gc());
// isDebug && (await gc());
}}
>
<MemoryRounded {...iconStyle} />
+153 -150
View File
@@ -22,9 +22,9 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppData } from "@/providers/app-data-provider";
import { proxyProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import parseTraffic from "@/utils/parse-traffic";
import { updateProxyProvider } from "tauri-plugin-mihomo-api";
// 定义代理提供者类型
interface ProxyProviderItem {
@@ -74,7 +74,7 @@ export const ProviderButton = () => {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 刷新数据
await refreshProxy();
@@ -115,7 +115,7 @@ export const ProviderButton = () => {
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await proxyProviderUpdate(name);
await updateProxyProvider(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
@@ -177,161 +177,164 @@ export const ProviderButton = () => {
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(proxyProviders || {}).map(([key, item]) => {
const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
{Object.entries(proxyProviders || {})
.sort()
.map(([key, item]) => {
const provider = item as ProxyProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 订阅信息
const sub = provider.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
// 流量使用进度
const progress =
total > 0
? Math.min(
Math.round(((download + upload) * 100) / total) + 1,
100,
)
: 0;
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
},
};
},
]}
>
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.proxies.length}
</TypeBox>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<>
{/* 订阅信息 */}
{hasSubInfo && (
<>
<Box
sx={{
mb: 1,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
{parseExpire(expire)}
</span>
</Box>
{/* 进度条 */}
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 6,
borderRadius: 3,
opacity: total > 0 ? 1 : 0,
}}
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
<IconButton
size="small"
color="primary"
onClick={() => {
updateProvider(key);
}}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</DialogContent>
+3 -3
View File
@@ -37,11 +37,11 @@ import useSWR from "swr";
import { useAppData } from "@/providers/app-data-provider";
import {
closeAllConnections,
getProxies,
calcuProxies,
updateProxyAndSync,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
interface ProxyChainItem {
id: string;
@@ -204,7 +204,7 @@ export const ProxyChain = ({
// 获取当前代理信息以检查连接状态
const { data: currentProxies, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
calcuProxies,
{
revalidateOnFocus: true,
revalidateIfStale: true,
+13 -21
View File
@@ -3,29 +3,21 @@ import {
Snackbar,
Alert,
Chip,
Stack,
Typography,
IconButton,
Collapse,
Menu,
MenuItem,
Divider,
} from "@mui/material";
import { ArchiveOutlined, ExpandMoreRounded } from "@mui/icons-material";
import { ExpandMoreRounded } from "@mui/icons-material";
import { useLockFn } from "ahooks";
import { useRef, useState, useEffect, useCallback } from "react";
import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { useProxySelection } from "@/hooks/use-proxy-selection";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import {
providerHealthCheck,
getGroupProxyDelays,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import { updateProxyChainConfigInRuntime } from "@/services/cmds";
import delayManager from "@/services/delay";
import { BaseEmpty } from "../base";
@@ -34,6 +26,7 @@ import { ScrollTopButton } from "../layout/scroll-top-button";
import { ProxyChain } from "./proxy-chain";
import { ProxyRender } from "./proxy-render";
import { useRenderList } from "./use-render-list";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
interface Props {
mode: string;
@@ -154,15 +147,14 @@ export const ProxyGroups = (props: Props) => {
// 添加和清理滚动事件监听器
useEffect(() => {
const currentScroller = scrollerRef.current;
if (currentScroller) {
currentScroller.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
currentScroller.removeEventListener("scroll", handleScroll);
};
}
if (!scrollerRef.current) return;
scrollerRef.current.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
scrollerRef.current?.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
// 滚动到顶部
@@ -274,7 +266,7 @@ export const ProxyGroups = (props: Props) => {
if (providers.size) {
console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`);
Promise.allSettled(
[...providers].map((p) => providerHealthCheck(p)),
[...providers].map((p) => healthcheckProxyProvider(p)),
).then(() => {
console.log(`[ProxyGroups] 提供者健康检查完成`);
onProxies();
@@ -290,7 +282,7 @@ export const ProxyGroups = (props: Props) => {
try {
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, url, timeout).then((result) => {
delayGroup(groupName, url, timeout).then((result) => {
console.log(
`[ProxyGroups] getGroupProxyDelays返回结果数量:`,
Object.keys(result || {}).length,
+109 -104
View File
@@ -20,9 +20,9 @@ import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppData } from "@/providers/app-data-provider";
import { ruleProviderUpdate } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { updateRuleProvider } from "tauri-plugin-mihomo-api";
import { useAppData } from "@/providers/app-data-provider";
// 定义规则提供者类型
interface RuleProviderItem {
@@ -60,7 +60,7 @@ export const ProviderButton = () => {
// 设置更新状态
setUpdating((prev) => ({ ...prev, [name]: true }));
await ruleProviderUpdate(name);
await updateRuleProvider(name);
// 刷新数据
await refreshRules();
@@ -101,7 +101,7 @@ export const ProviderButton = () => {
// 改为串行逐个更新所有provider
for (const name of allProviders) {
try {
await ruleProviderUpdate(name);
await updateRuleProvider(name);
// 每个更新完成后更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
} catch (err) {
@@ -160,112 +160,117 @@ export const ProviderButton = () => {
<DialogContent>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(ruleProviders || {}).map(([key, item]) => {
const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
{Object.entries(ruleProviders || {})
.sort()
.map(([key, item]) => {
const provider = item as RuleProviderItem;
const time = dayjs(provider.updatedAt);
const isUpdating = updating[key];
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor = mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return (
<ListItem
key={key}
sx={[
{
p: 0,
mb: "8px",
borderRadius: 2,
overflow: "hidden",
transition: "all 0.2s",
},
({ palette: { mode, primary } }) => {
const bgcolor =
mode === "light" ? "#ffffff" : "#24252f";
const hoverColor =
mode === "light"
? alpha(primary.main, 0.1)
: alpha(primary.main, 0.2);
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3),
},
};
},
]}
>
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.ruleCount}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<Box sx={{ display: "flex" }}>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">{provider.behavior}</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
return {
backgroundColor: bgcolor,
"&:hover": {
backgroundColor: hoverColor,
borderColor: alpha(primary.main, 0.3),
},
};
},
]}
>
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
<ListItemText
sx={{ px: 2, py: 1 }}
primary={
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography
variant="subtitle1"
component="div"
noWrap
title={key}
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ marginRight: "8px" }}>{key}</span>
<TypeBox component="span">
{provider.ruleCount}
</TypeBox>
</Typography>
<Typography
variant="body2"
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
{time.fromNow()}
</Typography>
</Box>
}
secondary={
<Box sx={{ display: "flex" }}>
<TypeBox component="span">
{provider.vehicleType}
</TypeBox>
<TypeBox component="span">
{provider.behavior}
</TypeBox>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<Box
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
width: 40,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
<IconButton
size="small"
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
: "none",
"@keyframes spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
>
<RefreshRounded />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</DialogContent>
@@ -19,12 +19,8 @@ import { mutate } from "swr";
import { BaseDialog, DialogRef } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import { changeClashCore, restartCore } from "@/services/cmds";
import {
closeAllConnections,
upgradeCore,
forceRefreshClashConfig,
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api";
const VALID_CORE = [
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
@@ -64,8 +60,6 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
mutateVerge();
setTimeout(async () => {
// 核心切换后强制刷新配置缓存
await forceRefreshClashConfig();
mutate("getClashConfig");
mutate("getVersion");
setChangingCore(null);
+12 -23
View File
@@ -11,13 +11,7 @@ import {
Typography,
} from "@mui/material";
import { useLockFn } from "ahooks";
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from "react";
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import useSWR, { mutate } from "swr";
@@ -27,7 +21,6 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { getClashConfig } from "@/services/cmds";
import {
getAutotemProxy,
getNetworkInterfacesInfo,
@@ -37,6 +30,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import { getBaseConfig } from "tauri-plugin-mihomo-api";
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
@@ -123,26 +117,21 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
};
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, {
const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, {
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000,
});
const [prevMixedPort, setPrevMixedPort] = useState(
clashConfig?.["mixed-port"],
);
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.mixedPort);
useEffect(() => {
if (
clashConfig?.["mixed-port"] &&
clashConfig?.["mixed-port"] !== prevMixedPort
) {
setPrevMixedPort(clashConfig?.["mixed-port"]);
resetSystemProxy();
}
}, [clashConfig?.["mixed-port"]]);
// useEffect(() => {
// if (clashConfig?.mixedPort && clashConfig.mixedPort !== prevMixedPort) {
// setPrevMixedPort(clashConfig.mixedPort);
// resetSystemProxy();
// }
// }, [clashConfig]);
const resetSystemProxy = async () => {
try {
@@ -180,7 +169,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (isPacMode) {
const host = value.proxy_host || "127.0.0.1";
const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897;
const port = verge?.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${host}:${port}`;
} else {
return systemProxyAddress;
@@ -332,7 +321,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (pacContent) {
pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host);
// 将 mixed-port 转换为字符串
const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString();
const mixedPortStr = (clashConfig?.mixedPort || "").toString();
pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr);
}
+8 -3
View File
@@ -10,7 +10,6 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useClash } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { invoke_uwp_tool } from "@/services/cmds";
import { updateGeoData } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
@@ -23,6 +22,8 @@ import { GuardState } from "./mods/guard-state";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
import { SettingItem, SettingList } from "./mods/setting-comp";
import { WebUIViewer } from "./mods/web-ui-viewer";
import { updateGeo } from "tauri-plugin-mihomo-api";
import { useClashLog } from "@/services/states";
const isWIN = getSystem() === "windows";
@@ -35,6 +36,7 @@ const SettingClash = ({ onError }: Props) => {
const { clash, version, mutateClash, patchClash } = useClash();
const { verge, patchVerge } = useVerge();
const [clashLog, setClashLog] = useClashLog();
const {
ipv6,
@@ -64,7 +66,7 @@ const SettingClash = ({ onError }: Props) => {
};
const onUpdateGeo = async () => {
try {
await updateGeoData();
await updateGeo();
showNotice("success", t("GeoData Updated"));
} catch (err: any) {
showNotice("error", err?.response.data.message || err.toString());
@@ -186,7 +188,10 @@ const SettingClash = ({ onError }: Props) => {
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ "log-level": e })}
onGuard={(e) => patchClash({ "log-level": e })}
onGuard={(e) => {
setClashLog((pre: any) => ({ ...pre, logLevel: e }));
return patchClash({ "log-level": e });
}}
>
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
<MenuItem value="debug">Debug</MenuItem>
+4 -10
View File
@@ -1,13 +1,12 @@
import { useLockFn } from "ahooks";
import useSWR, { mutate } from "swr";
import { getVersion } from "@/services/cmds";
import {
getClashInfo,
patchClashConfig,
getRuntimeConfig,
forceRefreshClashConfig,
} from "@/services/cmds";
import { getVersion } from "tauri-plugin-mihomo-api";
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
@@ -25,11 +24,9 @@ export const useClash = () => {
mutateClash();
});
const version = versionData?.premium
? `${versionData.version} Premium`
: versionData?.meta
? `${versionData.version} Mihomo`
: versionData?.version || "-";
const version = versionData?.meta
? `${versionData.version} Mihomo`
: versionData?.version || "-";
return {
clash,
@@ -123,10 +120,7 @@ export const useClashInfo = () => {
await patchClashConfig(patch);
mutateInfo();
// 配置修改后强制刷新缓存
await forceRefreshClashConfig();
mutate("getClashConfig");
// IPC调用不需要刷新axios实例
};
return {
+125
View File
@@ -0,0 +1,125 @@
import { listen } from "@tauri-apps/api/event";
import { useLocalStorage } from "foxact/use-local-storage";
import { useEffect, useRef } from "react";
import { mutate } from "swr";
import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export const initConnData: IConnections = {
uploadTotal: 0,
downloadTotal: 0,
connections: [],
};
export const useConnectionData = () => {
const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now());
const subscriptKey = `getClashConnection-${date}`;
const ws = useRef<MihomoWebSocket | null>(null);
const wsFirstConnection = useRef<boolean>(true);
const listenerRef = useRef<() => void | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<IConnections, any, string | null>(
subscriptKey,
(_key, { next }) => {
const connect = () =>
MihomoWebSocket.connect_connections()
.then((ws_) => {
ws.current = ws_;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
listenerRef.current = ws_.addListener(async (msg) => {
if (msg.type === "Text") {
if (msg.data.startsWith("Websocket error")) {
next(msg.data);
await ws.current?.close();
ws.current = null;
timeoutRef.current = setTimeout(
async () => await connect(),
500,
);
} else {
const data = JSON.parse(msg.data) as IConnections;
next(null, (old = initConnData) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = [];
const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) {
const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
connections[index] = each;
return false;
}
return true;
});
for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return { ...data, connections };
});
}
}
});
})
.catch((_) => {
if (!ws.current) {
timeoutRef.current = setTimeout(async () => await connect(), 500);
}
});
if (
wsFirstConnection.current ||
(ws.current && !wsFirstConnection.current)
) {
wsFirstConnection.current = false;
if (ws.current) {
ws.current.close();
ws.current = null;
}
connect();
}
return () => {
ws.current?.close();
listenerRef.current?.();
listenerRef.current = null;
};
},
{
fallbackData: initConnData,
keepPreviousData: true,
},
);
useEffect(() => {
const unlistenRefreshWebsocket = listen(
"verge://refresh-websocket",
async () => {
await ws.current?.close();
setDate(Date.now());
},
);
return () => {
unlistenRefreshWebsocket.then((fn) => fn());
};
}, [setDate]);
useEffect(() => {
mutate(`$sub$${subscriptKey}`);
}, [date, subscriptKey]);
const refreshGetClashConnection = () => {
setDate(Date.now());
};
return { response, refreshGetClashConnection };
};
+170
View File
@@ -0,0 +1,170 @@
// import { getClashLogs } from "@/services/cmds";
import { listen } from "@tauri-apps/api/event";
import dayjs from "dayjs";
import { useLocalStorage } from "foxact/use-local-storage";
import { useEffect, useRef } from "react";
import { mutate } from "swr";
import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
import { useClashLog } from "@/services/states";
import { getClashLogs } from "@/services/cmds";
const MAX_LOG_NUM = 1000;
export const useLogData = () => {
// const [enableLog] = useEnableLog();
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const [clashLog] = useClashLog();
const enableLog = clashLog.enable;
const logLevel = clashLog.logLevel;
const [date, setDate] = useLocalStorage("mihomo_logs_date", Date.now());
const subscriptKey = enableLog ? `getClashLog-${date}` : null;
const ws = useRef<MihomoWebSocket | null>(null);
const wsFirstConnection = useRef<boolean>(true);
const listenerRef = useRef<() => void | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<ILogItem[], any, string | null>(
subscriptKey,
(_key, { next }) => {
const connect = () =>
MihomoWebSocket.connect_logs(logLevel)
.then(async (ws_) => {
ws.current = ws_;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
const logs = await getClashLogs();
let filterLogs: ILogItem[] = [];
switch (logLevel) {
case "debug":
filterLogs = logs.filter((i) =>
["debug", "info", "warning", "error"].includes(i.type),
);
break;
case "info":
filterLogs = logs.filter((i) =>
["info", "warning", "error"].includes(i.type),
);
break;
case "warning":
filterLogs = logs.filter((i) =>
["warning", "error"].includes(i.type),
);
break;
case "error":
filterLogs = logs.filter((i) => i.type === "error");
break;
case "silent":
filterLogs = [];
break;
default:
filterLogs = logs;
break;
}
next(null, filterLogs);
const buffer: ILogItem[] = [];
let flushTimer: ReturnType<typeof setTimeout> | null = null;
const flush = () => {
if (buffer.length > 0) {
next(null, (l) => {
let newList = [...(l ?? []), ...buffer.splice(0)];
if (newList.length > MAX_LOG_NUM) {
newList = newList.slice(
-Math.min(MAX_LOG_NUM, newList.length),
);
}
return newList;
});
}
flushTimer = null;
};
listenerRef.current = ws_.addListener(async (msg) => {
if (msg.type === "Text") {
if (msg.data.startsWith("Websocket error")) {
next(msg.data);
await ws.current?.close();
ws.current = null;
timeoutRef.current = setTimeout(
async () => await connect(),
500,
);
} else {
const data = JSON.parse(msg.data) as ILogItem;
data.time = dayjs().format("MM-DD HH:mm:ss");
buffer.push(data);
// flush data
if (!flushTimer) {
flushTimer = setTimeout(flush, 50);
}
}
}
});
})
.catch((_) => {
if (!ws.current) {
timeoutRef.current = setTimeout(async () => await connect(), 500);
}
});
if (
wsFirstConnection.current ||
(ws.current && !wsFirstConnection.current)
) {
wsFirstConnection.current = false;
if (ws.current) {
ws.current.close();
ws.current = null;
}
connect();
}
return () => {
ws.current?.close();
listenerRef.current?.();
listenerRef.current = null;
};
},
{
fallbackData: [],
keepPreviousData: true,
},
);
useEffect(() => {
const unlistenRefreshWebsocket = listen(
"verge://refresh-websocket",
async () => {
await ws.current?.close();
setDate(Date.now());
},
);
return () => {
unlistenRefreshWebsocket.then((fn) => fn());
};
}, [setDate]);
useEffect(() => {
mutate(`$sub$${subscriptKey}`);
}, [date, subscriptKey]);
useEffect(() => {
if (!logLevel) return;
ws.current?.close();
setDate(Date.now());
}, [logLevel]);
const refreshGetClashLog = (clear = false) => {
if (clear) {
mutate(`$sub$${subscriptKey}`, []);
} else {
setDate(Date.now());
}
};
return { response, refreshGetClashLog };
};
+101
View File
@@ -0,0 +1,101 @@
import { listen } from "@tauri-apps/api/event";
import { useLocalStorage } from "foxact/use-local-storage";
import { useEffect, useRef } from "react";
import { mutate } from "swr";
import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export interface IMemoryUsageItem {
inuse: number;
oslimit?: number;
}
export const useMemoryData = () => {
const [date, setDate] = useLocalStorage("mihomo_memory_date", Date.now());
const subscriptKey = `getClashMemory-${date}`;
const ws = useRef<MihomoWebSocket | null>(null);
const wsFirstConnection = useRef<boolean>(true);
const listenerRef = useRef<() => void | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<IMemoryUsageItem, any, string | null>(
subscriptKey,
(_key, { next }) => {
const connect = () =>
MihomoWebSocket.connect_memory()
.then((ws_) => {
ws.current = ws_;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
listenerRef.current = ws_.addListener(async (msg) => {
if (msg.type === "Text") {
if (msg.data.startsWith("Websocket error")) {
next(msg.data, { inuse: 0 });
await ws.current?.close();
ws.current = null;
timeoutRef.current = setTimeout(
async () => await connect(),
500,
);
} else {
const data = JSON.parse(msg.data) as IMemoryUsageItem;
next(null, data);
}
}
});
})
.catch((_) => {
if (!ws.current) {
timeoutRef.current = setTimeout(async () => await connect(), 500);
}
});
if (
wsFirstConnection.current ||
(ws.current && !wsFirstConnection.current)
) {
wsFirstConnection.current = false;
if (ws.current) {
ws.current.close();
ws.current = null;
}
connect();
}
return () => {
ws.current?.close();
listenerRef.current?.();
listenerRef.current = null;
};
},
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
},
);
useEffect(() => {
const unlistenRefreshWebsocket = listen(
"verge://refresh-websocket",
async () => {
await ws.current?.close();
setDate(Date.now());
},
);
return () => {
unlistenRefreshWebsocket.then((fn) => fn());
};
}, [setDate]);
useEffect(() => {
mutate(`$sub$${subscriptKey}`);
}, [date, subscriptKey]);
const refreshGetClashMemory = () => {
setDate(Date.now());
};
return { response, refreshGetClashMemory };
};
+5 -8
View File
@@ -4,9 +4,9 @@ import {
getProfiles,
patchProfile,
patchProfilesConfig,
forceRefreshProxies,
} from "@/services/cmds";
import { getProxies, updateProxy } from "@/services/cmds";
import { calcuProxies } from "@/services/cmds";
import { selectNodeForGroup } from "tauri-plugin-mihomo-api";
export const useProfiles = () => {
const {
@@ -72,7 +72,7 @@ export const useProfiles = () => {
console.log("[ActivateSelected] 开始处理代理选择");
const [proxiesData, profileData] = await Promise.all([
getProxies(),
calcuProxies(),
getProfiles(),
]);
@@ -124,7 +124,7 @@ export const useProfiles = () => {
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`,
);
hasChange = true;
updateProxy(name, targetProxy);
selectNodeForGroup(name, targetProxy);
}
newSelected.push({ name, now: targetProxy || now });
@@ -141,11 +141,8 @@ export const useProfiles = () => {
await patchProfile(profileData.current!, { selected: newSelected });
console.log("[ActivateSelected] 代理选择配置保存成功");
// 切换节点后强制刷新后端缓存
await forceRefreshProxies();
setTimeout(() => {
mutate("getProxies", getProxies());
mutate("getProxies", calcuProxies());
}, 100);
} catch (error: any) {
console.error(
+7 -10
View File
@@ -3,22 +3,20 @@ import { useCallback, useMemo } from "react";
import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge";
import { updateProxyAndSync, syncTrayProxySelection } from "@/services/cmds";
import {
updateProxy,
updateProxyAndSync,
forceRefreshProxies,
syncTrayProxySelection,
closeConnections,
getConnections,
deleteConnection,
} from "@/services/cmds";
selectNodeForGroup,
} from "tauri-plugin-mihomo-api";
// 缓存连接清理
const cleanupConnections = async (previousProxy: string) => {
try {
const { connections } = await getConnections();
const cleanupPromises = connections
const cleanupPromises = (connections ?? [])
.filter((conn) => conn.chains.includes(previousProxy))
.map((conn) => deleteConnection(conn.id));
.map((conn) => closeConnections(conn.id));
if (cleanupPromises.length > 0) {
await Promise.allSettled(cleanupPromises);
@@ -98,8 +96,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
);
try {
await updateProxy(groupName, proxyName);
await forceRefreshProxies();
await selectNodeForGroup(groupName, proxyName);
await syncTrayProxySelection();
onSuccess?.();
console.log(
+1 -1
View File
@@ -3,7 +3,7 @@ import useSWR, { mutate } from "swr";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
import { getAutotemProxy } from "@/services/cmds";
import { closeAllConnections } from "@/services/cmds";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
// 系统代理状态检测统一逻辑
export const useSystemProxyState = () => {
+100
View File
@@ -0,0 +1,100 @@
import { TrafficRef } from "@/components/layout/traffic-graph";
import { listen } from "@tauri-apps/api/event";
import { useLocalStorage } from "foxact/use-local-storage";
import { useEffect, useRef } from "react";
import { mutate } from "swr";
import useSWRSubscription from "swr/subscription";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
export const useTrafficData = () => {
const [date, setDate] = useLocalStorage("mihomo_traffic_date", Date.now());
const subscriptKey = `getClashTraffic-${date}`;
const trafficRef = useRef<TrafficRef>(null);
const ws = useRef<MihomoWebSocket | null>(null);
const wsFirstConnection = useRef<boolean>(true);
const listenerRef = useRef<() => void | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const response = useSWRSubscription<ITrafficItem, any, string | null>(
subscriptKey,
(_key, { next }) => {
const connect = async () => {
MihomoWebSocket.connect_traffic()
.then(async (ws_) => {
ws.current = ws_;
if (timeoutRef.current) clearTimeout(timeoutRef.current);
listenerRef.current = ws_.addListener(async (msg) => {
if (msg.type === "Text") {
if (msg.data.startsWith("Websocket error")) {
next(msg.data, { up: 0, down: 0 });
await ws.current?.close();
ws.current = null;
timeoutRef.current = setTimeout(
async () => await connect(),
500,
);
} else {
const data = JSON.parse(msg.data) as ITrafficItem;
trafficRef.current?.appendData(data);
next(null, data);
}
}
});
})
.catch((_) => {
if (!ws.current) {
timeoutRef.current = setTimeout(async () => await connect(), 500);
}
});
};
if (
wsFirstConnection.current ||
(ws.current && !wsFirstConnection.current)
) {
wsFirstConnection.current = false;
if (ws.current) {
ws.current.close();
ws.current = null;
}
connect();
}
return () => {
ws.current?.close();
listenerRef.current?.();
listenerRef.current = null;
};
},
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
},
);
useEffect(() => {
const unlistenRefreshWebsocket = listen(
"verge://refresh-websocket",
async () => {
await ws.current?.close();
setDate(Date.now());
},
);
return () => {
unlistenRefreshWebsocket.then((fn) => fn());
};
}, [setDate]);
useEffect(() => {
mutate(`$sub$${subscriptKey}`);
}, [date, subscriptKey]);
const refreshGetClashTraffic = () => {
setDate(Date.now());
};
return { response, refreshGetClashTraffic };
};
+84 -66
View File
@@ -1,9 +1,8 @@
import { useState, useEffect, useRef, useCallback } from "react";
import useSWR from "swr";
import { useClashInfo } from "@/hooks/use-clash";
import { useVisibility } from "@/hooks/use-visibility";
import { getSystemMonitorOverviewSafe } from "@/services/cmds";
import { useTrafficData } from "./use-traffic-data";
// 增强的流量数据点接口
export interface ITrafficDataPoint {
@@ -183,6 +182,9 @@ let lastValidData: ISystemMonitorOverview | null = null;
export const useTrafficMonitorEnhanced = () => {
const { clashInfo } = useClashInfo();
const pageVisible = useVisibility();
const {
response: { data: traffic },
} = useTrafficData();
// 初始化采样器
if (!globalSampler) {
@@ -230,69 +232,87 @@ export const useTrafficMonitorEnhanced = () => {
refCounter.onCountChange(handleCountChange);
}, []);
// 只有在有引用时才启用SWR
const shouldFetch = clashInfo && pageVisible && refCounter.getCount() > 0;
const monitorData = useRef<ISystemMonitorOverview | null>(null);
useEffect(() => {
if (globalSampler) {
// 添加到采样器
const timestamp = Date.now();
const dataPoint: ITrafficDataPoint = {
up: traffic?.up || 0,
down: traffic?.down || 0,
timestamp,
name: new Date(timestamp).toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
globalSampler.addDataPoint(dataPoint);
triggerUpdate();
}
}, [traffic, triggerUpdate]);
const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
shouldFetch ? "getSystemMonitorOverviewSafe" : null,
getSystemMonitorOverviewSafe,
{
refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
keepPreviousData: true,
onSuccess: (data) => {
// console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
// const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
// shouldFetch ? "getSystemMonitorOverviewSafe" : null,
// getSystemMonitorOverviewSafe,
// {
// refreshInterval: shouldFetch ? 1000 : 0, // 只有在需要时才刷新
// keepPreviousData: true,
// onSuccess: (data) => {
// // console.log("[TrafficMonitorEnhanced] 获取到监控数据:", data);
if (data?.traffic?.raw && globalSampler) {
// 保存最后有效数据
lastValidData = data;
// if (data?.traffic?.raw && globalSampler) {
// // 保存最后有效数据
// lastValidData = data;
// 添加到采样器
const timestamp = Date.now();
const dataPoint: ITrafficDataPoint = {
up: data.traffic.raw.up_rate || 0,
down: data.traffic.raw.down_rate || 0,
timestamp,
name: new Date(timestamp).toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
// // 添加到采样器
// const timestamp = Date.now();
// const dataPoint: ITrafficDataPoint = {
// up: data.traffic.raw.up_rate || 0,
// down: data.traffic.raw.down_rate || 0,
// timestamp,
// name: new Date(timestamp).toLocaleTimeString("en-US", {
// hour12: false,
// hour: "2-digit",
// minute: "2-digit",
// second: "2-digit",
// }),
// };
globalSampler.addDataPoint(dataPoint);
triggerUpdate();
}
},
onError: (error) => {
console.error(
"[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
{
message: error?.message || "未知错误",
stack: error?.stack || "无堆栈信息",
},
);
// 网络错误时不清空数据,继续使用最后有效值
// 但是添加一个错误标记的数据点(流量为0)
if (globalSampler) {
const timestamp = Date.now();
const errorPoint: ITrafficDataPoint = {
up: 0,
down: 0,
timestamp,
name: new Date(timestamp).toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
};
globalSampler.addDataPoint(errorPoint);
triggerUpdate();
}
},
},
);
// globalSampler.addDataPoint(dataPoint);
// triggerUpdate();
// }
// },
// onError: (error) => {
// console.error(
// "[TrafficMonitorEnhanced] 网络错误,使用最后有效数据. 错误详情:",
// {
// message: error?.message || "未知错误",
// stack: error?.stack || "无堆栈信息",
// },
// );
// // 网络错误时不清空数据,继续使用最后有效值
// // 但是添加一个错误标记的数据点(流量为0)
// if (globalSampler) {
// const timestamp = Date.now();
// const errorPoint: ITrafficDataPoint = {
// up: 0,
// down: 0,
// timestamp,
// name: new Date(timestamp).toLocaleTimeString("en-US", {
// hour12: false,
// hour: "2-digit",
// minute: "2-digit",
// second: "2-digit",
// }),
// };
// globalSampler.addDataPoint(errorPoint);
// triggerUpdate();
// }
// },
// },
// );
// 获取指定时间范围的数据
const getDataForTimeRange = useCallback(
@@ -324,7 +344,7 @@ export const useTrafficMonitorEnhanced = () => {
}, []);
// 构建返回的监控数据,优先使用当前数据,fallback到最后有效数据
const currentData = monitorData || lastValidData;
const currentData = monitorData.current || lastValidData;
const trafficMonitorData = {
traffic: currentData?.traffic || {
raw: { up: 0, down: 0, up_rate: 0, down_rate: 0 },
@@ -355,8 +375,7 @@ export const useTrafficMonitorEnhanced = () => {
},
// 状态信息
isLoading: !currentData && !error,
error,
isLoading: !currentData,
isDataFresh: currentData?.traffic?.is_fresh || false,
hasValidData: !!lastValidData,
@@ -370,14 +389,13 @@ export const useTrafficMonitorEnhanced = () => {
* 轻量级流量数据Hook
*/
export const useTrafficDataEnhanced = () => {
const { monitorData, isLoading, error, isDataFresh, hasValidData } =
const { monitorData, isLoading, isDataFresh, hasValidData } =
useTrafficMonitorEnhanced();
return {
traffic: monitorData.traffic,
memory: monitorData.memory,
isLoading,
error,
isDataFresh,
hasValidData,
};
+7
View File
@@ -21,6 +21,7 @@ import {
ThemeModeProvider,
UpdateStateProvider,
} from "./services/states";
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
const mainElementId = "root";
const container = document.getElementById(mainElementId);
@@ -90,3 +91,9 @@ window.addEventListener("error", (event) => {
window.addEventListener("unhandledrejection", (event) => {
console.error("[main.tsx] 未处理的Promise拒绝:", event.reason);
});
// 页面关闭/刷新事件
window.addEventListener("beforeunload", async () => {
// 强制清理所有 WebSocket 实例, 防止内存泄漏
await MihomoWebSocket.cleanupAll();
});
+18 -12
View File
@@ -19,8 +19,7 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { useI18n } from "@/hooks/use-i18n";
import { useVerge } from "@/hooks/use-verge";
import { getAxios } from "@/services/api";
import { forceRefreshClashConfig } from "@/services/cmds";
import { useThemeMode, useEnableLog } from "@/services/states";
import { useClashLog, useThemeMode } from "@/services/states";
import getSystem from "@/utils/get-system";
import { routers } from "./_routers";
@@ -33,13 +32,16 @@ import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager";
import { LogLevel } from "@/hooks/use-log-data";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useConnectionData } from "@/hooks/use-connection-data";
import { useLogData } from "@/hooks/use-log-data-new";
const appWindow = getCurrentWebviewWindow();
export const portableFlag = false;
@@ -157,14 +159,20 @@ const handleNoticeMessage = (
};
const Layout = () => {
useTrafficData();
useMemoryData();
useConnectionData();
useLogData();
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const [clashLog] = useClashLog();
const enableLog = clashLog.enable;
const logLevel = clashLog.logLevel;
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
const { language, start_page } = verge ?? {};
const { switchLanguage } = useI18n();
const navigate = useNavigate();
@@ -193,19 +201,17 @@ const Layout = () => {
);
// 初始化全局日志服务
useEffect(() => {
if (clashInfo) {
initGlobalLogService(enableLog, logLevel);
}
}, [clashInfo, enableLog, logLevel]);
// useEffect(() => {
// if (clashInfo) {
// initGlobalLogService(enableLog, logLevel);
// }
// }, [clashInfo, enableLog, logLevel]);
// 设置监听器
useEffect(() => {
const listeners = [
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
// 后端配置变更事件触发,强制刷新配置缓存
await forceRefreshClashConfig();
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
+17 -16
View File
@@ -20,10 +20,10 @@ import {
import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table";
import { useVisibility } from "@/hooks/use-visibility";
import { useAppData } from "@/providers/app-data-provider";
import { closeAllConnections } from "@/services/cmds";
import { useConnectionSetting } from "@/services/states";
import parseTraffic from "@/utils/parse-traffic";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useConnectionData } from "@/hooks/use-connection-data";
const initConn: IConnections = {
uploadTotal: 0,
@@ -39,8 +39,9 @@ const ConnectionsPage = () => {
const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default");
// 使用全局数据
const { connections } = useAppData();
const {
response: { data: connections },
} = useConnectionData();
const [setting, setSetting] = useConnectionSetting();
@@ -72,30 +73,30 @@ const ConnectionsPage = () => {
if (isPaused) {
return (
frozenData ?? {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
}
);
}
return {
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal,
downloadTotal: connections?.downloadTotal,
connections: connections?.connections,
};
}, [isPaused, frozenData, connections, pageVisible]);
const [filterConn] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt];
let conns = displayData.connections.filter((conn) => {
let conns = displayData.connections?.filter((conn) => {
const { host, destinationIP, process } = conn.metadata;
return (
match(host || "") || match(destinationIP || "") || match(process || "")
);
});
if (orderFunc) conns = orderFunc(conns);
if (orderFunc) conns = orderFunc(conns ?? []);
return [conns];
}, [displayData, match, curOrderOpt, orderOpts]);
@@ -112,9 +113,9 @@ const ConnectionsPage = () => {
setIsPaused((prev) => {
if (!prev) {
setFrozenData({
uploadTotal: connections.uploadTotal,
downloadTotal: connections.downloadTotal,
connections: connections.data,
uploadTotal: connections?.uploadTotal ?? 0,
downloadTotal: connections?.downloadTotal ?? 0,
connections: connections?.connections ?? [],
});
} else {
setFrozenData(null);
@@ -206,7 +207,7 @@ const ConnectionsPage = () => {
<BaseSearchBox onSearch={handleSearch} />
</Box>
{filterConn.length === 0 ? (
{!filterConn || filterConn.length === 0 ? (
<BaseEmpty />
) : isTableLayout ? (
<ConnectionTable
+25 -26
View File
@@ -3,7 +3,6 @@ import {
PauseCircleOutlineRounded,
} from "@mui/icons-material";
import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { useLocalStorage } from "foxact/use-local-storage";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
@@ -14,26 +13,22 @@ import { SearchState } from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import LogItem from "@/components/log/log-item";
import { LogLevel } from "@/hooks/use-log-data";
import {
useGlobalLogData,
clearGlobalLogs,
changeLogLevel,
toggleLogEnabled,
} from "@/services/global-log-service";
import { useEnableLog } from "@/services/states";
// 后端通过 /logs?level={level} 进行筛选,前端不再需要手动筛选日志级别
import { toggleLogEnabled } from "@/services/global-log-service";
import { useClashLog } from "@/services/states";
import { useLogData } from "@/hooks/use-log-data-new";
const LogPage = () => {
const { t } = useTranslation();
const [enableLog, setEnableLog] = useEnableLog();
const [logLevel, setLogLevel] = useLocalStorage<LogLevel>(
"log:log-level",
"info",
);
const [clashLog, setClashLog] = useClashLog();
const enableLog = clashLog.enable;
const logState = clashLog.logFilter;
const [match, setMatch] = useState(() => (_: string) => true);
const logData = useGlobalLogData("all");
const [searchState, setSearchState] = useState<SearchState>();
const {
response: { data: logData },
refreshGetClashLog,
} = useLogData();
const filterLogs = useMemo(() => {
if (!logData || logData.length === 0) {
@@ -49,18 +44,21 @@ const LogPage = () => {
const matchesSearch = match(searchText);
return matchesSearch;
return (
(logState == "all" ? true : data.type.includes(logState)) &&
matchesSearch
);
});
}, [logData, match]);
}, [logData, logState, match]);
const handleLogLevelChange = (newLevel: LogLevel) => {
setLogLevel(newLevel);
changeLogLevel(newLevel);
const handleLogLevelChange = (newLevel: string) => {
setClashLog((pre: any) => ({ ...pre, logFilter: newLevel }));
// changeLogLevel(newLevel);
};
const handleToggleLog = async () => {
await toggleLogEnabled();
setEnableLog(!enableLog);
setClashLog((pre: any) => ({ ...pre, enable: !enableLog }));
};
return (
@@ -92,7 +90,8 @@ const LogPage = () => {
size="small"
variant="contained"
onClick={() => {
clearGlobalLogs();
refreshGetClashLog(true);
// clearGlobalLogs();
}}
>
{t("Clear")}
@@ -111,13 +110,13 @@ const LogPage = () => {
}}
>
<BaseStyledSelect
value={logLevel}
value={logState}
onChange={(e) => handleLogLevelChange(e.target.value as LogLevel)}
>
<MenuItem value="all">ALL</MenuItem>
<MenuItem value="debug">DEBUG</MenuItem>
<MenuItem value="info">INFO</MenuItem>
<MenuItem value="warning">WARNING</MenuItem>
<MenuItem value="inf">INFO</MenuItem>
<MenuItem value="warn">WARN</MenuItem>
<MenuItem value="error">ERROR</MenuItem>
</BaseStyledSelect>
<BaseSearchBox
+1 -1
View File
@@ -42,7 +42,6 @@ import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { useListen } from "@/hooks/use-listen";
import { useProfiles } from "@/hooks/use-profiles";
import {
closeAllConnections,
createProfile,
deleteProfile,
enhanceProfiles,
@@ -55,6 +54,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useSetLoadingCache, useThemeMode } from "@/services/states";
import { closeAllConnections } from "tauri-plugin-mihomo-api";
// 记录profile切换状态
const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
+2 -3
View File
@@ -9,12 +9,11 @@ import { ProviderButton } from "@/components/proxy/provider-button";
import { ProxyGroups } from "@/components/proxy/proxy-groups";
import { useVerge } from "@/hooks/use-verge";
import {
closeAllConnections,
getClashConfig,
getRuntimeProxyChainConfig,
patchClashMode,
updateProxyChainConfigInRuntime,
} from "@/services/cmds";
import { closeAllConnections, getBaseConfig } from "tauri-plugin-mihomo-api";
const ProxyPage = () => {
const { t } = useTranslation();
@@ -33,7 +32,7 @@ const ProxyPage = () => {
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig,
getBaseConfig,
{
revalidateOnFocus: false,
revalidateIfStale: true,
+1 -1
View File
@@ -74,7 +74,7 @@ const RulesPage = () => {
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
</Box>
{filteredRules.length > 0 ? (
{filteredRules && filteredRules.length > 0 ? (
<>
<Virtuoso
ref={virtuosoRef}
+109 -169
View File
@@ -12,19 +12,17 @@ import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { useVisibility } from "@/hooks/use-visibility";
import {
forceRefreshProxies,
getAppUptime,
getClashConfig,
getConnections,
getMemoryData,
getProxies,
getProxyProviders,
getRuleProviders,
getRules,
calcuProxies,
calcuProxyProviders,
getRunningMode,
getSystemProxy,
getTrafficData,
} from "@/services/cmds";
import {
getBaseConfig,
getRuleProviders,
getRules,
} from "tauri-plugin-mihomo-api";
// 连接速度计算接口
interface ConnectionSpeedData {
@@ -49,14 +47,14 @@ interface AppDataContextType {
uptime: number;
proxyProviders: any;
ruleProviders: any;
connections: {
data: ConnectionWithSpeed[];
count: number;
uploadTotal: number;
downloadTotal: number;
};
traffic: { up: number; down: number };
memory: { inuse: number };
// connections: {
// data: ConnectionWithSpeed[];
// count: number;
// uploadTotal: number;
// downloadTotal: number;
// };
// traffic: { up: number; down: number };
// memory: { inuse: number };
systemProxyAddress: string;
refreshProxy: () => Promise<any>;
@@ -130,7 +128,7 @@ export const AppDataProvider = ({
// 基础数据 - 中频率更新 (5秒)
const { data: proxiesData, mutate: refreshProxy } = useSWR(
"getProxies",
getProxies,
calcuProxies,
{
refreshInterval: 5000,
revalidateOnFocus: true,
@@ -165,21 +163,6 @@ export const AppDataProvider = ({
lastProfileId = newProfileId;
lastUpdateTime = now;
setTimeout(() => {
// 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading
forceRefreshProxies()
.catch((e) =>
console.warn("[AppDataProvider] forceRefreshProxies 失败:", e),
)
.finally(() => {
setTimeout(() => {
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] 普通刷新也失败:", e),
);
}, 200); // 200ms 延迟,保证后端缓存已清理
});
}, 0);
});
// 监听Clash配置刷新事件(enhance操作等)
@@ -194,18 +177,6 @@ export const AppDataProvider = ({
try {
console.log("[AppDataProvider] Clash刷新 - 强制刷新代理缓存");
// 添加超时保护
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
await refreshProxy();
} catch (error) {
console.error(
@@ -236,29 +207,6 @@ export const AppDataProvider = ({
}
};
// 监听强制代理刷新事件(托盘代理切换立即刷新)
const handleForceRefreshProxies = () => {
console.log("[AppDataProvider] 强制代理刷新事件");
// 立即刷新,无延迟,无防抖
forceRefreshProxies()
.then(() => {
console.log("[AppDataProvider] 强制刷新代理缓存完成");
// 强制刷新完成后,立即刷新前端显示
return refreshProxy();
})
.then(() => {
console.log("[AppDataProvider] 前端代理数据刷新完成");
})
.catch((e) => {
console.warn("[AppDataProvider] 强制代理刷新失败:", e);
// 如果强制刷新失败,尝试普通刷新
refreshProxy().catch((e2) =>
console.warn("[AppDataProvider] 普通代理刷新也失败:", e2),
);
});
};
// 使用 Tauri 事件监听器替代 window 事件监听器
const setupTauriListeners = async () => {
try {
@@ -270,15 +218,10 @@ export const AppDataProvider = ({
"verge://refresh-proxy-config",
handleRefreshProxy,
);
const unlistenForceRefresh = await listen(
"verge://force-refresh-proxies",
handleForceRefreshProxies,
);
return () => {
unlistenClash();
unlistenProxy();
unlistenForceRefresh();
};
} catch (error) {
console.warn("[AppDataProvider] 设置 Tauri 事件监听器失败:", error);
@@ -292,10 +235,6 @@ export const AppDataProvider = ({
"verge://refresh-proxy-config",
handleRefreshProxy,
);
window.addEventListener(
"verge://force-refresh-proxies",
handleForceRefreshProxies,
);
return () => {
window.removeEventListener(
@@ -306,10 +245,6 @@ export const AppDataProvider = ({
"verge://refresh-proxy-config",
handleRefreshProxy,
);
window.removeEventListener(
"verge://force-refresh-proxies",
handleForceRefreshProxies,
);
};
}
};
@@ -336,7 +271,7 @@ export const AppDataProvider = ({
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getClashConfig,
getBaseConfig,
{
refreshInterval: 60000, // 60秒刷新间隔,减少频繁请求
revalidateOnFocus: false,
@@ -348,7 +283,7 @@ export const AppDataProvider = ({
// 提供者数据
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
"getProxyProviders",
getProxyProviders,
calcuProxyProviders,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
@@ -398,86 +333,93 @@ export const AppDataProvider = ({
// 高频率更新数据 (2秒)
const { data: uptimeData } = useSWR("appUptime", getAppUptime, {
refreshInterval: 2000,
// TODO: 运行时间
refreshInterval: 20000,
revalidateOnFocus: false,
suspense: false,
});
// 连接数据 - 使用IPC轮询更新并计算速度
const {
data: connectionsData = {
connections: [],
uploadTotal: 0,
downloadTotal: 0,
},
} = useSWR(
clashInfo && pageVisible ? "getConnections" : null,
async () => {
const data = await getConnections();
const rawConnections: IConnectionsItem[] = data.connections || [];
// const {
// data: connectionsData = {
// connections: [],
// uploadTotal: 0,
// downloadTotal: 0,
// },
// } = useSWR(
// clashInfo && pageVisible ? "getConnections" : null,
// async () => {
// const data = await getConnections();
// const rawConnections =
// data.connections?.map((item) => {
// // TODO: transform bigint to number
// return { ...item, upload: 0, download: 0 } as IConnectionsItem;
// }) || [];
// 计算带速度的连接数据
const connectionsWithSpeed = calculateConnectionSpeeds(rawConnections);
// // 计算带速度的连接数据
// const connectionsWithSpeed = calculateConnectionSpeeds(rawConnections);
// 更新上一次数据的引用
const currentMap = new Map<string, ConnectionSpeedData>();
const now = Date.now();
rawConnections.forEach((conn) => {
currentMap.set(conn.id, {
id: conn.id,
upload: conn.upload,
download: conn.download,
timestamp: now,
});
});
previousConnectionsRef.current = currentMap;
// // 更新上一次数据的引用
// const currentMap = new Map<string, ConnectionSpeedData>();
// const now = Date.now();
// rawConnections.forEach((conn) => {
// currentMap.set(conn.id, {
// id: conn.id,
// upload: conn.upload,
// download: conn.download,
// timestamp: now,
// });
// });
// previousConnectionsRef.current = currentMap;
return {
connections: connectionsWithSpeed,
uploadTotal: data.uploadTotal || 0,
downloadTotal: data.downloadTotal || 0,
};
},
{
refreshInterval: 1000, // 1秒刷新一次
fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 },
keepPreviousData: true,
onError: (error) => {
console.error("[Connections] IPC 获取数据错误:", error);
},
},
);
// return {
// connections: connectionsWithSpeed,
// uploadTotal: data.uploadTotal || 0,
// downloadTotal: data.downloadTotal || 0,
// };
// },
// {
// refreshInterval: 1000, // 1秒刷新一次
// fallbackData: { connections: [], uploadTotal: 0, downloadTotal: 0 },
// keepPreviousData: true,
// onError: (error) => {
// console.error("[Connections] IPC 获取数据错误:", error);
// },
// },
// );
// 流量数据 - 使用IPC轮询更新
const { data: trafficData = { up: 0, down: 0 } } = useSWR(
clashInfo && pageVisible ? "getTrafficData" : null,
getTrafficData,
{
refreshInterval: 1000, // 1秒刷新一次
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
onSuccess: () => {
// console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data);
},
onError: (error) => {
console.error("[Traffic][AppDataProvider] IPC 获取数据错误:", error);
},
},
);
// const trafficData = { up: 0, down: 0 };
// const { data: trafficData = { up: 0, down: 0 } } = useSWR(
// clashInfo && pageVisible ? "getTrafficData" : null,
// getTrafficData,
// {
// refreshInterval: 1000, // 1秒刷新一次
// fallbackData: { up: 0, down: 0 },
// keepPreviousData: true,
// onSuccess: () => {
// // console.log("[Traffic][AppDataProvider] IPC 获取到流量数据:", data);
// },
// onError: (error) => {
// console.error("[Traffic][AppDataProvider] IPC 获取数据错误:", error);
// },
// },
// );
// 内存数据 - 使用IPC轮询更新
const { data: memoryData = { inuse: 0 } } = useSWR(
clashInfo && pageVisible ? "getMemoryData" : null,
getMemoryData,
{
refreshInterval: 2000, // 2秒刷新一次
fallbackData: { inuse: 0 },
keepPreviousData: true,
onError: (error) => {
console.error("[Memory] IPC 获取数据错误:", error);
},
},
);
// const memoryData = { inuse: 0 };
// const { data: memoryData = { inuse: 0 } } = useSWR(
// clashInfo && pageVisible ? "getMemoryData" : null,
// getMemoryData,
// {
// refreshInterval: 2000, // 2秒刷新一次
// fallbackData: { inuse: 0 },
// keepPreviousData: true,
// onError: (error) => {
// console.error("[Memory] IPC 获取数据错误:", error);
// },
// },
// );
// 提供统一的刷新方法
const refreshAll = React.useCallback(async () => {
@@ -510,7 +452,7 @@ export const AppDataProvider = ({
// PAC模式:显示我们期望设置的代理地址
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort =
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${proxyHost}:${proxyPort}`;
} else {
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
@@ -525,7 +467,7 @@ export const AppDataProvider = ({
// 系统地址无效,返回期望的代理地址
const proxyHost = verge.proxy_host || "127.0.0.1";
const proxyPort =
verge.verge_mixed_port || clashConfig["mixed-port"] || 7897;
verge.verge_mixed_port || clashConfig.mixedPort || 7897;
return `${proxyHost}:${proxyPort}`;
}
}
@@ -535,26 +477,26 @@ export const AppDataProvider = ({
// 数据
proxies: proxiesData,
clashConfig,
rules: rulesData || [],
rules: rulesData?.rules || [],
sysproxy,
runningMode,
uptime: uptimeData || 0,
// 提供者数据
proxyProviders: proxyProviders || {},
ruleProviders: ruleProviders || {},
ruleProviders: ruleProviders?.providers || {},
// 连接数据
connections: {
data: connectionsData.connections || [],
count: connectionsData.connections?.length || 0,
uploadTotal: connectionsData.uploadTotal || 0,
downloadTotal: connectionsData.downloadTotal || 0,
},
// connections: {
// data: connectionsData.connections || [],
// count: connectionsData.connections?.length || 0,
// uploadTotal: connectionsData.uploadTotal || 0,
// downloadTotal: connectionsData.downloadTotal || 0,
// },
// 实时流量数据
traffic: trafficData,
memory: memoryData,
// traffic: trafficData,
// memory: memoryData,
systemProxyAddress: calculateSystemProxyAddress(),
@@ -574,9 +516,9 @@ export const AppDataProvider = ({
sysproxy,
runningMode,
uptimeData,
connectionsData,
trafficData,
memoryData,
// connectionsData,
// trafficData,
// memoryData,
proxyProviders,
ruleProviders,
verge,
@@ -589,9 +531,7 @@ export const AppDataProvider = ({
refreshAll,
]);
return (
<AppDataContext.Provider value={value}>{children}</AppDataContext.Provider>
);
return <AppDataContext value={value}>{children}</AppDataContext>;
};
// 自定义Hook访问全局数据
+36 -253
View File
@@ -1,6 +1,8 @@
import { invoke } from "@tauri-apps/api/core";
import { getProxies, getProxyProviders } from "tauri-plugin-mihomo-api";
import { showNotice } from "@/services/noticeService";
import dayjs from "dayjs";
export async function copyClashEnv() {
return invoke<void>("copy_clash_env");
@@ -87,7 +89,7 @@ export async function getRuntimeLogs() {
return invoke<Record<string, [string, string][]>>("get_runtime_logs");
}
export async function getRuntimeProxyChainConfig(proxyChainExitNode: String) {
export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) {
return invoke<string>("get_runtime_proxy_chain_config", {
proxyChainExitNode,
});
@@ -107,55 +109,6 @@ export async function patchClashMode(payload: string) {
return invoke<void>("patch_clash_mode", { payload });
}
// New IPC-based API functions to replace HTTP API calls
export async function getVersion() {
return invoke<{
premium: boolean;
meta?: boolean;
version: string;
}>("get_clash_version");
}
export async function getClashConfig() {
return invoke<IConfigData>("get_clash_config");
}
export async function forceRefreshClashConfig() {
return invoke<IConfigData>("force_refresh_clash_config");
}
export async function updateGeoData() {
return invoke<void>("update_geo_data");
}
export async function upgradeCore() {
return invoke<void>("upgrade_clash_core");
}
export async function getRules() {
const response = await invoke<{ rules: IRuleItem[] }>("get_clash_rules");
return response?.rules || [];
}
export async function getProxyDelay(
name: string,
url?: string,
timeout?: number,
) {
return invoke<{ delay: number }>("clash_api_get_proxy_delay", {
name,
url,
timeout: timeout || 10000,
});
}
export async function updateProxy(group: string, proxy: string) {
// const start = Date.now();
await invoke<void>("update_proxy_choice", { group, proxy });
// const duration = Date.now() - start;
// console.log(`[API] updateProxy 耗时: ${duration}ms`);
}
export async function syncTrayProxySelection() {
return invoke<void>("sync_tray_proxy_selection");
}
@@ -164,7 +117,7 @@ export async function updateProxyAndSync(group: string, proxy: string) {
return invoke<void>("update_proxy_and_sync", { group, proxy });
}
export async function getProxies(): Promise<{
export async function calcuProxies(): Promise<{
global: IProxyGroupItem;
direct: IProxyItem;
groups: IProxyGroupItem[];
@@ -172,19 +125,17 @@ export async function getProxies(): Promise<{
proxies: IProxyItem[];
}> {
const [proxyResponse, providerResponse] = await Promise.all([
invoke<{ proxies: Record<string, IProxyItem> }>("get_proxies"),
invoke<{ providers: Record<string, IProxyProviderItem> }>(
"get_providers_proxies",
),
getProxies(),
calcuProxyProviders(),
]);
const proxyRecord = proxyResponse.proxies;
const providerRecord = providerResponse.providers || {};
const providerRecord = providerResponse;
// provider name map
const providerMap = Object.fromEntries(
Object.entries(providerRecord).flatMap(([provider, item]) =>
item.proxies.map((p) => [p.name, { ...p, provider }]),
item!.proxies.map((p) => [p.name, { ...p, provider }]),
),
);
@@ -209,7 +160,7 @@ export async function getProxies(): Promise<{
let groups: IProxyGroupItem[] = Object.values(proxyRecord).reduce<
IProxyGroupItem[]
>((acc, each) => {
if (each.name !== "GLOBAL" && each.all) {
if (each?.name !== "GLOBAL" && each?.all) {
acc.push({
...each,
all: each.all!.map((item) => generateItem(item)),
@@ -242,7 +193,7 @@ export async function getProxies(): Promise<{
const proxies = [direct, reject].concat(
Object.values(proxyRecord).filter(
(p) => !p.all?.length && p.name !== "DIRECT" && p.name !== "REJECT",
(p) => !p?.all?.length && p?.name !== "DIRECT" && p?.name !== "REJECT",
),
);
@@ -254,197 +205,39 @@ export async function getProxies(): Promise<{
return { global: _global, direct, groups, records: proxyRecord, proxies };
}
export async function getProxyProviders() {
const response = await invoke<{
providers: Record<string, IProxyProviderItem>;
}>("get_providers_proxies");
if (!response || !response.providers) {
console.warn(
"getProxyProviders: Invalid response structure, returning empty object",
);
return {};
}
const providers = response.providers as Record<string, IProxyProviderItem>;
export async function calcuProxyProviders() {
const providers = await getProxyProviders();
return Object.fromEntries(
Object.entries(providers).filter(([, item]) => {
const type = item.vehicleType.toLowerCase();
return type === "http" || type === "file";
}),
Object.entries(providers.providers)
.sort()
.filter(
([_key, item]) =>
item?.vehicleType === "HTTP" || item?.vehicleType === "File",
),
);
}
export async function getRuleProviders() {
const response = await invoke<{
providers: Record<string, IRuleProviderItem>;
}>("get_rule_providers");
const providers = (response.providers || {}) as Record<
string,
IRuleProviderItem
>;
return Object.fromEntries(
Object.entries(providers).filter(([, item]) => {
const type = item.vehicleType.toLowerCase();
return type === "http" || type === "file";
}),
);
}
export async function providerHealthCheck(name: string) {
return invoke<void>("proxy_provider_health_check", { name });
}
export async function proxyProviderUpdate(name: string) {
return invoke<void>("update_proxy_provider", { name });
}
export async function ruleProviderUpdate(name: string) {
return invoke<void>("update_rule_provider", { name });
}
export async function getConnections() {
return invoke<IConnections>("get_clash_connections");
}
export async function deleteConnection(id: string) {
return invoke<void>("delete_clash_connection", { id });
}
export async function closeAllConnections() {
return invoke<void>("close_all_clash_connections");
}
export async function getGroupProxyDelays(
groupName: string,
url?: string,
timeout?: number,
) {
return invoke<Record<string, number>>("get_group_proxy_delays", {
groupName,
url,
timeout,
});
}
export async function getTrafficData() {
// console.log("[Traffic][Service] 开始调用 get_traffic_data");
const result = await invoke<ITrafficItem>("get_traffic_data");
// console.log("[Traffic][Service] get_traffic_data 返回结果:", result);
return result;
}
export async function getMemoryData() {
console.log("[Memory][Service] 开始调用 get_memory_data");
const result = await invoke<{
inuse: number;
oslimit?: number;
usage_percent?: number;
last_updated?: number;
}>("get_memory_data");
// console.debug("[Memory][Service] get_memory_data 返回结果:", result);
return result;
}
export async function getFormattedTrafficData() {
console.log("[Traffic][Service] 开始调用 get_formatted_traffic_data");
const result = await invoke<IFormattedTrafficData>(
"get_formatted_traffic_data",
);
// console.debug(
// "[Traffic][Service] get_formatted_traffic_data 返回结果:",
// result,
// );
return result;
}
export async function getFormattedMemoryData() {
console.log("[Memory][Service] 开始调用 get_formatted_memory_data");
const result = await invoke<IFormattedMemoryData>(
"get_formatted_memory_data",
);
// console.debug("[Memory][Service] get_formatted_memory_data 返回结果:", result);
return result;
}
export async function getSystemMonitorOverview() {
console.log("[Monitor][Service] 开始调用 get_system_monitor_overview");
const result = await invoke<ISystemMonitorOverview>(
"get_system_monitor_overview",
);
// console.debug(
// "[Monitor][Service] get_system_monitor_overview 返回结果:",
// result,
// );
return result;
}
// 带数据验证的安全版本
export async function getSystemMonitorOverviewSafe() {
// console.log(
// "[Monitor][Service] 开始调用安全版本 get_system_monitor_overview",
// );
try {
const result = await invoke<any>("get_system_monitor_overview");
// console.log("[Monitor][Service] 原始数据:", result);
// 导入验证器(动态导入避免循环依赖)
const { systemMonitorValidator } = await import("@/utils/data-validator");
if (systemMonitorValidator.validate(result)) {
// console.log("[Monitor][Service] 数据验证通过");
return result as ISystemMonitorOverview;
} else {
// console.warn("[Monitor][Service] 数据验证失败,使用清理后的数据");
return systemMonitorValidator.sanitize(result);
}
} catch {
// console.error("[Monitor][Service] API调用失败:", error);
// 返回安全的默认值
const { systemMonitorValidator } = await import("@/utils/data-validator");
return systemMonitorValidator.sanitize(null);
}
}
export async function startTrafficService() {
console.log("[Traffic][Service] 开始调用 start_traffic_service");
try {
const result = await invoke<void>("start_traffic_service");
console.log("[Traffic][Service] start_traffic_service 调用成功");
return result;
} catch (error) {
console.error("[Traffic][Service] start_traffic_service 调用失败:", error);
throw error;
}
}
export async function stopTrafficService() {
console.log("[Traffic][Service] 开始调用 stop_traffic_service");
const result = await invoke<void>("stop_traffic_service");
console.log("[Traffic][Service] stop_traffic_service 调用成功");
return result;
}
export async function isDebugEnabled() {
return invoke<boolean>("is_clash_debug_enabled");
}
export async function gc() {
return invoke<void>("clash_gc");
}
export async function getClashLogs() {
return invoke<any>("get_clash_logs");
}
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
const logs = await invoke<string[]>("get_clash_logs");
export async function startLogsMonitoring(level?: string) {
return invoke<void>("start_logs_monitoring", { level });
}
return logs.reduce<ILogItem[]>((acc, log) => {
const result = log.match(regex);
if (result) {
const [_, _time, type, payload] = result;
const time = dayjs(_time).format("MM-DD HH:mm:ss");
acc.push({ time, type, payload });
return acc;
}
export async function stopLogsMonitoring() {
return invoke<void>("stop_logs_monitoring");
const result2 = log.match(newRegex);
if (result2) {
const [_, time, type, payload] = result2;
acc.push({ time, type, payload });
}
return acc;
}, []);
}
export async function clearLogs() {
@@ -576,16 +369,6 @@ export async function cmdGetProxyDelay(
}
}
/// 用于profile切换等场景
export async function forceRefreshProxies() {
const start = Date.now();
console.log("[API] 强制刷新代理缓存");
const result = await invoke<any>("force_refresh_proxies");
const duration = Date.now() - start;
console.log(`[API] 代理缓存刷新完成,耗时: ${duration}ms`);
return result;
}
export async function cmdTestDelay(url: string) {
return invoke<number>("test_delay", { url });
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { cmdGetProxyDelay } from "@/services/cmds";
import { delayProxyByName } from "tauri-plugin-mihomo-api";
const hashKey = (name: string, group: string) => `${group ?? ""}::${name}`;
@@ -106,7 +106,7 @@ class DelayManager {
// 使用Promise.race来实现超时控制
const result = await Promise.race([
cmdGetProxyDelay(name, timeout, url),
delayProxyByName(name, url, timeout),
timeoutPromise,
]);
+4 -5
View File
@@ -3,7 +3,6 @@ import { create } from "zustand";
import {
fetchLogsViaIPC,
startLogsStreaming,
stopLogsStreaming,
clearLogs as clearLogsIPC,
} from "@/services/ipc-log-service";
@@ -96,7 +95,7 @@ export const initGlobalLogService = (
console.log("[GlobalLog-IPC] 启用IPC流式日志服务");
// 启动流式监控
startLogsStreaming(logLevel);
// startLogsStreaming(logLevel);
// 立即获取一次日志
fetchLogsViaIPCPeriodically();
@@ -152,7 +151,7 @@ export const changeLogLevel = (level: LogLevel) => {
if (enabled) {
// IPC流式模式下重新启动监控
startLogsStreaming(level);
// startLogsStreaming(level);
fetchLogsViaIPCPeriodically();
}
};
@@ -180,11 +179,11 @@ export const clearGlobalLogs = () => {
};
// 自定义钩子,用于获取过滤后的日志数据
export const useGlobalLogData = (logLevel: LogLevel = "all") => {
export const useGlobalLogData = (logLevel: LogLevel = "info") => {
const logs = useGlobalLogStore((state) => state.logs);
// 根据当前选择的日志等级过滤日志
if (logLevel === "all") {
if (logLevel === "info") {
return logs;
} else {
return logs.filter((log) => log.type.toLowerCase() === logLevel);
+12 -17
View File
@@ -1,12 +1,7 @@
// IPC-based log service using Tauri commands with streaming support
import dayjs from "dayjs";
import {
getClashLogs,
startLogsMonitoring,
stopLogsMonitoring,
clearLogs as clearLogsCmd,
} from "@/services/cmds";
import { clearLogs as clearLogsCmd } from "@/services/cmds";
type LogLevel = "debug" | "info" | "warning" | "error" | "all";
@@ -21,7 +16,7 @@ interface ILogItem {
export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
try {
const level = logLevel === "all" ? undefined : logLevel;
await startLogsMonitoring(level);
// await startLogsMonitoring(level);
console.log(
`[IPC-LogService] Started logs monitoring with level: ${logLevel}`,
);
@@ -33,7 +28,7 @@ export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
// Stop logs monitoring
export const stopLogsStreaming = async () => {
try {
await stopLogsMonitoring();
// await stopLogsMonitoring();
console.log("[IPC-LogService] Stopped logs monitoring");
} catch (error) {
console.error("[IPC-LogService] Failed to stop logs monitoring:", error);
@@ -45,16 +40,16 @@ export const fetchLogsViaIPC = async (): Promise<ILogItem[]> => {
try {
// Server-side filtering handles the level via /logs?level={level}
// We just fetch all cached logs regardless of the logLevel parameter
const response = await getClashLogs();
// const response = await getClashLogs();
// The response should be in the format expected by the frontend
// Transform the logs to match the expected format
if (Array.isArray(response)) {
return response.map((log: any) => ({
...log,
time: log.time || dayjs().format("HH:mm:ss"),
}));
}
// // The response should be in the format expected by the frontend
// // Transform the logs to match the expected format
// if (Array.isArray(response)) {
// return response.map((log: any) => ({
// ...log,
// time: log.time || dayjs().format("HH:mm:ss"),
// }));
// }
return [];
} catch (error) {
+18 -1
View File
@@ -1,11 +1,28 @@
import { createContextState } from "foxact/create-context-state";
import { useLocalStorage } from "foxact/use-local-storage";
import { LogLevel } from "tauri-plugin-mihomo-api";
const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
"light" | "dark"
>("light");
export const useEnableLog = () => useLocalStorage("enable-log", false);
interface IClashLog {
enable: boolean;
logLevel: LogLevel;
logFilter: "all" | "debug" | "inf" | "warn" | "err";
}
const defaultClashLog: IClashLog = {
enable: true,
logLevel: "info",
logFilter: "all",
};
export const useClashLog = () =>
useLocalStorage<IClashLog>("clash-log", defaultClashLog, {
serializer: JSON.stringify,
deserializer: JSON.parse,
});
// export const useEnableLog = () => useLocalStorage("enable-log", false);
interface IConnectionSetting {
layout: "table" | "list";