From 5fc33102a9db4d2221dc5124dc940edccaec8bf1 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 8 Jul 2026 21:37:10 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(updater):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=85=B3=E4=BA=8E=E9=A1=B5=E6=AD=A3=E5=BC=8F=E7=89=88?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为关于页补充固定项目元数据兜底 - 过滤 Unknown/未知 等无效版本与作者信息 - 增加正式版 AppInfo 缺失时的回归测试 --- .../src/hooks/useAppUpdateManager.test.tsx | 36 +++++++ frontend/src/hooks/useAppUpdateManager.ts | 97 ++++++++++++++++--- 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx index a7eea921..f8cb56b2 100644 --- a/frontend/src/hooks/useAppUpdateManager.test.tsx +++ b/frontend/src/hooks/useAppUpdateManager.test.tsx @@ -245,6 +245,42 @@ describe('useAppUpdateManager', () => { expect(hook?.lastUpdateInfo?.releaseNotesUrl).toBe('https://github.com/Syngnat/GoNavi/releases/tag/dev-latest'); }); + it('keeps official about metadata usable when backend app info is incomplete', async () => { + backendApp.GetAppInfo.mockResolvedValue({ + success: true, + data: { + version: '', + author: 'Unknown', + }, + }); + backendApp.CheckForUpdates.mockResolvedValue({ + success: true, + data: { + hasUpdate: false, + currentVersion: '0.8.5', + latestVersion: '0.8.5', + }, + }); + + renderHook(); + + await act(async () => { + await hook?.checkForUpdates(false); + }); + await act(async () => { + hook?.setIsAboutOpen(true); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(hook?.aboutDisplayVersion).toBe('0.8.5'); + expect(hook?.aboutInfo?.author).toBe('Syngnat'); + expect(hook?.aboutInfo?.repoUrl).toBe('https://github.com/Syngnat/GoNavi'); + expect(hook?.aboutInfo?.issueUrl).toBe('https://github.com/Syngnat/GoNavi/issues'); + expect(hook?.aboutInfo?.releaseUrl).toBe('https://github.com/Syngnat/GoNavi/releases'); + expect(messageApi.error).not.toHaveBeenCalled(); + }); + it('opens the downloaded update directory when a package is already downloaded', async () => { backendApp.CheckForUpdates.mockResolvedValue({ success: true, diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts index 286d9448..f31c0fd9 100644 --- a/frontend/src/hooks/useAppUpdateManager.ts +++ b/frontend/src/hooks/useAppUpdateManager.ts @@ -45,6 +45,25 @@ type UseAppUpdateManagerOptions = { t: Translator; }; +type AboutInfo = { + version: string; + author: string; + buildTime?: string; + repoUrl?: string; + issueUrl?: string; + releaseUrl?: string; + communityUrl?: string; +}; + +const DEFAULT_ABOUT_INFO: AboutInfo = { + version: '', + author: 'Syngnat', + repoUrl: 'https://github.com/Syngnat/GoNavi', + issueUrl: 'https://github.com/Syngnat/GoNavi/issues', + releaseUrl: 'https://github.com/Syngnat/GoNavi/releases', + communityUrl: 'https://aibook.ren', +}; + const createEmptyDownloadProgress = () => ({ open: false, version: '', @@ -64,6 +83,44 @@ const buildUpdateKey = (info: Pick | nu ? `${normalizeUpdateChannel(info.channel)}:${String(info.latestVersion || '').trim()}` : ''; +const isUnknownAboutValue = (value: string): boolean => { + const normalized = value.trim().toLowerCase(); + return normalized === 'unknown' || normalized === '未知' || normalized === 'common.unknown'; +}; + +const normalizeAboutText = (value: unknown): string => + String(value || '').trim(); + +const normalizeAboutVersion = (value: unknown): string => { + const text = normalizeAboutText(value); + if (!text || text === '0.0.0' || isUnknownAboutValue(text)) { + return ''; + } + return text; +}; + +const normalizeAboutInfo = (value: unknown): AboutInfo => { + const source = (value && typeof value === 'object' ? value : {}) as Record; + const version = normalizeAboutVersion(source.version); + const author = normalizeAboutText(source.author); + const buildTime = normalizeAboutText(source.buildTime); + const repoUrl = normalizeAboutText(source.repoUrl); + const issueUrl = normalizeAboutText(source.issueUrl); + const releaseUrl = normalizeAboutText(source.releaseUrl); + const communityUrl = normalizeAboutText(source.communityUrl); + + return { + ...DEFAULT_ABOUT_INFO, + version, + author: author && !isUnknownAboutValue(author) ? author : DEFAULT_ABOUT_INFO.author, + buildTime: buildTime || undefined, + repoUrl: repoUrl || DEFAULT_ABOUT_INFO.repoUrl, + issueUrl: issueUrl || DEFAULT_ABOUT_INFO.issueUrl, + releaseUrl: releaseUrl || DEFAULT_ABOUT_INFO.releaseUrl, + communityUrl: communityUrl || DEFAULT_ABOUT_INFO.communityUrl, + }; +}; + const shouldAutoInstallDownloadedUpdate = (resultData: UpdateDownloadResultData | null | undefined): boolean => { const platform = String(resultData?.platform || '').trim().toLowerCase(); return platform === 'darwin' && resultData?.autoRelaunch !== false; @@ -87,19 +144,14 @@ export const useAppUpdateManager = ({ const [updateChannel, setUpdateChannelState] = useState('latest'); const [isUpdateChannelLoading, setIsUpdateChannelLoading] = useState(false); const [isUpdateChannelSaving, setIsUpdateChannelSaving] = useState(false); - const [aboutInfo, setAboutInfo] = useState<{ - version: string; - author: string; - buildTime?: string; - repoUrl?: string; - issueUrl?: string; - releaseUrl?: string; - communityUrl?: string; - } | null>(null); - const aboutDisplayVersion = resolveAboutDisplayVersion(runtimeBuildType, aboutInfo?.version); + const [aboutInfo, setAboutInfo] = useState(() => DEFAULT_ABOUT_INFO); const [aboutUpdateStatus, setAboutUpdateStatus] = useState(''); const [lastUpdateInfo, setLastUpdateInfo] = useState(null); const [updateDownloadProgress, setUpdateDownloadProgress] = useState(createEmptyDownloadProgress); + const aboutDisplayVersion = resolveAboutDisplayVersion( + runtimeBuildType, + normalizeAboutVersion(aboutInfo.version) || normalizeAboutVersion(lastUpdateInfo?.currentVersion), + ); const lastUpdateKey = buildUpdateKey(lastUpdateInfo); const formatAboutUpdateStatus = useCallback((info: UpdateInfo | null): string => { @@ -404,13 +456,26 @@ export const useAppUpdateManager = ({ const loadAboutInfo = useCallback(async () => { setAboutLoading(true); - const res = await (window as any).go.app.App.GetAppInfo(); - if (res?.success) { - setAboutInfo(res.data); - } else { - void message.error(t('app.about.message.load_failed', { error: res?.message || t('common.unknown') })); + try { + const backendApp = (window as any).go?.app?.App; + if (typeof backendApp?.GetAppInfo !== 'function') { + setAboutInfo(DEFAULT_ABOUT_INFO); + return; + } + const res = await backendApp.GetAppInfo(); + if (res?.success) { + setAboutInfo(normalizeAboutInfo(res.data)); + } else { + setAboutInfo(DEFAULT_ABOUT_INFO); + void message.error(t('app.about.message.load_failed', { error: res?.message || t('common.unknown') })); + } + } catch (e: any) { + setAboutInfo(DEFAULT_ABOUT_INFO); + const error = e?.message || t('common.unknown'); + void message.error(t('app.about.message.load_failed', { error })); + } finally { + setAboutLoading(false); } - setAboutLoading(false); }, [t]); const loadUpdateChannel = useCallback(async () => {