feat: enhance profile management and proxy refresh with improved event listening and state updates
This commit is contained in:
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user