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 深色模式下首次启动客户端标题栏颜色异常
|
||||
- 修复静默启动不加载完整 WebView 的问题
|
||||
- 修复 Linux WebKit 网络进程的崩溃
|
||||
- 修复 Linux GNOME/KDE 桌面下,应用主题颜色选择“系统”后,不随操作系统主题(Dark/Light)切换
|
||||
- 修复实际导入成功但显示导入失败的问题
|
||||
|
||||
## v2.4.2
|
||||
|
||||
|
||||
@@ -99,6 +99,112 @@ const isOperationAborted = (
|
||||
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 { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@@ -276,19 +382,55 @@ const ProfilePage = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
// 保存导入前的配置状态用于故障恢复
|
||||
const preImportProfilesCount = profiles?.items?.length || 0;
|
||||
const importVerifier = createImportLandingVerifier(profiles?.items, url);
|
||||
|
||||
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 {
|
||||
// 尝试正常导入
|
||||
await importProfile(url);
|
||||
showNotice("success", t("Profile Imported Successfully"));
|
||||
setUrl("");
|
||||
await handleImportSuccess("Profile Imported Successfully");
|
||||
return;
|
||||
} catch (initialErr) {
|
||||
console.warn("[订阅导入] 首次导入失败:", initialErr);
|
||||
|
||||
// 增强的刷新策略
|
||||
await performRobustRefresh(preImportProfilesCount);
|
||||
} catch {
|
||||
// 首次导入失败,尝试使用自身代理
|
||||
const alreadyImported = await waitForImportLanding();
|
||||
if (alreadyImported) {
|
||||
console.warn(
|
||||
"[订阅导入] 接口返回失败,但检测到订阅已导入,跳过回退导入流程",
|
||||
);
|
||||
await handleImportSuccess("Profile Imported Successfully");
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次导入失败且未检测到数据变更,尝试使用自身代理
|
||||
showNotice("info", t("Import failed, retrying with Clash proxy..."));
|
||||
try {
|
||||
// 使用自身代理尝试导入
|
||||
@@ -296,12 +438,7 @@ const ProfilePage = () => {
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
});
|
||||
// 回退导入成功
|
||||
showNotice("success", t("Profile Imported with Clash proxy"));
|
||||
setUrl("");
|
||||
|
||||
// 增强的刷新策略
|
||||
await performRobustRefresh(preImportProfilesCount);
|
||||
await handleImportSuccess("Profile Imported with Clash proxy");
|
||||
} catch (retryErr: any) {
|
||||
// 回退导入也失败
|
||||
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;
|
||||
const maxRetries = 5;
|
||||
const baseDelay = 200;
|
||||
@@ -341,14 +481,20 @@ const ProfilePage = () => {
|
||||
const currentProfiles = await getProfiles();
|
||||
const currentCount = currentProfiles?.items?.length || 0;
|
||||
|
||||
if (currentCount > expectedMinCount) {
|
||||
if (currentCount > baselineCount) {
|
||||
console.log(
|
||||
`[导入刷新] 配置刷新成功,配置数量: ${expectedMinCount} -> ${currentCount}`,
|
||||
`[导入刷新] 配置刷新成功,配置数量 ${baselineCount} -> ${currentCount}`,
|
||||
);
|
||||
await onEnhance(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasLanding(currentProfiles)) {
|
||||
console.log("[导入刷新] 检测到订阅内容更新,判定刷新成功");
|
||||
await onEnhance(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user