mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: add server version check for desktop app (#11710)
* ✨ 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 <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type StorageMode = 'local' | 'cloud' | 'selfHost';
|
||||
export type StorageMode = 'cloud' | 'selfHost';
|
||||
export enum StorageModeEnum {
|
||||
Cloud = 'cloud',
|
||||
SelfHost = 'selfHost',
|
||||
|
||||
@@ -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 (
|
||||
<Flexbox
|
||||
align={mobile ? 'stretch' : 'center'}
|
||||
@@ -46,6 +54,9 @@ const Version = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
<div style={{ fontSize: 18, fontWeight: 'bolder' }}>{BRANDING_NAME}</div>
|
||||
<Flexbox gap={6} horizontal={!mobile}>
|
||||
<Tag>v{CURRENT_VERSION}</Tag>
|
||||
{showServerVersion && (
|
||||
<Tag>{t('upgradeVersion.serverVersion', { version: `v${serverVersion}` })}</Tag>
|
||||
)}
|
||||
{hasNewVersion && (
|
||||
<Tag color={'info'}>
|
||||
{t('upgradeVersion.newVersion', { version: `v${latestVersion}` })}
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
131
src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx
Normal file
131
src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx
Normal file
@@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.closeButton} onClick={() => setDismissed(true)}>
|
||||
<Icon icon={X} />
|
||||
</div>
|
||||
|
||||
<Flexbox gap={16}>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
<Icon className={styles.titleIcon} icon={TriangleAlert} />
|
||||
<div className={styles.title}>{t('serverVersionOutdated.title')}</div>
|
||||
</Flexbox>
|
||||
|
||||
<div className={styles.desc}>
|
||||
{t('serverVersionOutdated.desc', { version: CURRENT_VERSION })}
|
||||
</div>
|
||||
|
||||
<div className={styles.warning}>{t('serverVersionOutdated.warning')}</div>
|
||||
|
||||
<Flexbox gap={8} horizontal justify="flex-end" style={{ marginTop: 8 }}>
|
||||
<a href={MANUAL_UPGRADE_URL} rel="noreferrer" target="_blank">
|
||||
<Button size="small" type="primary">
|
||||
{t('serverVersionOutdated.upgrade')}
|
||||
</Button>
|
||||
</a>
|
||||
<Button onClick={() => setDismissed(true)} size="small">
|
||||
{t('serverVersionOutdated.dismiss')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerVersionOutdatedAlert;
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ({
|
||||
>
|
||||
<QueryProvider>
|
||||
<StoreInitialization />
|
||||
|
||||
{isDesktop && <ServerVersionOutdatedAlert />}
|
||||
<FaviconProvider>
|
||||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
|
||||
@@ -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}}',
|
||||
|
||||
@@ -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<string | null> => {
|
||||
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<GlobalRuntimeConfig> => {
|
||||
return lambdaClient.config.getGlobalConfig.query();
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface GlobalGeneralAction {
|
||||
updateResourceManagerColumnWidth: (column: 'name' | 'date' | 'size', width: number) => void;
|
||||
updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
|
||||
useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
|
||||
useCheckServerVersion: (enabledCheck?: boolean) => SWRResponse<string | null>;
|
||||
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<SystemStatus>(
|
||||
'initSystemStatus',
|
||||
|
||||
@@ -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<SystemStatus>;
|
||||
|
||||
Reference in New Issue
Block a user