@lobehub/chat 0.152.6 → 0.152.8

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.
Files changed (206) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/settings-url-share.mdx +100 -0
  3. package/docs/self-hosting/advanced/settings-url-share.zh-CN.mdx +100 -0
  4. package/locales/ar/common.json +1 -0
  5. package/locales/ar/setting.json +4 -1
  6. package/locales/bg-BG/common.json +1 -0
  7. package/locales/bg-BG/setting.json +4 -1
  8. package/locales/de-DE/common.json +1 -0
  9. package/locales/de-DE/setting.json +4 -1
  10. package/locales/en-US/common.json +1 -0
  11. package/locales/en-US/setting.json +4 -1
  12. package/locales/es-ES/common.json +1 -0
  13. package/locales/es-ES/setting.json +4 -1
  14. package/locales/fr-FR/common.json +1 -0
  15. package/locales/fr-FR/setting.json +4 -1
  16. package/locales/it-IT/common.json +1 -0
  17. package/locales/it-IT/setting.json +4 -1
  18. package/locales/ja-JP/common.json +1 -0
  19. package/locales/ja-JP/setting.json +4 -1
  20. package/locales/ko-KR/common.json +1 -0
  21. package/locales/ko-KR/setting.json +4 -1
  22. package/locales/nl-NL/common.json +1 -0
  23. package/locales/nl-NL/setting.json +4 -1
  24. package/locales/pl-PL/common.json +1 -0
  25. package/locales/pl-PL/setting.json +4 -1
  26. package/locales/pt-BR/common.json +1 -0
  27. package/locales/pt-BR/setting.json +4 -1
  28. package/locales/ru-RU/common.json +1 -0
  29. package/locales/ru-RU/setting.json +4 -1
  30. package/locales/tr-TR/common.json +1 -0
  31. package/locales/tr-TR/setting.json +4 -1
  32. package/locales/vi-VN/common.json +1 -0
  33. package/locales/vi-VN/setting.json +4 -1
  34. package/locales/zh-CN/common.json +1 -0
  35. package/locales/zh-CN/setting.json +4 -1
  36. package/locales/zh-TW/common.json +1 -0
  37. package/locales/zh-TW/setting.json +4 -1
  38. package/package.json +1 -1
  39. package/src/app/(main)/(mobile)/me/features/AvatarBanner.tsx +52 -0
  40. package/src/app/(main)/(mobile)/me/features/Cate.tsx +35 -0
  41. package/src/app/(main)/(mobile)/me/features/ExtraCate.tsx +26 -0
  42. package/src/app/(main)/(mobile)/me/features/useExtraCate.tsx +62 -0
  43. package/src/app/(main)/(mobile)/me/layout.tsx +11 -0
  44. package/src/app/(main)/(mobile)/me/loading.tsx +17 -0
  45. package/src/app/(main)/(mobile)/me/page.tsx +31 -0
  46. package/src/app/(main)/@nav/_layout/Desktop/index.tsx +1 -1
  47. package/src/app/(main)/@nav/_layout/Mobile.tsx +3 -3
  48. package/src/app/(main)/chat/(mobile)/features/SessionHeader.tsx +3 -3
  49. package/src/app/(main)/chat/_layout/Desktop/index.tsx +6 -8
  50. package/src/app/(main)/chat/_layout/Mobile/index.tsx +5 -3
  51. package/src/app/(main)/chat/_layout/type.ts +5 -0
  52. package/src/app/(main)/chat/features/SettingButton.tsx +3 -4
  53. package/src/app/(main)/chat/features/ShareButton/ShareModal.tsx +2 -2
  54. package/src/app/(main)/chat/layout.ts +5 -2
  55. package/src/app/(main)/chat/settings/{(desktop) → _layout/Desktop}/Header.tsx +2 -0
  56. package/src/app/(main)/chat/settings/_layout/Desktop/index.tsx +28 -0
  57. package/src/app/(main)/chat/settings/{(mobile) → _layout/Mobile}/Header.tsx +3 -2
  58. package/src/app/(main)/chat/settings/_layout/Mobile/index.tsx +15 -0
  59. package/src/app/(main)/chat/settings/error.tsx +5 -0
  60. package/src/app/(main)/chat/settings/features/HeaderContent.tsx +15 -9
  61. package/src/app/(main)/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +2 -0
  62. package/src/app/(main)/chat/settings/features/SubmitAgentButton/index.tsx +16 -8
  63. package/src/app/(main)/chat/settings/layout.tsx +9 -2
  64. package/src/app/(main)/chat/settings/loading.tsx +3 -0
  65. package/src/app/(main)/chat/settings/not-found.tsx +3 -0
  66. package/src/app/(main)/chat/settings/page.tsx +2 -9
  67. package/src/app/(main)/market/@detail/default.tsx +1 -10
  68. package/src/app/(main)/market/{@detail/_layout/Desktop.tsx → _layout/Desktop/DetailSidebar.tsx} +2 -2
  69. package/src/app/(main)/market/_layout/Desktop/index.tsx +2 -1
  70. package/src/app/(main)/market/{@detail/_layout/Mobile.tsx → _layout/Mobile/DetailModal.tsx} +2 -2
  71. package/src/app/(main)/market/_layout/Mobile/index.tsx +3 -1
  72. package/src/app/(main)/settings/@category/default.tsx +16 -0
  73. package/src/app/(main)/settings/@category/features/CategoryContent.tsx +33 -0
  74. package/src/app/(main)/settings/@category/features/UpgradeAlert.tsx +38 -0
  75. package/src/app/(main)/settings/_layout/Desktop/Header.tsx +78 -23
  76. package/src/app/(main)/settings/_layout/Desktop/SideBar.tsx +39 -27
  77. package/src/app/(main)/settings/_layout/Desktop/index.tsx +41 -17
  78. package/src/app/(main)/settings/_layout/Mobile/{SubSettingHeader.tsx → Header.tsx} +3 -1
  79. package/src/app/(main)/settings/_layout/Mobile/index.tsx +7 -18
  80. package/src/app/(main)/settings/_layout/type.ts +6 -0
  81. package/src/app/(main)/settings/about/features/AboutList.tsx +134 -0
  82. package/src/app/(main)/settings/about/features/Analytics.tsx +42 -0
  83. package/src/app/(main)/settings/about/index.tsx +46 -0
  84. package/src/app/(main)/settings/about/page.tsx +13 -33
  85. package/src/app/(main)/settings/agent/{Agent.tsx → index.tsx} +8 -4
  86. package/src/app/(main)/settings/agent/page.tsx +8 -16
  87. package/src/app/(main)/settings/common/{Common.tsx → features/Common.tsx} +6 -4
  88. package/src/app/(main)/settings/common/{Theme.tsx → features/Theme/index.tsx} +8 -6
  89. package/src/app/(main)/settings/common/index.tsx +11 -16
  90. package/src/app/(main)/settings/common/page.tsx +8 -7
  91. package/src/app/(main)/settings/error.tsx +5 -0
  92. package/src/app/(main)/settings/features/Footer.tsx +2 -0
  93. package/src/app/(main)/settings/features/UpgradeAlert.tsx +21 -13
  94. package/src/app/(main)/settings/hooks/useCategory.tsx +54 -0
  95. package/src/app/(main)/settings/hooks/useSyncSettings.ts +2 -2
  96. package/src/app/(main)/settings/layout.ts +4 -1
  97. package/src/app/(main)/settings/llm/Anthropic/index.tsx +4 -8
  98. package/src/app/(main)/settings/llm/Azure/index.tsx +3 -1
  99. package/src/app/(main)/settings/llm/Bedrock/index.tsx +3 -1
  100. package/src/app/(main)/settings/llm/Google/index.tsx +4 -2
  101. package/src/app/(main)/settings/llm/Groq/index.tsx +3 -1
  102. package/src/app/(main)/settings/llm/Minimax/index.tsx +3 -9
  103. package/src/app/(main)/settings/llm/Mistral/index.tsx +3 -9
  104. package/src/app/(main)/settings/llm/Moonshot/index.tsx +3 -1
  105. package/src/app/(main)/settings/llm/Ollama/index.tsx +3 -1
  106. package/src/app/(main)/settings/llm/OpenAI/index.tsx +2 -0
  107. package/src/app/(main)/settings/llm/OpenRouter/index.tsx +3 -9
  108. package/src/app/(main)/settings/llm/Perplexity/index.tsx +3 -9
  109. package/src/app/(main)/settings/llm/TogetherAI/index.tsx +3 -9
  110. package/src/app/(main)/settings/llm/ZeroOne/index.tsx +3 -9
  111. package/src/app/(main)/settings/llm/Zhipu/index.tsx +3 -10
  112. package/src/app/(main)/settings/llm/components/Checker.tsx +2 -0
  113. package/src/app/(main)/settings/llm/components/Footer.tsx +26 -0
  114. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +29 -3
  115. package/src/app/(main)/settings/llm/index.tsx +11 -23
  116. package/src/app/(main)/settings/llm/page.tsx +15 -0
  117. package/src/app/(main)/settings/loading.tsx +9 -0
  118. package/src/app/(main)/settings/not-found.tsx +3 -0
  119. package/src/app/(main)/settings/page.tsx +2 -14
  120. package/src/app/(main)/settings/sync/{DeviceInfo → features/DeviceInfo}/Card.tsx +6 -5
  121. package/src/app/(main)/settings/sync/features/DeviceInfo/DeviceName.tsx +63 -0
  122. package/src/app/(main)/settings/sync/{components → features/DeviceInfo}/SystemIcon.tsx +6 -14
  123. package/src/app/(main)/settings/sync/{DeviceInfo → features/DeviceInfo}/index.tsx +22 -36
  124. package/src/app/(main)/settings/sync/{WebRTC → features/WebRTC}/ChannelNameInput.tsx +3 -3
  125. package/src/app/(main)/settings/sync/{WebRTC → features/WebRTC}/index.tsx +10 -9
  126. package/src/app/(main)/settings/sync/index.tsx +17 -0
  127. package/src/app/(main)/settings/sync/page.tsx +11 -15
  128. package/src/app/(main)/settings/tts/features/OpenAI.tsx +54 -0
  129. package/src/app/(main)/settings/tts/{TTS/index.tsx → features/STT.tsx} +11 -27
  130. package/src/app/(main)/settings/tts/index.tsx +15 -0
  131. package/src/app/(main)/settings/tts/page.tsx +8 -16
  132. package/src/app/layout.tsx +6 -2
  133. package/src/components/BrandWatermark/index.tsx +39 -0
  134. package/src/components/Cell/Divider.tsx +19 -0
  135. package/src/components/Cell/index.tsx +38 -0
  136. package/src/components/Menu/index.tsx +97 -0
  137. package/src/components/SkeletonLoading/index.tsx +21 -0
  138. package/src/const/url.ts +2 -0
  139. package/src/features/AgentSetting/AgentChat/index.tsx +135 -0
  140. package/src/features/AgentSetting/AgentMeta/index.tsx +4 -3
  141. package/src/features/AgentSetting/AgentModal/index.tsx +95 -0
  142. package/src/features/AgentSetting/AgentPlugin/index.tsx +65 -66
  143. package/src/features/AgentSetting/AgentPrompt/index.tsx +101 -47
  144. package/src/features/AgentSetting/AgentTTS/index.tsx +4 -0
  145. package/src/features/AgentSetting/StoreUpdater.tsx +2 -0
  146. package/src/features/AgentSetting/index.tsx +6 -6
  147. package/src/features/AgentSetting/store/index.ts +2 -0
  148. package/src/features/AvatarWithUpload/index.tsx +4 -2
  149. package/src/hooks/useQuery.test.ts +20 -0
  150. package/src/hooks/useQuery.ts +7 -0
  151. package/src/hooks/useQueryRoute.test.ts +86 -0
  152. package/src/hooks/useQueryRoute.ts +46 -0
  153. package/src/layout/GlobalProvider/index.tsx +7 -1
  154. package/src/locales/default/common.ts +1 -0
  155. package/src/locales/default/setting.ts +3 -0
  156. package/src/services/chat.ts +2 -2
  157. package/src/store/chat/slices/message/selectors.ts +2 -2
  158. package/src/store/global/initialState.ts +1 -0
  159. package/src/store/serverConfig/Provider.tsx +3 -2
  160. package/src/store/serverConfig/selectors.ts +1 -0
  161. package/src/store/serverConfig/store.ts +1 -0
  162. package/src/store/user/initialState.ts +5 -3
  163. package/src/store/user/selectors.ts +1 -1
  164. package/src/store/user/slices/auth/action.test.ts +118 -0
  165. package/src/store/user/slices/auth/action.ts +81 -0
  166. package/src/store/user/slices/auth/initialState.ts +20 -0
  167. package/src/store/user/slices/auth/selectors.ts +6 -0
  168. package/src/store/user/slices/common/action.test.ts +1 -224
  169. package/src/store/user/slices/common/action.ts +3 -112
  170. package/src/store/user/slices/settings/initialState.ts +0 -2
  171. package/src/store/user/slices/sync/action.test.ts +150 -0
  172. package/src/store/user/slices/sync/action.ts +94 -0
  173. package/src/store/user/slices/{common → sync}/initialState.ts +2 -7
  174. package/src/store/user/store.ts +11 -2
  175. package/src/app/(main)/chat/settings/(desktop)/index.tsx +0 -23
  176. package/src/app/(main)/chat/settings/(mobile)/index.tsx +0 -16
  177. package/src/app/(main)/settings/(desktop)/index.tsx +0 -23
  178. package/src/app/(main)/settings/(mobile)/features/AvatarBanner.tsx +0 -68
  179. package/src/app/(main)/settings/(mobile)/features/ExtraList.tsx +0 -65
  180. package/src/app/(main)/settings/(mobile)/index.tsx +0 -53
  181. package/src/app/(main)/settings/about/AboutList.tsx +0 -53
  182. package/src/app/(main)/settings/about/Analytics.tsx +0 -40
  183. package/src/app/(main)/settings/about/style.ts +0 -22
  184. package/src/app/(main)/settings/agent/loading.tsx +0 -3
  185. package/src/app/(main)/settings/common/loading.tsx +0 -3
  186. package/src/app/(main)/settings/features/SettingList/index.tsx +0 -47
  187. package/src/app/(main)/settings/llm/layout.tsx +0 -11
  188. package/src/app/(main)/settings/llm/loading.tsx +0 -3
  189. package/src/app/(main)/settings/sync/DeviceInfo/DeviceName.tsx +0 -66
  190. package/src/app/(main)/settings/sync/PageTitle.tsx +0 -11
  191. package/src/app/(main)/settings/sync/layout.tsx +0 -12
  192. package/src/app/(main)/settings/sync/loading.tsx +0 -3
  193. package/src/app/(main)/settings/tts/loading.tsx +0 -3
  194. package/src/features/AgentSetting/AgentConfig/index.tsx +0 -202
  195. package/src/features/AgentSetting/AgentConfig/useSyncConfig.ts +0 -23
  196. package/src/store/user/slices/common/selectors.ts +0 -6
  197. /package/src/app/(main)/settings/{features/SettingList → about/features}/Item.tsx +0 -0
  198. /package/src/app/(main)/settings/{features → common/features/Theme}/ThemeSwatches/ThemeSwatchesNeutral.tsx +0 -0
  199. /package/src/app/(main)/settings/{features → common/features/Theme}/ThemeSwatches/ThemeSwatchesPrimary.tsx +0 -0
  200. /package/src/app/(main)/settings/{features → common/features/Theme}/ThemeSwatches/index.ts +0 -0
  201. /package/src/app/(main)/settings/sync/{Alert.tsx → features/Alert.tsx} +0 -0
  202. /package/src/app/(main)/settings/sync/{components → features/WebRTC}/SyncSwitch/index.css +0 -0
  203. /package/src/app/(main)/settings/sync/{components → features/WebRTC}/SyncSwitch/index.tsx +0 -0
  204. /package/src/app/(main)/settings/sync/{util.ts → features/WebRTC/generateRandomRoomName.ts} +0 -0
  205. /package/src/app/(main)/settings/tts/{TTS/options.ts → features/const.ts} +0 -0
  206. /package/src/features/AgentSetting/{AgentConfig → AgentModal}/ModelSelect.tsx +0 -0
@@ -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 />
@@ -133,6 +133,7 @@ export default {
133
133
  tab: {
134
134
  chat: '会话',
135
135
  market: '发现',
136
+ me: '我',
136
137
  setting: '设置',
137
138
  },
138
139
  telemetry: {
@@ -27,9 +27,12 @@ export default {
27
27
  },
28
28
  },
29
29
  header: {
30
+ desc: '偏好与模型设置',
30
31
  global: '全局设置',
31
32
  session: '会话设置',
33
+ sessionDesc: '角色设定与会话偏好',
32
34
  sessionWithName: '会话设置 · {{name}}',
35
+ title: '设置',
33
36
  },
34
37
  llm: {
35
38
  checker: {
@@ -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
 
@@ -6,6 +6,7 @@ import { AsyncLocalStorage } from '@/utils/localStorage';
6
6
  export enum SidebarTabKey {
7
7
  Chat = 'chat',
8
8
  Market = 'market',
9
+ Me = 'me',
9
10
  Setting = 'settings',
10
11
  }
11
12
 
@@ -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
+ };
@@ -7,18 +7,11 @@ import { globalService } from '@/services/global';
7
7
  import { messageService } from '@/services/message';
8
8
  import { userService } from '@/services/user';
9
9
  import { useUserStore } from '@/store/user';
10
- import { commonSelectors } from '@/store/user/slices/common/selectors';
11
- import { preferenceSelectors } from '@/store/user/slices/preference/selectors';
12
- import { syncSettingsSelectors } from '@/store/user/slices/settings/selectors';
10
+ import { preferenceSelectors } from '@/store/user/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
  });