Skip to content

Commit 0cf2723

Browse files
authored
✨ feat: add server version check for desktop app (lobehub#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>
1 parent bf244f9 commit 0cf2723

File tree

14 files changed

+269
-15
lines changed

14 files changed

+269
-15
lines changed

apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export default class RemoteServerConfigCtr extends ControllerModule {
5050
* Local mode has been removed; fall back to cloud.
5151
*/
5252
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;
5455

5556
const nextConfig: DataSyncConfig = {
5657
...config,

apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('RemoteServerConfigCtr', () => {
6060
ipcMainHandleMock.mockClear();
6161
mockStoreManager.get.mockReturnValue({
6262
active: false,
63-
storageMode: 'local',
63+
storageMode: 'cloud',
6464
});
6565
controller = new RemoteServerConfigCtr(mockApp);
6666
});
@@ -85,7 +85,7 @@ describe('RemoteServerConfigCtr', () => {
8585
it('should update configuration', async () => {
8686
const prevConfig: DataSyncConfig = {
8787
active: false,
88-
storageMode: 'local',
88+
storageMode: 'cloud',
8989
};
9090
mockStoreManager.get.mockReturnValue(prevConfig);
9191

@@ -195,7 +195,7 @@ describe('RemoteServerConfigCtr', () => {
195195
refreshToken: Buffer.from('stored-refresh-token').toString('base64'),
196196
};
197197
}
198-
return { active: false, storageMode: 'local' };
198+
return { active: false, storageMode: 'cloud' };
199199
});
200200

201201
// Create new controller to test loading from store
@@ -210,7 +210,7 @@ describe('RemoteServerConfigCtr', () => {
210210
if (key === 'encryptedTokens') {
211211
return null;
212212
}
213-
return { active: false, storageMode: 'local' };
213+
return { active: false, storageMode: 'cloud' };
214214
});
215215

216216
const newController = new RemoteServerConfigCtr(mockApp);
@@ -243,7 +243,7 @@ describe('RemoteServerConfigCtr', () => {
243243
refreshToken: 'invalid-encrypted-token',
244244
};
245245
}
246-
return { active: false, storageMode: 'local' };
246+
return { active: false, storageMode: 'cloud' };
247247
});
248248

249249
const newController = new RemoteServerConfigCtr(mockApp);
@@ -273,7 +273,7 @@ describe('RemoteServerConfigCtr', () => {
273273
if (key === 'encryptedTokens') {
274274
return null;
275275
}
276-
return { active: false, storageMode: 'local' };
276+
return { active: false, storageMode: 'cloud' };
277277
});
278278

279279
const newController = new RemoteServerConfigCtr(mockApp);
@@ -417,7 +417,7 @@ describe('RemoteServerConfigCtr', () => {
417417
it('should return error when remote server is not active', async () => {
418418
mockStoreManager.get.mockImplementation((key) => {
419419
if (key === 'dataSyncConfig') {
420-
return { active: false, storageMode: 'local' };
420+
return { active: false, storageMode: 'cloud' };
421421
}
422422
return null;
423423
});
@@ -648,7 +648,7 @@ describe('RemoteServerConfigCtr', () => {
648648
refreshToken: 'stored-refresh',
649649
};
650650
}
651-
return { active: false, storageMode: 'local' };
651+
return { active: false, storageMode: 'cloud' };
652652
});
653653

654654
const newController = new RemoteServerConfigCtr(mockApp);

locales/en-US/common.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@
332332
"run": "Run",
333333
"save": "Save",
334334
"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.",
335340
"setting": "Settings",
336341
"share": "Share",
337342
"stop": "Stop",
@@ -380,6 +385,7 @@
380385
"upgradeVersion.action": "Upgrade",
381386
"upgradeVersion.hasNew": "Update available",
382387
"upgradeVersion.newVersion": "Update available: {{version}}",
388+
"upgradeVersion.serverVersion": "Server: {{version}}",
383389
"userPanel.anonymousNickName": "Anonymous User",
384390
"userPanel.billing": "Billing Management",
385391
"userPanel.cloud": "Launch {{name}}",

locales/zh-CN/common.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@
332332
"run": "运行",
333333
"save": "保存",
334334
"send": "发送",
335+
"serverVersionOutdated.desc": "当前客户端版本(v{{version}})需要更新的服务端版本。",
336+
"serverVersionOutdated.dismiss": "继续使用",
337+
"serverVersionOutdated.title": "服务端版本过旧",
338+
"serverVersionOutdated.upgrade": "升级指南",
339+
"serverVersionOutdated.warning": "部分功能可能无法正常使用或出现非预期行为。建议更新服务端以获得最佳体验。",
335340
"setting": "设置",
336341
"share": "分享",
337342
"stop": "停止",
@@ -380,6 +385,7 @@
380385
"upgradeVersion.action": "升级",
381386
"upgradeVersion.hasNew": "有可用更新",
382387
"upgradeVersion.newVersion": "可用更新版本:{{version}}",
388+
"upgradeVersion.serverVersion": "服务端:{{version}}",
383389
"userPanel.anonymousNickName": "匿名用户",
384390
"userPanel.billing": "账单管理",
385391
"userPanel.cloud": "体验 {{name}}",

packages/electron-client-ipc/src/types/dataSync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type StorageMode = 'local' | 'cloud' | 'selfHost';
1+
export type StorageMode = 'cloud' | 'selfHost';
22
export enum StorageModeEnum {
33
Cloud = 'cloud',
44
SelfHost = 'selfHost',

src/app/[variants]/(main)/settings/about/features/Version.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
66

77
import { ProductLogo } from '@/components/Branding';
88
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';
1010
import { useNewVersion } from '@/features/User/UserPanel/useNewVersion';
1111
import { useGlobalStore } from '@/store/global';
1212

@@ -18,9 +18,17 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
1818

1919
const Version = memo<{ mobile?: boolean }>(({ mobile }) => {
2020
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+
]);
2226
const { t } = useTranslation('common');
2327

28+
useCheckServerVersion(isDesktop);
29+
30+
const showServerVersion = serverVersion && serverVersion !== CURRENT_VERSION;
31+
2432
return (
2533
<Flexbox
2634
align={mobile ? 'stretch' : 'center'}
@@ -46,6 +54,9 @@ const Version = memo<{ mobile?: boolean }>(({ mobile }) => {
4654
<div style={{ fontSize: 18, fontWeight: 'bolder' }}>{BRANDING_NAME}</div>
4755
<Flexbox gap={6} horizontal={!mobile}>
4856
<Tag>v{CURRENT_VERSION}</Tag>
57+
{showServerVersion && (
58+
<Tag>{t('upgradeVersion.serverVersion', { version: `v${serverVersion}` })}</Tag>
59+
)}
4960
{hasNewVersion && (
5061
<Tag color={'info'}>
5162
{t('upgradeVersion.newVersion', { version: `v${latestVersion}` })}

src/hooks/useUserAvatar.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ describe('useUserAvatar', () => {
6262
expect(result.current).toBe(mockAvatar);
6363
});
6464

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)', () => {
6666
mockIsDesktop = true;
6767
const mockAvatar = '/api/avatar.png';
6868

6969
act(() => {
7070
useUserStore.setState({ user: { avatar: mockAvatar } as any });
7171
useElectronStore.setState({
72-
dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'local' },
72+
dataSyncConfig: { remoteServerUrl: undefined, storageMode: 'selfHost' },
7373
});
7474
});
7575

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

src/layout/GlobalProvider/StoreInitialization.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { memo } from 'react';
55
import { useTranslation } from 'react-i18next';
66
import { createStoreUpdater } from 'zustand-utils';
77

8+
import { isDesktop } from '@/const/version';
89
import { enableNextAuth } from '@/envs/auth';
910
import { useIsMobile } from '@/hooks/useIsMobile';
1011
import { useAgentStore } from '@/store/agent';
@@ -32,7 +33,10 @@ const StoreInitialization = memo(() => {
3233

3334
const { serverConfig } = useServerConfigStore();
3435

35-
const useInitSystemStatus = useGlobalStore((s) => s.useInitSystemStatus);
36+
const [useInitSystemStatus, useCheckServerVersion] = useGlobalStore((s) => [
37+
s.useInitSystemStatus,
38+
s.useCheckServerVersion,
39+
]);
3640

3741
const useInitBuiltinAgent = useAgentStore((s) => s.useInitBuiltinAgent);
3842
const useInitAiProviderKeyVaults = useAiInfraStore((s) => s.useFetchAiProviderRuntimeState);
@@ -41,6 +45,9 @@ const StoreInitialization = memo(() => {
4145
// init the system preference
4246
useInitSystemStatus();
4347

48+
// check server version in desktop app
49+
useCheckServerVersion(isDesktop);
50+
4451
// fetch server config
4552
const useFetchServerConfig = useServerConfigStore((s) => s.useInitServerConfig);
4653
useFetchServerConfig();

src/layout/GlobalProvider/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ReferralProvider } from '@/business/client/ReferralProvider';
77
import { LobeAnalyticsProviderWrapper } from '@/components/Analytics/LobeAnalyticsProviderWrapper';
88
import { DragUploadProvider } from '@/components/DragUploadZone/DragUploadProvider';
99
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
10+
import { isDesktop } from '@/const/version';
1011
import { appEnv } from '@/envs/app';
1112
import DevPanel from '@/features/DevPanel';
1213
import { getServerGlobalConfig } from '@/server/globalConfig';
@@ -20,6 +21,7 @@ import ImportSettings from './ImportSettings';
2021
import Locale from './Locale';
2122
import NextThemeProvider from './NextThemeProvider';
2223
import QueryProvider from './Query';
24+
import ServerVersionOutdatedAlert from './ServerVersionOutdatedAlert';
2325
import StoreInitialization from './StoreInitialization';
2426
import StyleRegistry from './StyleRegistry';
2527

@@ -66,6 +68,8 @@ const GlobalLayout = async ({
6668
>
6769
<QueryProvider>
6870
<StoreInitialization />
71+
72+
{isDesktop && <ServerVersionOutdatedAlert />}
6973
<FaviconProvider>
7074
<GroupWizardProvider>
7175
<DragUploadProvider>

0 commit comments

Comments
 (0)