@lobehub/chat 0.150.8 → 0.150.10

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 (161) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/upstream-sync.mdx +74 -0
  3. package/docs/self-hosting/advanced/upstream-sync.zh-CN.mdx +71 -0
  4. package/docs/usage/providers/ollama.mdx +1 -1
  5. package/docs/usage/providers/ollama.zh-CN.mdx +1 -1
  6. package/package.json +1 -1
  7. package/src/app/chat/(desktop)/features/ChatHeader/HeaderAction.tsx +2 -2
  8. package/src/app/chat/(desktop)/features/ChatHeader/Tags.tsx +3 -3
  9. package/src/app/chat/(desktop)/features/ChatInput/Footer/DragUpload.tsx +3 -3
  10. package/src/app/chat/(desktop)/features/ChatInput/Footer/SendMore.tsx +3 -3
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +3 -3
  12. package/src/app/chat/(desktop)/features/ChatInput/TextArea.test.tsx +5 -5
  13. package/src/app/chat/(desktop)/features/ChatInput/TextArea.tsx +3 -3
  14. package/src/app/chat/(desktop)/features/SideBar/index.tsx +2 -2
  15. package/src/app/chat/(mobile)/features/SessionHeader.tsx +5 -5
  16. package/src/app/chat/(mobile)/mobile/ChatHeader/index.tsx +2 -2
  17. package/src/app/chat/_layout/Desktop/SessionHeader.tsx +2 -2
  18. package/src/app/chat/features/PluginTag/index.tsx +2 -2
  19. package/src/app/chat/features/SessionListContent/List/index.tsx +2 -2
  20. package/src/app/chat/features/ShareButton/ShareModal.tsx +3 -3
  21. package/src/app/chat/features/TelemetryNotification/index.tsx +6 -4
  22. package/src/app/chat/features/TopicListContent/Topic/index.tsx +2 -2
  23. package/src/app/chat/settings/features/SubmitAgentButton/SubmitAgentModal.tsx +3 -3
  24. package/src/app/settings/(mobile)/index.tsx +3 -3
  25. package/src/app/settings/about/Analytics.tsx +4 -4
  26. package/src/app/settings/about/page.tsx +3 -3
  27. package/src/app/settings/agent/Agent.tsx +4 -4
  28. package/src/app/settings/common/Common.tsx +4 -4
  29. package/src/app/settings/common/Theme.tsx +4 -4
  30. package/src/app/settings/features/SettingList/index.tsx +2 -2
  31. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesNeutral.tsx +3 -3
  32. package/src/app/settings/features/ThemeSwatches/ThemeSwatchesPrimary.tsx +3 -3
  33. package/src/app/settings/hooks/useSyncSettings.ts +3 -3
  34. package/src/app/settings/llm/Azure/index.tsx +3 -3
  35. package/src/app/settings/llm/OpenAI/index.tsx +2 -2
  36. package/src/app/settings/llm/components/ProviderConfig/index.tsx +3 -3
  37. package/src/app/settings/llm/components/ProviderModelList/CustomModelOption.tsx +4 -4
  38. package/src/app/settings/llm/components/ProviderModelList/ModelConfigModal.tsx +4 -4
  39. package/src/app/settings/llm/components/ProviderModelList/ModelFetcher.tsx +6 -6
  40. package/src/app/settings/llm/components/ProviderModelList/Option.tsx +3 -3
  41. package/src/app/settings/llm/components/ProviderModelList/index.tsx +6 -6
  42. package/src/app/settings/sync/Alert.tsx +3 -3
  43. package/src/app/settings/sync/DeviceInfo/DeviceName.tsx +3 -3
  44. package/src/app/settings/sync/WebRTC/index.tsx +2 -2
  45. package/src/app/settings/tts/TTS/index.tsx +4 -4
  46. package/src/chains/__tests__/summaryAgentName.test.ts +2 -2
  47. package/src/chains/__tests__/summaryDescription.test.ts +2 -2
  48. package/src/chains/__tests__/summaryTags.test.ts +2 -2
  49. package/src/chains/__tests__/summaryTitle.test.ts +2 -2
  50. package/src/chains/summaryAgentName.ts +1 -1
  51. package/src/chains/summaryDescription.ts +1 -1
  52. package/src/chains/summaryTags.ts +1 -1
  53. package/src/chains/summaryTitle.ts +1 -1
  54. package/src/features/AgentSetting/AgentConfig/ModelSelect.tsx +3 -6
  55. package/src/features/AgentSetting/AgentMeta/index.tsx +3 -3
  56. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  57. package/src/features/AgentSetting/AgentPrompt/TokenTag.tsx +4 -4
  58. package/src/features/AgentSetting/AgentTTS/index.tsx +3 -3
  59. package/src/features/AvatarWithUpload/index.tsx +3 -3
  60. package/src/features/ChatInput/ActionBar/FileUpload.tsx +3 -3
  61. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +4 -4
  62. package/src/features/ChatInput/ActionBar/Token/index.tsx +3 -3
  63. package/src/features/ChatInput/ActionBar/Tools/index.tsx +5 -5
  64. package/src/features/ChatInput/STT/browser.tsx +4 -4
  65. package/src/features/ChatInput/STT/index.tsx +3 -3
  66. package/src/features/ChatInput/STT/openai.tsx +4 -4
  67. package/src/features/ChatInput/useChatInput.ts +3 -3
  68. package/src/features/Conversation/Error/APIKeyForm/Bedrock.tsx +3 -3
  69. package/src/features/Conversation/Error/APIKeyForm/ProviderApiKeyForm.tsx +3 -3
  70. package/src/features/Conversation/Error/AccessCodeForm.tsx +3 -3
  71. package/src/features/Conversation/Error/InvalidAccessCode.tsx +3 -3
  72. package/src/features/Conversation/Extras/TTS/index.tsx +3 -3
  73. package/src/features/Conversation/Plugins/Render/MarkdownType/index.tsx +3 -3
  74. package/src/features/Conversation/components/ChatItem/index.tsx +3 -3
  75. package/src/features/ModelSwitchPanel/index.tsx +3 -6
  76. package/src/features/PluginDevModal/LocalForm.tsx +3 -3
  77. package/src/features/SyncStatusInspector/DisableSync.tsx +3 -3
  78. package/src/features/SyncStatusInspector/EnableSync.tsx +4 -4
  79. package/src/features/SyncStatusInspector/index.tsx +2 -2
  80. package/src/hooks/_header.ts +4 -4
  81. package/src/hooks/useSyncData.ts +3 -3
  82. package/src/hooks/useTTS.ts +4 -4
  83. package/src/layout/DefaultLayout/Desktop/SideBar/BottomActions.tsx +2 -2
  84. package/src/layout/DefaultLayout/Desktop/SideBar/TopActions.tsx +2 -2
  85. package/src/layout/DefaultLayout/Mobile/index.tsx +1 -1
  86. package/src/layout/GlobalProvider/AppTheme.tsx +4 -4
  87. package/src/layout/GlobalProvider/StoreInitialization.tsx +6 -1
  88. package/src/layout/GlobalProvider/index.tsx +5 -3
  89. package/src/server/globalConfig/index.ts +119 -0
  90. package/src/server/routers/config/index.ts +3 -112
  91. package/src/services/__tests__/chat.test.ts +17 -20
  92. package/src/services/__tests__/tool.test.ts +2 -2
  93. package/src/services/_auth.test.ts +2 -2
  94. package/src/services/_auth.ts +7 -7
  95. package/src/services/_header.ts +4 -4
  96. package/src/services/chat.ts +13 -13
  97. package/src/services/config.ts +4 -4
  98. package/src/services/models.ts +3 -3
  99. package/src/services/ollama.ts +3 -3
  100. package/src/services/session/client.ts +2 -2
  101. package/src/services/tool.ts +1 -1
  102. package/src/services/trace.ts +3 -3
  103. package/src/store/agent/slices/chat/selectors.test.ts +2 -2
  104. package/src/store/chat/slices/message/selectors.test.ts +1 -1
  105. package/src/store/chat/slices/message/selectors.ts +3 -3
  106. package/src/store/global/{slices/preference/action.test.ts → action.test.ts} +65 -13
  107. package/src/store/global/{slices/preference/action.ts → action.ts} +30 -16
  108. package/src/store/global/initialState.ts +58 -8
  109. package/src/store/global/selectors.ts +9 -8
  110. package/src/store/global/store.ts +3 -7
  111. package/src/store/market/action.ts +1 -1
  112. package/src/store/serverConfig/Provider.tsx +22 -0
  113. package/src/store/serverConfig/index.ts +3 -0
  114. package/src/store/serverConfig/selectors.test.ts +72 -0
  115. package/src/store/serverConfig/selectors.ts +11 -0
  116. package/src/store/serverConfig/store.test.ts +53 -0
  117. package/src/store/serverConfig/store.ts +61 -0
  118. package/src/store/session/slices/session/action.ts +3 -3
  119. package/src/store/{global → user}/helpers.ts +2 -2
  120. package/src/store/user/index.ts +1 -0
  121. package/src/store/user/initialState.ts +11 -0
  122. package/src/store/user/selectors.ts +8 -0
  123. package/src/store/{global → user}/slices/common/action.test.ts +29 -81
  124. package/src/store/{global → user}/slices/common/action.ts +2 -20
  125. package/src/store/user/slices/common/initialState.ts +18 -0
  126. package/src/store/user/slices/common/selectors.ts +6 -0
  127. package/src/store/user/slices/preference/action.test.ts +41 -0
  128. package/src/store/user/slices/preference/action.ts +50 -0
  129. package/src/store/user/slices/preference/initialState.ts +33 -0
  130. package/src/store/user/slices/preference/selectors.ts +13 -0
  131. package/src/store/{global → user}/slices/settings/actions/general.test.ts +6 -6
  132. package/src/store/{global → user}/slices/settings/actions/general.ts +2 -2
  133. package/src/store/{global → user}/slices/settings/actions/index.ts +2 -2
  134. package/src/store/{global → user}/slices/settings/actions/llm.test.ts +11 -14
  135. package/src/store/{global → user}/slices/settings/actions/llm.ts +2 -2
  136. package/src/store/{global → user}/slices/settings/initialState.ts +2 -2
  137. package/src/store/{global → user}/slices/settings/selectors/modelConfig.test.ts +8 -8
  138. package/src/store/{global → user}/slices/settings/selectors/modelConfig.ts +12 -12
  139. package/src/store/{global → user}/slices/settings/selectors/modelProvider.test.ts +17 -17
  140. package/src/store/{global → user}/slices/settings/selectors/modelProvider.ts +19 -20
  141. package/src/store/{global → user}/slices/settings/selectors/selectors.test.ts +8 -8
  142. package/src/store/{global → user}/slices/settings/selectors/settings.ts +12 -12
  143. package/src/store/user/slices/settings/selectors/sync.ts +14 -0
  144. package/src/store/user/store.ts +33 -0
  145. package/src/tools/dalle/Render/ToolBar.tsx +3 -3
  146. package/src/utils/localStorage.ts +3 -1
  147. package/src/store/featureFlags/Provider.tsx +0 -18
  148. package/src/store/featureFlags/index.ts +0 -3
  149. package/src/store/featureFlags/selectors.ts +0 -5
  150. package/src/store/featureFlags/store.ts +0 -42
  151. package/src/store/global/slices/common/initialState.ts +0 -42
  152. package/src/store/global/slices/common/selectors.ts +0 -8
  153. package/src/store/global/slices/preference/initialState.ts +0 -51
  154. package/src/store/global/slices/preference/selectors.ts +0 -18
  155. package/src/store/global/slices/settings/selectors/sync.ts +0 -14
  156. /package/src/server/{routers/config → globalConfig}/parseDefaultAgent.test.ts +0 -0
  157. /package/src/server/{routers/config → globalConfig}/parseDefaultAgent.ts +0 -0
  158. /package/src/store/{global → user}/slices/settings/reducers/customModelCard.test.ts +0 -0
  159. /package/src/store/{global → user}/slices/settings/reducers/customModelCard.ts +0 -0
  160. /package/src/store/{global → user}/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +0 -0
  161. /package/src/store/{global → user}/slices/settings/selectors/index.ts +0 -0
@@ -0,0 +1,61 @@
1
+ import { StoreApi } from 'zustand';
2
+ import { createContext } from 'zustand-utils';
3
+ import { devtools } from 'zustand/middleware';
4
+ import { shallow } from 'zustand/shallow';
5
+ import { createWithEqualityFn } from 'zustand/traditional';
6
+ import { StateCreator } from 'zustand/vanilla';
7
+
8
+ import { DEFAULT_FEATURE_FLAGS, IFeatureFlags } from '@/config/featureFlags';
9
+ import { GlobalServerConfig } from '@/types/serverConfig';
10
+ import { isDev } from '@/utils/env';
11
+ import { merge } from '@/utils/merge';
12
+ import { StoreApiWithSelector } from '@/utils/zustand';
13
+
14
+ const initialState: ServerConfigStore = {
15
+ featureFlags: DEFAULT_FEATURE_FLAGS,
16
+ serverConfig: { telemetry: {} },
17
+ };
18
+
19
+ // =============== 聚合 createStoreFn ============ //
20
+
21
+ export interface ServerConfigStore {
22
+ featureFlags: IFeatureFlags;
23
+ serverConfig: GlobalServerConfig;
24
+ }
25
+
26
+ type CreateStore = (
27
+ initState: Partial<ServerConfigStore>,
28
+ ) => StateCreator<ServerConfigStore, [['zustand/devtools', never]]>;
29
+
30
+ const createStore: CreateStore = (runtimeState) => () => ({
31
+ ...merge(initialState, runtimeState),
32
+ });
33
+
34
+ // =============== 实装 useStore ============ //
35
+
36
+ let store: StoreApi<ServerConfigStore>;
37
+
38
+ export const initServerConfigStore = (initState: Partial<ServerConfigStore>) =>
39
+ createWithEqualityFn<ServerConfigStore>()(
40
+ devtools(createStore(initState || {}), {
41
+ name: 'LobeChat_ServerConfig' + (isDev ? '_DEV' : ''),
42
+ }),
43
+ shallow,
44
+ );
45
+
46
+ export const createServerConfigStore = (initState?: Partial<ServerConfigStore>) => {
47
+ // make sure there is only one store
48
+ if (!store) {
49
+ store = createWithEqualityFn<ServerConfigStore>()(
50
+ devtools(createStore(initState || {}), {
51
+ name: 'LobeChat_ServerConfig' + (isDev ? '_DEV' : ''),
52
+ }),
53
+ shallow,
54
+ );
55
+ }
56
+
57
+ return store;
58
+ };
59
+
60
+ export const { useStore: useServerConfigStore, Provider } =
61
+ createContext<StoreApiWithSelector<ServerConfigStore>>();
@@ -8,9 +8,9 @@ import { message } from '@/components/AntdStaticMethods';
8
8
  import { DEFAULT_AGENT_LOBE_SESSION, INBOX_SESSION_ID } from '@/const/session';
9
9
  import { useClientDataSWR } from '@/libs/swr';
10
10
  import { sessionService } from '@/services/session';
11
- import { useGlobalStore } from '@/store/global';
12
- import { settingsSelectors } from '@/store/global/selectors';
13
11
  import { SessionStore } from '@/store/session';
12
+ import { useUserStore } from '@/store/user';
13
+ import { settingsSelectors } from '@/store/user/selectors';
14
14
  import { MetaData } from '@/types/meta';
15
15
  import {
16
16
  ChatSessionList,
@@ -111,7 +111,7 @@ export const createSessionSlice: StateCreator<
111
111
  // merge the defaultAgent in settings
112
112
  const defaultAgent = merge(
113
113
  DEFAULT_AGENT_LOBE_SESSION,
114
- settingsSelectors.defaultAgent(useGlobalStore.getState()),
114
+ settingsSelectors.defaultAgent(useUserStore.getState()),
115
115
  );
116
116
 
117
117
  const newSession: LobeAgentSession = merge(defaultAgent, agent);
@@ -1,7 +1,7 @@
1
1
  import { settingsSelectors } from './slices/settings/selectors';
2
- import { useGlobalStore } from './store';
2
+ import { useUserStore } from './store';
3
3
 
4
- const getCurrentLanguage = () => settingsSelectors.currentLanguage(useGlobalStore.getState());
4
+ const getCurrentLanguage = () => settingsSelectors.currentLanguage(useUserStore.getState());
5
5
 
6
6
  export const globalHelpers = {
7
7
  getCurrentLanguage,
@@ -0,0 +1 @@
1
+ export * from './store';
@@ -0,0 +1,11 @@
1
+ import { UserCommonState, initialCommonState } from './slices/common/initialState';
2
+ import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState';
3
+ import { UserSettingsState, initialSettingsState } from './slices/settings/initialState';
4
+
5
+ export type UserState = UserCommonState & UserSettingsState & UserPreferenceState;
6
+
7
+ export const initialState: UserState = {
8
+ ...initialCommonState,
9
+ ...initialSettingsState,
10
+ ...initialPreferenceState,
11
+ };
@@ -0,0 +1,8 @@
1
+ export { commonSelectors } from './slices/common/selectors';
2
+ export { preferenceSelectors } from './slices/preference/selectors';
3
+ export {
4
+ modelConfigSelectors,
5
+ modelProviderSelectors,
6
+ settingsSelectors,
7
+ syncSettingsSelectors,
8
+ } from './slices/settings/selectors';
@@ -6,10 +6,10 @@ import { withSWR } from '~test-utils';
6
6
  import { globalService } from '@/services/global';
7
7
  import { messageService } from '@/services/message';
8
8
  import { userService } from '@/services/user';
9
- import { useGlobalStore } from '@/store/global';
10
- import { commonSelectors } from '@/store/global/slices/common/selectors';
11
- import { preferenceSelectors } from '@/store/global/slices/preference/selectors';
12
- import { syncSettingsSelectors } from '@/store/global/slices/settings/selectors';
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';
13
13
  import { GlobalServerConfig } from '@/types/serverConfig';
14
14
  import { switchLang } from '@/utils/client/switchLang';
15
15
 
@@ -32,24 +32,9 @@ afterEach(() => {
32
32
  });
33
33
 
34
34
  describe('createCommonSlice', () => {
35
- describe('switchBackToChat', () => {
36
- it('should switch back to chat', () => {
37
- const { result } = renderHook(() => useGlobalStore());
38
- const sessionId = 'session-id';
39
- const router = { push: vi.fn() } as any;
40
-
41
- act(() => {
42
- useGlobalStore.setState({ router });
43
- result.current.switchBackToChat(sessionId);
44
- });
45
-
46
- expect(router.push).toHaveBeenCalledWith('/chat?session=session-id');
47
- });
48
- });
49
-
50
35
  describe('refreshUserConfig', () => {
51
36
  it('should refresh user config', async () => {
52
- const { result } = renderHook(() => useGlobalStore());
37
+ const { result } = renderHook(() => useUserStore());
53
38
 
54
39
  await act(async () => {
55
40
  await result.current.refreshUserConfig();
@@ -61,7 +46,7 @@ describe('createCommonSlice', () => {
61
46
 
62
47
  describe('updateAvatar', () => {
63
48
  it('should update avatar', async () => {
64
- const { result } = renderHook(() => useGlobalStore());
49
+ const { result } = renderHook(() => useUserStore());
65
50
  const avatar = 'new-avatar';
66
51
 
67
52
  const spyOn = vi.spyOn(result.current, 'refreshUserConfig');
@@ -76,42 +61,6 @@ describe('createCommonSlice', () => {
76
61
  });
77
62
  });
78
63
 
79
- describe('useCheckLatestVersion', () => {
80
- it('should set hasNewVersion to false if there is no new version', async () => {
81
- const latestVersion = '0.0.1';
82
-
83
- vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
84
-
85
- const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
86
- wrapper: withSWR,
87
- });
88
-
89
- await waitFor(() => {
90
- expect(result.current.data).toBe(latestVersion);
91
- });
92
-
93
- expect(useGlobalStore.getState().hasNewVersion).toBeUndefined();
94
- expect(useGlobalStore.getState().latestVersion).toBeUndefined();
95
- });
96
-
97
- it('should set hasNewVersion to true if there is a new version', async () => {
98
- const latestVersion = '10000000.0.0';
99
-
100
- vi.spyOn(globalService, 'getLatestVersion').mockResolvedValueOnce(latestVersion);
101
-
102
- const { result } = renderHook(() => useGlobalStore().useCheckLatestVersion(), {
103
- wrapper: withSWR,
104
- });
105
-
106
- await waitFor(() => {
107
- expect(result.current.data).toBe(latestVersion);
108
- });
109
-
110
- expect(useGlobalStore.getState().hasNewVersion).toBe(true);
111
- expect(useGlobalStore.getState().latestVersion).toBe(latestVersion);
112
- });
113
- });
114
-
115
64
  describe('useFetchServerConfig', () => {
116
65
  it('should fetch server config correctly', async () => {
117
66
  const mockServerConfig = {
@@ -121,7 +70,7 @@ describe('createCommonSlice', () => {
121
70
  } as GlobalServerConfig;
122
71
  vi.spyOn(globalService, 'getGlobalConfig').mockResolvedValueOnce(mockServerConfig);
123
72
 
124
- const { result } = renderHook(() => useGlobalStore().useFetchServerConfig());
73
+ const { result } = renderHook(() => useUserStore().useFetchServerConfig());
125
74
 
126
75
  await waitFor(() => expect(result.current.data).toEqual(mockServerConfig));
127
76
  });
@@ -132,7 +81,7 @@ describe('createCommonSlice', () => {
132
81
  const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况
133
82
  vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
134
83
 
135
- const { result } = renderHook(() => useGlobalStore().useFetchUserConfig(false), {
84
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(false), {
136
85
  wrapper: withSWR,
137
86
  });
138
87
 
@@ -151,7 +100,7 @@ describe('createCommonSlice', () => {
151
100
  };
152
101
  vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
153
102
 
154
- const { result } = renderHook(() => useGlobalStore().useFetchUserConfig(true), {
103
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
155
104
  wrapper: withSWR,
156
105
  });
157
106
 
@@ -159,8 +108,8 @@ describe('createCommonSlice', () => {
159
108
  await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
160
109
 
161
110
  // 验证状态是否正确更新
162
- expect(useGlobalStore.getState().avatar).toBe(mockUserConfig.avatar);
163
- expect(useGlobalStore.getState().settings).toEqual(mockUserConfig.settings);
111
+ expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
112
+ expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
164
113
 
165
114
  // 验证是否正确处理了语言设置
166
115
  expect(switchLang).not.toHaveBeenCalledWith('auto');
@@ -174,7 +123,7 @@ describe('createCommonSlice', () => {
174
123
  };
175
124
  vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig);
176
125
 
177
- const { result } = renderHook(() => useGlobalStore().useFetchUserConfig(true), {
126
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
178
127
  wrapper: withSWR,
179
128
  });
180
129
 
@@ -182,8 +131,8 @@ describe('createCommonSlice', () => {
182
131
  await waitFor(() => expect(result.current.data).toEqual(mockUserConfig));
183
132
 
184
133
  // 验证状态是否正确更新
185
- expect(useGlobalStore.getState().avatar).toBe(mockUserConfig.avatar);
186
- expect(useGlobalStore.getState().settings).toEqual(mockUserConfig.settings);
134
+ expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar);
135
+ expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings);
187
136
 
188
137
  // 验证是否正确处理了语言设置
189
138
  expect(switchLang).toHaveBeenCalledWith('auto');
@@ -192,7 +141,7 @@ describe('createCommonSlice', () => {
192
141
  it('should handle the case when user config is null', async () => {
193
142
  vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(null as any);
194
143
 
195
- const { result } = renderHook(() => useGlobalStore().useFetchUserConfig(true), {
144
+ const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), {
196
145
  wrapper: withSWR,
197
146
  });
198
147
 
@@ -200,14 +149,14 @@ describe('createCommonSlice', () => {
200
149
  await waitFor(() => expect(result.current.data).toBeNull());
201
150
 
202
151
  // 验证状态未被错误更新
203
- expect(useGlobalStore.getState().avatar).toBeUndefined();
204
- expect(useGlobalStore.getState().settings).toEqual({});
152
+ expect(useUserStore.getState().avatar).toBeUndefined();
153
+ expect(useUserStore.getState().settings).toEqual({});
205
154
  });
206
155
  });
207
156
 
208
157
  describe('refreshConnection', () => {
209
158
  it('should not call triggerEnableSync when userId is empty', async () => {
210
- const { result } = renderHook(() => useGlobalStore());
159
+ const { result } = renderHook(() => useUserStore());
211
160
  const onEvent = vi.fn();
212
161
 
213
162
  vi.spyOn(commonSelectors, 'userId').mockReturnValueOnce(undefined);
@@ -221,7 +170,7 @@ describe('createCommonSlice', () => {
221
170
  });
222
171
 
223
172
  it('should call triggerEnableSync when userId exists', async () => {
224
- const { result } = renderHook(() => useGlobalStore());
173
+ const { result } = renderHook(() => useUserStore());
225
174
  const onEvent = vi.fn();
226
175
  const userId = 'user-id';
227
176
 
@@ -238,7 +187,7 @@ describe('createCommonSlice', () => {
238
187
 
239
188
  describe('triggerEnableSync', () => {
240
189
  it('should return false when sync.channelName is empty', async () => {
241
- const { result } = renderHook(() => useGlobalStore());
190
+ const { result } = renderHook(() => useUserStore());
242
191
  const userId = 'user-id';
243
192
  const onEvent = vi.fn();
244
193
 
@@ -270,7 +219,7 @@ describe('createCommonSlice', () => {
270
219
  });
271
220
  vi.spyOn(syncSettingsSelectors, 'deviceName').mockReturnValueOnce(deviceName);
272
221
  const enabledSyncSpy = vi.spyOn(globalService, 'enabledSync').mockResolvedValueOnce(true);
273
- const { result } = renderHook(() => useGlobalStore());
222
+ const { result } = renderHook(() => useUserStore());
274
223
 
275
224
  const data = await act(async () => {
276
225
  return result.current.triggerEnableSync(userId, onEvent);
@@ -290,7 +239,7 @@ describe('createCommonSlice', () => {
290
239
 
291
240
  describe('useCheckTrace', () => {
292
241
  it('should return false when shouldFetch is false', async () => {
293
- const { result } = renderHook(() => useGlobalStore().useCheckTrace(false), {
242
+ const { result } = renderHook(() => useUserStore().useCheckTrace(false), {
294
243
  wrapper: withSWR,
295
244
  });
296
245
 
@@ -300,7 +249,7 @@ describe('createCommonSlice', () => {
300
249
  it('should return false when userAllowTrace is already set', async () => {
301
250
  vi.spyOn(preferenceSelectors, 'userAllowTrace').mockReturnValueOnce(true);
302
251
 
303
- const { result } = renderHook(() => useGlobalStore().useCheckTrace(true), {
252
+ const { result } = renderHook(() => useUserStore().useCheckTrace(true), {
304
253
  wrapper: withSWR,
305
254
  });
306
255
 
@@ -313,7 +262,7 @@ describe('createCommonSlice', () => {
313
262
  .spyOn(messageService, 'messageCountToCheckTrace')
314
263
  .mockResolvedValueOnce(true);
315
264
 
316
- const { result } = renderHook(() => useGlobalStore().useCheckTrace(true), {
265
+ const { result } = renderHook(() => useUserStore().useCheckTrace(true), {
317
266
  wrapper: withSWR,
318
267
  });
319
268
 
@@ -324,10 +273,9 @@ describe('createCommonSlice', () => {
324
273
 
325
274
  describe('useEnabledSync', () => {
326
275
  it('should return false when userId is empty', async () => {
327
- const { result } = renderHook(
328
- () => useGlobalStore().useEnabledSync(true, undefined, vi.fn()),
329
- { wrapper: withSWR },
330
- );
276
+ const { result } = renderHook(() => useUserStore().useEnabledSync(true, undefined, vi.fn()), {
277
+ wrapper: withSWR,
278
+ });
331
279
 
332
280
  await waitFor(() => expect(result.current.data).toBe(false));
333
281
  });
@@ -336,7 +284,7 @@ describe('createCommonSlice', () => {
336
284
  const disableSyncSpy = vi.spyOn(globalService, 'disableSync').mockResolvedValueOnce(false);
337
285
 
338
286
  const { result } = renderHook(
339
- () => useGlobalStore().useEnabledSync(false, 'user-id', vi.fn()),
287
+ () => useUserStore().useEnabledSync(false, 'user-id', vi.fn()),
340
288
  { wrapper: withSWR },
341
289
  );
342
290
 
@@ -349,7 +297,7 @@ describe('createCommonSlice', () => {
349
297
  const onEvent = vi.fn();
350
298
  const triggerEnableSyncSpy = vi.fn().mockResolvedValueOnce(true);
351
299
 
352
- const { result } = renderHook(() => useGlobalStore());
300
+ const { result } = renderHook(() => useUserStore());
353
301
 
354
302
  // replace triggerEnableSync as a mock
355
303
  result.current.triggerEnableSync = triggerEnableSyncSpy;
@@ -1,15 +1,11 @@
1
- import { gt } from 'semver';
2
1
  import useSWR, { SWRResponse, mutate } from 'swr';
3
2
  import { DeepPartial } from 'utility-types';
4
3
  import type { StateCreator } from 'zustand/vanilla';
5
4
 
6
- import { INBOX_SESSION_ID } from '@/const/session';
7
- import { SESSION_CHAT_URL } from '@/const/url';
8
- import { CURRENT_VERSION } from '@/const/version';
9
5
  import { globalService } from '@/services/global';
10
6
  import { messageService } from '@/services/message';
11
7
  import { UserConfig, userService } from '@/services/user';
12
- import type { GlobalStore } from '@/store/global';
8
+ import type { UserStore } from '@/store/user';
13
9
  import type { GlobalServerConfig } from '@/types/serverConfig';
14
10
  import type { GlobalSettings } from '@/types/settings';
15
11
  import { OnSyncEvent, PeerSyncStatus } from '@/types/sync';
@@ -30,10 +26,8 @@ const n = setNamespace('common');
30
26
  export interface CommonAction {
31
27
  refreshConnection: (onEvent: OnSyncEvent) => Promise<void>;
32
28
  refreshUserConfig: () => Promise<void>;
33
- switchBackToChat: (sessionId?: string) => void;
34
29
  triggerEnableSync: (userId: string, onEvent: OnSyncEvent) => Promise<boolean>;
35
30
  updateAvatar: (avatar: string) => Promise<void>;
36
- useCheckLatestVersion: () => SWRResponse<string>;
37
31
  useCheckTrace: (shouldFetch: boolean) => SWRResponse;
38
32
  useEnabledSync: (
39
33
  userEnableSync: boolean,
@@ -47,7 +41,7 @@ export interface CommonAction {
47
41
  const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
48
42
 
49
43
  export const createCommonSlice: StateCreator<
50
- GlobalStore,
44
+ UserStore,
51
45
  [['zustand/devtools', never]],
52
46
  [],
53
47
  CommonAction
@@ -67,9 +61,6 @@ export const createCommonSlice: StateCreator<
67
61
  get().refreshModelProviderList();
68
62
  },
69
63
 
70
- switchBackToChat: (sessionId) => {
71
- get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
72
- },
73
64
  triggerEnableSync: async (userId: string, onEvent: OnSyncEvent) => {
74
65
  // double-check the sync ability
75
66
  // if there is no channelName, don't start sync
@@ -106,15 +97,6 @@ export const createCommonSlice: StateCreator<
106
97
  await userService.updateAvatar(avatar);
107
98
  await get().refreshUserConfig();
108
99
  },
109
- useCheckLatestVersion: () =>
110
- useSWR('checkLatestVersion', globalService.getLatestVersion, {
111
- // check latest version every 30 minutes
112
- focusThrottleInterval: 1000 * 60 * 30,
113
- onSuccess: (data: string) => {
114
- if (gt(data, CURRENT_VERSION))
115
- set({ hasNewVersion: true, latestVersion: data }, false, n('checkLatestVersion'));
116
- },
117
- }),
118
100
  useCheckTrace: (shouldFetch) =>
119
101
  useSWR<boolean>(
120
102
  ['checkTrace', shouldFetch],
@@ -0,0 +1,18 @@
1
+ import { PeerSyncStatus, SyncAwarenessState } from '@/types/sync';
2
+
3
+ export interface Guide {
4
+ // Topic 引导
5
+ topic?: boolean;
6
+ }
7
+
8
+ export interface UserCommonState {
9
+ syncAwareness: SyncAwarenessState[];
10
+ syncEnabled: boolean;
11
+ syncStatus: PeerSyncStatus;
12
+ }
13
+
14
+ export const initialCommonState: UserCommonState = {
15
+ syncAwareness: [],
16
+ syncEnabled: false,
17
+ syncStatus: PeerSyncStatus.Disabled,
18
+ };
@@ -0,0 +1,6 @@
1
+ import { UserStore } from '@/store/user';
2
+
3
+ export const commonSelectors = {
4
+ userAvatar: (s: UserStore) => s.avatar || '',
5
+ userId: (s: UserStore) => s.userId,
6
+ };
@@ -0,0 +1,41 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { useUserStore } from '@/store/user';
5
+
6
+ import { type Guide } from './initialState';
7
+
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ });
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ describe('createPreferenceSlice', () => {
17
+ describe('updateGuideState', () => {
18
+ it('should update guide state', () => {
19
+ const { result } = renderHook(() => useUserStore());
20
+ const guide: Guide = { topic: true };
21
+
22
+ act(() => {
23
+ result.current.updateGuideState(guide);
24
+ });
25
+
26
+ expect(result.current.preference.guide).toEqual(guide);
27
+ });
28
+ });
29
+
30
+ describe('updatePreference', () => {
31
+ it('should update preference', () => {
32
+ const { result } = renderHook(() => useUserStore());
33
+
34
+ act(() => {
35
+ result.current.updatePreference({ hideSyncAlert: true });
36
+ });
37
+
38
+ expect(result.current.preference.hideSyncAlert).toEqual(true);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,50 @@
1
+ import { SWRResponse } from 'swr';
2
+ import type { StateCreator } from 'zustand/vanilla';
3
+
4
+ import { useClientDataSWR } from '@/libs/swr';
5
+ import type { UserStore } from '@/store/user';
6
+ import { merge } from '@/utils/merge';
7
+ import { setNamespace } from '@/utils/storeDebug';
8
+
9
+ import type { Guide, UserPreference } from './initialState';
10
+
11
+ const n = setNamespace('preference');
12
+
13
+ export interface PreferenceAction {
14
+ updateGuideState: (guide: Partial<Guide>) => void;
15
+ updatePreference: (preference: Partial<UserPreference>, action?: any) => void;
16
+ useInitPreference: () => SWRResponse;
17
+ }
18
+
19
+ export const createPreferenceSlice: StateCreator<
20
+ UserStore,
21
+ [['zustand/devtools', never]],
22
+ [],
23
+ PreferenceAction
24
+ > = (set, get) => ({
25
+ updateGuideState: (guide) => {
26
+ const { updatePreference } = get();
27
+ const nextGuide = merge(get().preference.guide, guide);
28
+ updatePreference({ guide: nextGuide });
29
+ },
30
+ updatePreference: (preference, action) => {
31
+ const nextPreference = merge(get().preference, preference);
32
+
33
+ set({ preference: nextPreference }, false, action || n('updatePreference'));
34
+
35
+ get().preferenceStorage.saveToLocalStorage(nextPreference);
36
+ },
37
+
38
+ useInitPreference: () =>
39
+ useClientDataSWR<UserPreference>(
40
+ 'initUserPreference',
41
+ () => get().preferenceStorage.getFromLocalStorage(),
42
+ {
43
+ onSuccess: (preference) => {
44
+ if (preference) {
45
+ set({ preference }, false, n('initPreference'));
46
+ }
47
+ },
48
+ },
49
+ ),
50
+ });
@@ -0,0 +1,33 @@
1
+ import { AsyncLocalStorage } from '@/utils/localStorage';
2
+
3
+ export interface Guide {
4
+ // Topic 引导
5
+ topic?: boolean;
6
+ }
7
+
8
+ export interface UserPreference {
9
+ guide?: Guide;
10
+ hideSyncAlert?: boolean;
11
+ telemetry: boolean | null;
12
+ /**
13
+ * whether to use cmd + enter to send message
14
+ */
15
+ useCmdEnterToSend?: boolean;
16
+ }
17
+
18
+ export interface UserPreferenceState {
19
+ /**
20
+ * the user preference, which only store in local storage
21
+ */
22
+ preference: UserPreference;
23
+ preferenceStorage: AsyncLocalStorage<UserPreference>;
24
+ }
25
+
26
+ export const initialPreferenceState: UserPreferenceState = {
27
+ preference: {
28
+ guide: {},
29
+ telemetry: null,
30
+ useCmdEnterToSend: false,
31
+ },
32
+ preferenceStorage: new AsyncLocalStorage('LOBE_PREFERENCE'),
33
+ };
@@ -0,0 +1,13 @@
1
+ import { UserStore } from '@/store/user';
2
+
3
+ const useCmdEnterToSend = (s: UserStore): boolean => s.preference.useCmdEnterToSend || false;
4
+
5
+ const userAllowTrace = (s: UserStore) => s.preference.telemetry;
6
+
7
+ const hideSyncAlert = (s: UserStore) => s.preference.hideSyncAlert;
8
+
9
+ export const preferenceSelectors = {
10
+ hideSyncAlert,
11
+ useCmdEnterToSend,
12
+ userAllowTrace,
13
+ };
@@ -5,7 +5,7 @@ import { withSWR } from '~test-utils';
5
5
 
6
6
  import { DEFAULT_AGENT, DEFAULT_SETTINGS } from '@/const/settings';
7
7
  import { userService } from '@/services/user';
8
- import { useGlobalStore } from '@/store/global';
8
+ import { useUserStore } from '@/store/user';
9
9
  import { LobeAgentSettings } from '@/types/session';
10
10
  import { GlobalSettings } from '@/types/settings';
11
11
 
@@ -20,7 +20,7 @@ vi.mock('@/services/user', () => ({
20
20
  describe('SettingsAction', () => {
21
21
  describe('importAppSettings', () => {
22
22
  it('should import app settings', async () => {
23
- const { result } = renderHook(() => useGlobalStore());
23
+ const { result } = renderHook(() => useUserStore());
24
24
  const newSettings: GlobalSettings = {
25
25
  ...DEFAULT_SETTINGS,
26
26
  themeMode: 'dark',
@@ -51,7 +51,7 @@ describe('SettingsAction', () => {
51
51
 
52
52
  describe('resetSettings', () => {
53
53
  it('should reset settings to default', async () => {
54
- const { result } = renderHook(() => useGlobalStore());
54
+ const { result } = renderHook(() => useUserStore());
55
55
 
56
56
  // Perform the action
57
57
  await act(async () => {
@@ -68,7 +68,7 @@ describe('SettingsAction', () => {
68
68
 
69
69
  describe('setSettings', () => {
70
70
  it('should set partial settings', async () => {
71
- const { result } = renderHook(() => useGlobalStore());
71
+ const { result } = renderHook(() => useUserStore());
72
72
  const partialSettings: Partial<GlobalSettings> = { themeMode: 'dark' };
73
73
 
74
74
  // Perform the action
@@ -83,7 +83,7 @@ describe('SettingsAction', () => {
83
83
 
84
84
  describe('switchThemeMode', () => {
85
85
  it('should switch theme mode', async () => {
86
- const { result } = renderHook(() => useGlobalStore());
86
+ const { result } = renderHook(() => useUserStore());
87
87
  const themeMode = 'light';
88
88
 
89
89
  // Perform the action
@@ -98,7 +98,7 @@ describe('SettingsAction', () => {
98
98
 
99
99
  describe('updateDefaultAgent', () => {
100
100
  it('should update default agent settings', async () => {
101
- const { result } = renderHook(() => useGlobalStore());
101
+ const { result } = renderHook(() => useUserStore());
102
102
  const updatedAgent: Partial<LobeAgentSettings> = {
103
103
  meta: { title: 'docs' },
104
104
  };