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:
Sline
2025-10-12 23:21:32 +08:00
committed by GitHub
Unverified
parent b91087e175
commit 19246ac616
2 changed files with 164 additions and 18 deletions

View File

@@ -47,7 +47,7 @@
- 修复 Windows 深色模式下首次启动客户端标题栏颜色异常
- 修复静默启动不加载完整 WebView 的问题
- 修复 Linux WebKit 网络进程的崩溃
- 修复 Linux GNOME/KDE 桌面下应用主题颜色选择“系统”后不随操作系统主题Dark/Light切换
- 修复实际导入成功但显示导入失败的问题
## v2.4.2

View File

@@ -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}), 继续重试...`,
);