feat: enhance profile management and proxy refresh with improved event listening and state updates

This commit is contained in:
wonfen
2025-06-17 11:38:53 +08:00
parent 4068e5ec9c
commit ac7307b1f7
9 changed files with 468 additions and 103 deletions

View File

@@ -10,11 +10,32 @@ export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR(
"getProfiles",
getProfiles,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 2000,
errorRetryCount: 2,
errorRetryInterval: 1000,
},
);
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
await patchProfilesConfig(value);
mutateProfiles();
// 立即更新本地状态
if (value.current && profiles) {
const optimisticUpdate = {
...profiles,
current: value.current,
};
mutateProfiles(optimisticUpdate, false); // 不重新验证
}
try {
await patchProfilesConfig(value);
mutateProfiles();
} catch (error) {
mutateProfiles();
throw error;
}
};
const patchCurrent = async (value: Partial<IProfileItem>) => {
@@ -26,40 +47,90 @@ export const useProfiles = () => {
// 根据selected的节点选择
const activateSelected = async () => {
const proxiesData = await getProxies();
const profileData = await getProfiles();
try {
console.log("[ActivateSelected] 开始处理代理选择");
if (!profileData || !proxiesData) return;
const [proxiesData, profileData] = await Promise.all([
getProxies(),
getProfiles(),
]);
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current,
);
if (!current) return;
// init selected array
const { selected = [] } = current;
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]),
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") return;
if (selectedMap[name] != null && selectedMap[name] !== now) {
hasChange = true;
updateProxy(name, selectedMap[name]);
if (!profileData || !proxiesData) {
console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理");
return;
}
newSelected.push({ name, now: selectedMap[name] });
});
if (hasChange) {
patchProfile(profileData.current!, { selected: newSelected });
mutate("getProxies", getProxies());
const current = profileData.items?.find(
(e) => e && e.uid === profileData.current,
);
if (!current) {
console.log("[ActivateSelected] 未找到当前profile配置");
return;
}
// 检查是否有saved的代理选择
const { selected = [] } = current;
if (selected.length === 0) {
console.log("[ActivateSelected] 当前profile无保存的代理选择跳过");
return;
}
console.log(
`[ActivateSelected] 当前profile有 ${selected.length} 个代理选择配置`,
);
const selectedMap = Object.fromEntries(
selected.map((each) => [each.name!, each.now!]),
);
let hasChange = false;
const newSelected: typeof selected = [];
const { global, groups } = proxiesData;
// 处理所有代理组
[global, ...groups].forEach(({ type, name, now }) => {
if (!now || type !== "Selector") {
if (selectedMap[name] != null) {
newSelected.push({ name, now: now || selectedMap[name] });
}
return;
}
const targetProxy = selectedMap[name];
if (targetProxy != null && targetProxy !== now) {
console.log(
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${targetProxy}`,
);
hasChange = true;
updateProxy(name, targetProxy);
}
newSelected.push({ name, now: targetProxy || now });
});
if (!hasChange) {
console.log("[ActivateSelected] 所有代理选择已经是目标状态,无需更新");
return;
}
console.log(`[ActivateSelected] 完成代理切换,保存新的选择配置`);
try {
await patchProfile(profileData.current!, { selected: newSelected });
console.log("[ActivateSelected] 代理选择配置保存成功");
setTimeout(() => {
mutate("getProxies", getProxies());
}, 100);
} catch (error: any) {
console.error(
"[ActivateSelected] 保存代理选择配置失败:",
error.message,
);
}
} catch (error: any) {
console.error("[ActivateSelected] 处理代理选择失败:", error.message);
}
};

View File

@@ -169,7 +169,13 @@ const Layout = () => {
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload;
handleNoticeMessage(status, msg, t, navigate);
setTimeout(() => {
try {
handleNoticeMessage(status, msg, t, navigate);
} catch (error) {
console.error("[Layout] 处理通知消息失败:", error);
}
}, 0);
},
[t, navigate],
);
@@ -220,12 +226,35 @@ const Layout = () => {
const cleanupWindow = setupWindowListeners();
return () => {
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
cleanupWindow.then((cleanup) => cleanup());
setTimeout(() => {
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener
.then((unlisten) => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取unlisten函数失败:", error);
});
}
});
cleanupWindow
.then((cleanup) => {
try {
cleanup();
} catch (error) {
console.error("[Layout] 清理窗口监听器失败:", error);
}
})
.catch((error) => {
console.error("[Layout] 获取cleanup函数失败:", error);
});
}, 0);
};
}, [handleNotice]);

View File

@@ -190,27 +190,53 @@ const ProfilePage = () => {
}
};
const activateProfile = async (profile: string, notifySuccess: boolean) => {
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => {
setActivatings((prev) => [...prev, profile]);
}, 100);
try {
const success = await patchProfiles({ current: profile });
await mutateLogs();
closeAllConnections();
await activateSelected();
if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000);
const activateProfile = useLockFn(
async (profile: string, notifySuccess: boolean) => {
if (profiles.current === profile && !notifySuccess) {
console.log(
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
);
return;
}
} catch (err: any) {
showNotice("error", err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset);
setActivatings([]);
}
};
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => {
setActivatings((prev) => [...prev, profile]);
}, 100);
try {
console.log(`[Profile] 开始切换到: ${profile}`);
const success = await patchProfiles({ current: profile });
await mutateLogs();
closeAllConnections();
if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000);
}
// 立即清除loading状态
clearTimeout(reset);
setActivatings([]);
console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`);
setTimeout(async () => {
try {
await activateSelected();
console.log(`[Profile] 后台处理完成`);
} catch (err: any) {
console.warn("Failed to activate selected proxies:", err);
}
}, 50);
} catch (err: any) {
console.error(`[Profile] 切换失败:`, err);
showNotice("error", err?.message || err.toString(), 4000);
clearTimeout(reset);
setActivatings([]);
}
},
);
const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return;
await activateProfile(current, true);
@@ -300,31 +326,45 @@ const ProfilePage = () => {
// 监听后端配置变更
useEffect(() => {
let unlistenPromise: Promise<() => void> | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const debounceDelay = 200;
const setupListener = async () => {
unlistenPromise = listen<string>("profile-changed", (event) => {
console.log("Profile changed event received:", event.payload);
if (timeoutId) {
clearTimeout(timeoutId);
const newProfileId = event.payload;
const now = Date.now();
console.log(`[Profile] 收到配置变更事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < debounceDelay
) {
console.log(`[Profile] 重复事件被防抖,跳过`);
return;
}
timeoutId = setTimeout(() => {
mutateProfiles();
timeoutId = undefined;
}, 300);
lastProfileId = newProfileId;
lastUpdateTime = now;
console.log(`[Profile] 执行配置数据刷新`);
// 使用异步调度避免阻塞事件处理
setTimeout(() => {
mutateProfiles().catch((error) => {
console.error("[Profile] 配置数据刷新失败:", error);
});
}, 0);
});
};
setupListener();
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
unlistenPromise?.then((unlisten) => unlisten());
unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
};
}, [mutateProfiles, t]);
}, [mutateProfiles]);
return (
<BasePage

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useMemo } from "react";
import { createContext, useContext, useMemo, useEffect } from "react";
import useSWR from "swr";
import useSWRSubscription from "swr/subscription";
import {
@@ -8,10 +8,16 @@ import {
getProxyProviders,
getRuleProviders,
} from "@/services/api";
import { getSystemProxy, getRunningMode, getAppUptime } from "@/services/cmds";
import {
getSystemProxy,
getRunningMode,
getAppUptime,
forceRefreshProxies,
} from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash";
import { createAuthSockette } from "@/utils/websocket";
import { useVisibility } from "@/hooks/use-visibility";
import { listen } from "@tauri-apps/api/event";
// 定义AppDataContext类型 - 使用宽松类型
interface AppDataContextType {
@@ -64,6 +70,126 @@ export const AppDataProvider = ({
},
);
// 监听profile和clash配置变更事件
useEffect(() => {
let profileUnlisten: Promise<() => void> | undefined;
let lastProfileId: string | null = null;
let lastUpdateTime = 0;
const refreshThrottle = 500;
const setupEventListeners = async () => {
try {
// 监听profile切换事件
profileUnlisten = listen<string>("profile-changed", (event) => {
const newProfileId = event.payload;
const now = Date.now();
console.log(`[AppDataProvider] Profile切换事件: ${newProfileId}`);
if (
lastProfileId === newProfileId &&
now - lastUpdateTime < refreshThrottle
) {
console.log("[AppDataProvider] 重复事件被防抖,跳过");
return;
}
lastProfileId = newProfileId;
lastUpdateTime = now;
setTimeout(async () => {
try {
console.log("[AppDataProvider] 强制刷新代理缓存");
const refreshPromise = Promise.race([
forceRefreshProxies(),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("forceRefreshProxies timeout")),
8000,
),
),
]);
await refreshPromise;
console.log("[AppDataProvider] 刷新前端代理数据");
await refreshProxy();
console.log("[AppDataProvider] Profile切换的代理数据刷新完成");
} catch (error) {
console.error("[AppDataProvider] 强制刷新代理缓存失败:", error);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] 普通刷新也失败:", e),
);
}
}, 0);
});
// 监听Clash配置刷新事件(enhance操作等)
const handleRefreshClash = () => {
const now = Date.now();
console.log("[AppDataProvider] Clash配置刷新事件");
if (now - lastUpdateTime > refreshThrottle) {
lastUpdateTime = now;
setTimeout(async () => {
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(
"[AppDataProvider] Clash刷新时强制刷新代理缓存失败:",
error,
);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] Clash刷新普通刷新也失败:", e),
);
}
}, 0);
}
};
window.addEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
return () => {
window.removeEventListener(
"verge://refresh-clash-config",
handleRefreshClash,
);
};
} catch (error) {
console.error("[AppDataProvider] 事件监听器设置失败:", error);
return () => {};
}
};
const cleanupPromise = setupEventListeners();
return () => {
profileUnlisten?.then((unlisten) => unlisten()).catch(console.error);
cleanupPromise.then((cleanup) => cleanup());
};
}, [refreshProxy]);
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
"getClashConfig",
getClashConfig,

View File

@@ -220,6 +220,12 @@ export async function cmdGetProxyDelay(
}
}
/// 用于profile切换等场景
export async function forceRefreshProxies() {
console.log("[API] 强制刷新代理缓存");
return invoke<any>("force_refresh_proxies");
}
export async function cmdTestDelay(url: string) {
return invoke<number>("test_delay", { url });
}