mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
feat: enhance profile import functionality with timeout and robust refresh strategy
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新排序配置文件
|
||||
|
||||
@@ -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, // 检测是否处于异常状态
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user