@lobehub/lobehub 2.0.0-next.26 → 2.0.0-next.28

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 (73) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/types/src/discover/mcp.ts +6 -0
  5. package/packages/types/src/plugins/mcp.ts +4 -1
  6. package/packages/types/src/topic/topic.ts +14 -0
  7. package/renovate.json +4 -30
  8. package/src/features/MCP/utils.test.ts +91 -0
  9. package/src/features/MCP/utils.ts +20 -2
  10. package/src/features/PluginStore/Content.tsx +2 -3
  11. package/src/features/PluginStore/McpList/index.tsx +6 -2
  12. package/src/server/routers/lambda/market/index.ts +4 -2
  13. package/src/server/routers/lambda/topic.ts +7 -1
  14. package/src/services/aiModel/index.test.ts +3 -3
  15. package/src/services/aiModel/index.ts +56 -2
  16. package/src/services/aiProvider/index.test.ts +2 -2
  17. package/src/services/aiProvider/index.ts +48 -2
  18. package/src/services/chatGroup/index.ts +66 -2
  19. package/src/services/export/index.ts +10 -2
  20. package/src/services/file/index.ts +61 -2
  21. package/src/services/import/index.ts +133 -2
  22. package/src/services/mcp.ts +40 -6
  23. package/src/services/message/index.ts +176 -2
  24. package/src/services/message/{__tests__/server.test.ts → server.test.ts} +3 -3
  25. package/src/services/plugin/index.test.ts +8 -0
  26. package/src/services/plugin/index.ts +53 -2
  27. package/src/services/session/index.test.ts +8 -0
  28. package/src/services/session/index.ts +145 -2
  29. package/src/services/thread/index.test.ts +8 -0
  30. package/src/services/thread/index.ts +38 -2
  31. package/src/services/topic/index.test.ts +8 -0
  32. package/src/services/topic/index.ts +76 -2
  33. package/src/services/user/index.test.ts +8 -0
  34. package/src/services/user/index.ts +53 -2
  35. package/src/store/aiInfra/slices/aiModel/action.test.ts +17 -9
  36. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +4 -2
  37. package/src/store/chat/slices/topic/action.test.ts +1 -1
  38. package/src/store/chat/slices/topic/action.ts +1 -2
  39. package/src/store/chat/slices/topic/reducer.ts +1 -2
  40. package/src/store/file/slices/chat/action.ts +1 -4
  41. package/src/store/file/slices/fileManager/action.ts +2 -3
  42. package/src/store/session/slices/sessionGroup/action.test.ts +5 -5
  43. package/src/store/tool/slices/mcpStore/action.test.ts +95 -3
  44. package/src/store/tool/slices/mcpStore/action.ts +177 -53
  45. package/src/store/tool/slices/oldStore/initialState.ts +1 -2
  46. package/src/store/user/slices/common/action.test.ts +1 -1
  47. package/src/services/aiModel/server.test.ts +0 -122
  48. package/src/services/aiModel/server.ts +0 -51
  49. package/src/services/aiModel/type.ts +0 -32
  50. package/src/services/aiProvider/server.ts +0 -43
  51. package/src/services/aiProvider/type.ts +0 -27
  52. package/src/services/chatGroup/server.ts +0 -67
  53. package/src/services/chatGroup/type.ts +0 -22
  54. package/src/services/export/server.ts +0 -9
  55. package/src/services/export/type.ts +0 -5
  56. package/src/services/file/server.ts +0 -53
  57. package/src/services/file/type.ts +0 -13
  58. package/src/services/import/server.ts +0 -133
  59. package/src/services/import/type.ts +0 -17
  60. package/src/services/message/server.ts +0 -151
  61. package/src/services/message/type.ts +0 -55
  62. package/src/services/plugin/server.ts +0 -42
  63. package/src/services/plugin/type.ts +0 -23
  64. package/src/services/session/server.test.ts +0 -260
  65. package/src/services/session/server.ts +0 -125
  66. package/src/services/session/type.ts +0 -82
  67. package/src/services/thread/server.ts +0 -32
  68. package/src/services/thread/type.ts +0 -21
  69. package/src/services/topic/server.ts +0 -57
  70. package/src/services/topic/type.ts +0 -40
  71. package/src/services/user/server.test.ts +0 -149
  72. package/src/services/user/server.ts +0 -47
  73. package/src/services/user/type.ts +0 -21
@@ -1,3 +1,39 @@
1
- import { ServerService } from './server';
1
+ import { CreateMessageParams } from '@lobechat/types';
2
2
 
3
- export const threadService = new ServerService();
3
+ import { INBOX_SESSION_ID } from '@/const/session';
4
+ import { lambdaClient } from '@/libs/trpc/client';
5
+ import { CreateThreadParams, ThreadItem } from '@/types/topic';
6
+
7
+ interface CreateThreadWithMessageParams extends CreateThreadParams {
8
+ message: CreateMessageParams;
9
+ }
10
+
11
+ export class ThreadService {
12
+ getThreads = (topicId: string): Promise<ThreadItem[]> => {
13
+ return lambdaClient.thread.getThreads.query({ topicId });
14
+ };
15
+
16
+ createThreadWithMessage = async ({
17
+ message,
18
+ ...params
19
+ }: CreateThreadWithMessageParams): Promise<{ messageId: string; threadId: string }> => {
20
+ return lambdaClient.thread.createThreadWithMessage.mutate({
21
+ ...params,
22
+ message: { ...message, sessionId: this.toDbSessionId(message.sessionId) },
23
+ });
24
+ };
25
+
26
+ updateThread = async (id: string, data: Partial<ThreadItem>) => {
27
+ return lambdaClient.thread.updateThread.mutate({ id, value: data });
28
+ };
29
+
30
+ removeThread = async (id: string) => {
31
+ return lambdaClient.thread.removeThread.mutate({ id });
32
+ };
33
+
34
+ private toDbSessionId = (sessionId: string | undefined) => {
35
+ return sessionId === INBOX_SESSION_ID ? null : sessionId;
36
+ };
37
+ }
38
+
39
+ export const threadService = new ThreadService();
@@ -0,0 +1,8 @@
1
+ import { describe } from 'vitest';
2
+ import { testService } from '~test-utils';
3
+
4
+ import { TopicService } from './index';
5
+
6
+ describe('TopicService', () => {
7
+ testService(TopicService, { checkAsync: false });
8
+ });
@@ -1,3 +1,77 @@
1
- import { ServerService } from './server';
1
+ import { INBOX_SESSION_ID } from '@/const/session';
2
+ import { lambdaClient } from '@/libs/trpc/client';
3
+ import { BatchTaskResult } from '@/types/service';
4
+ import { ChatTopic, CreateTopicParams, QueryTopicParams, TopicRankItem } from '@/types/topic';
2
5
 
3
- export const topicService = new ServerService();
6
+ export class TopicService {
7
+ createTopic = (params: CreateTopicParams): Promise<string> => {
8
+ return lambdaClient.topic.createTopic.mutate({
9
+ ...params,
10
+ sessionId: this.toDbSessionId(params.sessionId),
11
+ });
12
+ };
13
+
14
+ batchCreateTopics = (importTopics: ChatTopic[]): Promise<BatchTaskResult> => {
15
+ return lambdaClient.topic.batchCreateTopics.mutate(importTopics);
16
+ };
17
+
18
+ cloneTopic = (id: string, newTitle?: string): Promise<string> => {
19
+ return lambdaClient.topic.cloneTopic.mutate({ id, newTitle });
20
+ };
21
+
22
+ getTopics = (params: QueryTopicParams): Promise<ChatTopic[]> => {
23
+ return lambdaClient.topic.getTopics.query({
24
+ ...params,
25
+ containerId: this.toDbSessionId(params.containerId),
26
+ }) as any;
27
+ };
28
+
29
+ getAllTopics = (): Promise<ChatTopic[]> => {
30
+ return lambdaClient.topic.getAllTopics.query() as any;
31
+ };
32
+
33
+ countTopics = async (params?: {
34
+ endDate?: string;
35
+ range?: [string, string];
36
+ startDate?: string;
37
+ }): Promise<number> => {
38
+ return lambdaClient.topic.countTopics.query(params);
39
+ };
40
+
41
+ rankTopics = async (limit?: number): Promise<TopicRankItem[]> => {
42
+ return lambdaClient.topic.rankTopics.query(limit);
43
+ };
44
+
45
+ searchTopics = (keywords: string, sessionId?: string, groupId?: string): Promise<ChatTopic[]> => {
46
+ return lambdaClient.topic.searchTopics.query({
47
+ groupId,
48
+ keywords,
49
+ sessionId: this.toDbSessionId(sessionId),
50
+ }) as any;
51
+ };
52
+
53
+ updateTopic = (id: string, data: Partial<ChatTopic>) => {
54
+ return lambdaClient.topic.updateTopic.mutate({ id, value: data });
55
+ };
56
+
57
+ removeTopic = (id: string) => {
58
+ return lambdaClient.topic.removeTopic.mutate({ id });
59
+ };
60
+
61
+ removeTopics = (sessionId: string) => {
62
+ return lambdaClient.topic.batchDeleteBySessionId.mutate({ id: this.toDbSessionId(sessionId) });
63
+ };
64
+
65
+ batchRemoveTopics = (topics: string[]) => {
66
+ return lambdaClient.topic.batchDelete.mutate({ ids: topics });
67
+ };
68
+
69
+ removeAllTopic = () => {
70
+ return lambdaClient.topic.removeAllTopics.mutate();
71
+ };
72
+
73
+ private toDbSessionId = (sessionId?: string | null) =>
74
+ sessionId === INBOX_SESSION_ID ? null : sessionId;
75
+ }
76
+
77
+ export const topicService = new TopicService();
@@ -0,0 +1,8 @@
1
+ import { describe } from 'vitest';
2
+ import { testService } from '~test-utils';
3
+
4
+ import { UserService } from './index';
5
+
6
+ describe('UserService', () => {
7
+ testService(UserService);
8
+ });
@@ -1,3 +1,54 @@
1
- import { ServerService } from './server';
1
+ import type { AdapterAccount } from 'next-auth/adapters';
2
+ import type { PartialDeep } from 'type-fest';
2
3
 
3
- export const userService = new ServerService();
4
+ import { lambdaClient } from '@/libs/trpc/client';
5
+ import { UserGuide, UserInitializationState, UserPreference } from '@/types/user';
6
+ import { UserSettings } from '@/types/user/settings';
7
+
8
+ export class UserService {
9
+ getUserRegistrationDuration = async (): Promise<{
10
+ createdAt: string;
11
+ duration: number;
12
+ updatedAt: string;
13
+ }> => {
14
+ return lambdaClient.user.getUserRegistrationDuration.query();
15
+ };
16
+
17
+ getUserState = async (): Promise<UserInitializationState> => {
18
+ return lambdaClient.user.getUserState.query();
19
+ };
20
+
21
+ getUserSSOProviders = async (): Promise<AdapterAccount[]> => {
22
+ return lambdaClient.user.getUserSSOProviders.query();
23
+ };
24
+
25
+ unlinkSSOProvider = async (provider: string, providerAccountId: string) => {
26
+ return lambdaClient.user.unlinkSSOProvider.mutate({ provider, providerAccountId });
27
+ };
28
+
29
+ makeUserOnboarded = async () => {
30
+ return lambdaClient.user.makeUserOnboarded.mutate();
31
+ };
32
+
33
+ updateAvatar = async (avatar: string) => {
34
+ return lambdaClient.user.updateAvatar.mutate(avatar);
35
+ };
36
+
37
+ updatePreference = async (preference: Partial<UserPreference>) => {
38
+ return lambdaClient.user.updatePreference.mutate(preference);
39
+ };
40
+
41
+ updateGuide = async (guide: Partial<UserGuide>) => {
42
+ return lambdaClient.user.updateGuide.mutate(guide);
43
+ };
44
+
45
+ updateUserSettings = async (value: PartialDeep<UserSettings>, signal?: AbortSignal) => {
46
+ return lambdaClient.user.updateSettings.mutate(value, { signal });
47
+ };
48
+
49
+ resetUserSettings = async () => {
50
+ return lambdaClient.user.resetSettings.mutate();
51
+ };
52
+ }
53
+
54
+ export const userService = new UserService();
@@ -101,7 +101,7 @@ describe('AiModelAction', () => {
101
101
  .mockResolvedValue(undefined);
102
102
  const serviceSpy = vi
103
103
  .spyOn(aiModelService, 'batchUpdateAiModels')
104
- .mockResolvedValue(undefined);
104
+ .mockResolvedValue(undefined as any);
105
105
 
106
106
  await act(async () => {
107
107
  await result.current.batchUpdateAiModels(models);
@@ -119,7 +119,7 @@ describe('AiModelAction', () => {
119
119
  const { result } = renderHook(() => useStore());
120
120
  const serviceSpy = vi
121
121
  .spyOn(aiModelService, 'batchUpdateAiModels')
122
- .mockResolvedValue(undefined);
122
+ .mockResolvedValue(undefined as any);
123
123
 
124
124
  await act(async () => {
125
125
  await result.current.batchUpdateAiModels([]);
@@ -137,7 +137,7 @@ describe('AiModelAction', () => {
137
137
  .mockResolvedValue(undefined);
138
138
  const serviceSpy = vi
139
139
  .spyOn(aiModelService, 'clearModelsByProvider')
140
- .mockResolvedValue(undefined);
140
+ .mockResolvedValue(undefined as any);
141
141
 
142
142
  await act(async () => {
143
143
  await result.current.clearModelsByProvider('test-provider');
@@ -154,7 +154,9 @@ describe('AiModelAction', () => {
154
154
  const refreshSpy = vi
155
155
  .spyOn(result.current, 'refreshAiModelList')
156
156
  .mockResolvedValue(undefined);
157
- const serviceSpy = vi.spyOn(aiModelService, 'clearRemoteModels').mockResolvedValue(undefined);
157
+ const serviceSpy = vi
158
+ .spyOn(aiModelService, 'clearRemoteModels')
159
+ .mockResolvedValue(undefined as any);
158
160
 
159
161
  await act(async () => {
160
162
  await result.current.clearRemoteModels('test-provider');
@@ -178,7 +180,9 @@ describe('AiModelAction', () => {
178
180
  const refreshSpy = vi
179
181
  .spyOn(result.current, 'refreshAiModelList')
180
182
  .mockResolvedValue(undefined);
181
- const serviceSpy = vi.spyOn(aiModelService, 'createAiModel').mockResolvedValue(undefined);
183
+ const serviceSpy = vi
184
+ .spyOn(aiModelService, 'createAiModel')
185
+ .mockResolvedValue(undefined as any);
182
186
 
183
187
  await act(async () => {
184
188
  await result.current.createNewAiModel(params);
@@ -349,7 +353,9 @@ describe('AiModelAction', () => {
349
353
  const refreshSpy = vi
350
354
  .spyOn(result.current, 'refreshAiModelList')
351
355
  .mockResolvedValue(undefined);
352
- const serviceSpy = vi.spyOn(aiModelService, 'deleteAiModel').mockResolvedValue(undefined);
356
+ const serviceSpy = vi
357
+ .spyOn(aiModelService, 'deleteAiModel')
358
+ .mockResolvedValue(undefined as any);
353
359
 
354
360
  await act(async () => {
355
361
  await result.current.removeAiModel('model-1', 'test-provider');
@@ -371,7 +377,7 @@ describe('AiModelAction', () => {
371
377
  .mockResolvedValue(undefined);
372
378
  const serviceSpy = vi
373
379
  .spyOn(aiModelService, 'toggleModelEnabled')
374
- .mockResolvedValue(undefined);
380
+ .mockResolvedValue(undefined as any);
375
381
 
376
382
  await act(async () => {
377
383
  await result.current.toggleModelEnabled({ enabled: true, id: 'model-1' });
@@ -395,7 +401,7 @@ describe('AiModelAction', () => {
395
401
  const { result } = renderHook(() => useStore());
396
402
  const serviceSpy = vi
397
403
  .spyOn(aiModelService, 'toggleModelEnabled')
398
- .mockResolvedValue(undefined);
404
+ .mockResolvedValue(undefined as any);
399
405
 
400
406
  await act(async () => {
401
407
  await result.current.toggleModelEnabled({ enabled: true, id: 'model-1' });
@@ -435,7 +441,9 @@ describe('AiModelAction', () => {
435
441
  const refreshSpy = vi
436
442
  .spyOn(result.current, 'refreshAiModelList')
437
443
  .mockResolvedValue(undefined);
438
- const serviceSpy = vi.spyOn(aiModelService, 'updateAiModel').mockResolvedValue(undefined);
444
+ const serviceSpy = vi
445
+ .spyOn(aiModelService, 'updateAiModel')
446
+ .mockResolvedValue(undefined as any);
439
447
 
440
448
  await act(async () => {
441
449
  await result.current.updateAiModelsConfig('model-1', 'test-provider', updateData);
@@ -63,10 +63,12 @@ export const spyOnMessageService = () => {
63
63
  const updateMessageSpy = vi
64
64
  .spyOn(messageService, 'updateMessage')
65
65
  .mockResolvedValue({ messages: [], success: true });
66
- const removeMessageSpy = vi.spyOn(messageService, 'removeMessage').mockResolvedValue(undefined);
66
+ const removeMessageSpy = vi
67
+ .spyOn(messageService, 'removeMessage')
68
+ .mockResolvedValue(undefined as any);
67
69
  const updateMessageErrorSpy = vi
68
70
  .spyOn(messageService, 'updateMessageError')
69
- .mockResolvedValue(undefined);
71
+ .mockResolvedValue(undefined as any);
70
72
 
71
73
  return {
72
74
  createMessageSpy,
@@ -254,7 +254,7 @@ describe('topic action', () => {
254
254
 
255
255
  const updateFavoriteSpy = vi
256
256
  .spyOn(topicService, 'updateTopic')
257
- .mockResolvedValue({ success: 1 });
257
+ .mockResolvedValue(undefined as any);
258
258
 
259
259
  const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
260
260
 
@@ -15,7 +15,6 @@ import { useClientDataSWR } from '@/libs/swr';
15
15
  import { chatService } from '@/services/chat';
16
16
  import { messageService } from '@/services/message';
17
17
  import { topicService } from '@/services/topic';
18
- import { CreateTopicParams } from '@/services/topic/type';
19
18
  import type { ChatStore } from '@/store/chat';
20
19
  import type { ChatStoreState } from '@/store/chat/initialState';
21
20
  import { messageMapKey } from '@/store/chat/utils/messageMapKey';
@@ -24,7 +23,7 @@ import { useSessionStore } from '@/store/session';
24
23
  import { sessionSelectors } from '@/store/session/selectors';
25
24
  import { useUserStore } from '@/store/user';
26
25
  import { systemAgentSelectors } from '@/store/user/selectors';
27
- import { ChatTopic } from '@/types/topic';
26
+ import { ChatTopic, CreateTopicParams } from '@/types/topic';
28
27
  import { merge } from '@/utils/merge';
29
28
  import { setNamespace } from '@/utils/storeDebug';
30
29
 
@@ -1,7 +1,6 @@
1
1
  import { produce } from 'immer';
2
2
 
3
- import { CreateTopicParams } from '@/services/topic/type';
4
- import { ChatTopic } from '@/types/topic';
3
+ import { ChatTopic, CreateTopicParams } from '@/types/topic';
5
4
 
6
5
  interface AddChatTopicAction {
7
6
  type: 'addTopic';
@@ -4,7 +4,6 @@ import { StateCreator } from 'zustand/vanilla';
4
4
  import { notification } from '@/components/AntdStaticMethods';
5
5
  import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
6
6
  import { fileService } from '@/services/file';
7
- import { ServerService } from '@/services/file/server';
8
7
  import { ragService } from '@/services/rag';
9
8
  import { UPLOAD_NETWORK_ERROR } from '@/services/upload';
10
9
  import {
@@ -21,8 +20,6 @@ import { FileStore } from '../../store';
21
20
 
22
21
  const n = setNamespace('chat');
23
22
 
24
- const serverFileService = new ServerService();
25
-
26
23
  export interface FileAction {
27
24
  clearChatUploadFileList: () => void;
28
25
  dispatchChatUploadFileList: (payload: UploadFileListDispatch) => void;
@@ -71,7 +68,7 @@ export const createFileSlice: StateCreator<
71
68
  let fileItem: FileListItem | undefined = undefined;
72
69
 
73
70
  try {
74
- fileItem = await serverFileService.getFileItem(id);
71
+ fileItem = await fileService.getFileItem(id);
75
72
  } catch (e) {
76
73
  console.error('getFileItem Error:', e);
77
74
  continue;
@@ -4,8 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
6
6
  import { useClientDataSWR } from '@/libs/swr';
7
- import { fileService } from '@/services/file';
8
- import { ServerService } from '@/services/file/server';
7
+ import { fileService , FileService } from '@/services/file';
9
8
  import { ragService } from '@/services/rag';
10
9
  import {
11
10
  UploadFileListDispatch,
@@ -18,7 +17,7 @@ import { unzipFile } from '@/utils/unzipFile';
18
17
  import { FileStore } from '../../store';
19
18
  import { fileManagerSelectors } from './selectors';
20
19
 
21
- const serverFileService = new ServerService();
20
+ const serverFileService = new FileService();
22
21
 
23
22
  export interface FileManageAction {
24
23
  dispatchDockFileList: (payload: UploadFileListDispatch) => void;
@@ -42,7 +42,7 @@ describe('createSessionGroupSlice', () => {
42
42
  it('should clear session groups and refresh sessions', async () => {
43
43
  const spyOn = vi
44
44
  .spyOn(sessionService, 'removeSessionGroups')
45
- .mockResolvedValueOnce(undefined);
45
+ .mockResolvedValueOnce(undefined as any);
46
46
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
47
47
 
48
48
  const { result } = renderHook(() => useSessionStore());
@@ -59,7 +59,7 @@ describe('createSessionGroupSlice', () => {
59
59
  describe('removeSessionGroup', () => {
60
60
  it('should remove a session group and refresh sessions', async () => {
61
61
  const mockId = 'mock-id';
62
- vi.spyOn(sessionService, 'removeSessionGroup').mockResolvedValueOnce(undefined);
62
+ vi.spyOn(sessionService, 'removeSessionGroup').mockResolvedValueOnce(undefined as any);
63
63
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
64
64
 
65
65
  const { result } = renderHook(() => useSessionStore());
@@ -77,7 +77,7 @@ describe('createSessionGroupSlice', () => {
77
77
  it('should update a session group id and refresh sessions', async () => {
78
78
  const mockSessionId = 'session-id';
79
79
  const mockGroupId = 'group-id';
80
- vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined);
80
+ vi.spyOn(sessionService, 'updateSession').mockResolvedValueOnce(undefined as any);
81
81
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
82
82
 
83
83
  const { result } = renderHook(() => useSessionStore());
@@ -98,7 +98,7 @@ describe('createSessionGroupSlice', () => {
98
98
  const mockId = 'mock-id';
99
99
  const mockName = 'New Name';
100
100
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
101
- vi.spyOn(sessionService, 'updateSessionGroup').mockResolvedValueOnce(undefined);
101
+ vi.spyOn(sessionService, 'updateSessionGroup').mockResolvedValueOnce(undefined as any);
102
102
 
103
103
  const { result } = renderHook(() => useSessionStore());
104
104
 
@@ -117,7 +117,7 @@ describe('createSessionGroupSlice', () => {
117
117
  { id: 'id1', sort: 0 },
118
118
  { id: 'id2', sort: 1 },
119
119
  ];
120
- vi.spyOn(sessionService, 'updateSessionGroupOrder').mockResolvedValueOnce(undefined);
120
+ vi.spyOn(sessionService, 'updateSessionGroupOrder').mockResolvedValueOnce(undefined as any);
121
121
  const spyOnRefreshSessions = vi.spyOn(useSessionStore.getState(), 'refreshSessions');
122
122
 
123
123
  const { result } = renderHook(() => useSessionStore());
@@ -20,6 +20,44 @@ vi.mock('@/utils/sleep', () => ({
20
20
  sleep: vi.fn().mockResolvedValue(undefined),
21
21
  }));
22
22
 
23
+ const ORIGINAL_DESKTOP_ENV = process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
24
+
25
+ const bootstrapToolStoreWithDesktop = async (isDesktopEnv: boolean) => {
26
+ vi.resetModules();
27
+ vi.mock('zustand/traditional');
28
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = isDesktopEnv ? '1' : '0';
29
+
30
+ vi.doMock('@lobechat/const', async () => {
31
+ const actual = await vi.importActual<typeof import('@lobechat/const')>('@lobechat/const');
32
+ return {
33
+ ...actual,
34
+ isDesktop: isDesktopEnv,
35
+ };
36
+ });
37
+
38
+ const storeModule = await import('@/store/tool');
39
+ const discoverModule = await import('@/services/discover');
40
+ const helpersModule = await import('@/store/global/helpers');
41
+
42
+ const cleanup = () => {
43
+ vi.resetModules();
44
+ vi.doUnmock('@lobechat/const');
45
+ vi.mock('zustand/traditional');
46
+ if (ORIGINAL_DESKTOP_ENV === undefined) {
47
+ delete process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
48
+ } else {
49
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = ORIGINAL_DESKTOP_ENV;
50
+ }
51
+ };
52
+
53
+ return {
54
+ useToolStore: storeModule.useToolStore,
55
+ discoverService: discoverModule.discoverService,
56
+ globalHelpers: helpersModule.globalHelpers,
57
+ cleanup,
58
+ };
59
+ };
60
+
23
61
  beforeEach(() => {
24
62
  vi.clearAllMocks();
25
63
 
@@ -48,6 +86,14 @@ afterEach(() => {
48
86
  vi.restoreAllMocks();
49
87
  });
50
88
 
89
+ afterAll(() => {
90
+ if (ORIGINAL_DESKTOP_ENV === undefined) {
91
+ delete process.env.NEXT_PUBLIC_IS_DESKTOP_APP;
92
+ } else {
93
+ process.env.NEXT_PUBLIC_IS_DESKTOP_APP = ORIGINAL_DESKTOP_ENV;
94
+ }
95
+ });
96
+
51
97
  describe('mcpStore actions', () => {
52
98
  describe('updateMCPInstallProgress', () => {
53
99
  it('should update install progress for an identifier', () => {
@@ -487,7 +533,9 @@ describe('mcpStore actions', () => {
487
533
  expect(result.current.data).toEqual(mockData);
488
534
  });
489
535
 
490
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith({ page: 1, pageSize: 20 });
536
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
537
+ expect.objectContaining({ page: 1, pageSize: 20, connectionType: 'http' }),
538
+ );
491
539
 
492
540
  const state = useToolStore.getState();
493
541
  expect(state.mcpPluginItems).toEqual(mockData.items);
@@ -542,7 +590,9 @@ describe('mcpStore actions', () => {
542
590
  renderHook(() => useToolStore.getState().useFetchMCPPluginList(params));
543
591
 
544
592
  await waitFor(() => {
545
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(params);
593
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
594
+ expect.objectContaining({ ...params, connectionType: 'http' }),
595
+ );
546
596
  });
547
597
  });
548
598
 
@@ -561,9 +611,51 @@ describe('mcpStore actions', () => {
561
611
  renderHook(() => useToolStore.getState().useFetchMCPPluginList(params));
562
612
 
563
613
  await waitFor(() => {
564
- expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(params);
614
+ expect(discoverService.getMCPPluginList).toHaveBeenCalledWith(
615
+ expect.objectContaining({ ...params, connectionType: 'http' }),
616
+ );
565
617
  });
566
618
  });
619
+
620
+ it('should not append connectionType in desktop environment', async () => {
621
+ const {
622
+ useToolStore: desktopStore,
623
+ discoverService: desktopDiscoverService,
624
+ globalHelpers: desktopGlobalHelpers,
625
+ cleanup,
626
+ } = await bootstrapToolStoreWithDesktop(true);
627
+
628
+ const mockData = {
629
+ items: [{ identifier: 'desktop-plugin', name: 'Desktop Plugin' }] as PluginItem[],
630
+ categories: [],
631
+ totalCount: 1,
632
+ totalPages: 1,
633
+ currentPage: 1,
634
+ pageSize: 20,
635
+ };
636
+
637
+ try {
638
+ vi.spyOn(desktopGlobalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
639
+ const fetchSpy = vi
640
+ .spyOn(desktopDiscoverService, 'getMCPPluginList')
641
+ .mockResolvedValue(mockData);
642
+
643
+ const { result } = renderHook(() =>
644
+ desktopStore.getState().useFetchMCPPluginList({ page: 1, pageSize: 20 }),
645
+ );
646
+
647
+ await waitFor(() => {
648
+ expect(result.current.data).toEqual(mockData);
649
+ });
650
+
651
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
652
+ const [firstCallArgs] = fetchSpy.mock.calls[0];
653
+ expect(firstCallArgs).toMatchObject({ page: 1, pageSize: 20 });
654
+ expect(firstCallArgs.connectionType).toBeUndefined();
655
+ } finally {
656
+ cleanup();
657
+ }
658
+ });
567
659
  });
568
660
 
569
661
  describe('installMCPPlugin', () => {