From 0cf27230eae1a568675693fea08c74e87dcbbc61 Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 22 Jan 2026 23:20:07 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20server=20version=20ch?= =?UTF-8?q?eck=20for=20desktop=20app=20(#11710)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * โœจ feat: add server version check for desktop app - Add /api/version endpoint consumption in globalService - Add serverVersion and isServerVersionOutdated states to global store - Add useCheckServerVersion hook to detect outdated server - Show ServerVersionOutdatedAlert when server version is incompatible - Display server version tag in settings when different from client - Support version diff threshold (5 versions) for compatibility check * ๐Ÿ”ง chore: only show server version alert for self-hosted instances Check storageMode from electron store - only show alert when using 'selfHost' mode, not 'cloud' mode. * ๐Ÿ”ง chore: remove deprecated 'local' storage mode option * ๐Ÿ› fix: only treat 404 as outdated server, throw on other errors Previously any non-OK response was treated as "server doesn't support the API", causing transient failures (500s, network issues) to incorrectly show the outdated alert. Now only 404 returns null to indicate a missing API, while other errors throw to allow SWR retry. * โœจ feat: add server version check and update alerts - Implemented server version check functionality to notify users when their client version requires a newer server version. - Added localized messages for server version outdated alerts in English and Chinese. - Enhanced the global state management to track server version and its status. - Updated UI components to display server version information and warnings appropriately. - Introduced a new alert component to inform users about the need to upgrade their server for optimal performance. Signed-off-by: Innei --------- Signed-off-by: Innei --- .../main/controllers/RemoteServerConfigCtr.ts | 3 +- .../__tests__/RemoteServerConfigCtr.test.ts | 16 +-- locales/en-US/common.json | 6 + locales/zh-CN/common.json | 6 + .../electron-client-ipc/src/types/dataSync.ts | 2 +- .../settings/about/features/Version.tsx | 15 +- src/hooks/useUserAvatar.test.ts | 4 +- .../ServerVersionOutdatedAlert.tsx | 131 ++++++++++++++++++ .../GlobalProvider/StoreInitialization.tsx | 9 +- src/layout/GlobalProvider/index.tsx | 4 + src/locales/default/common.ts | 8 ++ src/services/global.ts | 25 ++++ src/store/global/actions/general.ts | 46 ++++++ src/store/global/initialState.ts | 9 ++ 14 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx diff --git a/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts b/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts index 5d772e2d86..3a886c3d14 100644 --- a/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +++ b/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts @@ -50,7 +50,8 @@ export default class RemoteServerConfigCtr extends ControllerModule { * Local mode has been removed; fall back to cloud. */ private normalizeConfig = (config: DataSyncConfig): DataSyncConfig => { - if (config.storageMode !== 'local') return config; + // Use type assertion to handle legacy 'local' value from stored data + if ((config.storageMode as string) !== 'local') return config; const nextConfig: DataSyncConfig = { ...config, diff --git a/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts b/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts index 6f98aa17ce..d96a5bc1ba 100644 --- a/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +++ b/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts @@ -60,7 +60,7 @@ describe('RemoteServerConfigCtr', () => { ipcMainHandleMock.mockClear(); mockStoreManager.get.mockReturnValue({ active: false, - storageMode: 'local', + storageMode: 'cloud', }); controller = new RemoteServerConfigCtr(mockApp); }); @@ -85,7 +85,7 @@ describe('RemoteServerConfigCtr', () => { it('should update configuration', async () => { const prevConfig: DataSyncConfig = { active: false, - storageMode: 'local', + storageMode: 'cloud', }; mockStoreManager.get.mockReturnValue(prevConfig); @@ -195,7 +195,7 @@ describe('RemoteServerConfigCtr', () => { refreshToken: Buffer.from('stored-refresh-token').toString('base64'), }; } - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; }); // Create new controller to test loading from store @@ -210,7 +210,7 @@ describe('RemoteServerConfigCtr', () => { if (key === 'encryptedTokens') { return null; } - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; }); const newController = new RemoteServerConfigCtr(mockApp); @@ -243,7 +243,7 @@ describe('RemoteServerConfigCtr', () => { refreshToken: 'invalid-encrypted-token', }; } - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; }); const newController = new RemoteServerConfigCtr(mockApp); @@ -273,7 +273,7 @@ describe('RemoteServerConfigCtr', () => { if (key === 'encryptedTokens') { return null; } - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; }); const newController = new RemoteServerConfigCtr(mockApp); @@ -417,7 +417,7 @@ describe('RemoteServerConfigCtr', () => { it('should return error when remote server is not active', async () => { mockStoreManager.get.mockImplementation((key) => { if (key === 'dataSyncConfig') { - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; } return null; }); @@ -648,7 +648,7 @@ describe('RemoteServerConfigCtr', () => { refreshToken: 'stored-refresh', }; } - return { active: false, storageMode: 'local' }; + return { active: false, storageMode: 'cloud' }; }); const newController = new RemoteServerConfigCtr(mockApp); diff --git a/locales/en-US/common.json b/locales/en-US/common.json index b8ef1429c8..3d99c81bb4 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -332,6 +332,11 @@ "run": "Run", "save": "Save", "send": "Send", + "serverVersionOutdated.desc": "Your client version (v{{version}}) requires a newer server version.", + "serverVersionOutdated.dismiss": "Continue Anyway", + "serverVersionOutdated.title": "Server Version Outdated", + "serverVersionOutdated.upgrade": "Upgrade Guide", + "serverVersionOutdated.warning": "Some features may not work properly or behave unexpectedly. Please update your server for the best experience.", "setting": "Settings", "share": "Share", "stop": "Stop", @@ -380,6 +385,7 @@ "upgradeVersion.action": "Upgrade", "upgradeVersion.hasNew": "Update available", "upgradeVersion.newVersion": "Update available: {{version}}", + "upgradeVersion.serverVersion": "Server: {{version}}", "userPanel.anonymousNickName": "Anonymous User", "userPanel.billing": "Billing Management", "userPanel.cloud": "Launch {{name}}", diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json index 7e85c53d97..01c036e25f 100644 --- a/locales/zh-CN/common.json +++ b/locales/zh-CN/common.json @@ -332,6 +332,11 @@ "run": "่ฟ่กŒ", "save": "ไฟๅญ˜", "send": "ๅ‘้€", + "serverVersionOutdated.desc": "ๅฝ“ๅ‰ๅฎขๆˆท็ซฏ็‰ˆๆœฌ๏ผˆv{{version}}๏ผ‰้œ€่ฆๆ›ดๆ–ฐ็š„ๆœๅŠก็ซฏ็‰ˆๆœฌใ€‚", + "serverVersionOutdated.dismiss": "็ปง็ปญไฝฟ็”จ", + "serverVersionOutdated.title": "ๆœๅŠก็ซฏ็‰ˆๆœฌ่ฟ‡ๆ—ง", + "serverVersionOutdated.upgrade": "ๅ‡็บงๆŒ‡ๅ—", + "serverVersionOutdated.warning": "้ƒจๅˆ†ๅŠŸ่ƒฝๅฏ่ƒฝๆ— ๆณ•ๆญฃๅธธไฝฟ็”จๆˆ–ๅ‡บ็Žฐ้ž้ข„ๆœŸ่กŒไธบใ€‚ๅปบ่ฎฎๆ›ดๆ–ฐๆœๅŠก็ซฏไปฅ่Žทๅพ—ๆœ€ไฝณไฝ“้ชŒใ€‚", "setting": "่ฎพ็ฝฎ", "share": "ๅˆ†ไบซ", "stop": "ๅœๆญข", @@ -380,6 +385,7 @@ "upgradeVersion.action": "ๅ‡็บง", "upgradeVersion.hasNew": "ๆœ‰ๅฏ็”จๆ›ดๆ–ฐ", "upgradeVersion.newVersion": "ๅฏ็”จๆ›ดๆ–ฐ็‰ˆๆœฌ๏ผš{{version}}", + "upgradeVersion.serverVersion": "ๆœๅŠก็ซฏ๏ผš{{version}}", "userPanel.anonymousNickName": "ๅŒฟๅ็”จๆˆท", "userPanel.billing": "่ดฆๅ•็ฎก็†", "userPanel.cloud": "ไฝ“้ชŒ {{name}}", diff --git a/packages/electron-client-ipc/src/types/dataSync.ts b/packages/electron-client-ipc/src/types/dataSync.ts index 51462491f1..55880a0a2a 100644 --- a/packages/electron-client-ipc/src/types/dataSync.ts +++ b/packages/electron-client-ipc/src/types/dataSync.ts @@ -1,4 +1,4 @@ -export type StorageMode = 'local' | 'cloud' | 'selfHost'; +export type StorageMode = 'cloud' | 'selfHost'; export enum StorageModeEnum { Cloud = 'cloud', SelfHost = 'selfHost', diff --git a/src/app/[variants]/(main)/settings/about/features/Version.tsx b/src/app/[variants]/(main)/settings/about/features/Version.tsx index 8ecc2bb722..bdfb70b973 100644 --- a/src/app/[variants]/(main)/settings/about/features/Version.tsx +++ b/src/app/[variants]/(main)/settings/about/features/Version.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { ProductLogo } from '@/components/Branding'; import { CHANGELOG_URL, MANUAL_UPGRADE_URL, OFFICIAL_SITE } from '@/const/url'; -import { CURRENT_VERSION } from '@/const/version'; +import { CURRENT_VERSION, isDesktop } from '@/const/version'; import { useNewVersion } from '@/features/User/UserPanel/useNewVersion'; import { useGlobalStore } from '@/store/global'; @@ -18,9 +18,17 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ const Version = memo<{ mobile?: boolean }>(({ mobile }) => { const hasNewVersion = useNewVersion(); - const [latestVersion] = useGlobalStore((s) => [s.latestVersion]); + const [latestVersion, serverVersion, useCheckServerVersion] = useGlobalStore((s) => [ + s.latestVersion, + s.serverVersion, + s.useCheckServerVersion, + ]); const { t } = useTranslation('common'); + useCheckServerVersion(isDesktop); + + const showServerVersion = serverVersion && serverVersion !== CURRENT_VERSION; + return ( (({ mobile }) => {
{BRANDING_NAME}
v{CURRENT_VERSION} + {showServerVersion && ( + {t('upgradeVersion.serverVersion', { version: `v${serverVersion}` })} + )} {hasNewVersion && ( {t('upgradeVersion.newVersion', { version: `v${latestVersion}` })} diff --git a/src/hooks/useUserAvatar.test.ts b/src/hooks/useUserAvatar.test.ts index dcb7f504fe..997d74ad87 100644 --- a/src/hooks/useUserAvatar.test.ts +++ b/src/hooks/useUserAvatar.test.ts @@ -62,14 +62,14 @@ describe('useUserAvatar', () => { expect(result.current).toBe(mockAvatar); }); - it('should return original avatar when no remote server URL in desktop environment', () => { + it('should return original avatar when no remote server URL in desktop environment (selfHost mode)', () => { mockIsDesktop = true; const mockAvatar = '/api/avatar.png'; act(() => { useUserStore.setState({ user: { avatar: mockAvatar } as any }); useElectronStore.setState({ - dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'local' }, + dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'selfHost' }, }); }); diff --git a/src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx b/src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx new file mode 100644 index 0000000000..5b69a7d1a8 --- /dev/null +++ b/src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { Button, Flexbox, Icon } from '@lobehub/ui'; +import { createStyles } from 'antd-style'; +import { TriangleAlert, X } from 'lucide-react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { MANUAL_UPGRADE_URL } from '@/const/url'; +import { CURRENT_VERSION } from '@/const/version'; +import { useElectronStore } from '@/store/electron'; +import { electronSyncSelectors } from '@/store/electron/selectors'; +import { useGlobalStore } from '@/store/global'; + +const useStyles = createStyles(({ css, token }) => ({ + closeButton: css` + cursor: pointer; + + position: absolute; + inset-block-start: 20px; + inset-inline-end: 20px; + + display: flex; + align-items: center; + justify-content: center; + + width: 28px; + height: 28px; + border-radius: ${token.borderRadius}px; + + color: ${token.colorTextSecondary}; + + transition: all 0.2s; + + &:hover { + color: ${token.colorText}; + background: ${token.colorFillSecondary}; + } + `, + container: css` + position: fixed; + z-index: 9999; + inset: 0; + + display: flex; + align-items: center; + justify-content: center; + + background: ${token.colorBgMask}; + `, + content: css` + position: relative; + + overflow: hidden; + + max-width: 480px; + padding: 24px; + border: 1px solid ${token.yellowBorder}; + border-radius: ${token.borderRadiusLG}px; + + background: ${token.colorBgContainer}; + box-shadow: ${token.boxShadowSecondary}; + `, + desc: css` + line-height: 1.6; + color: ${token.colorTextSecondary}; + `, + title: css` + font-size: 16px; + font-weight: bold; + color: ${token.colorWarningText}; + `, + titleIcon: css` + flex-shrink: 0; + color: ${token.colorWarning}; + `, + warning: css` + padding: 12px; + border-radius: ${token.borderRadius}px; + color: ${token.colorWarningText}; + background: ${token.yellowBg}; + `, +})); + +const ServerVersionOutdatedAlert = () => { + const { styles } = useStyles(); + const { t } = useTranslation('common'); + const [dismissed, setDismissed] = useState(false); + const isServerVersionOutdated = useGlobalStore((s) => s.isServerVersionOutdated); + const storageMode = useElectronStore(electronSyncSelectors.storageMode); + + // Only show alert when using self-hosted server, not cloud + if (storageMode !== 'selfHost') return null; + if (!isServerVersionOutdated || dismissed) return null; + + return ( +
+
+
setDismissed(true)}> + +
+ + + + +
{t('serverVersionOutdated.title')}
+
+ +
+ {t('serverVersionOutdated.desc', { version: CURRENT_VERSION })} +
+ +
{t('serverVersionOutdated.warning')}
+ + + + + + + +
+
+
+ ); +}; + +export default ServerVersionOutdatedAlert; diff --git a/src/layout/GlobalProvider/StoreInitialization.tsx b/src/layout/GlobalProvider/StoreInitialization.tsx index 0fc17004d6..9c2bcec03e 100644 --- a/src/layout/GlobalProvider/StoreInitialization.tsx +++ b/src/layout/GlobalProvider/StoreInitialization.tsx @@ -5,6 +5,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { createStoreUpdater } from 'zustand-utils'; +import { isDesktop } from '@/const/version'; import { enableNextAuth } from '@/envs/auth'; import { useIsMobile } from '@/hooks/useIsMobile'; import { useAgentStore } from '@/store/agent'; @@ -32,7 +33,10 @@ const StoreInitialization = memo(() => { const { serverConfig } = useServerConfigStore(); - const useInitSystemStatus = useGlobalStore((s) => s.useInitSystemStatus); + const [useInitSystemStatus, useCheckServerVersion] = useGlobalStore((s) => [ + s.useInitSystemStatus, + s.useCheckServerVersion, + ]); const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent); const useInitAiProviderKeyVaults = useAiInfraStore((s) => s.useFetchAiProviderRuntimeState); @@ -41,6 +45,9 @@ const StoreInitialization = memo(() => { // init the system preference useInitSystemStatus(); + // check server version in desktop app + useCheckServerVersion(isDesktop); + // fetch server config const useFetchServerConfig = useServerConfigStore((s) => s.useInitServerConfig); useFetchServerConfig(); diff --git a/src/layout/GlobalProvider/index.tsx b/src/layout/GlobalProvider/index.tsx index a8a5738b94..3c4d5cc7ae 100644 --- a/src/layout/GlobalProvider/index.tsx +++ b/src/layout/GlobalProvider/index.tsx @@ -7,6 +7,7 @@ import { ReferralProvider } from '@/business/client/ReferralProvider'; import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper'; import { DragUploadProvider } from '@/components/DragUploadZone/DragUploadProvider'; import { getServerFeatureFlagsValue } from '@/config/featureFlags'; +import { isDesktop } from '@/const/version'; import { appEnv } from '@/envs/app'; import DevPanel from '@/features/DevPanel'; import { getServerGlobalConfig } from '@/server/globalConfig'; @@ -20,6 +21,7 @@ import ImportSettings from './ImportSettings'; import Locale from './Locale'; import NextThemeProvider from './NextThemeProvider'; import QueryProvider from './Query'; +import ServerVersionOutdatedAlert from './ServerVersionOutdatedAlert'; import StoreInitialization from './StoreInitialization'; import StyleRegistry from './StyleRegistry'; @@ -66,6 +68,8 @@ const GlobalLayout = async ({ > + + {isDesktop && } diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 916cc4e28f..4ac7d5bc71 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -350,6 +350,13 @@ export default { 'run': 'Run', 'save': 'Save', 'send': 'Send', + 'serverVersionOutdated.desc': + 'Your client version (v{{version}}) requires a newer server version.', + 'serverVersionOutdated.dismiss': 'Continue Anyway', + 'serverVersionOutdated.title': 'Server Version Outdated', + 'serverVersionOutdated.upgrade': 'Upgrade Guide', + 'serverVersionOutdated.warning': + 'Some features may not work properly or behave unexpectedly. Please update your server for the best experience.', 'setting': 'Settings', 'share': 'Share', 'stop': 'Stop', @@ -401,6 +408,7 @@ export default { 'upgradeVersion.action': 'Upgrade', 'upgradeVersion.hasNew': 'Update available', 'upgradeVersion.newVersion': 'Update available: {{version}}', + 'upgradeVersion.serverVersion': 'Server: {{version}}', 'userPanel.anonymousNickName': 'Anonymous User', 'userPanel.billing': 'Billing Management', 'userPanel.cloud': 'Launch {{name}}', diff --git a/src/services/global.ts b/src/services/global.ts index bff12dae56..7d4a0237c4 100644 --- a/src/services/global.ts +++ b/src/services/global.ts @@ -1,11 +1,13 @@ import type { PartialDeep } from 'type-fest'; +import type { VersionResponseData } from '@/app/(backend)/api/version/route'; import { BusinessGlobalService } from '@/business/client/services/BusinessGlobalService'; import { lambdaClient } from '@/libs/trpc/client'; import { type LobeAgentConfig } from '@/types/agent'; import { type GlobalRuntimeConfig } from '@/types/serverConfig'; const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat/latest'; +const SERVER_VERSION_URL = '/api/version'; class GlobalService extends BusinessGlobalService { /** @@ -18,6 +20,29 @@ class GlobalService extends BusinessGlobalService { return data['version']; }; + /** + * get server version from /api/version + * @returns version string if available, null only if server returns 404 (API doesn't exist on old server) + * @throws Error for other failures (network errors, 500s, etc.) to allow SWR retry + */ + getServerVersion = async (): Promise => { + const res = await fetch(SERVER_VERSION_URL); + + // Only treat 404 as "server doesn't support version API" + // Other errors (500, network issues) should throw to allow retry + if (res.status === 404) { + return null; + } + + if (!res.ok) { + throw new Error(`Failed to fetch server version: ${res.status}`); + } + + const data: VersionResponseData = await res.json(); + + return data.version; + }; + getGlobalConfig = async (): Promise => { return lambdaClient.config.getGlobalConfig.query(); }; diff --git a/src/store/global/actions/general.ts b/src/store/global/actions/general.ts index b8334474e5..73f0e0acfe 100644 --- a/src/store/global/actions/general.ts +++ b/src/store/global/actions/general.ts @@ -23,6 +23,7 @@ export interface GlobalGeneralAction { updateResourceManagerColumnWidth: (column: 'name' | 'date' | 'size', width: number) => void; updateSystemStatus: (status: Partial, action?: any) => void; useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse; + useCheckServerVersion: (enabledCheck?: boolean) => SWRResponse; useInitSystemStatus: () => SWRResponse; } @@ -160,6 +161,51 @@ export const generalActionSlice: StateCreator< }, ), + useCheckServerVersion: (enabledCheck = true) => + useOnlyFetchOnceSWR( + enabledCheck ? 'checkServerVersion' : null, + async () => globalService.getServerVersion(), + { + onSuccess: (data: string | null) => { + if (data === null) { + set({ isServerVersionOutdated: true }, false); + return; + } + + set({ serverVersion: data }, false); + + if (!valid(CURRENT_VERSION) || !valid(data)) return; + + const clientVersion = parse(CURRENT_VERSION); + const serverVersion = parse(data); + + if (!clientVersion || !serverVersion) return; + + const DIFF_THRESHOLD = 5; + // ็‰ˆๆœฌๅทฎๅผ‚่ฎก็ฎ—่ง„ๅˆ™ + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + // โ”‚ ๅฎขๆˆท็ซฏ โ†’ ๆœๅŠก็ซฏ โ”‚ ๅทฎๅผ‚ๅ€ผ โ”‚ ็ป“ๆžœ โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 1.0.5 โ†’ 1.0.0 โ”‚ 5 โ”‚ โš ๏ธ ่ฟ‡ๆ—ง โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 1.1.0 โ†’ 1.0.5 โ”‚ 5 โ”‚ โš ๏ธ ่ฟ‡ๆ—ง โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 2.0.0 โ†’ 1.9.9 โ”‚ 91 โ”‚ โš ๏ธ ่ฟ‡ๆ—ง โ”‚ + // โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + // โ”‚ 1.0.4 โ†’ 1.0.0 โ”‚ 4 โ”‚ โœ… ๆญฃๅธธ โ”‚ + // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + const versionDiff = + (clientVersion.major - serverVersion.major) * 100 + + (clientVersion.minor - serverVersion.minor) * 10 + + (clientVersion.patch - serverVersion.patch); + + if (versionDiff >= DIFF_THRESHOLD) { + set({ isServerVersionOutdated: true }, false); + } + }, + }, + ), + useInitSystemStatus: () => useOnlyFetchOnceSWR( 'initSystemStatus', diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 8a9275d091..35570656f4 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -180,9 +180,18 @@ export interface GlobalState { */ initClientDBStage: DatabaseLoadingState; isMobile?: boolean; + /** + * ๆœๅŠก็ซฏ็‰ˆๆœฌ่ฟ‡ๆ—ง๏ผŒไธๆ”ฏๆŒ /api/version ๆŽฅๅฃ + * ้œ€่ฆๆ็คบ็”จๆˆทๆ›ดๆ–ฐๆœๅŠก็ซฏ + */ + isServerVersionOutdated?: boolean; isStatusInit?: boolean; latestVersion?: string; navigate?: NavigateFunction; + /** + * ๆœๅŠก็ซฏ็‰ˆๆœฌๅท๏ผŒ็”จไบŽๆฃ€ๆต‹ๅฎขๆˆท็ซฏไธŽๆœๅŠก็ซฏ็‰ˆๆœฌๆ˜ฏๅฆไธ€่‡ด + */ + serverVersion?: string; sidebarKey: SidebarTabKey; status: SystemStatus; statusStorage: AsyncLocalStorage;