@lobehub/lobehub 2.0.0-next.142 → 2.0.0-next.144

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 (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +1 -0
  3. package/apps/desktop/src/main/core/ui/__tests__/MenuManager.test.ts +320 -0
  4. package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +518 -0
  5. package/apps/desktop/src/main/core/ui/__tests__/TrayManager.test.ts +360 -0
  6. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.test.ts +49 -0
  7. package/apps/desktop/src/main/menus/impls/linux.test.ts +552 -0
  8. package/apps/desktop/src/main/menus/impls/macOS.test.ts +464 -0
  9. package/apps/desktop/src/main/menus/impls/windows.test.ts +429 -0
  10. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +2 -2
  11. package/apps/desktop/src/main/services/__tests__/fileSearchSrv.test.ts +402 -0
  12. package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +91 -0
  13. package/apps/desktop/src/main/utils/__tests__/logger.test.ts +229 -0
  14. package/apps/desktop/src/preload/electronApi.test.ts +142 -0
  15. package/apps/desktop/src/preload/invoke.test.ts +145 -0
  16. package/apps/desktop/src/preload/routeInterceptor.test.ts +374 -0
  17. package/apps/desktop/src/preload/streamer.test.ts +365 -0
  18. package/apps/desktop/vitest.config.mts +1 -0
  19. package/changelog/v1.json +18 -0
  20. package/locales/ar/marketAuth.json +13 -0
  21. package/locales/bg-BG/marketAuth.json +13 -0
  22. package/locales/de-DE/marketAuth.json +13 -0
  23. package/locales/en-US/marketAuth.json +13 -0
  24. package/locales/es-ES/marketAuth.json +13 -0
  25. package/locales/fa-IR/marketAuth.json +13 -0
  26. package/locales/fr-FR/marketAuth.json +13 -0
  27. package/locales/it-IT/marketAuth.json +13 -0
  28. package/locales/ja-JP/marketAuth.json +13 -0
  29. package/locales/ko-KR/marketAuth.json +13 -0
  30. package/locales/nl-NL/marketAuth.json +13 -0
  31. package/locales/pl-PL/marketAuth.json +13 -0
  32. package/locales/pt-BR/marketAuth.json +13 -0
  33. package/locales/ru-RU/marketAuth.json +13 -0
  34. package/locales/tr-TR/marketAuth.json +13 -0
  35. package/locales/vi-VN/marketAuth.json +13 -0
  36. package/locales/zh-CN/marketAuth.json +13 -0
  37. package/locales/zh-TW/marketAuth.json +13 -0
  38. package/package.json +1 -1
  39. package/packages/database/migrations/0054_better_auth_two_factor.sql +2 -0
  40. package/packages/database/src/core/migrations.json +1 -1
  41. package/packages/database/src/models/user.ts +27 -5
  42. package/packages/types/src/discover/mcp.ts +2 -1
  43. package/packages/types/src/tool/plugin.ts +2 -1
  44. package/scripts/migrateServerDB/errorHint.js +26 -0
  45. package/scripts/migrateServerDB/index.ts +5 -1
  46. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  47. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  48. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  49. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  50. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  51. package/src/libs/mcp/types.ts +8 -0
  52. package/src/locales/default/marketAuth.ts +13 -0
  53. package/src/server/routers/lambda/market/index.ts +85 -2
  54. package/src/server/services/discover/index.ts +45 -4
  55. package/src/services/discover.ts +1 -1
  56. package/src/services/mcp.ts +18 -3
  57. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  58. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -902,6 +902,147 @@ describe('mcpStore actions', () => {
902
902
  });
903
903
  });
904
904
 
905
+ describe('cloudEndPoint support', () => {
906
+ it('should create cloud type connection when cloudEndPoint is available on web', async () => {
907
+ const { result } = renderHook(() => useToolStore());
908
+
909
+ const mockManifestWithCloudEndpoint = {
910
+ ...mockManifest,
911
+ cloudEndPoint: true,
912
+ tools: [
913
+ {
914
+ name: 'testTool',
915
+ description: 'Test tool description',
916
+ inputSchema: {
917
+ type: 'object',
918
+ properties: {
919
+ param: { type: 'string' },
920
+ },
921
+ },
922
+ },
923
+ ],
924
+ author: 'Test Author',
925
+ createdAt: '2024-01-01T00:00:00Z',
926
+ homepage: 'https://example.com',
927
+ manifestUrl: 'https://example.com/manifest.json',
928
+ icon: 'https://example.com/icon.png',
929
+ description: 'Test description',
930
+ tags: ['test'],
931
+ deploymentOptions: [
932
+ {
933
+ connection: {
934
+ type: 'stdio',
935
+ configSchema: {
936
+ type: 'object',
937
+ properties: {
938
+ API_KEY: { type: 'string' },
939
+ },
940
+ },
941
+ },
942
+ installationMethod: 'uv',
943
+ },
944
+ ],
945
+ };
946
+
947
+ vi.spyOn(discoverService, 'getMCPPluginManifest').mockResolvedValue(
948
+ mockManifestWithCloudEndpoint as any,
949
+ );
950
+
951
+ // Mock isDesktop to false (web environment)
952
+ const originalIsDesktop = (await import('@lobechat/const')).isDesktop;
953
+ vi.spyOn(await import('@lobechat/const'), 'isDesktop', 'get').mockReturnValue(false);
954
+
955
+ // Create plugin with haveCloudEndpoint field
956
+ const mockPluginWithCloudEndpoint = {
957
+ ...mockPlugin,
958
+ haveCloudEndpoint: true,
959
+ } as any;
960
+
961
+ act(() => {
962
+ useToolStore.setState({
963
+ mcpPluginItems: [mockPluginWithCloudEndpoint],
964
+ });
965
+ });
966
+
967
+ const installPluginSpy = vi.spyOn(pluginService, 'installPlugin');
968
+
969
+ let installResult;
970
+ await act(async () => {
971
+ installResult = await result.current.installMCPPlugin('test-plugin');
972
+ });
973
+
974
+ expect(installResult).toBe(true);
975
+
976
+ // Should create cloud type connection
977
+ expect(installPluginSpy).toHaveBeenCalledWith(
978
+ expect.objectContaining({
979
+ customParams: expect.objectContaining({
980
+ mcp: expect.objectContaining({
981
+ type: 'cloud',
982
+ cloudEndPoint: true,
983
+ }),
984
+ }),
985
+ }),
986
+ );
987
+
988
+ // Should NOT call stdio connection
989
+ expect(mcpService.getStdioMcpServerManifest).not.toHaveBeenCalled();
990
+ // Should NOT call checkInstallation (skipped for cloud)
991
+ expect(mcpService.checkInstallation).not.toHaveBeenCalled();
992
+
993
+ // Restore original isDesktop
994
+ vi.spyOn(await import('@lobechat/const'), 'isDesktop', 'get').mockReturnValue(
995
+ originalIsDesktop,
996
+ );
997
+ });
998
+
999
+ it('should use stdio deployment when cloudEndPoint is not available', async () => {
1000
+ const { result } = renderHook(() => useToolStore());
1001
+
1002
+ // No cloudEndPoint in manifest
1003
+ const mockManifestWithoutCloudEndpoint = {
1004
+ ...mockManifest,
1005
+ deploymentOptions: [
1006
+ {
1007
+ connection: {
1008
+ type: 'stdio',
1009
+ },
1010
+ installationMethod: 'uv',
1011
+ },
1012
+ ],
1013
+ };
1014
+
1015
+ vi.spyOn(discoverService, 'getMCPPluginManifest').mockResolvedValue(
1016
+ mockManifestWithoutCloudEndpoint as any,
1017
+ );
1018
+
1019
+ // Mock isDesktop to false (web environment)
1020
+ const originalIsDesktop = (await import('@lobechat/const')).isDesktop;
1021
+ vi.spyOn(await import('@lobechat/const'), 'isDesktop', 'get').mockReturnValue(false);
1022
+
1023
+ act(() => {
1024
+ useToolStore.setState({
1025
+ mcpPluginItems: [mockPlugin],
1026
+ });
1027
+ });
1028
+
1029
+ let installResult;
1030
+ await act(async () => {
1031
+ installResult = await result.current.installMCPPlugin('test-plugin');
1032
+ });
1033
+
1034
+ expect(installResult).toBe(true);
1035
+ // Should fall back to stdio
1036
+ expect(mcpService.checkInstallation).toHaveBeenCalled();
1037
+ expect(mcpService.getStdioMcpServerManifest).toHaveBeenCalled();
1038
+
1039
+ // Restore original isDesktop
1040
+ vi.spyOn(await import('@lobechat/const'), 'isDesktop', 'get').mockReturnValue(
1041
+ originalIsDesktop,
1042
+ );
1043
+ });
1044
+ });
1045
+
905
1046
  describe('resume mode', () => {
906
1047
  it('should resume installation with previous config info', async () => {
907
1048
  const { result } = renderHook(() => useToolStore());
@@ -1,3 +1,4 @@
1
+ import { CURRENT_VERSION, isDesktop } from '@lobechat/const';
1
2
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
2
3
  import { PluginItem, PluginListResponse } from '@lobehub/market-sdk';
3
4
  import { TRPCClientError } from '@trpc/client';
@@ -8,7 +9,6 @@ import { gt, valid } from 'semver';
8
9
  import useSWR, { SWRResponse } from 'swr';
9
10
  import { StateCreator } from 'zustand/vanilla';
10
11
 
11
- import { CURRENT_VERSION, isDesktop } from '@lobechat/const';
12
12
  import { MCPErrorData } from '@/libs/mcp/types';
13
13
  import { discoverService } from '@/services/discover';
14
14
  import { mcpService } from '@/services/mcp';
@@ -65,6 +65,71 @@ const toNonEmptyStringRecord = (input?: Record<string, any>) => {
65
65
  }, {});
66
66
  };
67
67
 
68
+ /**
69
+ * Build manifest for cloud MCP connection from market data
70
+ * 从市场数据构建 Cloud MCP 的 manifest
71
+ */
72
+ const buildCloudMcpManifest = (params: {
73
+ data: any;
74
+ plugin: { description?: string, icon?: string; identifier: string; };
75
+ }): LobeChatPluginManifest => {
76
+ const { data, plugin } = params;
77
+
78
+ log('Using cloud connection, building manifest from market data');
79
+
80
+ // 从 data 中获取 tools(MCP 格式)或 api(LobeChat 格式)
81
+ const mcpTools = data.tools;
82
+ const lobeChatApi = data.api;
83
+
84
+ // 如果是 MCP 格式的 tools,需要转换为 LobeChat 的 api 格式
85
+ // MCP: { name, description, inputSchema }
86
+ // LobeChat: { name, description, parameters }
87
+ let apiArray: any[] = [];
88
+
89
+ if (lobeChatApi) {
90
+ // 已经是 LobeChat 格式,直接使用
91
+ apiArray = lobeChatApi;
92
+ log('[Cloud MCP] Using existing LobeChat API format');
93
+ } else if (mcpTools && Array.isArray(mcpTools)) {
94
+ // 转换 MCP tools 格式到 LobeChat api 格式
95
+ apiArray = mcpTools.map((tool: any) => ({
96
+ description: tool.description || '',
97
+ name: tool.name,
98
+ parameters: tool.inputSchema || {},
99
+ }));
100
+ log('[Cloud MCP] Converted %d MCP tools to LobeChat API format', apiArray.length);
101
+ } else {
102
+ console.warn('[Cloud MCP] No tools or api found in manifest data');
103
+ }
104
+
105
+ // 构建完整的 manifest
106
+ const manifest: LobeChatPluginManifest = {
107
+ api: apiArray,
108
+ author: data.author?.name || data.author || '',
109
+ createAt: data.createdAt || new Date().toISOString(),
110
+ homepage: data.homepage || '',
111
+ identifier: plugin.identifier,
112
+ manifest: data.manifestUrl || '',
113
+ meta: {
114
+ avatar: data.icon || plugin.icon,
115
+ description: plugin.description || data.description,
116
+ tags: data.tags || [],
117
+ title: data.name || plugin.identifier,
118
+ },
119
+ name: data.name || plugin.identifier,
120
+ type: 'mcp',
121
+ version: data.version,
122
+ } as unknown as LobeChatPluginManifest;
123
+
124
+ log('[Cloud MCP] Final manifest built:', {
125
+ apiCount: manifest.api?.length,
126
+ identifier: manifest.identifier,
127
+ version: manifest.version,
128
+ });
129
+
130
+ return manifest;
131
+ };
132
+
68
133
  // 测试连接结果类型
69
134
  export interface TestMcpConnectionResult {
70
135
  error?: string;
@@ -139,6 +204,9 @@ export const createMCPPluginStoreSlice: StateCreator<
139
204
  const normalizedConfig = toNonEmptyStringRecord(config);
140
205
  let plugin = mcpStoreSelectors.getPluginById(identifier)(get());
141
206
 
207
+ // @ts-expect-error
208
+ const { haveCloudEndpoint } = plugin || {};
209
+
142
210
  if (!plugin || !plugin.manifestUrl) {
143
211
  const data = await discoverService.getMcpDetail({ identifier });
144
212
  if (!data) return;
@@ -211,12 +279,18 @@ export const createMCPPluginStoreSlice: StateCreator<
211
279
  ? data.deploymentOptions
212
280
  : [];
213
281
 
214
- const httpOption = deploymentOptions.find(
215
- (option) => option?.connection?.url && option?.connection?.type === 'http',
216
- ) ||
282
+ const httpOption =
217
283
  deploymentOptions.find(
218
- (option) => option?.connection?.url && !option?.connection?.type,
219
- );
284
+ (option) => option?.connection?.url && option?.connection?.type === 'http',
285
+ ) ||
286
+ deploymentOptions.find((option) => option?.connection?.url && !option?.connection?.type);
287
+
288
+ // 查找 stdio 类型的部署选项
289
+ const stdioOption = deploymentOptions.find(
290
+ (option) =>
291
+ option?.connection?.type === 'stdio' ||
292
+ (!option?.connection?.type && !option?.connection?.url),
293
+ );
220
294
 
221
295
  const hasNonHttpDeployment = deploymentOptions.some((option) => {
222
296
  const type = option?.connection?.type;
@@ -225,9 +299,46 @@ export const createMCPPluginStoreSlice: StateCreator<
225
299
  return type && type !== 'http';
226
300
  });
227
301
 
228
- const shouldUseHttpDeployment = !!httpOption && (!hasNonHttpDeployment || !isDesktop);
302
+ // 🌐 检查是否有 cloudEndPoint:网页端 + stdio 类型 + 存在 haveCloudEndpoint
303
+ const hasCloudEndpoint = !isDesktop && stdioOption && haveCloudEndpoint;
304
+
305
+ console.log('hasCloudEndpoint', hasCloudEndpoint);
306
+
307
+ let shouldUseHttpDeployment = !!httpOption && (!hasNonHttpDeployment || !isDesktop);
308
+
309
+ if (hasCloudEndpoint) {
310
+ // 🌐 使用 cloudEndPoint,创建 cloud 类型的 connection
311
+ log('Using cloudEndPoint for stdio plugin: %s', haveCloudEndpoint);
312
+
313
+ connection = {
314
+ auth: stdioOption?.connection?.auth || { type: 'none' },
315
+ cloudEndPoint: haveCloudEndpoint,
316
+ headers: stdioOption?.connection?.headers,
317
+ type: 'cloud',
318
+ } as any;
319
+
320
+ log('Using cloud connection: %O', {
321
+ cloudEndPoint: haveCloudEndpoint,
322
+ type: connection.type,
323
+ });
324
+
325
+ const configSchema = stdioOption?.connection?.configSchema;
326
+ const needsConfig = doesConfigSchemaRequireInput(configSchema);
327
+
328
+ if (needsConfig && !normalizedConfig) {
329
+ updateMCPInstallProgress(identifier, {
330
+ configSchema,
331
+ connection,
332
+ manifest: data,
333
+ needsConfig: true,
334
+ progress: 50,
335
+ step: MCPInstallStep.CONFIGURATION_REQUIRED,
336
+ });
229
337
 
230
- if (shouldUseHttpDeployment && httpOption) {
338
+ updateInstallLoadingState(identifier, undefined);
339
+ return false;
340
+ }
341
+ } else if (shouldUseHttpDeployment && httpOption) {
231
342
  // ✅ HTTP 类型:跳过系统依赖检查,直接使用 URL
232
343
  log('HTTP MCP detected, skipping system dependency check');
233
344
 
@@ -317,6 +428,7 @@ export const createMCPPluginStoreSlice: StateCreator<
317
428
 
318
429
  let mergedHttpHeaders: Record<string, string> | undefined;
319
430
  let mergedStdioEnv: Record<string, string> | undefined;
431
+ let mergedCloudHeaders: Record<string, string> | undefined;
320
432
 
321
433
  if (connection?.type === 'http') {
322
434
  const baseHeaders = toNonEmptyStringRecord(connection.headers);
@@ -340,6 +452,17 @@ export const createMCPPluginStoreSlice: StateCreator<
340
452
  }
341
453
  }
342
454
 
455
+ if (connection?.type === 'cloud') {
456
+ const baseHeaders = toNonEmptyStringRecord(connection.headers);
457
+
458
+ if (baseHeaders || normalizedConfig) {
459
+ mergedCloudHeaders = {
460
+ ...baseHeaders,
461
+ ...normalizedConfig,
462
+ };
463
+ }
464
+ }
465
+
343
466
  // 获取服务器清单逻辑
344
467
  updateInstallLoadingState(identifier, true);
345
468
 
@@ -383,6 +506,10 @@ export const createMCPPluginStoreSlice: StateCreator<
383
506
  abortController.signal,
384
507
  );
385
508
  }
509
+ if (connection?.type === 'cloud') {
510
+ // 🌐 Cloud 类型:直接从市场数据构建 manifest
511
+ manifest = buildCloudMcpManifest({ data, plugin });
512
+ }
386
513
 
387
514
  // set version
388
515
  if (manifest) {
@@ -425,9 +552,21 @@ export const createMCPPluginStoreSlice: StateCreator<
425
552
  return;
426
553
  }
427
554
 
555
+ // 更新 connection 对象,将合并后的配置写入
556
+ const finalConnection = { ...connection };
557
+ if (finalConnection.type === 'http' && mergedHttpHeaders) {
558
+ finalConnection.headers = mergedHttpHeaders;
559
+ }
560
+ if (finalConnection.type === 'stdio' && mergedStdioEnv) {
561
+ finalConnection.env = mergedStdioEnv;
562
+ }
563
+ if (finalConnection.type === 'cloud' && mergedCloudHeaders) {
564
+ finalConnection.headers = mergedCloudHeaders;
565
+ }
566
+
428
567
  await pluginService.installPlugin({
429
568
  // 针对 mcp 先将 connection 信息存到 customParams 字段里
430
- customParams: { mcp: connection },
569
+ customParams: { mcp: finalConnection },
431
570
  identifier: plugin.identifier,
432
571
  manifest: manifest,
433
572
  settings: normalizedConfig,
@@ -693,7 +832,9 @@ export const createMCPPluginStoreSlice: StateCreator<
693
832
 
694
833
  useFetchMCPPluginList: (params) => {
695
834
  const locale = globalHelpers.getCurrentLanguage();
696
- const requestParams = isDesktop ? params : { ...params, connectionType: McpConnectionType.http };
835
+ const requestParams = isDesktop
836
+ ? params
837
+ : { ...params, connectionType: McpConnectionType.http };
697
838
  const swrKeyParts = [
698
839
  'useFetchMCPPluginList',
699
840
  locale,
@@ -702,7 +843,8 @@ export const createMCPPluginStoreSlice: StateCreator<
702
843
  requestParams.q,
703
844
  requestParams.connectionType,
704
845
  ];
705
- const swrKey = swrKeyParts.filter((part) => part !== undefined && part !== null && part !== '')
846
+ const swrKey = swrKeyParts
847
+ .filter((part) => part !== undefined && part !== null && part !== '')
706
848
  .join('-');
707
849
  const page = requestParams.page ?? 1;
708
850