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]
|
#[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())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重新排序配置文件
|
/// 重新排序配置文件
|
||||||
|
|||||||
@@ -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, // 检测是否处于异常状态
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user