diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7c9cb64e..4bcf5b75 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -2456,6 +2456,8 @@ function App() {
close: () => void;
isOpen: () => boolean;
} | null>(null);
+ // 手动「检查更新」发现新版本时,由 useAppUpdateManager 触发打开更新日志弹窗
+ const openReleaseNotesOnManualCheckRef = useRef<(() => void) | null>(null);
const {
aboutDisplayVersion,
aboutInfo,
@@ -2489,6 +2491,7 @@ function App() {
runtimeBuildType,
t,
updateCenterBridgeRef,
+ onManualCheckHasUpdateRef: openReleaseNotesOnManualCheckRef,
});
const [aboutLastCheckedAt, setAboutLastCheckedAt] = useState('');
const [releaseNotesModalOpen, setReleaseNotesModalOpen] = useState(false);
@@ -3637,6 +3640,14 @@ function App() {
updateCenterBridgeRef.current = null;
};
}, [handleOpenSettingsCenterPane]);
+ useEffect(() => {
+ openReleaseNotesOnManualCheckRef.current = () => {
+ setReleaseNotesModalOpen(true);
+ };
+ return () => {
+ openReleaseNotesOnManualCheckRef.current = null;
+ };
+ }, []);
useEffect(() => {
if (!isSettingsAboutPaneOpen) {
return;
@@ -5409,7 +5420,7 @@ function App() {
key="check"
icon={}
loading={isCheckingForUpdates}
- onClick={() => checkForUpdates(false)}
+ onClick={() => checkForUpdates(false, true)}
>
{t('app.about.action.check_updates')}
,
diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx
index 200d463b..8223f901 100644
--- a/frontend/src/hooks/useAppUpdateManager.test.tsx
+++ b/frontend/src/hooks/useAppUpdateManager.test.tsx
@@ -669,6 +669,46 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.channel).toBe('dev');
});
+ it('does not invoke the manual-check bridge when a channel change re-check finds an update', async () => {
+ const openReleaseNotes = vi.fn();
+ const openReleaseNotesRef = { current: openReleaseNotes };
+
+ backendApp.SetUpdateChannel.mockResolvedValue({ success: true, data: { channel: 'dev' } });
+ backendApp.CheckForUpdates.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: true,
+ channel: 'dev',
+ currentVersion: '0.8.1',
+ latestVersion: 'dev-a1b2c3d',
+ },
+ });
+
+ const Harness = () => {
+ hook = useAppUpdateManager({
+ runtimeBuildType: 'release',
+ t,
+ onManualCheckHasUpdateRef: openReleaseNotesRef,
+ });
+ return null;
+ };
+
+ act(() => {
+ renderer = create();
+ });
+
+ await act(async () => {
+ await hook?.changeUpdateChannel('dev');
+ });
+
+ expect(backendApp.SetUpdateChannel).toHaveBeenCalledWith('dev');
+ expect(backendApp.CheckForUpdates).toHaveBeenCalledTimes(1);
+ // 通道切换后的自动复查即便发现更新,也不应打开更新日志弹窗(#818 触发边界修正)
+ expect(openReleaseNotes).not.toHaveBeenCalled();
+ expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
+ expect(hook?.lastUpdateInfo?.latestVersion).toBe('dev-a1b2c3d');
+ });
+
it('keeps release metadata from the backend update response', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
@@ -770,6 +810,114 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.latestVersion).toBe('0.8.2');
});
+ it('invokes the manual-check bridge when a manual check finds an update', async () => {
+ const openReleaseNotes = vi.fn();
+ const openReleaseNotesRef = { current: openReleaseNotes };
+
+ backendApp.CheckForUpdates.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: true,
+ channel: 'latest',
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.2',
+ releaseNotesUrl: 'https://github.com/Syngnat/GoNavi/releases/tag/v0.8.2',
+ },
+ });
+
+ const Harness = () => {
+ hook = useAppUpdateManager({
+ runtimeBuildType: 'release',
+ t,
+ onManualCheckHasUpdateRef: openReleaseNotesRef,
+ });
+ return null;
+ };
+
+ act(() => {
+ renderer = create();
+ });
+
+ await act(async () => {
+ await hook?.checkForUpdates(false, true);
+ });
+
+ expect(openReleaseNotes).toHaveBeenCalledTimes(1);
+ expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
+ expect(hook?.lastUpdateInfo?.latestVersion).toBe('0.8.2');
+ });
+
+ it('does not invoke the manual-check bridge when a manual check finds no update', async () => {
+ const openReleaseNotes = vi.fn();
+ const openReleaseNotesRef = { current: openReleaseNotes };
+
+ backendApp.CheckForUpdates.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: false,
+ channel: 'latest',
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.1',
+ },
+ });
+
+ const Harness = () => {
+ hook = useAppUpdateManager({
+ runtimeBuildType: 'release',
+ t,
+ onManualCheckHasUpdateRef: openReleaseNotesRef,
+ });
+ return null;
+ };
+
+ act(() => {
+ renderer = create();
+ });
+
+ await act(async () => {
+ await hook?.checkForUpdates(false, true);
+ });
+
+ expect(openReleaseNotes).not.toHaveBeenCalled();
+ expect(hook?.lastUpdateInfo?.hasUpdate).toBe(false);
+ expect(messageApi.success).toHaveBeenCalled();
+ });
+
+ it('does not invoke the manual-check bridge on silent update discovery', async () => {
+ const openReleaseNotes = vi.fn();
+ const openReleaseNotesRef = { current: openReleaseNotes };
+
+ backendApp.CheckForUpdatesSilently.mockResolvedValue({
+ success: true,
+ data: {
+ hasUpdate: true,
+ channel: 'latest',
+ currentVersion: '0.8.1',
+ latestVersion: '0.8.2',
+ },
+ });
+
+ const Harness = () => {
+ hook = useAppUpdateManager({
+ runtimeBuildType: 'release',
+ t,
+ onManualCheckHasUpdateRef: openReleaseNotesRef,
+ });
+ return null;
+ };
+
+ act(() => {
+ renderer = create();
+ });
+
+ await act(async () => {
+ await hook?.checkForUpdates(true);
+ });
+
+ expect(openReleaseNotes).not.toHaveBeenCalled();
+ expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
+ });
+
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 dff9be4e..df01c867 100644
--- a/frontend/src/hooks/useAppUpdateManager.ts
+++ b/frontend/src/hooks/useAppUpdateManager.ts
@@ -63,6 +63,8 @@ type UseAppUpdateManagerOptions = {
runtimeBuildType: string;
t: Translator;
updateCenterBridgeRef?: MutableRefObject;
+ /** 手动「检查更新」发现新版本时,触发打开更新日志弹窗的桥接回调 */
+ onManualCheckHasUpdateRef?: MutableRefObject<(() => void) | null>;
};
type AboutInfo = {
@@ -183,6 +185,7 @@ export const useAppUpdateManager = ({
runtimeBuildType,
t,
updateCenterBridgeRef,
+ onManualCheckHasUpdateRef,
}: UseAppUpdateManagerOptions) => {
const autoCheckForUpdates = useStore((state) => state.autoCheckForUpdates);
const autoCheckForUpdatesIntervalMinutes = useStore(
@@ -470,7 +473,7 @@ export const useAppUpdateManager = ({
void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace'));
}, [t]);
- const checkForUpdates = useCallback(async (silent: boolean) => {
+ const checkForUpdates = useCallback(async (silent: boolean, openReleaseNotes = false) => {
if (updateCheckInFlightRef.current) return;
updateCheckInFlightRef.current = true;
setIsCheckingForUpdates(true);
@@ -565,6 +568,11 @@ export const useAppUpdateManager = ({
if (!silent) {
void message.info(t('app.about.message.new_version_found', { version: info.latestVersion }));
setAboutUpdateStatus(statusText);
+ // 仅当显式请求打开更新日志时(如用户点击「检查更新」按钮),才触发弹窗;
+ // 通道切换后的自动复查等场景不传 openReleaseNotes,避免越界打开弹窗(#818)
+ if (openReleaseNotes) {
+ onManualCheckHasUpdateRef?.current?.();
+ }
}
if (silent && aboutOpen) {
setAboutUpdateStatus(statusText);
@@ -598,7 +606,7 @@ export const useAppUpdateManager = ({
} else {
setLastUpdateInfo(info);
}
- }, [formatAboutUpdateStatus, isUpdateCenterOpen, openUpdateCenter, t]);
+ }, [formatAboutUpdateStatus, isUpdateCenterOpen, onManualCheckHasUpdateRef, openUpdateCenter, t]);
const loadAboutInfo = useCallback(async () => {
setAboutLoading(true);