🐛 fix(desktop): return OFFICIAL_URL in cloud mode for remoteServerUrl selector (#11502)

The remoteServerUrl selector was returning an empty string in cloud mode,
causing avatar URLs with relative paths to not be properly prefixed with
the remote server URL in desktop environment.

Changes:
- Update remoteServerUrl selector to return OFFICIAL_URL in cloud mode
- Add rawRemoteServerUrl selector for forms that need the original config value
- Fix avatar URL handling in UserAvatar and useCategory
- Update tests to reflect new selector behavior

Fixes LOBE-3197
This commit is contained in:
Innei
2026-01-14 22:05:15 +08:00
committed by GitHub
parent f397b7f944
commit 1d11fac4c6
4 changed files with 57 additions and 9 deletions

View File

@@ -26,6 +26,8 @@ import {
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useElectronStore } from '@/store/electron';
import { electronSyncSelectors } from '@/store/electron/selectors';
import { SettingsTabs } from '@/store/global/initialState';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import { useUserStore } from '@/store/user';
@@ -63,13 +65,24 @@ export const useCategory = () => {
userProfileSelectors.userAvatar(s),
userProfileSelectors.nickName(s),
]);
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
// Process avatar URL for desktop environment
const avatarUrl = useMemo(() => {
if (!avatar) return undefined;
if (isDesktop && avatar.startsWith('/') && remoteServerUrl) {
return remoteServerUrl + avatar;
}
return avatar;
}, [avatar, remoteServerUrl]);
const categoryGroups: CategoryGroup[] = useMemo(() => {
const groups: CategoryGroup[] = [];
// 个人资料组 - Profile 相关设置
const profileItems: CategoryItem[] = [
{
icon: avatar ? <Avatar avatar={avatar} shape={'square'} size={26} /> : UserCircle,
icon: avatarUrl ? <Avatar avatar={avatarUrl} shape={'square'} size={26} /> : UserCircle,
key: SettingsTabs.Profile,
label: username ? username : tAuth('tab.profile'),
},
@@ -227,7 +240,7 @@ export const useCategory = () => {
showAiImage,
showApiKeyManage,
isLoginWithClerk,
avatar,
avatarUrl,
username,
]);

View File

@@ -82,12 +82,12 @@ const ConnectionMode = memo<ConnectionModeProps>(({ setWaiting }) => {
const connect = useElectronStore((s) => s.connectRemoteServer);
const storageMode = useElectronStore(electronSyncSelectors.storageMode);
const remoteServerUrl = useElectronStore(electronSyncSelectors.remoteServerUrl);
const rawRemoteServerUrl = useElectronStore(electronSyncSelectors.rawRemoteServerUrl);
const [selectedOption, setSelectedOption] = useState<RemoteStorageMode>(
storageMode === StorageModeEnum.SelfHost ? StorageModeEnum.SelfHost : StorageModeEnum.Cloud,
);
const [selfHostedUrl, setSelfHostedUrl] = useState(remoteServerUrl);
const [selfHostedUrl, setSelfHostedUrl] = useState(rawRemoteServerUrl);
const validateUrl = useCallback((url: string) => {
if (!url) {

View File

@@ -19,6 +19,7 @@ vi.mock('@lobechat/const', async (importOriginal) => {
return mockIsDesktop;
},
DEFAULT_USER_AVATAR: 'default-avatar.png',
OFFICIAL_URL: 'https://app.lobehub.com',
};
});
@@ -77,7 +78,7 @@ describe('useUserAvatar', () => {
expect(result.current).toBe(mockAvatar);
});
it('should prepend remote server URL when avatar starts with / in desktop environment', () => {
it('should prepend remote server URL when avatar starts with / in desktop environment (selfHost mode)', () => {
mockIsDesktop = true;
const mockAvatar = '/api/avatar.png';
const mockServerUrl = 'https://server.com';
@@ -85,7 +86,7 @@ describe('useUserAvatar', () => {
act(() => {
useUserStore.setState({ user: { avatar: mockAvatar } as any });
useElectronStore.setState({
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
});
});
@@ -102,7 +103,7 @@ describe('useUserAvatar', () => {
act(() => {
useUserStore.setState({ user: { avatar: mockAvatar } as any });
useElectronStore.setState({
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'cloud' },
dataSyncConfig: { remoteServerUrl: mockServerUrl, storageMode: 'selfHost' },
});
});
@@ -111,7 +112,7 @@ describe('useUserAvatar', () => {
expect(result.current).toBe(mockAvatar);
});
it('should handle empty remote server URL in desktop environment', () => {
it('should use OFFICIAL_URL when storageMode is cloud in desktop environment', () => {
mockIsDesktop = true;
const mockAvatar = '/api/avatar.png';
@@ -124,6 +125,24 @@ describe('useUserAvatar', () => {
const { result } = renderHook(() => useUserAvatar());
// In cloud mode, selector returns OFFICIAL_URL regardless of remoteServerUrl config
expect(result.current).toBe('https://app.lobehub.com/api/avatar.png');
});
it('should return original avatar when storageMode is selfHost but no URL configured', () => {
mockIsDesktop = true;
const mockAvatar = '/api/avatar.png';
act(() => {
useUserStore.setState({ user: { avatar: mockAvatar } as any });
useElectronStore.setState({
dataSyncConfig: { remoteServerUrl: '', storageMode: 'selfHost' },
});
});
const { result } = renderHook(() => useUserAvatar());
// In selfHost mode with empty URL, avatar is not prepended
expect(result.current).toBe(mockAvatar);
});
});

View File

@@ -1,12 +1,28 @@
import { OFFICIAL_URL } from '@lobechat/const';
import { type ElectronState } from '../initialState';
const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
const remoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
/**
* Returns the effective remote server URL based on storage mode:
* - Cloud mode: returns OFFICIAL_URL
* - SelfHost mode: returns the configured remoteServerUrl
*/
const remoteServerUrl = (s: ElectronState) =>
s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
/**
* Returns the raw remoteServerUrl from config without transformation.
* Use this when you need the original configured value (e.g., for editing forms).
*/
const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
export const electronSyncSelectors = {
isSyncActive,
rawRemoteServerUrl,
remoteServerUrl,
storageMode,
};