fix(profile): fix false failure notice after successful import (#5038)
- normalize profile urls so matching ignores casing/trailing slashes - capture baseline profile state and confirm landing before showing success - reuse shared success handler for normal and clash proxy retries
This commit is contained in:
@@ -47,7 +47,7 @@
|
|||||||
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
|
||||||
- 修复静默启动不加载完整 WebView 的问题
|
- 修复静默启动不加载完整 WebView 的问题
|
||||||
- 修复 Linux WebKit 网络进程的崩溃
|
- 修复 Linux WebKit 网络进程的崩溃
|
||||||
- 修复 Linux GNOME/KDE 桌面下,应用主题颜色选择“系统”后,不随操作系统主题(Dark/Light)切换
|
- 修复实际导入成功但显示导入失败的问题
|
||||||
|
|
||||||
## v2.4.2
|
## v2.4.2
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,112 @@ const isOperationAborted = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeProfileUrl = (value?: string) => {
|
||||||
|
if (!value) return "";
|
||||||
|
const trimmed = value.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(trimmed);
|
||||||
|
const auth =
|
||||||
|
url.username || url.password
|
||||||
|
? `${url.username}${url.password ? `:${url.password}` : ""}@`
|
||||||
|
: "";
|
||||||
|
const normalized =
|
||||||
|
`${url.protocol.toLowerCase()}//${auth}${url.hostname.toLowerCase()}` +
|
||||||
|
`${url.port ? `:${url.port}` : ""}${url.pathname}${url.search}${url.hash}`;
|
||||||
|
|
||||||
|
return normalized.replace(/\/+$/, "");
|
||||||
|
} catch {
|
||||||
|
const schemeNormalized = trimmed.replace(
|
||||||
|
/^([a-z]+):\/\//i,
|
||||||
|
(match, scheme: string) => `${scheme.toLowerCase()}://`,
|
||||||
|
);
|
||||||
|
return schemeNormalized.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProfileSignature = (profile?: IProfileItem | null) => {
|
||||||
|
if (!profile) return "";
|
||||||
|
const { extra, selected, option, name, desc } = profile;
|
||||||
|
return JSON.stringify({
|
||||||
|
extra: extra ?? null,
|
||||||
|
selected: selected ?? null,
|
||||||
|
option: option ?? null,
|
||||||
|
name: name ?? null,
|
||||||
|
desc: desc ?? null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImportLandingVerifier = {
|
||||||
|
baselineCount: number;
|
||||||
|
hasLanding: (config?: IProfilesConfig | null) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createImportLandingVerifier = (
|
||||||
|
items: IProfileItem[] | undefined,
|
||||||
|
url: string,
|
||||||
|
): ImportLandingVerifier => {
|
||||||
|
const normalizedUrl = normalizeProfileUrl(url);
|
||||||
|
const baselineCount = items?.length ?? 0;
|
||||||
|
const baselineProfile = normalizedUrl
|
||||||
|
? items?.find((item) => normalizeProfileUrl(item?.url) === normalizedUrl)
|
||||||
|
: undefined;
|
||||||
|
const baselineSignature = getProfileSignature(baselineProfile);
|
||||||
|
const baselineUpdated = baselineProfile?.updated ?? 0;
|
||||||
|
const hadBaselineProfile = Boolean(baselineProfile);
|
||||||
|
|
||||||
|
const hasLanding = (config?: IProfilesConfig | null) => {
|
||||||
|
const currentItems = config?.items ?? [];
|
||||||
|
const currentCount = currentItems.length;
|
||||||
|
|
||||||
|
if (currentCount > baselineCount) {
|
||||||
|
console.log(
|
||||||
|
`[导入验证] 配置数量已增加: ${baselineCount} -> ${currentCount}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalizedUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingProfile = currentItems.find(
|
||||||
|
(item) => normalizeProfileUrl(item?.url) === normalizedUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingProfile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hadBaselineProfile) {
|
||||||
|
console.log("[导入验证] 检测到新的订阅记录,判定为导入成功");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSignature = getProfileSignature(matchingProfile);
|
||||||
|
const currentUpdated = matchingProfile.updated ?? 0;
|
||||||
|
|
||||||
|
if (currentUpdated > baselineUpdated) {
|
||||||
|
console.log(
|
||||||
|
`[导入验证] 订阅更新时间已更新 ${baselineUpdated} -> ${currentUpdated}`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSignature !== baselineSignature) {
|
||||||
|
console.log("[导入验证] 订阅详情发生变化,判定为导入成功");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
baselineCount,
|
||||||
|
hasLanding,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -276,19 +382,55 @@ const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// 保存导入前的配置状态用于故障恢复
|
const importVerifier = createImportLandingVerifier(profiles?.items, url);
|
||||||
const preImportProfilesCount = profiles?.items?.length || 0;
|
|
||||||
|
const handleImportSuccess = async (noticeKey: string) => {
|
||||||
|
showNotice("success", t(noticeKey));
|
||||||
|
setUrl("");
|
||||||
|
await performRobustRefresh(importVerifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForImportLanding = async () => {
|
||||||
|
const maxChecks = 2;
|
||||||
|
for (let attempt = 0; attempt <= maxChecks; attempt++) {
|
||||||
|
try {
|
||||||
|
const currentProfiles = await getProfiles();
|
||||||
|
if (importVerifier.hasLanding(currentProfiles)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxChecks) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 200 * (attempt + 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (verifyErr) {
|
||||||
|
console.warn("[导入验证] 获取配置状态失败:", verifyErr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试正常导入
|
// 尝试正常导入
|
||||||
await importProfile(url);
|
await importProfile(url);
|
||||||
showNotice("success", t("Profile Imported Successfully"));
|
await handleImportSuccess("Profile Imported Successfully");
|
||||||
setUrl("");
|
return;
|
||||||
|
} catch (initialErr) {
|
||||||
|
console.warn("[订阅导入] 首次导入失败:", initialErr);
|
||||||
|
|
||||||
// 增强的刷新策略
|
const alreadyImported = await waitForImportLanding();
|
||||||
await performRobustRefresh(preImportProfilesCount);
|
if (alreadyImported) {
|
||||||
} catch {
|
console.warn(
|
||||||
// 首次导入失败,尝试使用自身代理
|
"[订阅导入] 接口返回失败,但检测到订阅已导入,跳过回退导入流程",
|
||||||
|
);
|
||||||
|
await handleImportSuccess("Profile Imported Successfully");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次导入失败且未检测到数据变更,尝试使用自身代理
|
||||||
showNotice("info", t("Import failed, retrying with Clash proxy..."));
|
showNotice("info", t("Import failed, retrying with Clash proxy..."));
|
||||||
try {
|
try {
|
||||||
// 使用自身代理尝试导入
|
// 使用自身代理尝试导入
|
||||||
@@ -296,12 +438,7 @@ const ProfilePage = () => {
|
|||||||
with_proxy: false,
|
with_proxy: false,
|
||||||
self_proxy: true,
|
self_proxy: true,
|
||||||
});
|
});
|
||||||
// 回退导入成功
|
await handleImportSuccess("Profile Imported with Clash proxy");
|
||||||
showNotice("success", t("Profile Imported with Clash proxy"));
|
|
||||||
setUrl("");
|
|
||||||
|
|
||||||
// 增强的刷新策略
|
|
||||||
await performRobustRefresh(preImportProfilesCount);
|
|
||||||
} catch (retryErr: any) {
|
} catch (retryErr: any) {
|
||||||
// 回退导入也失败
|
// 回退导入也失败
|
||||||
const retryErrmsg = retryErr?.message || retryErr.toString();
|
const retryErrmsg = retryErr?.message || retryErr.toString();
|
||||||
@@ -317,7 +454,10 @@ const ProfilePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 强化的刷新策略
|
// 强化的刷新策略
|
||||||
const performRobustRefresh = async (expectedMinCount: number) => {
|
const performRobustRefresh = async (
|
||||||
|
importVerifier: ImportLandingVerifier,
|
||||||
|
) => {
|
||||||
|
const { baselineCount, hasLanding } = importVerifier;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 5;
|
const maxRetries = 5;
|
||||||
const baseDelay = 200;
|
const baseDelay = 200;
|
||||||
@@ -341,14 +481,20 @@ const ProfilePage = () => {
|
|||||||
const currentProfiles = await getProfiles();
|
const currentProfiles = await getProfiles();
|
||||||
const currentCount = currentProfiles?.items?.length || 0;
|
const currentCount = currentProfiles?.items?.length || 0;
|
||||||
|
|
||||||
if (currentCount > expectedMinCount) {
|
if (currentCount > baselineCount) {
|
||||||
console.log(
|
console.log(
|
||||||
`[导入刷新] 配置刷新成功,配置数量: ${expectedMinCount} -> ${currentCount}`,
|
`[导入刷新] 配置刷新成功,配置数量 ${baselineCount} -> ${currentCount}`,
|
||||||
);
|
);
|
||||||
await onEnhance(false);
|
await onEnhance(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasLanding(currentProfiles)) {
|
||||||
|
console.log("[导入刷新] 检测到订阅内容更新,判定刷新成功");
|
||||||
|
await onEnhance(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
`[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`,
|
`[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user