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]
pub async fn import_profile(url: String, option: Option<PrfOption>) -> 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())
}
}
}
/// 重新排序配置文件

View File

@@ -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<IProfilesConfig>,
@@ -153,5 +164,9 @@ export const useProfiles = () => {
patchProfiles,
patchCurrent,
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 { 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 = () => {
>
<LocalFireDepartmentRounded />
</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>
}
>