mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat: enhance profile management and proxy refresh with improved event listening and state updates
This commit is contained in:
@@ -12,45 +12,89 @@ use tokio::sync::Mutex;
|
|||||||
// 添加全局互斥锁防止并发配置更新
|
// 添加全局互斥锁防止并发配置更新
|
||||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||||
|
|
||||||
/// 获取配置文件列表
|
/// 获取配置文件避免锁竞争
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||||
let profiles_result = tokio::time::timeout(
|
// 策略1: 尝试快速获取latest数据
|
||||||
Duration::from_secs(3), // 3秒超时
|
let latest_result = tokio::time::timeout(
|
||||||
tokio::task::spawn_blocking(move || Config::profiles().data().clone()),
|
Duration::from_millis(500),
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let latest = profiles.latest();
|
||||||
|
IProfiles {
|
||||||
|
current: latest.current.clone(),
|
||||||
|
items: latest.items.clone(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match profiles_result {
|
match latest_result {
|
||||||
Ok(Ok(profiles)) => Ok(*profiles),
|
Ok(Ok(profiles)) => {
|
||||||
|
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
Ok(Err(join_err)) => {
|
Ok(Err(join_err)) => {
|
||||||
logging!(error, Type::Cmd, true, "获取配置列表任务失败: {}", join_err);
|
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
|
||||||
Ok(IProfiles {
|
|
||||||
current: None,
|
|
||||||
items: Some(vec![]),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// 超时情况
|
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 策略2: 如果快速获取失败,尝试获取data()
|
||||||
|
let data_result = tokio::time::timeout(
|
||||||
|
Duration::from_secs(2),
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let profiles = Config::profiles();
|
||||||
|
let data = profiles.data();
|
||||||
|
IProfiles {
|
||||||
|
current: data.current.clone(),
|
||||||
|
items: data.items.clone(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match data_result {
|
||||||
|
Ok(Ok(profiles)) => {
|
||||||
|
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
Ok(Err(join_err)) => {
|
||||||
logging!(
|
logging!(
|
||||||
error,
|
error,
|
||||||
Type::Cmd,
|
Type::Cmd,
|
||||||
true,
|
true,
|
||||||
"获取配置列表超时(3秒),可能存在锁竞争"
|
"获取draft配置任务失败: {}",
|
||||||
|
join_err
|
||||||
);
|
);
|
||||||
match tokio::task::spawn_blocking(move || Config::profiles().latest().clone()).await {
|
}
|
||||||
Ok(profiles) => {
|
Err(_) => {
|
||||||
logging!(info, Type::Cmd, true, "使用latest()成功获取配置");
|
logging!(error, Type::Cmd, true, "获取draft配置超时(2秒)");
|
||||||
Ok(*profiles)
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
|
||||||
logging!(error, Type::Cmd, true, "fallback获取配置也失败,返回空配置");
|
// 策略3: fallback,尝试重新创建配置
|
||||||
Ok(IProfiles {
|
logging!(
|
||||||
current: None,
|
warn,
|
||||||
items: Some(vec![]),
|
Type::Cmd,
|
||||||
})
|
true,
|
||||||
}
|
"所有获取配置策略都失败,尝试fallback"
|
||||||
}
|
);
|
||||||
|
|
||||||
|
match tokio::task::spawn_blocking(move || IProfiles::new()).await {
|
||||||
|
Ok(profiles) => {
|
||||||
|
logging!(info, Type::Cmd, true, "使用fallback配置成功");
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
|
||||||
|
// 返回空配置避免崩溃
|
||||||
|
Ok(IProfiles {
|
||||||
|
current: None,
|
||||||
|
items: Some(vec![]),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +290,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
|||||||
Config::profiles().apply();
|
Config::profiles().apply();
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
|
|
||||||
|
// 强制刷新代理缓存,确保profile切换后立即获取最新节点数据
|
||||||
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
|
if let Err(e) = super::proxy::force_refresh_proxies().await {
|
||||||
|
log::warn!(target: "app", "强制刷新代理缓存失败: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
crate::process::AsyncHandler::spawn(|| async move {
|
crate::process::AsyncHandler::spawn(|| async move {
|
||||||
if let Err(e) = Tray::global().update_tooltip() {
|
if let Err(e) = Tray::global().update_tooltip() {
|
||||||
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
log::warn!(target: "app", "异步更新托盘提示失败: {}", e);
|
||||||
|
|||||||
@@ -43,6 +43,28 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
|
|||||||
Ok(*proxies)
|
Ok(*proxies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 强制刷新代理缓存用于profile切换
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
|
||||||
|
let manager = MihomoManager::global();
|
||||||
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||||
|
|
||||||
|
log::debug!(target: "app", "强制刷新代理缓存");
|
||||||
|
|
||||||
|
let proxies = manager.get_refresh_proxies().await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = cmd_proxy_state.lock().unwrap();
|
||||||
|
state.proxies = Box::new(proxies.clone());
|
||||||
|
state.need_refresh = false;
|
||||||
|
state.last_refresh_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!(target: "app", "强制刷新代理缓存完成");
|
||||||
|
Ok(proxies)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
|
||||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ impl NotificationSystem {
|
|||||||
match window.emit(event_name_str, payload) {
|
match window.emit(event_name_str, payload) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
system.stats.total_sent.fetch_add(1, Ordering::SeqCst);
|
||||||
|
// 记录成功发送的事件
|
||||||
|
if log::log_enabled!(log::Level::Debug) {
|
||||||
|
log::debug!("Successfully emitted event: {}", event_name_str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
log::warn!("Failed to emit event {}: {}", event_name_str, e);
|
||||||
@@ -224,12 +228,27 @@ impl NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(&mut self) {
|
fn shutdown(&mut self) {
|
||||||
|
log::info!("NotificationSystem shutdown initiated");
|
||||||
self.is_running = false;
|
self.is_running = false;
|
||||||
self.sender = None;
|
|
||||||
|
|
||||||
if let Some(handle) = self.worker_handle.take() {
|
// 先关闭发送端,让接收端知道不会再有新消息
|
||||||
let _ = handle.join();
|
if let Some(sender) = self.sender.take() {
|
||||||
|
drop(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置超时避免无限等待
|
||||||
|
if let Some(handle) = self.worker_handle.take() {
|
||||||
|
match handle.join() {
|
||||||
|
Ok(_) => {
|
||||||
|
log::info!("NotificationSystem worker thread joined successfully");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("NotificationSystem worker thread join failed: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("NotificationSystem shutdown completed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ pub fn run() {
|
|||||||
cmd::invoke_uwp_tool,
|
cmd::invoke_uwp_tool,
|
||||||
cmd::copy_clash_env,
|
cmd::copy_clash_env,
|
||||||
cmd::get_proxies,
|
cmd::get_proxies,
|
||||||
|
cmd::force_refresh_proxies,
|
||||||
cmd::get_providers_proxies,
|
cmd::get_providers_proxies,
|
||||||
cmd::save_dns_config,
|
cmd::save_dns_config,
|
||||||
cmd::apply_dns_config,
|
cmd::apply_dns_config,
|
||||||
|
|||||||
@@ -10,11 +10,32 @@ export const useProfiles = () => {
|
|||||||
const { data: profiles, mutate: mutateProfiles } = useSWR(
|
const { data: profiles, mutate: mutateProfiles } = useSWR(
|
||||||
"getProfiles",
|
"getProfiles",
|
||||||
getProfiles,
|
getProfiles,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 2000,
|
||||||
|
errorRetryCount: 2,
|
||||||
|
errorRetryInterval: 1000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
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>) => {
|
const patchCurrent = async (value: Partial<IProfileItem>) => {
|
||||||
@@ -26,40 +47,90 @@ export const useProfiles = () => {
|
|||||||
|
|
||||||
// 根据selected的节点选择
|
// 根据selected的节点选择
|
||||||
const activateSelected = async () => {
|
const activateSelected = async () => {
|
||||||
const proxiesData = await getProxies();
|
try {
|
||||||
const profileData = await getProfiles();
|
console.log("[ActivateSelected] 开始处理代理选择");
|
||||||
|
|
||||||
if (!profileData || !proxiesData) return;
|
const [proxiesData, profileData] = await Promise.all([
|
||||||
|
getProxies(),
|
||||||
|
getProfiles(),
|
||||||
|
]);
|
||||||
|
|
||||||
const current = profileData.items?.find(
|
if (!profileData || !proxiesData) {
|
||||||
(e) => e && e.uid === profileData.current,
|
console.log("[ActivateSelected] 代理或配置数据不可用,跳过处理");
|
||||||
);
|
return;
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
newSelected.push({ name, now: selectedMap[name] });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasChange) {
|
const current = profileData.items?.find(
|
||||||
patchProfile(profileData.current!, { selected: newSelected });
|
(e) => e && e.uid === profileData.current,
|
||||||
mutate("getProxies", getProxies());
|
);
|
||||||
|
|
||||||
|
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(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
const [status, msg] = payload;
|
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],
|
[t, navigate],
|
||||||
);
|
);
|
||||||
@@ -220,12 +226,35 @@ const Layout = () => {
|
|||||||
const cleanupWindow = setupWindowListeners();
|
const cleanupWindow = setupWindowListeners();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
listeners.forEach((listener) => {
|
setTimeout(() => {
|
||||||
if (typeof listener.then === "function") {
|
listeners.forEach((listener) => {
|
||||||
listener.then((unlisten) => unlisten());
|
if (typeof listener.then === "function") {
|
||||||
}
|
listener
|
||||||
});
|
.then((unlisten) => {
|
||||||
cleanupWindow.then((cleanup) => cleanup());
|
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]);
|
}, [handleNotice]);
|
||||||
|
|
||||||
|
|||||||
@@ -190,27 +190,53 @@ const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activateProfile = async (profile: string, notifySuccess: boolean) => {
|
const activateProfile = useLockFn(
|
||||||
// 避免大多数情况下loading态闪烁
|
async (profile: string, notifySuccess: boolean) => {
|
||||||
const reset = setTimeout(() => {
|
if (profiles.current === profile && !notifySuccess) {
|
||||||
setActivatings((prev) => [...prev, profile]);
|
console.log(
|
||||||
}, 100);
|
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
|
||||||
|
);
|
||||||
try {
|
return;
|
||||||
const success = await patchProfiles({ current: profile });
|
|
||||||
await mutateLogs();
|
|
||||||
closeAllConnections();
|
|
||||||
await activateSelected();
|
|
||||||
if (notifySuccess && success) {
|
|
||||||
showNotice("success", t("Profile Switched"), 1000);
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
|
||||||
showNotice("error", err?.message || err.toString(), 4000);
|
// 避免大多数情况下loading态闪烁
|
||||||
} finally {
|
const reset = setTimeout(() => {
|
||||||
clearTimeout(reset);
|
setActivatings((prev) => [...prev, profile]);
|
||||||
setActivatings([]);
|
}, 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) => {
|
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||||
if (!force && current === profiles.current) return;
|
if (!force && current === profiles.current) return;
|
||||||
await activateProfile(current, true);
|
await activateProfile(current, true);
|
||||||
@@ -300,31 +326,45 @@ const ProfilePage = () => {
|
|||||||
// 监听后端配置变更
|
// 监听后端配置变更
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlistenPromise: Promise<() => void> | undefined;
|
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 () => {
|
const setupListener = async () => {
|
||||||
unlistenPromise = listen<string>("profile-changed", (event) => {
|
unlistenPromise = listen<string>("profile-changed", (event) => {
|
||||||
console.log("Profile changed event received:", event.payload);
|
const newProfileId = event.payload;
|
||||||
if (timeoutId) {
|
const now = Date.now();
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
console.log(`[Profile] 收到配置变更事件: ${newProfileId}`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastProfileId === newProfileId &&
|
||||||
|
now - lastUpdateTime < debounceDelay
|
||||||
|
) {
|
||||||
|
console.log(`[Profile] 重复事件被防抖,跳过`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
lastProfileId = newProfileId;
|
||||||
mutateProfiles();
|
lastUpdateTime = now;
|
||||||
timeoutId = undefined;
|
|
||||||
}, 300);
|
console.log(`[Profile] 执行配置数据刷新`);
|
||||||
|
|
||||||
|
// 使用异步调度避免阻塞事件处理
|
||||||
|
setTimeout(() => {
|
||||||
|
mutateProfiles().catch((error) => {
|
||||||
|
console.error("[Profile] 配置数据刷新失败:", error);
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
setupListener();
|
setupListener();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timeoutId) {
|
unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
unlistenPromise?.then((unlisten) => unlisten());
|
|
||||||
};
|
};
|
||||||
}, [mutateProfiles, t]);
|
}, [mutateProfiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useMemo, useEffect } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import useSWRSubscription from "swr/subscription";
|
import useSWRSubscription from "swr/subscription";
|
||||||
import {
|
import {
|
||||||
@@ -8,10 +8,16 @@ import {
|
|||||||
getProxyProviders,
|
getProxyProviders,
|
||||||
getRuleProviders,
|
getRuleProviders,
|
||||||
} from "@/services/api";
|
} 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 { useClashInfo } from "@/hooks/use-clash";
|
||||||
import { createAuthSockette } from "@/utils/websocket";
|
import { createAuthSockette } from "@/utils/websocket";
|
||||||
import { useVisibility } from "@/hooks/use-visibility";
|
import { useVisibility } from "@/hooks/use-visibility";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
// 定义AppDataContext类型 - 使用宽松类型
|
// 定义AppDataContext类型 - 使用宽松类型
|
||||||
interface AppDataContextType {
|
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(
|
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||||
"getClashConfig",
|
"getClashConfig",
|
||||||
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) {
|
export async function cmdTestDelay(url: string) {
|
||||||
return invoke<number>("test_delay", { url });
|
return invoke<number>("test_delay", { url });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user