From a66393c6099942b30c30cd4e866fa13a305e964c Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 5 Aug 2025 20:29:02 +0800 Subject: [PATCH] feat: enhance profile import functionality with timeout and robust refresh strategy --- src-tauri/src/cmd/profile.rs | 85 ++++++++++++++++++++++- src/hooks/use-profiles.ts | 35 +++++++--- src/pages/profiles.tsx | 131 +++++++++++++++++++++++++++++++++-- 3 files changed, 234 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index a6ff9153..1c2a0f53 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -129,8 +129,89 @@ pub async fn enhance_profiles() -> CmdResult { /// 导入配置文件 #[tauri::command] pub async fn import_profile(url: String, option: Option) -> CmdResult { - let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; - wrap_err!(Config::profiles().data_mut().append_item(item)) + logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url); + + // 使用超时保护避免长时间阻塞 + let import_result = tokio::time::timeout( + Duration::from_secs(60), // 60秒超时 + async { + let item = PrfItem::from_url(&url, None, None, option).await?; + logging!(info, Type::Cmd, true, "[导入订阅] 下载完成,开始保存配置"); + + // 获取导入前的配置数量用于验证 + let pre_count = Config::profiles().latest_ref().items.len(); + + Config::profiles().data_mut().append_item(item.clone())?; + + // 验证导入是否成功 + let post_count = Config::profiles().latest_ref().items.len(); + if post_count <= pre_count { + logging!( + error, + Type::Cmd, + true, + "[导入订阅] 配置未增加,导入可能失败" + ); + return Err(anyhow::anyhow!("配置导入后数量未增加")); + } + + logging!( + info, + Type::Cmd, + true, + "[导入订阅] 配置保存成功,数量: {} -> {}", + pre_count, + post_count + ); + + // 立即发送配置变更通知 + if let Some(uid) = &item.uid { + logging!( + info, + Type::Cmd, + true, + "[导入订阅] 发送配置变更通知: {}", + uid + ); + handle::Handle::notify_profile_changed(uid.clone()); + } + + // 异步保存配置文件并发送全局通知 + let uid_clone = item.uid.clone(); + crate::process::AsyncHandler::spawn(move || async move { + if let Err(e) = Config::profiles().data_mut().save_file() { + logging!(error, Type::Cmd, true, "[导入订阅] 保存配置文件失败: {}", e); + } else { + logging!(info, Type::Cmd, true, "[导入订阅] 配置文件保存成功"); + + // 发送全局配置更新通知 + if let Some(uid) = uid_clone { + // 延迟发送,确保文件已完全写入 + tokio::time::sleep(Duration::from_millis(100)).await; + handle::Handle::notify_profile_changed(uid); + } + } + }); + + Ok(()) + }, + ) + .await; + + match import_result { + Ok(Ok(())) => { + logging!(info, Type::Cmd, true, "[导入订阅] 导入完成: {}", url); + Ok(()) + } + Ok(Err(e)) => { + logging!(error, Type::Cmd, true, "[导入订阅] 导入失败: {}", e); + Err(format!("导入订阅失败: {}", e).into()) + } + Err(_) => { + logging!(error, Type::Cmd, true, "[导入订阅] 导入超时(60秒): {}", url); + Err("导入订阅超时,请检查网络连接".into()) + } + } } /// 重新排序配置文件 diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index 111bf78c..467b903f 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -8,17 +8,28 @@ import { import { getProxies, updateProxy } from "@/services/cmds"; export const useProfiles = () => { - const { data: profiles, mutate: mutateProfiles } = useSWR( - "getProfiles", - getProfiles, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - dedupingInterval: 2000, - errorRetryCount: 2, - errorRetryInterval: 1000, + const { + data: profiles, + mutate: mutateProfiles, + error, + isValidating, + } = useSWR("getProfiles", getProfiles, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + dedupingInterval: 500, // 减少去重时间,提高响应性 + errorRetryCount: 3, + errorRetryInterval: 1000, + refreshInterval: 0, // 完全由手动控制 + onError: (error) => { + console.error("[useProfiles] SWR错误:", error); }, - ); + onSuccess: (data) => { + console.log( + "[useProfiles] 配置数据更新成功,配置数量:", + data?.items?.length || 0, + ); + }, + }); const patchProfiles = async ( value: Partial, @@ -153,5 +164,9 @@ export const useProfiles = () => { patchProfiles, patchCurrent, mutateProfiles, + // 新增故障检测状态 + isLoading: isValidating, + error, + isStale: !profiles && !error && !isValidating, // 检测是否处于异常状态 }; }; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 3c90976e..f3e01a2d 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -1,4 +1,4 @@ -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; import { useEffect, useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material"; @@ -202,8 +202,36 @@ const ProfilePage = () => { activateSelected, patchProfiles, mutateProfiles, + isLoading, + error, + isStale, } = useProfiles(); + // 添加紧急恢复功能 + const onEmergencyRefresh = useLockFn(async () => { + console.log("[紧急刷新] 开始强制刷新所有数据"); + + try { + // 清除所有SWR缓存 + await mutate(() => true, undefined, { revalidate: false }); + + // 强制重新获取配置数据 + await mutateProfiles(undefined, { + revalidate: true, + rollbackOnError: false, + }); + + // 等待状态稳定后增强配置 + await new Promise((resolve) => setTimeout(resolve, 500)); + await onEnhance(false); + + showNotice("success", "数据已强制刷新", 2000); + } catch (error: any) { + console.error("[紧急刷新] 失败:", error); + showNotice("error", `紧急刷新失败: ${error.message}`, 4000); + } + }); + const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( "getRuntimeLogs", getRuntimeLogs, @@ -233,13 +261,18 @@ const ProfilePage = () => { return; } setLoading(true); + + // 保存导入前的配置状态用于故障恢复 + const preImportProfilesCount = profiles?.items?.length || 0; + try { // 尝试正常导入 await importProfile(url); showNotice("success", t("Profile Imported Successfully")); setUrl(""); - mutateProfiles(); - await onEnhance(false); + + // 增强的刷新策略 + await performRobustRefresh(preImportProfilesCount); } catch (err: any) { // 首次导入失败,尝试使用自身代理 const errmsg = err.message || err.toString(); @@ -253,8 +286,9 @@ const ProfilePage = () => { // 回退导入成功 showNotice("success", t("Profile Imported with Clash proxy")); setUrl(""); - mutateProfiles(); - await onEnhance(false); + + // 增强的刷新策略 + await performRobustRefresh(preImportProfilesCount); } catch (retryErr: any) { // 回退导入也失败 const retryErrmsg = retryErr?.message || retryErr.toString(); @@ -269,6 +303,73 @@ const ProfilePage = () => { } }; + // 强化的刷新策略 + const performRobustRefresh = async (expectedMinCount: number) => { + let retryCount = 0; + const maxRetries = 5; + const baseDelay = 200; + + while (retryCount < maxRetries) { + try { + console.log(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`); + + // 强制刷新,绕过所有缓存 + await mutateProfiles(undefined, { + revalidate: true, + rollbackOnError: false, + }); + + // 等待状态稳定 + await new Promise((resolve) => + setTimeout(resolve, baseDelay * (retryCount + 1)), + ); + + // 验证刷新是否成功 + const currentProfiles = await getProfiles(); + const currentCount = currentProfiles?.items?.length || 0; + + if (currentCount > expectedMinCount) { + console.log( + `[导入刷新] 配置刷新成功,配置数量: ${expectedMinCount} -> ${currentCount}`, + ); + await onEnhance(false); + return; + } + + console.warn( + `[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`, + ); + retryCount++; + } catch (error) { + console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error); + retryCount++; + await new Promise((resolve) => + setTimeout(resolve, baseDelay * retryCount), + ); + } + } + + // 所有重试失败后的最后尝试 + console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`); + try { + // 清除SWR缓存并重新获取 + await mutate("getProfiles", getProfiles(), { revalidate: true }); + await onEnhance(false); + showNotice( + "warning", + t("Profile imported but may need manual refresh"), + 3000, + ); + } catch (finalError) { + console.error(`[导入刷新] 最终刷新尝试失败:`, finalError); + showNotice( + "warning", + t("Profile imported successfully, please restart if not visible"), + 5000, + ); + } + }; + const onDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over) { @@ -618,6 +719,26 @@ const ProfilePage = () => { > + + {/* 故障检测和紧急恢复按钮 */} + {(error || isStale) && ( + + + + )} } >