@lobehub/chat 0.161.9 → 0.161.11
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 +50 -0
- package/package.json +2 -1
- package/src/app/(main)/_layout/Desktop.tsx +2 -2
- package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
- package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
- package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
- package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
- package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
- package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
- package/src/app/layout.tsx +2 -0
- package/src/components/ModelProviderIcon/index.tsx +2 -2
- package/src/components/ModelSelect/index.tsx +5 -14
- package/src/const/layoutTokens.ts +1 -0
- package/src/features/PWAInstall/index.tsx +22 -0
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/hooks/usePWAInstall.test.ts +78 -0
- package/src/hooks/usePWAInstall.ts +23 -2
- package/src/hooks/usePlatform.test.ts +82 -0
- package/src/hooks/usePlatform.ts +19 -2
- package/src/hooks/useSyncData.ts +3 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
- package/src/layout/GlobalProvider/index.tsx +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/services/message/client.test.ts +0 -24
- package/src/services/message/client.ts +0 -5
- package/src/services/message/type.ts +0 -1
- package/src/services/user/client.test.ts +100 -0
- package/src/services/user/client.ts +16 -14
- package/src/services/user/index.ts +0 -2
- package/src/services/user/type.ts +2 -4
- package/src/store/user/initialState.ts +10 -1
- package/src/store/user/selectors.ts +3 -7
- package/src/store/user/slices/auth/action.test.ts +5 -87
- package/src/store/user/slices/auth/action.ts +3 -58
- package/src/store/user/slices/auth/initialState.ts +2 -1
- package/src/store/user/slices/common/action.test.ts +196 -20
- package/src/store/user/slices/common/action.ts +55 -26
- package/src/store/user/slices/common/initialState.ts +9 -0
- package/src/store/user/slices/modelList/action.test.ts +363 -0
- package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
- package/src/store/user/slices/modelList/initialState.ts +15 -0
- package/src/store/user/slices/modelList/selectors/index.ts +2 -0
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
- package/src/store/user/slices/preference/action.test.ts +0 -52
- package/src/store/user/slices/preference/action.ts +1 -17
- package/src/store/user/slices/preference/initialState.ts +0 -5
- package/src/store/user/slices/preference/selectors.test.ts +2 -2
- package/src/store/user/slices/preference/selectors.ts +1 -1
- package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
- package/src/store/user/slices/settings/initialState.ts +0 -12
- package/src/store/user/slices/settings/selectors/index.ts +0 -3
- package/src/store/user/slices/sync/action.test.ts +19 -5
- package/src/store/user/slices/sync/action.ts +9 -6
- package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
- package/src/store/user/store.ts +5 -2
- package/src/types/serverConfig.ts +3 -1
- package/src/types/user/index.ts +13 -0
- package/src/utils/parseModels.test.ts +121 -1
- package/src/utils/parseModels.ts +9 -4
- package/src/utils/platform.test.ts +83 -0
- package/src/utils/platform.ts +33 -2
- package/src/hooks/useIsPWA.ts +0 -13
- package/src/store/user/slices/settings/actions/index.ts +0 -18
- package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
- package/src/utils/matchMedia.ts +0 -10
- /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
- /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
- /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
- /package/src/store/user/slices/settings/selectors/{selectors.test.ts → settings.test.ts} +0 -0
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
2
1
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
2
|
|
|
4
3
|
import { enableClerk } from '@/const/auth';
|
|
5
|
-
import { UserConfig, userService } from '@/services/user';
|
|
6
|
-
import { switchLang } from '@/utils/client/switchLang';
|
|
7
|
-
import { setNamespace } from '@/utils/storeDebug';
|
|
8
4
|
|
|
9
5
|
import { UserStore } from '../../store';
|
|
10
|
-
import { settingsSelectors } from '../settings/selectors';
|
|
11
|
-
|
|
12
|
-
const n = setNamespace('auth');
|
|
13
|
-
const USER_CONFIG_FETCH_KEY = 'fetchUserConfig';
|
|
14
6
|
|
|
15
7
|
export interface UserAuthAction {
|
|
16
8
|
enableAuth: () => boolean;
|
|
17
|
-
enabledNextAuth: () => boolean;
|
|
18
|
-
getUserConfig: () => void;
|
|
19
9
|
/**
|
|
20
10
|
* universal logout method
|
|
21
11
|
*/
|
|
@@ -25,9 +15,6 @@ export interface UserAuthAction {
|
|
|
25
15
|
*/
|
|
26
16
|
openLogin: () => Promise<void>;
|
|
27
17
|
openUserProfile: () => Promise<void>;
|
|
28
|
-
|
|
29
|
-
refreshUserConfig: () => Promise<void>;
|
|
30
|
-
useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>;
|
|
31
18
|
}
|
|
32
19
|
|
|
33
20
|
export const createAuthSlice: StateCreator<
|
|
@@ -37,13 +24,7 @@ export const createAuthSlice: StateCreator<
|
|
|
37
24
|
UserAuthAction
|
|
38
25
|
> = (set, get) => ({
|
|
39
26
|
enableAuth: () => {
|
|
40
|
-
return enableClerk || get()?.enabledNextAuth
|
|
41
|
-
},
|
|
42
|
-
enabledNextAuth: () => {
|
|
43
|
-
return !!get()?.serverConfig.enabledOAuthSSO;
|
|
44
|
-
},
|
|
45
|
-
getUserConfig: () => {
|
|
46
|
-
console.log(n('userconfig'));
|
|
27
|
+
return enableClerk || get()?.enabledNextAuth || false;
|
|
47
28
|
},
|
|
48
29
|
logout: async () => {
|
|
49
30
|
if (enableClerk) {
|
|
@@ -52,7 +33,7 @@ export const createAuthSlice: StateCreator<
|
|
|
52
33
|
return;
|
|
53
34
|
}
|
|
54
35
|
|
|
55
|
-
const enableNextAuth = get().enabledNextAuth
|
|
36
|
+
const enableNextAuth = get().enabledNextAuth;
|
|
56
37
|
if (enableNextAuth) {
|
|
57
38
|
const { signOut } = await import('next-auth/react');
|
|
58
39
|
signOut();
|
|
@@ -60,14 +41,12 @@ export const createAuthSlice: StateCreator<
|
|
|
60
41
|
},
|
|
61
42
|
openLogin: async () => {
|
|
62
43
|
if (enableClerk) {
|
|
63
|
-
console.log('fallbackRedirectUrl:', location.toString());
|
|
64
|
-
|
|
65
44
|
get().clerkSignIn?.({ fallbackRedirectUrl: location.toString() });
|
|
66
45
|
|
|
67
46
|
return;
|
|
68
47
|
}
|
|
69
48
|
|
|
70
|
-
const enableNextAuth = get().enabledNextAuth
|
|
49
|
+
const enableNextAuth = get().enabledNextAuth;
|
|
71
50
|
if (enableNextAuth) {
|
|
72
51
|
const { signIn } = await import('next-auth/react');
|
|
73
52
|
signIn();
|
|
@@ -81,38 +60,4 @@ export const createAuthSlice: StateCreator<
|
|
|
81
60
|
return;
|
|
82
61
|
}
|
|
83
62
|
},
|
|
84
|
-
refreshUserConfig: async () => {
|
|
85
|
-
await mutate([USER_CONFIG_FETCH_KEY, true]);
|
|
86
|
-
|
|
87
|
-
// when get the user config ,refresh the model provider list to the latest
|
|
88
|
-
get().refreshModelProviderList();
|
|
89
|
-
},
|
|
90
|
-
useFetchUserConfig: (initServer) =>
|
|
91
|
-
useSWR<UserConfig | undefined>(
|
|
92
|
-
[USER_CONFIG_FETCH_KEY, initServer],
|
|
93
|
-
async () => {
|
|
94
|
-
if (!initServer) return;
|
|
95
|
-
return userService.getUserConfig();
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
onSuccess: (data) => {
|
|
99
|
-
if (!data) return;
|
|
100
|
-
|
|
101
|
-
set(
|
|
102
|
-
{ avatar: data.avatar, settings: data.settings, userId: data.uuid },
|
|
103
|
-
false,
|
|
104
|
-
n('fetchUserConfig', data),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// when get the user config ,refresh the model provider list to the latest
|
|
108
|
-
get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' });
|
|
109
|
-
|
|
110
|
-
const { language } = settingsSelectors.currentSettings(get());
|
|
111
|
-
if (language === 'auto') {
|
|
112
|
-
switchLang('auto');
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
revalidateOnFocus: false,
|
|
116
|
-
},
|
|
117
|
-
),
|
|
118
63
|
});
|
|
@@ -15,12 +15,13 @@ export interface UserAuthState {
|
|
|
15
15
|
* @deprecated
|
|
16
16
|
*/
|
|
17
17
|
avatar?: string;
|
|
18
|
-
|
|
19
18
|
clerkOpenUserProfile?: (props?: UserProfileProps) => void;
|
|
19
|
+
|
|
20
20
|
clerkSession?: ActiveSessionResource;
|
|
21
21
|
clerkSignIn?: (props?: SignInProps) => void;
|
|
22
22
|
clerkSignOut?: SignOut;
|
|
23
23
|
clerkUser?: UserResource;
|
|
24
|
+
enabledNextAuth?: boolean;
|
|
24
25
|
|
|
25
26
|
isLoaded?: boolean;
|
|
26
27
|
isSignedIn?: boolean;
|
|
@@ -3,15 +3,20 @@ import { mutate } from 'swr';
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { withSWR } from '~test-utils';
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { messageService } from '@/services/message';
|
|
6
|
+
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
8
7
|
import { userService } from '@/services/user';
|
|
9
8
|
import { useUserStore } from '@/store/user';
|
|
10
9
|
import { preferenceSelectors } from '@/store/user/selectors';
|
|
11
10
|
import { GlobalServerConfig } from '@/types/serverConfig';
|
|
11
|
+
import { UserInitializationState, UserPreference } from '@/types/user';
|
|
12
|
+
import { switchLang } from '@/utils/client/switchLang';
|
|
12
13
|
|
|
13
14
|
vi.mock('zustand/traditional');
|
|
14
15
|
|
|
16
|
+
vi.mock('@/utils/client/switchLang', () => ({
|
|
17
|
+
switchLang: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
15
20
|
vi.mock('swr', async (importOriginal) => {
|
|
16
21
|
const modules = await importOriginal();
|
|
17
22
|
return {
|
|
@@ -30,7 +35,7 @@ describe('createCommonSlice', () => {
|
|
|
30
35
|
const { result } = renderHook(() => useUserStore());
|
|
31
36
|
const avatar = 'new-avatar';
|
|
32
37
|
|
|
33
|
-
const spyOn = vi.spyOn(result.current, '
|
|
38
|
+
const spyOn = vi.spyOn(result.current, 'refreshUserState');
|
|
34
39
|
const updateAvatarSpy = vi.spyOn(userService, 'updateAvatar');
|
|
35
40
|
|
|
36
41
|
await act(async () => {
|
|
@@ -42,28 +47,197 @@ describe('createCommonSlice', () => {
|
|
|
42
47
|
});
|
|
43
48
|
});
|
|
44
49
|
|
|
45
|
-
describe('
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
describe('useInitUserState', () => {
|
|
51
|
+
const mockServerConfig = {
|
|
52
|
+
defaultAgent: 'agent1',
|
|
53
|
+
languageModel: 'model1',
|
|
54
|
+
telemetry: {},
|
|
55
|
+
} as GlobalServerConfig;
|
|
56
|
+
|
|
57
|
+
it('should not fetch user state if user is not login', async () => {
|
|
58
|
+
const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况
|
|
59
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserConfig);
|
|
60
|
+
const successCallback = vi.fn();
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(
|
|
63
|
+
() =>
|
|
64
|
+
useUserStore().useInitUserState(false, mockServerConfig, {
|
|
65
|
+
onSuccess: successCallback,
|
|
66
|
+
}),
|
|
67
|
+
{ wrapper: withSWR },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 因为 initServer 为 false,所以不会触发 getUserState 的调用
|
|
71
|
+
expect(userService.getUserState).not.toHaveBeenCalled();
|
|
72
|
+
// 也不会触发 onSuccess 回调
|
|
73
|
+
expect(successCallback).not.toHaveBeenCalled();
|
|
74
|
+
// 确保状态未改变
|
|
75
|
+
expect(result.current.data).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should fetch user state correctly when user is login', async () => {
|
|
79
|
+
const mockUserState: UserInitializationState = {
|
|
80
|
+
userId: 'user-id',
|
|
81
|
+
isOnboard: true,
|
|
82
|
+
preference: {
|
|
83
|
+
telemetry: true,
|
|
84
|
+
},
|
|
85
|
+
settings: {
|
|
86
|
+
language: 'en-US',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
91
|
+
const successCallback = vi.fn();
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook(
|
|
94
|
+
() =>
|
|
95
|
+
useUserStore().useInitUserState(true, mockServerConfig, {
|
|
96
|
+
onSuccess: successCallback,
|
|
97
|
+
}),
|
|
98
|
+
{
|
|
99
|
+
wrapper: withSWR,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// 等待 SWR 完成数据获取
|
|
104
|
+
await waitFor(() => expect(result.current.data).toEqual(mockUserState));
|
|
105
|
+
|
|
106
|
+
// 验证状态是否正确更新
|
|
107
|
+
expect(useUserStore.getState().avatar).toBe(mockUserState.avatar);
|
|
108
|
+
expect(useUserStore.getState().settings).toEqual(mockUserState.settings);
|
|
109
|
+
expect(successCallback).toHaveBeenCalledWith(mockUserState);
|
|
110
|
+
|
|
111
|
+
// 验证是否正确处理了语言设置
|
|
112
|
+
expect(switchLang).not.toHaveBeenCalledWith('auto');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should call switch language when language is auto', async () => {
|
|
116
|
+
const mockUserState: UserInitializationState = {
|
|
117
|
+
userId: 'user-id',
|
|
118
|
+
isOnboard: true,
|
|
119
|
+
preference: {
|
|
120
|
+
telemetry: true,
|
|
121
|
+
},
|
|
122
|
+
settings: {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
126
|
+
|
|
127
|
+
const { result } = renderHook(() => useUserStore().useInitUserState(true, mockServerConfig), {
|
|
128
|
+
wrapper: withSWR,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 等待 SWR 完成数据获取
|
|
132
|
+
await waitFor(() => expect(result.current.data).toEqual(mockUserState));
|
|
133
|
+
|
|
134
|
+
// 验证是否正确处理了语言设置
|
|
135
|
+
expect(switchLang).toHaveBeenCalledWith('auto');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should fetch use server config correctly', async () => {
|
|
139
|
+
const mockUserState: UserInitializationState = {
|
|
140
|
+
userId: 'user-id',
|
|
141
|
+
isOnboard: true,
|
|
142
|
+
preference: {
|
|
143
|
+
telemetry: true,
|
|
144
|
+
},
|
|
145
|
+
settings: {},
|
|
146
|
+
};
|
|
147
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
148
|
+
|
|
149
|
+
const { result } = renderHook(() => useUserStore().useInitUserState(true, mockServerConfig));
|
|
150
|
+
|
|
151
|
+
await waitFor(() => expect(result.current.data).toEqual(mockUserState));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return saved preference when local storage has data', async () => {
|
|
155
|
+
const { result } = renderHook(() => useUserStore());
|
|
156
|
+
|
|
157
|
+
const savedPreference: UserPreference = {
|
|
158
|
+
...DEFAULT_PREFERENCE,
|
|
159
|
+
hideSyncAlert: true,
|
|
160
|
+
guide: { topic: false, moveSettingsToAvatar: true },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const mockUserState: UserInitializationState = {
|
|
164
|
+
userId: 'user-id',
|
|
165
|
+
isOnboard: true,
|
|
166
|
+
preference: savedPreference,
|
|
167
|
+
settings: {
|
|
168
|
+
language: 'en-US',
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
172
|
+
|
|
173
|
+
const { result: preference } = renderHook(
|
|
174
|
+
() => result.current.useInitUserState(true, mockServerConfig),
|
|
175
|
+
{ wrapper: withSWR },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(preference.current.data.preference).toEqual(savedPreference);
|
|
180
|
+
expect(result.current.isUserStateInit).toBeTruthy();
|
|
181
|
+
expect(result.current.preference).toEqual(savedPreference);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle the case when user config is null', async () => {
|
|
186
|
+
const { result } = renderHook(() => useUserStore());
|
|
187
|
+
const mockUserState: UserInitializationState = {
|
|
188
|
+
userId: 'user-id',
|
|
189
|
+
isOnboard: true,
|
|
190
|
+
preference: undefined as any,
|
|
191
|
+
settings: null as any,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
195
|
+
|
|
196
|
+
renderHook(() => result.current.useInitUserState(true, mockServerConfig), {
|
|
197
|
+
wrapper: withSWR,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 等待 SWR 完成数据获取
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(result.current.isUserStateInit).toBeTruthy();
|
|
203
|
+
// 验证状态未被错误更新
|
|
204
|
+
expect(result.current.avatar).toBeUndefined();
|
|
205
|
+
expect(result.current.settings).toEqual({});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should return default preference when local storage is empty', async () => {
|
|
210
|
+
const { result } = renderHook(() => useUserStore());
|
|
53
211
|
|
|
54
|
-
const
|
|
212
|
+
const mockUserState: UserInitializationState = {
|
|
213
|
+
userId: 'user-id',
|
|
214
|
+
isOnboard: true,
|
|
215
|
+
preference: {} as any,
|
|
216
|
+
settings: {
|
|
217
|
+
language: 'en-US',
|
|
218
|
+
},
|
|
219
|
+
};
|
|
55
220
|
|
|
56
|
-
|
|
221
|
+
vi.spyOn(userService, 'getUserState').mockResolvedValueOnce(mockUserState);
|
|
222
|
+
|
|
223
|
+
renderHook(() => result.current.useInitUserState(true, mockServerConfig), {
|
|
224
|
+
wrapper: withSWR,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(result.current.isUserStateInit).toBeTruthy();
|
|
229
|
+
expect(result.current.preference).toEqual(DEFAULT_PREFERENCE);
|
|
230
|
+
});
|
|
57
231
|
});
|
|
58
232
|
});
|
|
59
233
|
|
|
60
234
|
describe('useCheckTrace', () => {
|
|
61
|
-
it('should return
|
|
235
|
+
it('should return undefined when shouldFetch is false', async () => {
|
|
62
236
|
const { result } = renderHook(() => useUserStore().useCheckTrace(false), {
|
|
63
237
|
wrapper: withSWR,
|
|
64
238
|
});
|
|
65
239
|
|
|
66
|
-
await waitFor(() => expect(result.current.data).
|
|
240
|
+
await waitFor(() => expect(result.current.data).toBeUndefined());
|
|
67
241
|
});
|
|
68
242
|
|
|
69
243
|
it('should return false when userAllowTrace is already set', async () => {
|
|
@@ -78,16 +252,18 @@ describe('createCommonSlice', () => {
|
|
|
78
252
|
|
|
79
253
|
it('should call messageService.messageCountToCheckTrace when needed', async () => {
|
|
80
254
|
vi.spyOn(preferenceSelectors, 'userAllowTrace').mockReturnValueOnce(null);
|
|
81
|
-
const messageCountToCheckTraceSpy = vi
|
|
82
|
-
.spyOn(messageService, 'messageCountToCheckTrace')
|
|
83
|
-
.mockResolvedValueOnce(true);
|
|
84
255
|
|
|
85
|
-
|
|
256
|
+
act(() => {
|
|
257
|
+
useUserStore.setState({
|
|
258
|
+
isUserCanEnableTrace: true,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const { result } = renderHook(() => useUserStore.getState().useCheckTrace(true), {
|
|
86
263
|
wrapper: withSWR,
|
|
87
264
|
});
|
|
88
265
|
|
|
89
266
|
await waitFor(() => expect(result.current.data).toBe(true));
|
|
90
|
-
expect(messageCountToCheckTraceSpy).toHaveBeenCalled();
|
|
91
267
|
});
|
|
92
268
|
});
|
|
93
269
|
});
|
|
@@ -1,26 +1,38 @@
|
|
|
1
|
-
import useSWR, { SWRResponse } from 'swr';
|
|
1
|
+
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
2
2
|
import { DeepPartial } from 'utility-types';
|
|
3
3
|
import type { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_PREFERENCE } from '@/const/user';
|
|
6
6
|
import { userService } from '@/services/user';
|
|
7
7
|
import type { UserStore } from '@/store/user';
|
|
8
8
|
import type { GlobalServerConfig } from '@/types/serverConfig';
|
|
9
9
|
import type { GlobalSettings } from '@/types/settings';
|
|
10
|
+
import { UserInitializationState } from '@/types/user';
|
|
11
|
+
import { switchLang } from '@/utils/client/switchLang';
|
|
10
12
|
import { merge } from '@/utils/merge';
|
|
11
13
|
import { setNamespace } from '@/utils/storeDebug';
|
|
12
14
|
|
|
13
15
|
import { preferenceSelectors } from '../preference/selectors';
|
|
16
|
+
import { settingsSelectors } from '../settings/selectors';
|
|
14
17
|
|
|
15
18
|
const n = setNamespace('common');
|
|
16
19
|
|
|
20
|
+
const GET_USER_STATE_KEY = 'initUserState';
|
|
17
21
|
/**
|
|
18
22
|
* 设置操作
|
|
19
23
|
*/
|
|
20
24
|
export interface CommonAction {
|
|
25
|
+
refreshUserState: () => Promise<void>;
|
|
26
|
+
|
|
21
27
|
updateAvatar: (avatar: string) => Promise<void>;
|
|
22
28
|
useCheckTrace: (shouldFetch: boolean) => SWRResponse;
|
|
23
|
-
|
|
29
|
+
useInitUserState: (
|
|
30
|
+
isLogin: boolean | undefined,
|
|
31
|
+
serverConfig: GlobalServerConfig,
|
|
32
|
+
options?: {
|
|
33
|
+
onSuccess: (data: UserInitializationState) => void;
|
|
34
|
+
},
|
|
35
|
+
) => SWRResponse;
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
export const createCommonSlice: StateCreator<
|
|
@@ -29,55 +41,72 @@ export const createCommonSlice: StateCreator<
|
|
|
29
41
|
[],
|
|
30
42
|
CommonAction
|
|
31
43
|
> = (set, get) => ({
|
|
44
|
+
refreshUserState: async () => {
|
|
45
|
+
await mutate(GET_USER_STATE_KEY);
|
|
46
|
+
},
|
|
32
47
|
updateAvatar: async (avatar) => {
|
|
33
48
|
await userService.updateAvatar(avatar);
|
|
34
|
-
await get().
|
|
49
|
+
await get().refreshUserState();
|
|
35
50
|
},
|
|
36
51
|
|
|
37
52
|
useCheckTrace: (shouldFetch) =>
|
|
38
53
|
useSWR<boolean>(
|
|
39
|
-
|
|
54
|
+
shouldFetch ? 'checkTrace' : null,
|
|
40
55
|
() => {
|
|
41
56
|
const userAllowTrace = preferenceSelectors.userAllowTrace(get());
|
|
42
|
-
// if not init with server side, return false
|
|
43
|
-
if (!shouldFetch) return Promise.resolve(false);
|
|
44
57
|
|
|
45
58
|
// if user have set the trace, return false
|
|
46
59
|
if (typeof userAllowTrace === 'boolean') return Promise.resolve(false);
|
|
47
60
|
|
|
48
|
-
return
|
|
61
|
+
return Promise.resolve(get().isUserCanEnableTrace);
|
|
49
62
|
},
|
|
50
63
|
{
|
|
51
64
|
revalidateOnFocus: false,
|
|
52
65
|
},
|
|
53
66
|
),
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
useFetchServerConfig: () =>
|
|
61
|
-
useSWR<GlobalServerConfig>(
|
|
62
|
-
'fetchGlobalConfig',
|
|
63
|
-
async () => {
|
|
64
|
-
const { globalService } = await import('@/services/global');
|
|
65
|
-
|
|
66
|
-
return globalService.getGlobalConfig();
|
|
67
|
-
},
|
|
68
|
+
useInitUserState: (isLogin, serverConfig, options) =>
|
|
69
|
+
useSWR<UserInitializationState>(
|
|
70
|
+
!!isLogin ? GET_USER_STATE_KEY : null,
|
|
71
|
+
() => userService.getUserState(),
|
|
68
72
|
{
|
|
69
73
|
onSuccess: (data) => {
|
|
74
|
+
options?.onSuccess?.(data);
|
|
75
|
+
|
|
70
76
|
if (data) {
|
|
77
|
+
// merge settings
|
|
71
78
|
const serverSettings: DeepPartial<GlobalSettings> = {
|
|
72
|
-
defaultAgent:
|
|
73
|
-
languageModel:
|
|
79
|
+
defaultAgent: serverConfig.defaultAgent,
|
|
80
|
+
languageModel: serverConfig.languageModel,
|
|
74
81
|
};
|
|
75
|
-
|
|
76
82
|
const defaultSettings = merge(get().defaultSettings, serverSettings);
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
// merge preference
|
|
85
|
+
const isEmpty = Object.keys(data.preference || {}).length === 0;
|
|
86
|
+
const preference = isEmpty ? DEFAULT_PREFERENCE : data.preference;
|
|
87
|
+
|
|
88
|
+
set(
|
|
89
|
+
{
|
|
90
|
+
defaultSettings,
|
|
91
|
+
enabledNextAuth: serverConfig.enabledOAuthSSO,
|
|
92
|
+
isUserCanEnableTrace: data.canEnableTrace,
|
|
93
|
+
isUserStateInit: true,
|
|
94
|
+
preference,
|
|
95
|
+
serverLanguageModel: serverConfig.languageModel,
|
|
96
|
+
settings: data.settings || {},
|
|
97
|
+
userId: data.userId,
|
|
98
|
+
},
|
|
99
|
+
false,
|
|
100
|
+
n('initUserState'),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
get().refreshDefaultModelProviderList({ trigger: 'fetchUserState' });
|
|
79
104
|
|
|
80
|
-
|
|
105
|
+
// auto switch language
|
|
106
|
+
const { language } = settingsSelectors.currentSettings(get());
|
|
107
|
+
if (language === 'auto') {
|
|
108
|
+
switchLang('auto');
|
|
109
|
+
}
|
|
81
110
|
}
|
|
82
111
|
},
|
|
83
112
|
revalidateOnFocus: false,
|