feat: enhance profile import functionality with timeout and robust refresh strategy

This commit is contained in:
Tunglies
2025-08-05 20:29:02 +08:00
parent 776abaf56d
commit a66393c609
3 changed files with 234 additions and 17 deletions

View File

@@ -129,8 +129,89 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件 /// 导入配置文件
#[tauri::command] #[tauri::command]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult { pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; logging!(info, Type::Cmd, true, "[导入订阅] 开始导入: {}", url);
wrap_err!(Config::profiles().data_mut().append_item(item))
// 使用超时保护避免长时间阻塞
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())
}
}
} }
/// 重新排序配置文件 /// 重新排序配置文件

View File

@@ -8,17 +8,28 @@ import {
import { getProxies, updateProxy } from "@/services/cmds"; import { getProxies, updateProxy } from "@/services/cmds";
export const useProfiles = () => { export const useProfiles = () => {
const { data: profiles, mutate: mutateProfiles } = useSWR( const {
"getProfiles", data: profiles,
getProfiles, mutate: mutateProfiles,
{ error,
revalidateOnFocus: false, isValidating,
revalidateOnReconnect: false, } = useSWR("getProfiles", getProfiles, {
dedupingInterval: 2000, revalidateOnFocus: false,
errorRetryCount: 2, revalidateOnReconnect: false,
errorRetryInterval: 1000, 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 ( const patchProfiles = async (
value: Partial<IProfilesConfig>, value: Partial<IProfilesConfig>,
@@ -153,5 +164,9 @@ export const useProfiles = () => {
patchProfiles, patchProfiles,
patchCurrent, patchCurrent,
mutateProfiles, mutateProfiles,
// 新增故障检测状态
isLoading: isValidating,
error,
isStale: !profiles && !error && !isValidating, // 检测是否处于异常状态
}; };
}; };

View File

@@ -1,4 +1,4 @@
import useSWR from "swr"; import useSWR, { mutate } from "swr";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material"; import { Box, Button, IconButton, Stack, Divider, Grid } from "@mui/material";
@@ -202,8 +202,36 @@ const ProfilePage = () => {
activateSelected, activateSelected,
patchProfiles, patchProfiles,
mutateProfiles, mutateProfiles,
isLoading,
error,
isStale,
} = useProfiles(); } = 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( const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
"getRuntimeLogs", "getRuntimeLogs",
getRuntimeLogs, getRuntimeLogs,
@@ -233,13 +261,18 @@ const ProfilePage = () => {
return; return;
} }
setLoading(true); setLoading(true);
// 保存导入前的配置状态用于故障恢复
const preImportProfilesCount = profiles?.items?.length || 0;
try { try {
// 尝试正常导入 // 尝试正常导入
await importProfile(url); await importProfile(url);
showNotice("success", t("Profile Imported Successfully")); showNotice("success", t("Profile Imported Successfully"));
setUrl(""); setUrl("");
mutateProfiles();
await onEnhance(false); // 增强的刷新策略
await performRobustRefresh(preImportProfilesCount);
} catch (err: any) { } catch (err: any) {
// 首次导入失败,尝试使用自身代理 // 首次导入失败,尝试使用自身代理
const errmsg = err.message || err.toString(); const errmsg = err.message || err.toString();
@@ -253,8 +286,9 @@ const ProfilePage = () => {
// 回退导入成功 // 回退导入成功
showNotice("success", t("Profile Imported with Clash proxy")); showNotice("success", t("Profile Imported with Clash proxy"));
setUrl(""); setUrl("");
mutateProfiles();
await onEnhance(false); // 增强的刷新策略
await performRobustRefresh(preImportProfilesCount);
} catch (retryErr: any) { } catch (retryErr: any) {
// 回退导入也失败 // 回退导入也失败
const retryErrmsg = retryErr?.message || retryErr.toString(); 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 onDragEnd = async (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
if (over) { if (over) {
@@ -618,6 +719,26 @@ const ProfilePage = () => {
> >
<LocalFireDepartmentRounded /> <LocalFireDepartmentRounded />
</IconButton> </IconButton>
{/* 故障检测和紧急恢复按钮 */}
{(error || isStale) && (
<IconButton
size="small"
color="warning"
title="数据异常,点击强制刷新"
onClick={onEmergencyRefresh}
sx={{
animation: "pulse 2s infinite",
"@keyframes pulse": {
"0%": { opacity: 1 },
"50%": { opacity: 0.5 },
"100%": { opacity: 1 },
},
}}
>
<ClearRounded />
</IconButton>
)}
</Box> </Box>
} }
> >