@lobehub/lobehub 2.0.0-next.340 → 2.0.0-next.341

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.341](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.340...v2.0.0-next.341)
6
+
7
+ <sup>Released on **2026-01-22**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add server version check for desktop app.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add server version check for desktop app, closes [#11710](https://github.com/lobehub/lobe-chat/issues/11710) ([0cf2723](https://github.com/lobehub/lobe-chat/commit/0cf2723))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.340](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.339...v2.0.0-next.340)
6
31
 
7
32
  <sup>Released on **2026-01-22**</sup>
@@ -50,7 +50,8 @@ export default class RemoteServerConfigCtr extends ControllerModule {
50
50
  * Local mode has been removed; fall back to cloud.
51
51
  */
52
52
  private normalizeConfig = (config: DataSyncConfig): DataSyncConfig => {
53
- if (config.storageMode !== 'local') return config;
53
+ // Use type assertion to handle legacy 'local' value from stored data
54
+ if ((config.storageMode as string) !== 'local') return config;
54
55
 
55
56
  const nextConfig: DataSyncConfig = {
56
57
  ...config,
@@ -60,7 +60,7 @@ describe('RemoteServerConfigCtr', () => {
60
60
  ipcMainHandleMock.mockClear();
61
61
  mockStoreManager.get.mockReturnValue({
62
62
  active: false,
63
- storageMode: 'local',
63
+ storageMode: 'cloud',
64
64
  });
65
65
  controller = new RemoteServerConfigCtr(mockApp);
66
66
  });
@@ -85,7 +85,7 @@ describe('RemoteServerConfigCtr', () => {
85
85
  it('should update configuration', async () => {
86
86
  const prevConfig: DataSyncConfig = {
87
87
  active: false,
88
- storageMode: 'local',
88
+ storageMode: 'cloud',
89
89
  };
90
90
  mockStoreManager.get.mockReturnValue(prevConfig);
91
91
 
@@ -195,7 +195,7 @@ describe('RemoteServerConfigCtr', () => {
195
195
  refreshToken: Buffer.from('stored-refresh-token').toString('base64'),
196
196
  };
197
197
  }
198
- return { active: false, storageMode: 'local' };
198
+ return { active: false, storageMode: 'cloud' };
199
199
  });
200
200
 
201
201
  // Create new controller to test loading from store
@@ -210,7 +210,7 @@ describe('RemoteServerConfigCtr', () => {
210
210
  if (key === 'encryptedTokens') {
211
211
  return null;
212
212
  }
213
- return { active: false, storageMode: 'local' };
213
+ return { active: false, storageMode: 'cloud' };
214
214
  });
215
215
 
216
216
  const newController = new RemoteServerConfigCtr(mockApp);
@@ -243,7 +243,7 @@ describe('RemoteServerConfigCtr', () => {
243
243
  refreshToken: 'invalid-encrypted-token',
244
244
  };
245
245
  }
246
- return { active: false, storageMode: 'local' };
246
+ return { active: false, storageMode: 'cloud' };
247
247
  });
248
248
 
249
249
  const newController = new RemoteServerConfigCtr(mockApp);
@@ -273,7 +273,7 @@ describe('RemoteServerConfigCtr', () => {
273
273
  if (key === 'encryptedTokens') {
274
274
  return null;
275
275
  }
276
- return { active: false, storageMode: 'local' };
276
+ return { active: false, storageMode: 'cloud' };
277
277
  });
278
278
 
279
279
  const newController = new RemoteServerConfigCtr(mockApp);
@@ -417,7 +417,7 @@ describe('RemoteServerConfigCtr', () => {
417
417
  it('should return error when remote server is not active', async () => {
418
418
  mockStoreManager.get.mockImplementation((key) => {
419
419
  if (key === 'dataSyncConfig') {
420
- return { active: false, storageMode: 'local' };
420
+ return { active: false, storageMode: 'cloud' };
421
421
  }
422
422
  return null;
423
423
  });
@@ -648,7 +648,7 @@ describe('RemoteServerConfigCtr', () => {
648
648
  refreshToken: 'stored-refresh',
649
649
  };
650
650
  }
651
- return { active: false, storageMode: 'local' };
651
+ return { active: false, storageMode: 'cloud' };
652
652
  });
653
653
 
654
654
  const newController = new RemoteServerConfigCtr(mockApp);
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Add server version check for desktop app."
6
+ ]
7
+ },
8
+ "date": "2026-01-22",
9
+ "version": "2.0.0-next.341"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
@@ -332,6 +332,11 @@
332
332
  "run": "Run",
333
333
  "save": "Save",
334
334
  "send": "Send",
335
+ "serverVersionOutdated.desc": "Your client version (v{{version}}) requires a newer server version.",
336
+ "serverVersionOutdated.dismiss": "Continue Anyway",
337
+ "serverVersionOutdated.title": "Server Version Outdated",
338
+ "serverVersionOutdated.upgrade": "Upgrade Guide",
339
+ "serverVersionOutdated.warning": "Some features may not work properly or behave unexpectedly. Please update your server for the best experience.",
335
340
  "setting": "Settings",
336
341
  "share": "Share",
337
342
  "stop": "Stop",
@@ -380,6 +385,7 @@
380
385
  "upgradeVersion.action": "Upgrade",
381
386
  "upgradeVersion.hasNew": "Update available",
382
387
  "upgradeVersion.newVersion": "Update available: {{version}}",
388
+ "upgradeVersion.serverVersion": "Server: {{version}}",
383
389
  "userPanel.anonymousNickName": "Anonymous User",
384
390
  "userPanel.billing": "Billing Management",
385
391
  "userPanel.cloud": "Launch {{name}}",
@@ -332,6 +332,11 @@
332
332
  "run": "运行",
333
333
  "save": "保存",
334
334
  "send": "发送",
335
+ "serverVersionOutdated.desc": "当前客户端版本(v{{version}})需要更新的服务端版本。",
336
+ "serverVersionOutdated.dismiss": "继续使用",
337
+ "serverVersionOutdated.title": "服务端版本过旧",
338
+ "serverVersionOutdated.upgrade": "升级指南",
339
+ "serverVersionOutdated.warning": "部分功能可能无法正常使用或出现非预期行为。建议更新服务端以获得最佳体验。",
335
340
  "setting": "设置",
336
341
  "share": "分享",
337
342
  "stop": "停止",
@@ -380,6 +385,7 @@
380
385
  "upgradeVersion.action": "升级",
381
386
  "upgradeVersion.hasNew": "有可用更新",
382
387
  "upgradeVersion.newVersion": "可用更新版本:{{version}}",
388
+ "upgradeVersion.serverVersion": "服务端:{{version}}",
383
389
  "userPanel.anonymousNickName": "匿名用户",
384
390
  "userPanel.billing": "账单管理",
385
391
  "userPanel.cloud": "体验 {{name}}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.340",
3
+ "version": "2.0.0-next.341",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,4 +1,4 @@
1
- export type StorageMode = 'local' | 'cloud' | 'selfHost';
1
+ export type StorageMode = 'cloud' | 'selfHost';
2
2
  export enum StorageModeEnum {
3
3
  Cloud = 'cloud',
4
4
  SelfHost = 'selfHost',
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { ProductLogo } from '@/components/Branding';
8
8
  import { CHANGELOG_URL, MANUAL_UPGRADE_URL, OFFICIAL_SITE } from '@/const/url';
9
- import { CURRENT_VERSION } from '@/const/version';
9
+ import { CURRENT_VERSION, isDesktop } from '@/const/version';
10
10
  import { useNewVersion } from '@/features/User/UserPanel/useNewVersion';
11
11
  import { useGlobalStore } from '@/store/global';
12
12
 
@@ -18,9 +18,17 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
18
18
 
19
19
  const Version = memo<{ mobile?: boolean }>(({ mobile }) => {
20
20
  const hasNewVersion = useNewVersion();
21
- const [latestVersion] = useGlobalStore((s) => [s.latestVersion]);
21
+ const [latestVersion, serverVersion, useCheckServerVersion] = useGlobalStore((s) => [
22
+ s.latestVersion,
23
+ s.serverVersion,
24
+ s.useCheckServerVersion,
25
+ ]);
22
26
  const { t } = useTranslation('common');
23
27
 
28
+ useCheckServerVersion(isDesktop);
29
+
30
+ const showServerVersion = serverVersion && serverVersion !== CURRENT_VERSION;
31
+
24
32
  return (
25
33
  <Flexbox
26
34
  align={mobile ? 'stretch' : 'center'}
@@ -46,6 +54,9 @@ const Version = memo<{ mobile?: boolean }>(({ mobile }) => {
46
54
  <div style={{ fontSize: 18, fontWeight: 'bolder' }}>{BRANDING_NAME}</div>
47
55
  <Flexbox gap={6} horizontal={!mobile}>
48
56
  <Tag>v{CURRENT_VERSION}</Tag>
57
+ {showServerVersion && (
58
+ <Tag>{t('upgradeVersion.serverVersion', { version: `v${serverVersion}` })}</Tag>
59
+ )}
49
60
  {hasNewVersion && (
50
61
  <Tag color={'info'}>
51
62
  {t('upgradeVersion.newVersion', { version: `v${latestVersion}` })}
@@ -62,14 +62,14 @@ describe('useUserAvatar', () => {
62
62
  expect(result.current).toBe(mockAvatar);
63
63
  });
64
64
 
65
- it('should return original avatar when no remote server URL in desktop environment', () => {
65
+ it('should return original avatar when no remote server URL in desktop environment (selfHost mode)', () => {
66
66
  mockIsDesktop = true;
67
67
  const mockAvatar = '/api/avatar.png';
68
68
 
69
69
  act(() => {
70
70
  useUserStore.setState({ user: { avatar: mockAvatar } as any });
71
71
  useElectronStore.setState({
72
- dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'local' },
72
+ dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'selfHost' },
73
73
  });
74
74
  });
75
75
 
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import { Button, Flexbox, Icon } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { TriangleAlert, X } from 'lucide-react';
6
+ import { useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import { MANUAL_UPGRADE_URL } from '@/const/url';
10
+ import { CURRENT_VERSION } from '@/const/version';
11
+ import { useElectronStore } from '@/store/electron';
12
+ import { electronSyncSelectors } from '@/store/electron/selectors';
13
+ import { useGlobalStore } from '@/store/global';
14
+
15
+ const useStyles = createStyles(({ css, token }) => ({
16
+ closeButton: css`
17
+ cursor: pointer;
18
+
19
+ position: absolute;
20
+ inset-block-start: 20px;
21
+ inset-inline-end: 20px;
22
+
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+
27
+ width: 28px;
28
+ height: 28px;
29
+ border-radius: ${token.borderRadius}px;
30
+
31
+ color: ${token.colorTextSecondary};
32
+
33
+ transition: all 0.2s;
34
+
35
+ &:hover {
36
+ color: ${token.colorText};
37
+ background: ${token.colorFillSecondary};
38
+ }
39
+ `,
40
+ container: css`
41
+ position: fixed;
42
+ z-index: 9999;
43
+ inset: 0;
44
+
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+
49
+ background: ${token.colorBgMask};
50
+ `,
51
+ content: css`
52
+ position: relative;
53
+
54
+ overflow: hidden;
55
+
56
+ max-width: 480px;
57
+ padding: 24px;
58
+ border: 1px solid ${token.yellowBorder};
59
+ border-radius: ${token.borderRadiusLG}px;
60
+
61
+ background: ${token.colorBgContainer};
62
+ box-shadow: ${token.boxShadowSecondary};
63
+ `,
64
+ desc: css`
65
+ line-height: 1.6;
66
+ color: ${token.colorTextSecondary};
67
+ `,
68
+ title: css`
69
+ font-size: 16px;
70
+ font-weight: bold;
71
+ color: ${token.colorWarningText};
72
+ `,
73
+ titleIcon: css`
74
+ flex-shrink: 0;
75
+ color: ${token.colorWarning};
76
+ `,
77
+ warning: css`
78
+ padding: 12px;
79
+ border-radius: ${token.borderRadius}px;
80
+ color: ${token.colorWarningText};
81
+ background: ${token.yellowBg};
82
+ `,
83
+ }));
84
+
85
+ const ServerVersionOutdatedAlert = () => {
86
+ const { styles } = useStyles();
87
+ const { t } = useTranslation('common');
88
+ const [dismissed, setDismissed] = useState(false);
89
+ const isServerVersionOutdated = useGlobalStore((s) => s.isServerVersionOutdated);
90
+ const storageMode = useElectronStore(electronSyncSelectors.storageMode);
91
+
92
+ // Only show alert when using self-hosted server, not cloud
93
+ if (storageMode !== 'selfHost') return null;
94
+ if (!isServerVersionOutdated || dismissed) return null;
95
+
96
+ return (
97
+ <div className={styles.container}>
98
+ <div className={styles.content}>
99
+ <div className={styles.closeButton} onClick={() => setDismissed(true)}>
100
+ <Icon icon={X} />
101
+ </div>
102
+
103
+ <Flexbox gap={16}>
104
+ <Flexbox align="center" gap={8} horizontal>
105
+ <Icon className={styles.titleIcon} icon={TriangleAlert} />
106
+ <div className={styles.title}>{t('serverVersionOutdated.title')}</div>
107
+ </Flexbox>
108
+
109
+ <div className={styles.desc}>
110
+ {t('serverVersionOutdated.desc', { version: CURRENT_VERSION })}
111
+ </div>
112
+
113
+ <div className={styles.warning}>{t('serverVersionOutdated.warning')}</div>
114
+
115
+ <Flexbox gap={8} horizontal justify="flex-end" style={{ marginTop: 8 }}>
116
+ <a href={MANUAL_UPGRADE_URL} rel="noreferrer" target="_blank">
117
+ <Button size="small" type="primary">
118
+ {t('serverVersionOutdated.upgrade')}
119
+ </Button>
120
+ </a>
121
+ <Button onClick={() => setDismissed(true)} size="small">
122
+ {t('serverVersionOutdated.dismiss')}
123
+ </Button>
124
+ </Flexbox>
125
+ </Flexbox>
126
+ </div>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ export default ServerVersionOutdatedAlert;
@@ -5,6 +5,7 @@ import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { createStoreUpdater } from 'zustand-utils';
7
7
 
8
+ import { isDesktop } from '@/const/version';
8
9
  import { enableNextAuth } from '@/envs/auth';
9
10
  import { useIsMobile } from '@/hooks/useIsMobile';
10
11
  import { useAgentStore } from '@/store/agent';
@@ -32,7 +33,10 @@ const StoreInitialization = memo(() => {
32
33
 
33
34
  const { serverConfig } = useServerConfigStore();
34
35
 
35
- const useInitSystemStatus = useGlobalStore((s) => s.useInitSystemStatus);
36
+ const [useInitSystemStatus, useCheckServerVersion] = useGlobalStore((s) => [
37
+ s.useInitSystemStatus,
38
+ s.useCheckServerVersion,
39
+ ]);
36
40
 
37
41
  const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent);
38
42
  const useInitAiProviderKeyVaults = useAiInfraStore((s) => s.useFetchAiProviderRuntimeState);
@@ -41,6 +45,9 @@ const StoreInitialization = memo(() => {
41
45
  // init the system preference
42
46
  useInitSystemStatus();
43
47
 
48
+ // check server version in desktop app
49
+ useCheckServerVersion(isDesktop);
50
+
44
51
  // fetch server config
45
52
  const useFetchServerConfig = useServerConfigStore((s) => s.useInitServerConfig);
46
53
  useFetchServerConfig();
@@ -7,6 +7,7 @@ import { ReferralProvider } from '@/business/client/ReferralProvider';
7
7
  import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
8
8
  import { DragUploadProvider } from '@/components/DragUploadZone/DragUploadProvider';
9
9
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
10
+ import { isDesktop } from '@/const/version';
10
11
  import { appEnv } from '@/envs/app';
11
12
  import DevPanel from '@/features/DevPanel';
12
13
  import { getServerGlobalConfig } from '@/server/globalConfig';
@@ -20,6 +21,7 @@ import ImportSettings from './ImportSettings';
20
21
  import Locale from './Locale';
21
22
  import NextThemeProvider from './NextThemeProvider';
22
23
  import QueryProvider from './Query';
24
+ import ServerVersionOutdatedAlert from './ServerVersionOutdatedAlert';
23
25
  import StoreInitialization from './StoreInitialization';
24
26
  import StyleRegistry from './StyleRegistry';
25
27
 
@@ -66,6 +68,8 @@ const GlobalLayout = async ({
66
68
  >
67
69
  <QueryProvider>
68
70
  <StoreInitialization />
71
+
72
+ {isDesktop && <ServerVersionOutdatedAlert />}
69
73
  <FaviconProvider>
70
74
  <GroupWizardProvider>
71
75
  <DragUploadProvider>
@@ -350,6 +350,13 @@ export default {
350
350
  'run': 'Run',
351
351
  'save': 'Save',
352
352
  'send': 'Send',
353
+ 'serverVersionOutdated.desc':
354
+ 'Your client version (v{{version}}) requires a newer server version.',
355
+ 'serverVersionOutdated.dismiss': 'Continue Anyway',
356
+ 'serverVersionOutdated.title': 'Server Version Outdated',
357
+ 'serverVersionOutdated.upgrade': 'Upgrade Guide',
358
+ 'serverVersionOutdated.warning':
359
+ 'Some features may not work properly or behave unexpectedly. Please update your server for the best experience.',
353
360
  'setting': 'Settings',
354
361
  'share': 'Share',
355
362
  'stop': 'Stop',
@@ -401,6 +408,7 @@ export default {
401
408
  'upgradeVersion.action': 'Upgrade',
402
409
  'upgradeVersion.hasNew': 'Update available',
403
410
  'upgradeVersion.newVersion': 'Update available: {{version}}',
411
+ 'upgradeVersion.serverVersion': 'Server: {{version}}',
404
412
  'userPanel.anonymousNickName': 'Anonymous User',
405
413
  'userPanel.billing': 'Billing Management',
406
414
  'userPanel.cloud': 'Launch {{name}}',
@@ -1,11 +1,13 @@
1
1
  import type { PartialDeep } from 'type-fest';
2
2
 
3
+ import type { VersionResponseData } from '@/app/(backend)/api/version/route';
3
4
  import { BusinessGlobalService } from '@/business/client/services/BusinessGlobalService';
4
5
  import { lambdaClient } from '@/libs/trpc/client';
5
6
  import { type LobeAgentConfig } from '@/types/agent';
6
7
  import { type GlobalRuntimeConfig } from '@/types/serverConfig';
7
8
 
8
9
  const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat/latest';
10
+ const SERVER_VERSION_URL = '/api/version';
9
11
 
10
12
  class GlobalService extends BusinessGlobalService {
11
13
  /**
@@ -18,6 +20,29 @@ class GlobalService extends BusinessGlobalService {
18
20
  return data['version'];
19
21
  };
20
22
 
23
+ /**
24
+ * get server version from /api/version
25
+ * @returns version string if available, null only if server returns 404 (API doesn't exist on old server)
26
+ * @throws Error for other failures (network errors, 500s, etc.) to allow SWR retry
27
+ */
28
+ getServerVersion = async (): Promise<string | null> => {
29
+ const res = await fetch(SERVER_VERSION_URL);
30
+
31
+ // Only treat 404 as "server doesn't support version API"
32
+ // Other errors (500, network issues) should throw to allow retry
33
+ if (res.status === 404) {
34
+ return null;
35
+ }
36
+
37
+ if (!res.ok) {
38
+ throw new Error(`Failed to fetch server version: ${res.status}`);
39
+ }
40
+
41
+ const data: VersionResponseData = await res.json();
42
+
43
+ return data.version;
44
+ };
45
+
21
46
  getGlobalConfig = async (): Promise<GlobalRuntimeConfig> => {
22
47
  return lambdaClient.config.getGlobalConfig.query();
23
48
  };
@@ -23,6 +23,7 @@ export interface GlobalGeneralAction {
23
23
  updateResourceManagerColumnWidth: (column: 'name' | 'date' | 'size', width: number) => void;
24
24
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
25
25
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
26
+ useCheckServerVersion: (enabledCheck?: boolean) => SWRResponse<string | null>;
26
27
  useInitSystemStatus: () => SWRResponse;
27
28
  }
28
29
 
@@ -160,6 +161,51 @@ export const generalActionSlice: StateCreator<
160
161
  },
161
162
  ),
162
163
 
164
+ useCheckServerVersion: (enabledCheck = true) =>
165
+ useOnlyFetchOnceSWR(
166
+ enabledCheck ? 'checkServerVersion' : null,
167
+ async () => globalService.getServerVersion(),
168
+ {
169
+ onSuccess: (data: string | null) => {
170
+ if (data === null) {
171
+ set({ isServerVersionOutdated: true }, false);
172
+ return;
173
+ }
174
+
175
+ set({ serverVersion: data }, false);
176
+
177
+ if (!valid(CURRENT_VERSION) || !valid(data)) return;
178
+
179
+ const clientVersion = parse(CURRENT_VERSION);
180
+ const serverVersion = parse(data);
181
+
182
+ if (!clientVersion || !serverVersion) return;
183
+
184
+ const DIFF_THRESHOLD = 5;
185
+ // 版本差异计算规则
186
+ // ┌─────────────────┬────────┬─────────┐
187
+ // │ 客户端 → 服务端 │ 差异值 │ 结果 │
188
+ // ├─────────────────┼────────┼─────────┤
189
+ // │ 1.0.5 → 1.0.0 │ 5 │ ⚠️ 过旧 │
190
+ // ├─────────────────┼────────┼─────────┤
191
+ // │ 1.1.0 → 1.0.5 │ 5 │ ⚠️ 过旧 │
192
+ // ├─────────────────┼────────┼─────────┤
193
+ // │ 2.0.0 → 1.9.9 │ 91 │ ⚠️ 过旧 │
194
+ // ├─────────────────┼────────┼─────────┤
195
+ // │ 1.0.4 → 1.0.0 │ 4 │ ✅ 正常 │
196
+ // └─────────────────┴────────┴─────────┘
197
+ const versionDiff =
198
+ (clientVersion.major - serverVersion.major) * 100 +
199
+ (clientVersion.minor - serverVersion.minor) * 10 +
200
+ (clientVersion.patch - serverVersion.patch);
201
+
202
+ if (versionDiff >= DIFF_THRESHOLD) {
203
+ set({ isServerVersionOutdated: true }, false);
204
+ }
205
+ },
206
+ },
207
+ ),
208
+
163
209
  useInitSystemStatus: () =>
164
210
  useOnlyFetchOnceSWR<SystemStatus>(
165
211
  'initSystemStatus',
@@ -180,9 +180,18 @@ export interface GlobalState {
180
180
  */
181
181
  initClientDBStage: DatabaseLoadingState;
182
182
  isMobile?: boolean;
183
+ /**
184
+ * 服务端版本过旧,不支持 /api/version 接口
185
+ * 需要提示用户更新服务端
186
+ */
187
+ isServerVersionOutdated?: boolean;
183
188
  isStatusInit?: boolean;
184
189
  latestVersion?: string;
185
190
  navigate?: NavigateFunction;
191
+ /**
192
+ * 服务端版本号,用于检测客户端与服务端版本是否一致
193
+ */
194
+ serverVersion?: string;
186
195
  sidebarKey: SidebarTabKey;
187
196
  status: SystemStatus;
188
197
  statusStorage: AsyncLocalStorage<SystemStatus>;