From 19246ac6166a2b88722fa35bfa567ac6065713ef Mon Sep 17 00:00:00 2001 From: Sline Date: Sun, 12 Oct 2025 23:21:32 +0800 Subject: [PATCH] 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 --- UPDATELOG.md | 2 +- src/pages/profiles.tsx | 180 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 164 insertions(+), 18 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index b2682b85..c7262cc7 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -47,7 +47,7 @@ - 修复 Windows 深色模式下首次启动客户端标题栏颜色异常 - 修复静默启动不加载完整 WebView 的问题 - 修复 Linux WebKit 网络进程的崩溃 -- 修复 Linux GNOME/KDE 桌面下,应用主题颜色选择“系统”后,不随操作系统主题(Dark/Light)切换 +- 修复实际导入成功但显示导入失败的问题 ## v2.4.2 diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 37383685..22fece26 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -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}), 继续重试...`, );