@lobehub/chat 0.152.7 → 0.152.9

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,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.152.9](https://github.com/lobehub/lobe-chat/compare/v0.152.8...v0.152.9)
6
+
7
+ <sup>Released on **2024-05-03**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 0.152.8](https://github.com/lobehub/lobe-chat/compare/v0.152.7...v0.152.8)
23
+
24
+ <sup>Released on **2024-05-03**</sup>
25
+
26
+ #### ♻ Code Refactoring
27
+
28
+ - **misc**: User store add an auth slice.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Code refactoring
36
+
37
+ - **misc**: User store add an auth slice, closes [#2214](https://github.com/lobehub/lobe-chat/issues/2214) ([948b257](https://github.com/lobehub/lobe-chat/commit/948b257))
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 0.152.7](https://github.com/lobehub/lobe-chat/compare/v0.152.6...v0.152.7)
6
48
 
7
49
  <sup>Released on **2024-05-02**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.152.7",
3
+ "version": "0.152.9",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -103,7 +103,7 @@
103
103
  "@vercel/speed-insights": "^1.0.10",
104
104
  "ahooks": "^3.7.11",
105
105
  "ai": "3.0.19",
106
- "antd": "^5.16.5",
106
+ "antd": "5.16.5",
107
107
  "antd-style": "^3.6.2",
108
108
  "brotli-wasm": "^3.0.0",
109
109
  "chroma-js": "^2.4.2",
@@ -179,7 +179,7 @@
179
179
  "@next/bundle-analyzer": "^14.2.3",
180
180
  "@next/eslint-plugin-next": "^14.2.3",
181
181
  "@peculiar/webcrypto": "^1.4.6",
182
- "@testing-library/jest-dom": "^6.4.2",
182
+ "@testing-library/jest-dom": "6.4.2",
183
183
  "@testing-library/react": "^15.0.5",
184
184
  "@types/chroma-js": "^2.4.4",
185
185
  "@types/debug": "^4.1.12",
@@ -10,7 +10,7 @@ import SyncStatusInspector from '@/features/SyncStatusInspector';
10
10
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
11
11
  import { useSessionStore } from '@/store/session';
12
12
  import { useUserStore } from '@/store/user';
13
- import { commonSelectors } from '@/store/user/selectors';
13
+ import { userProfileSelectors } from '@/store/user/selectors';
14
14
  import { mobileHeaderSticky } from '@/styles/mobileHeader';
15
15
 
16
16
  export const useStyles = createStyles(({ css, token }) => ({
@@ -26,7 +26,7 @@ export const useStyles = createStyles(({ css, token }) => ({
26
26
  const Header = memo(() => {
27
27
  const [createSession] = useSessionStore((s) => [s.createSession]);
28
28
  const router = useRouter();
29
- const avatar = useUserStore(commonSelectors.userAvatar);
29
+ const avatar = useUserStore(userProfileSelectors.userAvatar);
30
30
  const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
31
31
 
32
32
  return (
@@ -7,7 +7,7 @@ import { Flexbox } from 'react-layout-kit';
7
7
  import { FORM_STYLE } from '@/const/layoutTokens';
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { useUserStore } from '@/store/user';
10
- import { commonSelectors } from '@/store/user/selectors';
10
+ import { userProfileSelectors } from '@/store/user/selectors';
11
11
 
12
12
  import Preview from './Preview';
13
13
  import { FieldType, ImageType } from './type';
@@ -49,7 +49,7 @@ const ShareModal = memo<ModalProps>(({ onCancel, open }) => {
49
49
  const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
50
50
  const [tab, setTab] = useState<Tab>(Tab.Screenshot);
51
51
  const { t } = useTranslation('chat');
52
- const avatar = useUserStore(commonSelectors.userAvatar);
52
+ const avatar = useUserStore(userProfileSelectors.userAvatar);
53
53
  const [shareLoading, shareToShareGPT] = useChatStore((s) => [s.shareLoading, s.shareToShareGPT]);
54
54
  const { loading, onDownload, title } = useScreenshot(fieldValue.imageType);
55
55
 
@@ -7,7 +7,7 @@ import { CSSProperties, memo, useCallback } from 'react';
7
7
 
8
8
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
9
9
  import { useUserStore } from '@/store/user';
10
- import { commonSelectors } from '@/store/user/selectors';
10
+ import { userProfileSelectors } from '@/store/user/selectors';
11
11
  import { imageToBase64 } from '@/utils/imageToBase64';
12
12
  import { createUploadImageHandler } from '@/utils/uploadFIle';
13
13
 
@@ -41,7 +41,7 @@ const AvatarWithUpload = memo<AvatarWithUploadProps>(
41
41
  ({ size = 40, compressSize = 256, style, id }) => {
42
42
  const { styles } = useStyle();
43
43
  const [avatar, updateAvatar] = useUserStore((s) => [
44
- commonSelectors.userAvatar(s),
44
+ userProfileSelectors.userAvatar(s),
45
45
  s.updateAvatar,
46
46
  ]);
47
47
 
@@ -13,6 +13,7 @@ import {
13
13
  import { getServerGlobalConfig } from '@/server/globalConfig';
14
14
  import { ServerConfigStoreProvider } from '@/store/serverConfig';
15
15
  import { getAntdLocale } from '@/utils/locale';
16
+ import { isMobileDevice } from '@/utils/responsive';
16
17
 
17
18
  import AppTheme from './AppTheme';
18
19
  import Locale from './Locale';
@@ -48,6 +49,7 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
48
49
  // get default feature flags to use with ssr
49
50
  const serverFeatureFlags = getServerFeatureFlagsValue();
50
51
  const serverConfig = getServerGlobalConfig();
52
+ const isMobile = isMobileDevice();
51
53
  return (
52
54
  <StyleRegistry>
53
55
  <Locale antdLocale={antdLocale} defaultLang={defaultLang?.value}>
@@ -57,7 +59,11 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
57
59
  defaultPrimaryColor={primaryColor?.value as any}
58
60
  >
59
61
  <StoreInitialization />
60
- <ServerConfigStoreProvider featureFlags={serverFeatureFlags} serverConfig={serverConfig}>
62
+ <ServerConfigStoreProvider
63
+ featureFlags={serverFeatureFlags}
64
+ isMobile={isMobile}
65
+ serverConfig={serverConfig}
66
+ >
61
67
  {children}
62
68
  </ServerConfigStoreProvider>
63
69
  <DebugUI />
@@ -15,10 +15,10 @@ import { useToolStore } from '@/store/tool';
15
15
  import { pluginSelectors, toolSelectors } from '@/store/tool/selectors';
16
16
  import { useUserStore } from '@/store/user';
17
17
  import {
18
- commonSelectors,
19
18
  modelConfigSelectors,
20
19
  modelProviderSelectors,
21
20
  preferenceSelectors,
21
+ userProfileSelectors,
22
22
  } from '@/store/user/selectors';
23
23
  import { ChatErrorType } from '@/types/fetch';
24
24
  import { ChatMessage } from '@/types/message';
@@ -482,7 +482,7 @@ class ChatService {
482
482
  ...trace,
483
483
  enabled: true,
484
484
  tags: [tag, ...(trace?.tags || []), ...tags].filter(Boolean) as string[],
485
- userId: commonSelectors.userId(useUserStore.getState()),
485
+ userId: userProfileSelectors.userId(useUserStore.getState()),
486
486
  };
487
487
  }
488
488
 
@@ -8,7 +8,7 @@ import { agentSelectors } from '@/store/agent/selectors';
8
8
  import { useSessionStore } from '@/store/session';
9
9
  import { sessionMetaSelectors } from '@/store/session/selectors';
10
10
  import { useUserStore } from '@/store/user';
11
- import { commonSelectors } from '@/store/user/selectors';
11
+ import { userProfileSelectors } from '@/store/user/selectors';
12
12
  import { ChatMessage } from '@/types/message';
13
13
  import { MetaData } from '@/types/meta';
14
14
  import { merge } from '@/utils/merge';
@@ -20,7 +20,7 @@ const getMeta = (message: ChatMessage) => {
20
20
  switch (message.role) {
21
21
  case 'user': {
22
22
  return {
23
- avatar: commonSelectors.userAvatar(useUserStore.getState()) || DEFAULT_USER_AVATAR,
23
+ avatar: userProfileSelectors.userAvatar(useUserStore.getState()) || DEFAULT_USER_AVATAR,
24
24
  };
25
25
  }
26
26
 
@@ -10,12 +10,13 @@ import { Provider, createServerConfigStore } from './store';
10
10
  interface GlobalStoreProviderProps {
11
11
  children: ReactNode;
12
12
  featureFlags?: Partial<IFeatureFlags>;
13
+ isMobile?: boolean;
13
14
  serverConfig?: GlobalServerConfig;
14
15
  }
15
16
 
16
17
  export const ServerConfigStoreProvider = memo<GlobalStoreProviderProps>(
17
- ({ children, featureFlags, serverConfig }) => (
18
- <Provider createStore={() => createServerConfigStore({ featureFlags, serverConfig })}>
18
+ ({ children, featureFlags, serverConfig, isMobile }) => (
19
+ <Provider createStore={() => createServerConfigStore({ featureFlags, isMobile, serverConfig })}>
19
20
  {children}
20
21
  </Provider>
21
22
  ),
@@ -8,4 +8,5 @@ export const featureFlagsSelectors = (s: ServerConfigStore) =>
8
8
  export const serverConfigSelectors = {
9
9
  enabledOAuthSSO: (s: ServerConfigStore) => s.serverConfig.enabledOAuthSSO,
10
10
  enabledTelemetryChat: (s: ServerConfigStore) => s.serverConfig.telemetry.langfuse || false,
11
+ isMobile: (s: ServerConfigStore) => s.isMobile || false,
11
12
  };
@@ -20,6 +20,7 @@ const initialState: ServerConfigStore = {
20
20
 
21
21
  export interface ServerConfigStore {
22
22
  featureFlags: IFeatureFlags;
23
+ isMobile?: boolean;
23
24
  serverConfig: GlobalServerConfig;
24
25
  }
25
26
 
@@ -1,11 +1,13 @@
1
- import { UserCommonState, initialCommonState } from './slices/common/initialState';
1
+ import { UserAuthState, initialAuthState } from './slices/auth/initialState';
2
2
  import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState';
3
3
  import { UserSettingsState, initialSettingsState } from './slices/settings/initialState';
4
+ import { UserSyncState, initialSyncState } from './slices/sync/initialState';
4
5
 
5
- export type UserState = UserCommonState & UserSettingsState & UserPreferenceState;
6
+ export type UserState = UserSyncState & UserSettingsState & UserPreferenceState & UserAuthState;
6
7
 
7
8
  export const initialState: UserState = {
8
- ...initialCommonState,
9
+ ...initialSyncState,
9
10
  ...initialSettingsState,
10
11
  ...initialPreferenceState,
12
+ ...initialAuthState,
11
13
  };
@@ -1,4 +1,4 @@
1
- export { commonSelectors } from './slices/common/selectors';
1
+ export { userProfileSelectors } from './slices/auth/selectors';
2
2
  export { preferenceSelectors } from './slices/preference/selectors';
3
3
  export {
4
4
  modelConfigSelectors,
@@ -0,0 +1,118 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { mutate } from 'swr';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { withSWR } from '~test-utils';
5
+
6
+ import { userService } from '@/services/user';
7
+ import { useUserStore } from '@/store/user';
8
+ import { switchLang } from '@/utils/client/switchLang';
9
+
10
+ vi.mock('zustand/traditional');
11
+
12
+ vi.mock('@/utils/client/switchLang', () => ({
13
+ switchLang: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('swr', async (importOriginal) => {
17
+ const modules = await importOriginal();
18
+ return {
19
+ ...(modules as any),
20
+ mutate: vi.fn(),
21
+ };
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ describe('createAuthSlice', () => {
29
+ describe('refreshUserConfig', () => {
30
+ it('should refresh user config', async () => {
31
+ const { result } = renderHook(() => useUserStore());
32
+
33
+ await act(async () => {
34
+ await result.current.refreshUserConfig();
35
+ });
36
+
37
+ expect(mutate).toHaveBeenCalledWith(['fetchUserConfig', true]);
38
+ });
39
+ });
40
+
41
+ describe('useFetchUserConfig', () => {
42
+ it('should not fetch user config if initServer is false', async () => {
43
+ const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况
44
+ vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
45
+
46
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(false), {
47
+ wrapper: withSWR,
48
+ });
49
+
50
+ // 因为 initServer 为 false,所以不会触发 getUserConfig 的调用
51
+ expect(userService.getUserConfig).not.toHaveBeenCalled();
52
+ // 确保状态未改变
53
+ expect(result.current.data).toBeUndefined();
54
+ });
55
+
56
+ it('should fetch user config correctly when initServer is true', async () => {
57
+ const mockUserConfig: any = {
58
+ avatar: 'new-avatar-url',
59
+ settings: {
60
+ language: 'en',
61
+ },
62
+ };
63
+ vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
64
+
65
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
66
+ wrapper: withSWR,
67
+ });
68
+
69
+ // 等待 SWR 完成数据获取
70
+ await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
71
+
72
+ // 验证状态是否正确更新
73
+ expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
74
+ expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
75
+
76
+ // 验证是否正确处理了语言设置
77
+ expect(switchLang).not.toHaveBeenCalledWith('auto');
78
+ });
79
+ it('should call switch language when language is auto', async () => {
80
+ const mockUserConfig: any = {
81
+ avatar: 'new-avatar-url',
82
+ settings: {
83
+ language: 'auto',
84
+ },
85
+ };
86
+ vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
87
+
88
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
89
+ wrapper: withSWR,
90
+ });
91
+
92
+ // 等待 SWR 完成数据获取
93
+ await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
94
+
95
+ // 验证状态是否正确更新
96
+ expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
97
+ expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
98
+
99
+ // 验证是否正确处理了语言设置
100
+ expect(switchLang).toHaveBeenCalledWith('auto');
101
+ });
102
+
103
+ it('should handle the case when user config is null', async () => {
104
+ vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(null as any);
105
+
106
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
107
+ wrapper: withSWR,
108
+ });
109
+
110
+ // 等待 SWR 完成数据获取
111
+ await waitFor(() => expect(result.current.data).toBeNull());
112
+
113
+ // 验证状态未被错误更新
114
+ expect(useUserStore.getState().avatar).toBeUndefined();
115
+ expect(useUserStore.getState().settings).toEqual({});
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,81 @@
1
+ import useSWR, { SWRResponse, mutate } from 'swr';
2
+ import { StateCreator } from 'zustand/vanilla';
3
+
4
+ import { UserConfig, userService } from '@/services/user';
5
+ import { switchLang } from '@/utils/client/switchLang';
6
+ import { setNamespace } from '@/utils/storeDebug';
7
+
8
+ import { UserStore } from '../../store';
9
+ import { settingsSelectors } from '../settings/selectors';
10
+
11
+ const n = setNamespace('auth');
12
+ const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
13
+
14
+ export interface UserAuthAction {
15
+ getUserConfig: () => void;
16
+ /**
17
+ * universal login method
18
+ */
19
+ login: () => Promise<void>;
20
+ /**
21
+ * universal logout method
22
+ */
23
+ logout: () => Promise<void>;
24
+ refreshUserConfig: () => Promise<void>;
25
+
26
+ useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
27
+ }
28
+
29
+ export const createAuthSlice: StateCreator<
30
+ UserStore,
31
+ [['zustand/devtools', never]],
32
+ [],
33
+ UserAuthAction
34
+ > = (set, get) => ({
35
+ getUserConfig: () => {
36
+ console.log(n('userconfig'));
37
+ },
38
+ login: async () => {
39
+ // TODO: 针对开启 next-auth 的场景,需要在这里调用登录方法
40
+ console.log(n('login'));
41
+ },
42
+ logout: async () => {
43
+ // TODO: 针对开启 next-auth 的场景,需要在这里调用登录方法
44
+ console.log(n('logout'));
45
+ },
46
+ refreshUserConfig: async () => {
47
+ await mutate([USER_CONFIG_FETCH_KEY, true]);
48
+
49
+ // when get the user config ,refresh the model provider list to the latest
50
+ get().refreshModelProviderList();
51
+ },
52
+
53
+ useFetchUserConfig: (initServer) =>
54
+ useSWR<UserConfig | undefined>(
55
+ [USER_CONFIG_FETCH_KEY, initServer],
56
+ async () => {
57
+ if (!initServer) return;
58
+ return userService.getUserConfig();
59
+ },
60
+ {
61
+ onSuccess: (data) => {
62
+ if (!data) return;
63
+
64
+ set(
65
+ { avatar: data.avatar, settings: data.settings, userId: data.uuid },
66
+ false,
67
+ n('fetchUserConfig', data),
68
+ );
69
+
70
+ // when get the user config ,refresh the model provider list to the latest
71
+ get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' });
72
+
73
+ const { language } = settingsSelectors.currentSettings(get());
74
+ if (language === 'auto') {
75
+ switchLang('auto');
76
+ }
77
+ },
78
+ revalidateOnFocus: false,
79
+ },
80
+ ),
81
+ });
@@ -0,0 +1,20 @@
1
+ export interface LobeUser {
2
+ avatar?: string;
3
+ firstName?: string | null;
4
+ fullName?: string | null;
5
+ id: string;
6
+ latestName?: string | null;
7
+ username?: string | null;
8
+ }
9
+
10
+ export interface UserAuthState {
11
+ /**
12
+ * @deprecated
13
+ */
14
+ avatar?: string;
15
+ isSignedIn?: boolean;
16
+ user?: LobeUser;
17
+ userId?: string;
18
+ }
19
+
20
+ export const initialAuthState: UserAuthState = {};
@@ -0,0 +1,6 @@
1
+ import { UserStore } from '@/store/user';
2
+
3
+ export const userProfileSelectors = {
4
+ userAvatar: (s: UserStore): string => s.avatar || '',
5
+ userId: (s: UserStore) => s.userId,
6
+ };
@@ -8,17 +8,10 @@ import { messageService } from '@/services/message';
8
8
  import { userService } from '@/services/user';
9
9
  import { useUserStore } from '@/store/user';
10
10
  import { preferenceSelectors } from '@/store/user/selectors';
11
- import { commonSelectors } from '@/store/user/slices/common/selectors';
12
- import { syncSettingsSelectors } from '@/store/user/slices/settings/selectors';
13
11
  import { GlobalServerConfig } from '@/types/serverConfig';
14
- import { switchLang } from '@/utils/client/switchLang';
15
12
 
16
13
  vi.mock('zustand/traditional');
17
14
 
18
- vi.mock('@/utils/client/switchLang', () => ({
19
- switchLang: vi.fn(),
20
- }));
21
-
22
15
  vi.mock('swr', async (importOriginal) => {
23
16
  const modules = await importOriginal();
24
17
  return {
@@ -32,18 +25,6 @@ afterEach(() => {
32
25
  });
33
26
 
34
27
  describe('createCommonSlice', () => {
35
- describe('refreshUserConfig', () => {
36
- it('should refresh user config', async () => {
37
- const { result } = renderHook(() => useUserStore());
38
-
39
- await act(async () => {
40
- await result.current.refreshUserConfig();
41
- });
42
-
43
- expect(mutate).toHaveBeenCalledWith(['fetchUserConfig', true]);
44
- });
45
- });
46
-
47
28
  describe('updateAvatar', () => {
48
29
  it('should update avatar', async () => {
49
30
  const { result } = renderHook(() => useUserStore());
@@ -76,167 +57,6 @@ describe('createCommonSlice', () => {
76
57
  });
77
58
  });
78
59
 
79
- describe('useFetchUserConfig', () => {
80
- it('should not fetch user config if initServer is false', async () => {
81
- const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况
82
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
83
-
84
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(false), {
85
- wrapper: withSWR,
86
- });
87
-
88
- // 因为 initServer 为 false,所以不会触发 getUserConfig 的调用
89
- expect(userService.getUserConfig).not.toHaveBeenCalled();
90
- // 确保状态未改变
91
- expect(result.current.data).toBeUndefined();
92
- });
93
-
94
- it('should fetch user config correctly when initServer is true', async () => {
95
- const mockUserConfig: any = {
96
- avatar: 'new-avatar-url',
97
- settings: {
98
- language: 'en',
99
- },
100
- };
101
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
102
-
103
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
104
- wrapper: withSWR,
105
- });
106
-
107
- // 等待 SWR 完成数据获取
108
- await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
109
-
110
- // 验证状态是否正确更新
111
- expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
112
- expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
113
-
114
- // 验证是否正确处理了语言设置
115
- expect(switchLang).not.toHaveBeenCalledWith('auto');
116
- });
117
- it('should call switch language when language is auto', async () => {
118
- const mockUserConfig: any = {
119
- avatar: 'new-avatar-url',
120
- settings: {
121
- language: 'auto',
122
- },
123
- };
124
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
125
-
126
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
127
- wrapper: withSWR,
128
- });
129
-
130
- // 等待 SWR 完成数据获取
131
- await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
132
-
133
- // 验证状态是否正确更新
134
- expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
135
- expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
136
-
137
- // 验证是否正确处理了语言设置
138
- expect(switchLang).toHaveBeenCalledWith('auto');
139
- });
140
-
141
- it('should handle the case when user config is null', async () => {
142
- vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(null as any);
143
-
144
- const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
145
- wrapper: withSWR,
146
- });
147
-
148
- // 等待 SWR 完成数据获取
149
- await waitFor(() => expect(result.current.data).toBeNull());
150
-
151
- // 验证状态未被错误更新
152
- expect(useUserStore.getState().avatar).toBeUndefined();
153
- expect(useUserStore.getState().settings).toEqual({});
154
- });
155
- });
156
-
157
- describe('refreshConnection', () => {
158
- it('should not call triggerEnableSync when userId is empty', async () => {
159
- const { result } = renderHook(() => useUserStore());
160
- const onEvent = vi.fn();
161
-
162
- vi.spyOn(commonSelectors, 'userId').mockReturnValueOnce(undefined);
163
- const triggerEnableSyncSpy = vi.spyOn(result.current, 'triggerEnableSync');
164
-
165
- await act(async () => {
166
- await result.current.refreshConnection(onEvent);
167
- });
168
-
169
- expect(triggerEnableSyncSpy).not.toHaveBeenCalled();
170
- });
171
-
172
- it('should call triggerEnableSync when userId exists', async () => {
173
- const { result } = renderHook(() => useUserStore());
174
- const onEvent = vi.fn();
175
- const userId = 'user-id';
176
-
177
- vi.spyOn(commonSelectors, 'userId').mockReturnValueOnce(userId);
178
- const triggerEnableSyncSpy = vi.spyOn(result.current, 'triggerEnableSync');
179
-
180
- await act(async () => {
181
- await result.current.refreshConnection(onEvent);
182
- });
183
-
184
- expect(triggerEnableSyncSpy).toHaveBeenCalledWith(userId, onEvent);
185
- });
186
- });
187
-
188
- describe('triggerEnableSync', () => {
189
- it('should return false when sync.channelName is empty', async () => {
190
- const { result } = renderHook(() => useUserStore());
191
- const userId = 'user-id';
192
- const onEvent = vi.fn();
193
-
194
- vi.spyOn(syncSettingsSelectors, 'webrtcConfig').mockReturnValueOnce({
195
- channelName: '',
196
- enabled: true,
197
- });
198
-
199
- const data = await act(async () => {
200
- return result.current.triggerEnableSync(userId, onEvent);
201
- });
202
-
203
- expect(data).toBe(false);
204
- });
205
-
206
- it('should call globalService.enabledSync when sync.channelName exists', async () => {
207
- const userId = 'user-id';
208
- const onEvent = vi.fn();
209
- const channelName = 'channel-name';
210
- const channelPassword = 'channel-password';
211
- const deviceName = 'device-name';
212
- const signaling = 'signaling';
213
-
214
- vi.spyOn(syncSettingsSelectors, 'webrtcConfig').mockReturnValueOnce({
215
- channelName,
216
- channelPassword,
217
- signaling,
218
- enabled: true,
219
- });
220
- vi.spyOn(syncSettingsSelectors, 'deviceName').mockReturnValueOnce(deviceName);
221
- const enabledSyncSpy = vi.spyOn(globalService, 'enabledSync').mockResolvedValueOnce(true);
222
- const { result } = renderHook(() => useUserStore());
223
-
224
- const data = await act(async () => {
225
- return result.current.triggerEnableSync(userId, onEvent);
226
- });
227
-
228
- expect(enabledSyncSpy).toHaveBeenCalledWith({
229
- channel: { name: channelName, password: channelPassword },
230
- onAwarenessChange: expect.any(Function),
231
- onSyncEvent: onEvent,
232
- onSyncStatusChange: expect.any(Function),
233
- signaling,
234
- user: expect.objectContaining({ id: userId, name: deviceName }),
235
- });
236
- expect(data).toBe(true);
237
- });
238
- });
239
-
240
60
  describe('useCheckTrace', () => {
241
61
  it('should return false when shouldFetch is false', async () => {
242
62
  const { result } = renderHook(() => useUserStore().useCheckTrace(false), {
@@ -270,47 +90,4 @@ describe('createCommonSlice', () => {
270
90
  expect(messageCountToCheckTraceSpy).toHaveBeenCalled();
271
91
  });
272
92
  });
273
-
274
- describe('useEnabledSync', () => {
275
- it('should return false when userId is empty', async () => {
276
- const { result } = renderHook(() => useUserStore().useEnabledSync(true, undefined, vi.fn()), {
277
- wrapper: withSWR,
278
- });
279
-
280
- await waitFor(() => expect(result.current.data).toBe(false));
281
- });
282
-
283
- it('should call globalService.disableSync when userEnableSync is false', async () => {
284
- const disableSyncSpy = vi.spyOn(globalService, 'disableSync').mockResolvedValueOnce(false);
285
-
286
- const { result } = renderHook(
287
- () => useUserStore().useEnabledSync(false, 'user-id', vi.fn()),
288
- { wrapper: withSWR },
289
- );
290
-
291
- await waitFor(() => expect(result.current.data).toBeUndefined());
292
- expect(disableSyncSpy).toHaveBeenCalled();
293
- });
294
-
295
- it('should call triggerEnableSync when userEnableSync and userId exist', async () => {
296
- const userId = 'user-id';
297
- const onEvent = vi.fn();
298
- const triggerEnableSyncSpy = vi.fn().mockResolvedValueOnce(true);
299
-
300
- const { result } = renderHook(() => useUserStore());
301
-
302
- // replace triggerEnableSync as a mock
303
- result.current.triggerEnableSync = triggerEnableSyncSpy;
304
-
305
- const { result: swrResult } = renderHook(
306
- () => result.current.useEnabledSync(true, userId, onEvent),
307
- {
308
- wrapper: withSWR,
309
- },
310
- );
311
-
312
- await waitFor(() => expect(swrResult.current.data).toBe(true));
313
- expect(triggerEnableSyncSpy).toHaveBeenCalledWith(userId, onEvent);
314
- });
315
- });
316
93
  });
@@ -1,22 +1,17 @@
1
- import useSWR, { SWRResponse, mutate } from 'swr';
1
+ import useSWR, { SWRResponse } from 'swr';
2
2
  import { DeepPartial } from 'utility-types';
3
3
  import type { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { globalService } from '@/services/global';
6
6
  import { messageService } from '@/services/message';
7
- import { UserConfig, userService } from '@/services/user';
7
+ import { userService } from '@/services/user';
8
8
  import type { UserStore } from '@/store/user';
9
9
  import type { GlobalServerConfig } from '@/types/serverConfig';
10
10
  import type { GlobalSettings } from '@/types/settings';
11
- import { OnSyncEvent, PeerSyncStatus } from '@/types/sync';
12
- import { switchLang } from '@/utils/client/switchLang';
13
11
  import { merge } from '@/utils/merge';
14
- import { browserInfo } from '@/utils/platform';
15
12
  import { setNamespace } from '@/utils/storeDebug';
16
13
 
17
14
  import { preferenceSelectors } from '../preference/selectors';
18
- import { settingsSelectors, syncSettingsSelectors } from '../settings/selectors';
19
- import { commonSelectors } from './selectors';
20
15
 
21
16
  const n = setNamespace('common');
22
17
 
@@ -24,79 +19,22 @@ const n = setNamespace('common');
24
19
  * 设置操作
25
20
  */
26
21
  export interface CommonAction {
27
- refreshConnection: (onEvent: OnSyncEvent) => Promise<void>;
28
- refreshUserConfig: () => Promise<void>;
29
- triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise<boolean>;
30
22
  updateAvatar: (avatar: string) => Promise<void>;
31
23
  useCheckTrace: (shouldFetch: boolean) => SWRResponse;
32
- useEnabledSync: (
33
- userEnableSync: boolean,
34
- userId: string | undefined,
35
- onEvent: OnSyncEvent,
36
- ) => SWRResponse;
37
24
  useFetchServerConfig: () => SWRResponse;
38
- useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
39
25
  }
40
26
 
41
- const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
42
-
43
27
  export const createCommonSlice: StateCreator<
44
28
  UserStore,
45
29
  [['zustand/devtools', never]],
46
30
  [],
47
31
  CommonAction
48
32
  > = (set, get) => ({
49
- refreshConnection: async (onEvent) => {
50
- const userId = commonSelectors.userId(get());
51
-
52
- if (!userId) return;
53
-
54
- await get().triggerEnableSync(userId, onEvent);
55
- },
56
-
57
- refreshUserConfig: async () => {
58
- await mutate([USER_CONFIG_FETCH_KEY, true]);
59
-
60
- // when get the user config ,refresh the model provider list to the latest
61
- get().refreshModelProviderList();
62
- },
63
-
64
- triggerEnableSync: async (userId: string, onEvent: OnSyncEvent) => {
65
- // double-check the sync ability
66
- // if there is no channelName, don't start sync
67
- const sync = syncSettingsSelectors.webrtcConfig(get());
68
- if (!sync.channelName) return false;
69
-
70
- const name = syncSettingsSelectors.deviceName(get());
71
-
72
- const defaultUserName = `My ${browserInfo.browser} (${browserInfo.os})`;
73
-
74
- set({ syncStatus: PeerSyncStatus.Connecting });
75
- return globalService.enabledSync({
76
- channel: {
77
- name: sync.channelName,
78
- password: sync.channelPassword,
79
- },
80
- onAwarenessChange(state) {
81
- set({ syncAwareness: state });
82
- },
83
- onSyncEvent: onEvent,
84
- onSyncStatusChange: (status) => {
85
- set({ syncStatus: status });
86
- },
87
- signaling: sync.signaling,
88
- user: {
89
- id: userId,
90
- // if user don't set the name, use default name
91
- name: name || defaultUserName,
92
- ...browserInfo,
93
- },
94
- });
95
- },
96
33
  updateAvatar: async (avatar) => {
97
34
  await userService.updateAvatar(avatar);
98
35
  await get().refreshUserConfig();
99
36
  },
37
+
100
38
  useCheckTrace: (shouldFetch) =>
101
39
  useSWR<boolean>(
102
40
  ['checkTrace', shouldFetch],
@@ -115,25 +53,6 @@ export const createCommonSlice: StateCreator<
115
53
  },
116
54
  ),
117
55
 
118
- useEnabledSync: (userEnableSync, userId, onEvent) =>
119
- useSWR<boolean>(
120
- ['enableSync', userEnableSync, userId],
121
- async () => {
122
- // if user don't enable sync or no userId ,don't start sync
123
- if (!userId) return false;
124
-
125
- // if user don't enable sync, stop sync
126
- if (!userEnableSync) return globalService.disableSync();
127
-
128
- return get().triggerEnableSync(userId, onEvent);
129
- },
130
- {
131
- onSuccess: (syncEnabled) => {
132
- set({ syncEnabled }, false, n('useEnabledSync'));
133
- },
134
- revalidateOnFocus: false,
135
- },
136
- ),
137
56
  useFetchServerConfig: () =>
138
57
  useSWR<GlobalServerConfig>('fetchGlobalConfig', globalService.getGlobalConfig, {
139
58
  onSuccess: (data) => {
@@ -152,32 +71,4 @@ export const createCommonSlice: StateCreator<
152
71
  },
153
72
  revalidateOnFocus: false,
154
73
  }),
155
- useFetchUserConfig: (initServer) =>
156
- useSWR<UserConfig | undefined>(
157
- [USER_CONFIG_FETCH_KEY, initServer],
158
- async () => {
159
- if (!initServer) return;
160
- return userService.getUserConfig();
161
- },
162
- {
163
- onSuccess: (data) => {
164
- if (!data) return;
165
-
166
- set(
167
- { avatar: data.avatar, settings: data.settings, userId: data.uuid },
168
- false,
169
- n('fetchUserConfig', data),
170
- );
171
-
172
- // when get the user config ,refresh the model provider list to the latest
173
- get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' });
174
-
175
- const { language } = settingsSelectors.currentSettings(get());
176
- if (language === 'auto') {
177
- switchLang('auto');
178
- }
179
- },
180
- revalidateOnFocus: false,
181
- },
182
- ),
183
74
  });
@@ -7,14 +7,12 @@ import { GlobalServerConfig } from '@/types/serverConfig';
7
7
  import { GlobalSettings } from '@/types/settings';
8
8
 
9
9
  export interface UserSettingsState {
10
- avatar?: string;
11
10
  defaultModelProviderList: ModelProviderCard[];
12
11
  defaultSettings: GlobalSettings;
13
12
  editingCustomCardModel?: { id: string; provider: string } | undefined;
14
13
  modelProviderList: ModelProviderCard[];
15
14
  serverConfig: GlobalServerConfig;
16
15
  settings: DeepPartial<GlobalSettings>;
17
- userId?: string;
18
16
  }
19
17
 
20
18
  export const initialSettingsState: UserSettingsState = {
@@ -0,0 +1,150 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { withSWR } from '~test-utils';
4
+
5
+ import { globalService } from '@/services/global';
6
+ import { useUserStore } from '@/store/user';
7
+ import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
8
+ import { syncSettingsSelectors } from '@/store/user/slices/settings/selectors';
9
+
10
+ vi.mock('zustand/traditional');
11
+
12
+ vi.mock('swr', async (importOriginal) => {
13
+ const modules = await importOriginal();
14
+ return {
15
+ ...(modules as any),
16
+ mutate: vi.fn(),
17
+ };
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ describe('createSyncSlice', () => {
25
+ describe('refreshConnection', () => {
26
+ it('should not call triggerEnableSync when userId is empty', async () => {
27
+ const { result } = renderHook(() => useUserStore());
28
+ const onEvent = vi.fn();
29
+
30
+ vi.spyOn(userProfileSelectors, 'userId').mockReturnValueOnce(undefined as any);
31
+ const triggerEnableSyncSpy = vi.spyOn(result.current, 'triggerEnableSync');
32
+
33
+ await act(async () => {
34
+ await result.current.refreshConnection(onEvent);
35
+ });
36
+
37
+ expect(triggerEnableSyncSpy).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it('should call triggerEnableSync when userId exists', async () => {
41
+ const { result } = renderHook(() => useUserStore());
42
+ const onEvent = vi.fn();
43
+ const userId = 'user-id';
44
+
45
+ vi.spyOn(userProfileSelectors, 'userId').mockReturnValueOnce(userId);
46
+ const triggerEnableSyncSpy = vi.spyOn(result.current, 'triggerEnableSync');
47
+
48
+ await act(async () => {
49
+ await result.current.refreshConnection(onEvent);
50
+ });
51
+
52
+ expect(triggerEnableSyncSpy).toHaveBeenCalledWith(userId, onEvent);
53
+ });
54
+ });
55
+
56
+ describe('triggerEnableSync', () => {
57
+ it('should return false when sync.channelName is empty', async () => {
58
+ const { result } = renderHook(() => useUserStore());
59
+ const userId = 'user-id';
60
+ const onEvent = vi.fn();
61
+
62
+ vi.spyOn(syncSettingsSelectors, 'webrtcConfig').mockReturnValueOnce({
63
+ channelName: '',
64
+ enabled: true,
65
+ });
66
+
67
+ const data = await act(async () => {
68
+ return result.current.triggerEnableSync(userId, onEvent);
69
+ });
70
+
71
+ expect(data).toBe(false);
72
+ });
73
+
74
+ it('should call globalService.enabledSync when sync.channelName exists', async () => {
75
+ const userId = 'user-id';
76
+ const onEvent = vi.fn();
77
+ const channelName = 'channel-name';
78
+ const channelPassword = 'channel-password';
79
+ const deviceName = 'device-name';
80
+ const signaling = 'signaling';
81
+
82
+ vi.spyOn(syncSettingsSelectors, 'webrtcConfig').mockReturnValueOnce({
83
+ channelName,
84
+ channelPassword,
85
+ signaling,
86
+ enabled: true,
87
+ });
88
+ vi.spyOn(syncSettingsSelectors, 'deviceName').mockReturnValueOnce(deviceName);
89
+ const enabledSyncSpy = vi.spyOn(globalService, 'enabledSync').mockResolvedValueOnce(true);
90
+ const { result } = renderHook(() => useUserStore());
91
+
92
+ const data = await act(async () => {
93
+ return result.current.triggerEnableSync(userId, onEvent);
94
+ });
95
+
96
+ expect(enabledSyncSpy).toHaveBeenCalledWith({
97
+ channel: { name: channelName, password: channelPassword },
98
+ onAwarenessChange: expect.any(Function),
99
+ onSyncEvent: onEvent,
100
+ onSyncStatusChange: expect.any(Function),
101
+ signaling,
102
+ user: expect.objectContaining({ id: userId, name: deviceName }),
103
+ });
104
+ expect(data).toBe(true);
105
+ });
106
+ });
107
+
108
+ describe('useEnabledSync', () => {
109
+ it('should return false when userId is empty', async () => {
110
+ const { result } = renderHook(() => useUserStore().useEnabledSync(true, undefined, vi.fn()), {
111
+ wrapper: withSWR,
112
+ });
113
+
114
+ await waitFor(() => expect(result.current.data).toBe(false));
115
+ });
116
+
117
+ it('should call globalService.disableSync when userEnableSync is false', async () => {
118
+ const disableSyncSpy = vi.spyOn(globalService, 'disableSync').mockResolvedValueOnce(false);
119
+
120
+ const { result } = renderHook(
121
+ () => useUserStore().useEnabledSync(false, 'user-id', vi.fn()),
122
+ { wrapper: withSWR },
123
+ );
124
+
125
+ await waitFor(() => expect(result.current.data).toBeUndefined());
126
+ expect(disableSyncSpy).toHaveBeenCalled();
127
+ });
128
+
129
+ it('should call triggerEnableSync when userEnableSync and userId exist', async () => {
130
+ const userId = 'user-id';
131
+ const onEvent = vi.fn();
132
+ const triggerEnableSyncSpy = vi.fn().mockResolvedValueOnce(true);
133
+
134
+ const { result } = renderHook(() => useUserStore());
135
+
136
+ // replace triggerEnableSync as a mock
137
+ result.current.triggerEnableSync = triggerEnableSyncSpy;
138
+
139
+ const { result: swrResult } = renderHook(
140
+ () => result.current.useEnabledSync(true, userId, onEvent),
141
+ {
142
+ wrapper: withSWR,
143
+ },
144
+ );
145
+
146
+ await waitFor(() => expect(swrResult.current.data).toBe(true));
147
+ expect(triggerEnableSyncSpy).toHaveBeenCalledWith(userId, onEvent);
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,94 @@
1
+ import useSWR, { SWRResponse } from 'swr';
2
+ import type { StateCreator } from 'zustand/vanilla';
3
+
4
+ import { globalService } from '@/services/global';
5
+ import type { UserStore } from '@/store/user';
6
+ import { OnSyncEvent, PeerSyncStatus } from '@/types/sync';
7
+ import { browserInfo } from '@/utils/platform';
8
+ import { setNamespace } from '@/utils/storeDebug';
9
+
10
+ import { userProfileSelectors } from '../auth/selectors';
11
+ import { syncSettingsSelectors } from '../settings/selectors';
12
+
13
+ const n = setNamespace('sync');
14
+
15
+ /**
16
+ * 设置操作
17
+ */
18
+ export interface SyncAction {
19
+ refreshConnection: (onEvent: OnSyncEvent) => Promise<void>;
20
+ triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise<boolean>;
21
+ useEnabledSync: (
22
+ userEnableSync: boolean,
23
+ userId: string | undefined,
24
+ onEvent: OnSyncEvent,
25
+ ) => SWRResponse;
26
+ }
27
+
28
+ export const createSyncSlice: StateCreator<
29
+ UserStore,
30
+ [['zustand/devtools', never]],
31
+ [],
32
+ SyncAction
33
+ > = (set, get) => ({
34
+ refreshConnection: async (onEvent) => {
35
+ const userId = userProfileSelectors.userId(get());
36
+
37
+ if (!userId) return;
38
+
39
+ await get().triggerEnableSync(userId, onEvent);
40
+ },
41
+
42
+ triggerEnableSync: async (userId: string, onEvent: OnSyncEvent) => {
43
+ // double-check the sync ability
44
+ // if there is no channelName, don't start sync
45
+ const sync = syncSettingsSelectors.webrtcConfig(get());
46
+ if (!sync.channelName) return false;
47
+
48
+ const name = syncSettingsSelectors.deviceName(get());
49
+
50
+ const defaultUserName = `My ${browserInfo.browser} (${browserInfo.os})`;
51
+
52
+ set({ syncStatus: PeerSyncStatus.Connecting });
53
+ return globalService.enabledSync({
54
+ channel: {
55
+ name: sync.channelName,
56
+ password: sync.channelPassword,
57
+ },
58
+ onAwarenessChange(state) {
59
+ set({ syncAwareness: state });
60
+ },
61
+ onSyncEvent: onEvent,
62
+ onSyncStatusChange: (status) => {
63
+ set({ syncStatus: status });
64
+ },
65
+ signaling: sync.signaling,
66
+ user: {
67
+ id: userId,
68
+ // if user don't set the name, use default name
69
+ name: name || defaultUserName,
70
+ ...browserInfo,
71
+ },
72
+ });
73
+ },
74
+
75
+ useEnabledSync: (userEnableSync, userId, onEvent) =>
76
+ useSWR<boolean>(
77
+ ['enableSync', userEnableSync, userId],
78
+ async () => {
79
+ // if user don't enable sync or no userId ,don't start sync
80
+ if (!userId) return false;
81
+
82
+ // if user don't enable sync, stop sync
83
+ if (!userEnableSync) return globalService.disableSync();
84
+
85
+ return get().triggerEnableSync(userId, onEvent);
86
+ },
87
+ {
88
+ onSuccess: (syncEnabled) => {
89
+ set({ syncEnabled }, false, n('useEnabledSync'));
90
+ },
91
+ revalidateOnFocus: false,
92
+ },
93
+ ),
94
+ });
@@ -1,17 +1,12 @@
1
1
  import { PeerSyncStatus, SyncAwarenessState } from '@/types/sync';
2
2
 
3
- export interface Guide {
4
- // Topic 引导
5
- topic?: boolean;
6
- }
7
-
8
- export interface UserCommonState {
3
+ export interface UserSyncState {
9
4
  syncAwareness: SyncAwarenessState[];
10
5
  syncEnabled: boolean;
11
6
  syncStatus: PeerSyncStatus;
12
7
  }
13
8
 
14
- export const initialCommonState: UserCommonState = {
9
+ export const initialSyncState: UserSyncState = {
15
10
  syncAwareness: [],
16
11
  syncEnabled: false,
17
12
  syncStatus: PeerSyncStatus.Disabled,
@@ -6,19 +6,28 @@ import { StateCreator } from 'zustand/vanilla';
6
6
  import { isDev } from '@/utils/env';
7
7
 
8
8
  import { type UserState, initialState } from './initialState';
9
+ import { type UserAuthAction, createAuthSlice } from './slices/auth/action';
9
10
  import { type CommonAction, createCommonSlice } from './slices/common/action';
10
11
  import { type PreferenceAction, createPreferenceSlice } from './slices/preference/action';
11
12
  import { type SettingsAction, createSettingsSlice } from './slices/settings/actions';
13
+ import { type SyncAction, createSyncSlice } from './slices/sync/action';
12
14
 
13
15
  // =============== 聚合 createStoreFn ============ //
14
16
 
15
- export type UserStore = CommonAction & UserState & SettingsAction & PreferenceAction;
17
+ export type UserStore = SyncAction &
18
+ UserState &
19
+ SettingsAction &
20
+ PreferenceAction &
21
+ UserAuthAction &
22
+ CommonAction;
16
23
 
17
24
  const createStore: StateCreator<UserStore, [['zustand/devtools', never]]> = (...parameters) => ({
18
25
  ...initialState,
19
- ...createCommonSlice(...parameters),
26
+ ...createSyncSlice(...parameters),
20
27
  ...createSettingsSlice(...parameters),
21
28
  ...createPreferenceSlice(...parameters),
29
+ ...createAuthSlice(...parameters),
30
+ ...createCommonSlice(...parameters),
22
31
  });
23
32
 
24
33
  // =============== 实装 useStore ============ //
@@ -1,6 +0,0 @@
1
- import { UserStore } from '@/store/user';
2
-
3
- export const commonSelectors = {
4
- userAvatar: (s: UserStore) => s.avatar || '',
5
- userId: (s: UserStore) => s.userId,
6
- };