@lobehub/chat 1.82.10 → 1.83.0

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 (195) hide show
  1. package/.env.desktop +1 -2
  2. package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
  3. package/.github/workflows/release-desktop-beta.yml +196 -0
  4. package/CHANGELOG.md +25 -0
  5. package/apps/desktop/.i18nrc.js +31 -0
  6. package/apps/desktop/Development.md +47 -0
  7. package/apps/desktop/README.md +6 -0
  8. package/apps/desktop/build/Icon-beta.icns +0 -0
  9. package/apps/desktop/build/Icon-nightly.icns +0 -0
  10. package/apps/desktop/build/Icon.icns +0 -0
  11. package/apps/desktop/build/entitlements.mac.plist +12 -0
  12. package/apps/desktop/build/favicon.ico +0 -0
  13. package/apps/desktop/build/icon-beta.png +0 -0
  14. package/apps/desktop/build/icon-dev.png +0 -0
  15. package/apps/desktop/build/icon-nightly.ico +0 -0
  16. package/apps/desktop/build/icon-nightly.png +0 -0
  17. package/apps/desktop/build/icon.ico +0 -0
  18. package/apps/desktop/build/icon.png +0 -0
  19. package/apps/desktop/dev-app-update.yml +6 -0
  20. package/apps/desktop/electron-builder.js +92 -0
  21. package/apps/desktop/electron.vite.config.ts +40 -0
  22. package/apps/desktop/package.json +72 -0
  23. package/apps/desktop/pnpm-workspace.yaml +5 -0
  24. package/apps/desktop/resources/error.html +136 -0
  25. package/apps/desktop/resources/locales/ar/common.json +32 -0
  26. package/apps/desktop/resources/locales/ar/dialog.json +31 -0
  27. package/apps/desktop/resources/locales/ar/menu.json +70 -0
  28. package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
  29. package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
  30. package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
  31. package/apps/desktop/resources/locales/de-DE/common.json +32 -0
  32. package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
  33. package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
  34. package/apps/desktop/resources/locales/en-US/common.json +32 -0
  35. package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
  36. package/apps/desktop/resources/locales/en-US/menu.json +70 -0
  37. package/apps/desktop/resources/locales/es-ES/common.json +32 -0
  38. package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
  39. package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
  40. package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
  41. package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
  42. package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
  43. package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
  44. package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
  45. package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
  46. package/apps/desktop/resources/locales/it-IT/common.json +32 -0
  47. package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
  48. package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
  49. package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
  50. package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
  51. package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
  52. package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
  53. package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
  54. package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
  55. package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
  56. package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
  57. package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
  58. package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
  59. package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
  60. package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
  61. package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
  62. package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
  63. package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
  64. package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
  65. package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
  66. package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
  67. package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
  68. package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
  69. package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
  70. package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
  71. package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
  72. package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
  73. package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
  74. package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
  75. package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
  76. package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
  77. package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
  78. package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
  79. package/apps/desktop/resources/splash.html +88 -0
  80. package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
  81. package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
  82. package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
  83. package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
  84. package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
  85. package/apps/desktop/scripts/pglite-server.ts +14 -0
  86. package/apps/desktop/src/common/routes.ts +78 -0
  87. package/apps/desktop/src/main/appBrowsers.ts +47 -0
  88. package/apps/desktop/src/main/const/dir.ts +29 -0
  89. package/apps/desktop/src/main/const/env.ts +3 -0
  90. package/apps/desktop/src/main/const/store.ts +22 -0
  91. package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
  92. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
  93. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
  94. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
  95. package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
  96. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
  97. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
  98. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
  99. package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
  100. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
  101. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
  102. package/apps/desktop/src/main/controllers/_template.ts +9 -0
  103. package/apps/desktop/src/main/controllers/index.ts +58 -0
  104. package/apps/desktop/src/main/core/App.ts +370 -0
  105. package/apps/desktop/src/main/core/Browser.ts +345 -0
  106. package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
  107. package/apps/desktop/src/main/core/I18nManager.ts +185 -0
  108. package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
  109. package/apps/desktop/src/main/core/MenuManager.ts +64 -0
  110. package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
  111. package/apps/desktop/src/main/core/StoreManager.ts +89 -0
  112. package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
  113. package/apps/desktop/src/main/index.ts +5 -0
  114. package/apps/desktop/src/main/locales/default/common.ts +34 -0
  115. package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
  116. package/apps/desktop/src/main/locales/default/index.ts +11 -0
  117. package/apps/desktop/src/main/locales/default/menu.ts +72 -0
  118. package/apps/desktop/src/main/locales/resources.ts +35 -0
  119. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
  120. package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
  121. package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
  122. package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
  123. package/apps/desktop/src/main/menus/index.ts +34 -0
  124. package/apps/desktop/src/main/menus/types.ts +28 -0
  125. package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
  126. package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
  127. package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
  128. package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
  129. package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
  130. package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
  131. package/apps/desktop/src/main/services/fileSrv.ts +255 -0
  132. package/apps/desktop/src/main/services/index.ts +9 -0
  133. package/apps/desktop/src/main/shortcuts/config.ts +18 -0
  134. package/apps/desktop/src/main/shortcuts/index.ts +1 -0
  135. package/apps/desktop/src/main/types/fileSearch.ts +51 -0
  136. package/apps/desktop/src/main/types/store.ts +14 -0
  137. package/apps/desktop/src/main/utils/file-system.ts +15 -0
  138. package/apps/desktop/src/main/utils/logger.ts +44 -0
  139. package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
  140. package/apps/desktop/src/preload/electronApi.ts +18 -0
  141. package/apps/desktop/src/preload/index.ts +14 -0
  142. package/apps/desktop/src/preload/invoke.ts +10 -0
  143. package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
  144. package/apps/desktop/tsconfig.json +21 -0
  145. package/changelog/v1.json +9 -0
  146. package/package.json +1 -1
  147. package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
  148. package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
  149. package/packages/electron-client-ipc/src/types/index.ts +2 -1
  150. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
  151. package/packages/electron-server-ipc/src/const.ts +3 -3
  152. package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
  153. package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
  154. package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
  155. package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
  156. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  157. package/src/components/Analytics/Desktop.tsx +19 -0
  158. package/src/components/Analytics/index.tsx +3 -0
  159. package/src/database/core/db-adaptor.ts +4 -1
  160. package/src/database/core/electron.ts +317 -0
  161. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  162. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  163. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  164. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  166. package/src/libs/trpc/client/async.ts +6 -0
  167. package/src/libs/trpc/client/edge.ts +6 -0
  168. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  169. package/src/libs/trpc/client/index.ts +1 -0
  170. package/src/libs/trpc/client/lambda.ts +10 -1
  171. package/src/libs/trpc/client/tools.ts +6 -0
  172. package/src/server/globalConfig/index.ts +0 -3
  173. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  174. package/src/server/routers/desktop/index.ts +2 -0
  175. package/src/server/routers/desktop/mcp.ts +47 -0
  176. package/src/server/routers/lambda/user.ts +38 -23
  177. package/src/server/routers/tools/mcp.ts +0 -6
  178. package/src/services/electron/remoteServer.ts +4 -4
  179. package/src/services/mcp.ts +17 -7
  180. package/src/services/upload.ts +9 -0
  181. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  182. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  183. package/src/store/electron/actions/sync.ts +20 -19
  184. package/src/store/electron/initialState.ts +3 -3
  185. package/src/store/electron/selectors/sync.ts +6 -3
  186. package/src/store/electron/store.ts +2 -0
  187. package/src/store/file/slices/upload/action.ts +11 -3
  188. package/src/store/tool/selectors/tool.ts +10 -1
  189. package/src/utils/fetch/headers.ts +27 -0
  190. package/src/utils/fetch/index.ts +2 -0
  191. package/src/utils/fetch/request.ts +28 -0
  192. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  193. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -2,6 +2,7 @@ import { createTRPCClient, httpBatchLink } from '@trpc/client';
2
2
  import { createTRPCReact } from '@trpc/react-query';
3
3
  import superjson from 'superjson';
4
4
 
5
+ import { isDesktop } from '@/const/version';
5
6
  import { ModelProvider } from '@/libs/agent-runtime';
6
7
  import type { LambdaRouter } from '@/server/routers/lambda';
7
8
 
@@ -10,7 +11,16 @@ import { ErrorResponse } from './types';
10
11
  const links = [
11
12
  httpBatchLink({
12
13
  fetch: async (input, init) => {
14
+ if (isDesktop) {
15
+ const { desktopRemoteRPCFetch } = await import('./helpers/desktopRemoteRPCFetch');
16
+
17
+ const res = await desktopRemoteRPCFetch(input as string, init);
18
+
19
+ if (res) return res;
20
+ }
21
+
13
22
  const response = await fetch(input, init);
23
+
14
24
  if (response.ok) return response;
15
25
 
16
26
  const errorRes: ErrorResponse = await response.clone().json();
@@ -20,7 +30,6 @@ const links = [
20
30
 
21
31
  errorRes.forEach((item) => {
22
32
  const errorData = item.error.json;
23
-
24
33
  const status = errorData.data.httpStatus;
25
34
 
26
35
  switch (status) {
@@ -1,11 +1,17 @@
1
1
  import { createTRPCClient, httpBatchLink } from '@trpc/client';
2
2
  import superjson from 'superjson';
3
3
 
4
+ import { isDesktop } from '@/const/version';
4
5
  import type { ToolsRouter } from '@/server/routers/tools';
5
6
 
7
+ import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
8
+
6
9
  export const toolsClient = createTRPCClient<ToolsRouter>({
7
10
  links: [
8
11
  httpBatchLink({
12
+ fetch: isDesktop
13
+ ? (input, init) => fetchWithDesktopRemoteRPC(input as string, init)
14
+ : undefined,
9
15
  headers: async () => {
10
16
  // dynamic import to avoid circular dependency
11
17
  const { createHeaderWithAuth } = await import('@/services/_auth');
@@ -40,9 +40,6 @@ export const getServerGlobalConfig = async () => {
40
40
  enabled: isDesktop ? true : undefined,
41
41
  fetchOnClient: isDesktop ? false : !process.env.OLLAMA_PROXY_URL,
42
42
  },
43
- openai: {
44
- enabled: isDesktop ? false : undefined,
45
- },
46
43
  tencentcloud: {
47
44
  enabledKey: 'ENABLED_TENCENT_CLOUD',
48
45
  modelListKey: 'TENCENT_CLOUD_MODEL_LIST',
@@ -1,5 +1,7 @@
1
1
  import { ElectronIpcClient } from '@lobechat/electron-server-ipc';
2
2
 
3
+ import packageJSON from '@/../apps/desktop/package.json';
4
+
3
5
  class LobeHubElectronIpcClient extends ElectronIpcClient {
4
6
  // 获取数据库路径
5
7
  getDatabasePath = async (): Promise<string> => {
@@ -33,4 +35,4 @@ class LobeHubElectronIpcClient extends ElectronIpcClient {
33
35
  };
34
36
  }
35
37
 
36
- export const electronIpcClient = new LobeHubElectronIpcClient();
38
+ export const electronIpcClient = new LobeHubElectronIpcClient(packageJSON.name);
@@ -1,8 +1,10 @@
1
1
  import { router } from '@/libs/trpc/lambda';
2
+ import { mcpRouter } from '@/server/routers/desktop/mcp';
2
3
 
3
4
  import { pgTableRouter } from './pgTable';
4
5
 
5
6
  export const desktopRouter = router({
7
+ mcp: mcpRouter,
6
8
  pgTable: pgTableRouter,
7
9
  });
8
10
 
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+
3
+ import { isServerMode } from '@/const/version';
4
+ import { passwordProcedure } from '@/libs/trpc/edge';
5
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
6
+ import { mcpService } from '@/server/services/mcp';
7
+
8
+ const stdioParamsSchema = z.object({
9
+ args: z.array(z.string()).optional().default([]),
10
+ command: z.string().min(1),
11
+ name: z.string().min(1),
12
+ type: z.literal('stdio').default('stdio'),
13
+ });
14
+
15
+ const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
16
+
17
+ export const mcpRouter = router({
18
+ getStdioMcpServerManifest: mcpProcedure.input(stdioParamsSchema).query(async ({ input }) => {
19
+ return await mcpService.getStdioMcpServerManifest(input.name, input.command, input.args);
20
+ }),
21
+
22
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
23
+ // --- MCP Interaction ---
24
+ // listTools now accepts MCPClientParams directly
25
+ listTools: mcpProcedure
26
+ .input(stdioParamsSchema) // Use the unified schema
27
+ .query(async ({ input }) => {
28
+ // Pass the validated MCPClientParams to the service
29
+ return await mcpService.listTools(input);
30
+ }),
31
+
32
+ // callTool now accepts MCPClientParams, toolName, and args
33
+ callTool: mcpProcedure
34
+ .input(
35
+ z.object({
36
+ params: stdioParamsSchema, // Use the unified schema for client params
37
+ args: z.any(), // Arguments for the tool call
38
+ toolName: z.string(),
39
+ }),
40
+ )
41
+ .mutation(async ({ input }) => {
42
+ // Pass the validated params, toolName, and args to the service
43
+ const data = await mcpService.callTool(input.params, input.toolName, input.args);
44
+
45
+ return JSON.stringify(data);
46
+ }),
47
+ });
@@ -3,10 +3,12 @@ import { z } from 'zod';
3
3
  import { v4 as uuidv4 } from 'uuid'; // 需要添加此导入
4
4
 
5
5
  import { enableClerk } from '@/const/auth';
6
+ import { isDesktop } from '@/const/version';
6
7
  import { MessageModel } from '@/database/models/message';
7
8
  import { SessionModel } from '@/database/models/session';
8
9
  import { UserModel, UserNotFoundError } from '@/database/models/user';
9
10
  import { ClerkAuth } from '@/libs/clerk-auth';
11
+ import { pino } from '@/libs/logger';
10
12
  import { LobeNextAuthDbAdapter } from '@/libs/next-auth/adapter';
11
13
  import { authedProcedure, router } from '@/libs/trpc/lambda';
12
14
  import { serverDatabase } from '@/libs/trpc/lambda/middleware';
@@ -50,33 +52,46 @@ export const userRouter = router({
50
52
  try {
51
53
  state = await ctx.userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
52
54
  } catch (error) {
53
- if (enableClerk && error instanceof UserNotFoundError) {
54
- const user = await ctx.clerkAuth.getCurrentUser();
55
- if (user) {
56
- const userService = new UserService();
57
-
58
- await userService.createUser(user.id, {
59
- created_at: user.createdAt,
60
- email_addresses: user.emailAddresses.map((e) => ({
61
- email_address: e.emailAddress,
62
- id: e.id,
63
- })),
64
- first_name: user.firstName,
65
- id: user.id,
66
- image_url: user.imageUrl,
67
- last_name: user.lastName,
68
- phone_numbers: user.phoneNumbers.map((e) => ({
69
- id: e.id,
70
- phone_number: e.phoneNumber,
71
- })),
72
- primary_email_address_id: user.primaryEmailAddressId,
73
- primary_phone_number_id: user.primaryPhoneNumberId,
74
- username: user.username,
75
- } as UserJSON);
55
+ // user not create yet
56
+ if (error instanceof UserNotFoundError) {
57
+ // if in clerk auth mode
58
+ if (enableClerk) {
59
+ const user = await ctx.clerkAuth.getCurrentUser();
60
+ if (user) {
61
+ const userService = new UserService();
62
+
63
+ await userService.createUser(user.id, {
64
+ created_at: user.createdAt,
65
+ email_addresses: user.emailAddresses.map((e) => ({
66
+ email_address: e.emailAddress,
67
+ id: e.id,
68
+ })),
69
+ first_name: user.firstName,
70
+ id: user.id,
71
+ image_url: user.imageUrl,
72
+ last_name: user.lastName,
73
+ phone_numbers: user.phoneNumbers.map((e) => ({
74
+ id: e.id,
75
+ phone_number: e.phoneNumber,
76
+ })),
77
+ primary_email_address_id: user.primaryEmailAddressId,
78
+ primary_phone_number_id: user.primaryPhoneNumberId,
79
+ username: user.username,
80
+ } as UserJSON);
81
+
82
+ continue;
83
+ }
84
+ }
76
85
 
86
+ // if in desktop mode, make sure desktop user exist
87
+ else if (isDesktop) {
88
+ await UserModel.makeSureUserExist(ctx.serverDB, ctx.userId);
89
+ pino.info('create desktop user');
77
90
  continue;
78
91
  }
79
92
  }
93
+
94
+ console.error('getUserState:', error);
80
95
  throw error;
81
96
  }
82
97
  }
@@ -35,12 +35,6 @@ const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) =>
35
35
  const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
36
36
 
37
37
  export const mcpRouter = router({
38
- getStdioMcpServerManifest: mcpProcedure.input(stdioParamsSchema).query(async ({ input }) => {
39
- // Stdio check can be done here or rely on the service/client layer
40
- checkStdioEnvironment(input);
41
-
42
- return await mcpService.getStdioMcpServerManifest(input.name, input.command, input.args);
43
- }),
44
38
  getStreamableMcpServerManifest: mcpProcedure
45
39
  .input(
46
40
  z.object({
@@ -1,4 +1,4 @@
1
- import { RemoteServerConfig, dispatch } from '@lobechat/electron-client-ipc';
1
+ import { DataSyncConfig, dispatch } from '@lobechat/electron-client-ipc';
2
2
 
3
3
  class RemoteServerService {
4
4
  /**
@@ -11,7 +11,7 @@ class RemoteServerService {
11
11
  /**
12
12
  * 设置远程服务器配置
13
13
  */
14
- setRemoteServerConfig = async (config: RemoteServerConfig) => {
14
+ setRemoteServerConfig = async (config: DataSyncConfig) => {
15
15
  return dispatch('setRemoteServerConfig', config);
16
16
  };
17
17
 
@@ -25,8 +25,8 @@ class RemoteServerService {
25
25
  /**
26
26
  * 请求授权
27
27
  */
28
- requestAuthorization = async (serverUrl: string) => {
29
- return dispatch('requestAuthorization', serverUrl);
28
+ requestAuthorization = async (config: DataSyncConfig) => {
29
+ return dispatch('requestAuthorization', config);
30
30
  };
31
31
 
32
32
  /**
@@ -1,4 +1,5 @@
1
- import { toolsClient } from '@/libs/trpc/client';
1
+ import { isDesktop } from '@/const/version';
2
+ import { desktopClient, toolsClient } from '@/libs/trpc/client';
2
3
  import { ChatToolPayload } from '@/types/message';
3
4
 
4
5
  class MCPService {
@@ -13,10 +14,20 @@ class MCPService {
13
14
 
14
15
  if (!plugin) return;
15
16
 
16
- return toolsClient.mcp.callTool.mutate(
17
- { args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
18
- { signal },
19
- );
17
+ const data = {
18
+ args,
19
+ params: { ...plugin.customParams?.mcp, name: identifier } as any,
20
+ toolName: apiName,
21
+ };
22
+
23
+ const isStdio = plugin?.customParams?.mcp?.type === 'stdio';
24
+
25
+ // For desktop and stdio, use the desktopClient
26
+ if (isDesktop && isStdio) {
27
+ return desktopClient.mcp.callTool.mutate(data, { signal });
28
+ }
29
+
30
+ return toolsClient.mcp.callTool.mutate(data, { signal });
20
31
  }
21
32
 
22
33
  async getStreamableMcpServerManifest(identifier: string, url: string) {
@@ -24,11 +35,10 @@ class MCPService {
24
35
  }
25
36
 
26
37
  async getStdioMcpServerManifest(identifier: string, command: string, args?: string[]) {
27
- return toolsClient.mcp.getStdioMcpServerManifest.query({
38
+ return desktopClient.mcp.getStdioMcpServerManifest.query({
28
39
  args: args,
29
40
  command,
30
41
  name: identifier,
31
- type: 'stdio',
32
42
  });
33
43
  }
34
44
  }
@@ -148,6 +148,15 @@ class UploadService {
148
148
  return result;
149
149
  };
150
150
 
151
+ uploadToDesktop = async (file: File) => {
152
+ const fileArrayBuffer = await file.arrayBuffer();
153
+ const hash = sha256(fileArrayBuffer);
154
+
155
+ const { desktopFileAPI } = await import('@/services/electron/file');
156
+ const { metadata } = await desktopFileAPI.uploadFile(file, hash);
157
+ return metadata;
158
+ };
159
+
151
160
  uploadToClientS3 = async (hash: string, file: File): Promise<FileMetadata> => {
152
161
  await clientS3Storage.putObject(hash, file);
153
162
 
@@ -602,13 +602,22 @@ export const generateAIChat: StateCreator<
602
602
  }
603
603
  }
604
604
 
605
- if (toolCalls && toolCalls.length > 0) {
605
+ let parsedToolCalls = toolCalls;
606
+ if (parsedToolCalls && parsedToolCalls.length > 0) {
606
607
  internal_toggleToolCallingStreaming(messageId, undefined);
608
+ parsedToolCalls = parsedToolCalls.map((item) => ({
609
+ ...item,
610
+ function: {
611
+ ...item.function,
612
+ arguments: !!item.function.arguments ? item.function.arguments : '{}',
613
+ },
614
+ }));
615
+ isFunctionCall = true;
607
616
  }
608
617
 
609
618
  // update the content after fetch result
610
619
  await internal_updateMessageContent(messageId, content, {
611
- toolCalls,
620
+ toolCalls: parsedToolCalls,
612
621
  reasoning: !!reasoning ? { ...reasoning, duration } : undefined,
613
622
  search: !!grounding?.citations ? grounding : undefined,
614
623
  imageList: finalImages.length > 0 ? finalImages : undefined,
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  ListLocalFileParams,
3
+ LocalMoveFilesResultItem,
3
4
  LocalReadFileParams,
4
5
  LocalReadFilesParams,
5
6
  LocalSearchFilesParams,
7
+ MoveLocalFilesParams,
8
+ RenameLocalFileParams,
6
9
  } from '@lobechat/electron-client-ipc';
7
10
  import { StateCreator } from 'zustand/vanilla';
8
11
 
@@ -11,16 +14,27 @@ import { ChatStore } from '@/store/chat/store';
11
14
  import {
12
15
  LocalFileListState,
13
16
  LocalFileSearchState,
17
+ LocalMoveFilesState,
14
18
  LocalReadFileState,
15
19
  LocalReadFilesState,
20
+ LocalRenameFileState,
16
21
  } from '@/tools/local-files/type';
17
22
 
18
23
  export interface LocalFileAction {
24
+ internal_triggerLocalFileToolCalling: <T = any>(
25
+ id: string,
26
+ callingService: () => Promise<{ content: any; state: T }>,
27
+ ) => Promise<boolean>;
28
+
19
29
  listLocalFiles: (id: string, params: ListLocalFileParams) => Promise<boolean>;
30
+ moveLocalFiles: (id: string, params: MoveLocalFilesParams) => Promise<boolean>;
20
31
  reSearchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
21
32
  readLocalFile: (id: string, params: LocalReadFileParams) => Promise<boolean>;
22
33
  readLocalFiles: (id: string, params: LocalReadFilesParams) => Promise<boolean>;
34
+ renameLocalFile: (id: string, params: RenameLocalFileParams) => Promise<boolean>;
35
+ // Added rename action
23
36
  searchLocalFiles: (id: string, params: LocalSearchFilesParams) => Promise<boolean>;
37
+
24
38
  toggleLocalFileLoading: (id: string, loading: boolean) => void;
25
39
  }
26
40
 
@@ -30,15 +44,13 @@ export const localFileSlice: StateCreator<
30
44
  [],
31
45
  LocalFileAction
32
46
  > = (set, get) => ({
33
- listLocalFiles: async (id, params) => {
47
+ internal_triggerLocalFileToolCalling: async (id, callingService) => {
34
48
  get().toggleLocalFileLoading(id, true);
35
49
  try {
36
- const data = await localFileService.listLocalFiles(params);
37
- console.log(data);
38
- await get().updatePluginState(id, { listResults: data } as LocalFileListState);
39
- await get().internal_updateMessageContent(id, JSON.stringify(data));
50
+ const { state, content } = await callingService();
51
+ await get().updatePluginState(id, state as any);
52
+ await get().internal_updateMessageContent(id, JSON.stringify(content));
40
53
  } catch (error) {
41
- console.error('Error listing local files:', error);
42
54
  await get().internal_updateMessagePluginError(id, {
43
55
  body: error,
44
56
  message: (error as Error).message,
@@ -50,6 +62,41 @@ export const localFileSlice: StateCreator<
50
62
  return true;
51
63
  },
52
64
 
65
+ listLocalFiles: async (id, params) => {
66
+ return get().internal_triggerLocalFileToolCalling<LocalFileListState>(id, async () => {
67
+ const result = await localFileService.listLocalFiles(params);
68
+ const state: LocalFileListState = { listResults: result };
69
+ return { content: result, state };
70
+ });
71
+ },
72
+
73
+ moveLocalFiles: async (id, params) => {
74
+ return get().internal_triggerLocalFileToolCalling<LocalMoveFilesState>(id, async () => {
75
+ const results: LocalMoveFilesResultItem[] = await localFileService.moveLocalFiles(params);
76
+
77
+ // 检查所有文件是否成功移动以更新消息内容
78
+ const allSucceeded = results.every((r) => r.success);
79
+ const someFailed = results.some((r) => !r.success);
80
+ const successCount = results.filter((r) => r.success).length;
81
+ const failedCount = results.length - successCount;
82
+
83
+ let message = '';
84
+
85
+ if (allSucceeded) {
86
+ message = `Successfully moved ${results.length} item(s).`;
87
+ } else if (someFailed) {
88
+ message = `Moved ${successCount} item(s) successfully. Failed to move ${failedCount} item(s).`;
89
+ } else {
90
+ // 所有都失败了?
91
+ message = `Failed to move all ${results.length} item(s).`;
92
+ }
93
+
94
+ const state: LocalMoveFilesState = { results, successCount, totalCount: results.length };
95
+
96
+ return { content: { message, results }, state };
97
+ });
98
+ },
99
+
53
100
  reSearchLocalFiles: async (id, params) => {
54
101
  get().toggleLocalFileLoading(id, true);
55
102
 
@@ -59,63 +106,73 @@ export const localFileSlice: StateCreator<
59
106
  },
60
107
 
61
108
  readLocalFile: async (id, params) => {
62
- get().toggleLocalFileLoading(id, true);
63
-
64
- try {
109
+ return get().internal_triggerLocalFileToolCalling<LocalReadFileState>(id, async () => {
65
110
  const result = await localFileService.readLocalFile(params);
66
-
67
- await get().updatePluginState(id, { fileContent: result } as LocalReadFileState);
68
- await get().internal_updateMessageContent(id, JSON.stringify(result));
69
- } catch (error) {
70
- console.error('Error reading local file:', error);
71
- await get().internal_updateMessagePluginError(id, {
72
- body: error,
73
- message: (error as Error).message,
74
- type: 'PluginServerError',
75
- });
76
- }
77
- get().toggleLocalFileLoading(id, false);
78
- return true;
111
+ const state: LocalReadFileState = { fileContent: result };
112
+ return { content: result, state };
113
+ });
79
114
  },
80
115
 
81
116
  readLocalFiles: async (id, params) => {
82
- get().toggleLocalFileLoading(id, true);
83
-
84
- try {
117
+ return get().internal_triggerLocalFileToolCalling<LocalReadFilesState>(id, async () => {
85
118
  const results = await localFileService.readLocalFiles(params);
86
- await get().updatePluginState(id, { filesContent: results } as LocalReadFilesState);
87
- await get().internal_updateMessageContent(id, JSON.stringify(results));
88
- } catch (error) {
89
- console.error('Error reading local files:', error);
90
- await get().internal_updateMessagePluginError(id, {
91
- body: error,
92
- message: (error as Error).message,
93
- type: 'PluginServerError',
94
- });
95
- }
96
- get().toggleLocalFileLoading(id, false);
119
+ const state: LocalReadFilesState = { filesContent: results };
120
+ return { content: results, state };
121
+ });
122
+ },
97
123
 
98
- return true;
124
+ renameLocalFile: async (id, params) => {
125
+ return get().internal_triggerLocalFileToolCalling<LocalRenameFileState>(id, async () => {
126
+ const { path: currentPath, newName } = params;
127
+
128
+ // Basic validation for newName (can be done here or backend, maybe better in backend)
129
+ if (
130
+ !newName ||
131
+ newName.includes('/') ||
132
+ newName.includes('\\') ||
133
+ newName === '.' ||
134
+ newName === '..' ||
135
+ /["*/:<>?\\|]/.test(newName)
136
+ ) {
137
+ throw new Error(
138
+ 'Invalid new name provided. It cannot be empty, contain path separators, or invalid characters.',
139
+ );
140
+ }
141
+
142
+ const result = await localFileService.renameLocalFile({ newName, path: currentPath }); // Call the specific service
143
+
144
+ let state: LocalRenameFileState;
145
+ let content: { message: string; success: boolean };
146
+
147
+ if (result.success) {
148
+ state = { newPath: result.newPath!, oldPath: currentPath, success: true };
149
+ // Simplified message
150
+ content = {
151
+ message: `Successfully renamed file ${currentPath} to ${newName}.`,
152
+ success: true,
153
+ };
154
+ } else {
155
+ const errorMessage = result.error;
156
+ state = {
157
+ error: errorMessage,
158
+ newPath: '',
159
+ oldPath: params.path,
160
+ success: false,
161
+ };
162
+ content = { message: errorMessage, success: false };
163
+ }
164
+ return { content, state };
165
+ });
99
166
  },
100
167
 
101
168
  searchLocalFiles: async (id, params) => {
102
- get().toggleLocalFileLoading(id, true);
103
- try {
104
- const data = await localFileService.searchLocalFiles(params);
105
- await get().updatePluginState(id, { searchResults: data } as LocalFileSearchState);
106
- await get().internal_updateMessageContent(id, JSON.stringify(data));
107
- } catch (error) {
108
- console.error('Error searching local files:', error);
109
- await get().internal_updateMessagePluginError(id, {
110
- body: error,
111
- message: (error as Error).message,
112
- type: 'PluginServerError',
113
- });
114
- }
115
- get().toggleLocalFileLoading(id, false);
116
-
117
- return true;
169
+ return get().internal_triggerLocalFileToolCalling<LocalFileSearchState>(id, async () => {
170
+ const result = await localFileService.searchLocalFiles(params);
171
+ const state: LocalFileSearchState = { searchResults: result };
172
+ return { content: result, state };
173
+ });
118
174
  },
175
+
119
176
  toggleLocalFileLoading: (id, loading) => {
120
177
  // Assuming a loading state structure similar to searchLoading
121
178
  set(
@@ -1,8 +1,8 @@
1
- import { RemoteServerConfig } from '@lobechat/electron-client-ipc';
1
+ import { DataSyncConfig } from '@lobechat/electron-client-ipc';
2
+ import isEqual from 'fast-deep-equal';
2
3
  import useSWR, { SWRResponse, mutate } from 'swr';
3
4
  import type { StateCreator } from 'zustand/vanilla';
4
5
 
5
- import { INBOX_SESSION_ID } from '@/const/session';
6
6
  import { remoteServerService } from '@/services/electron/remoteServer';
7
7
 
8
8
  import { initialState } from '../initialState';
@@ -12,11 +12,12 @@ import type { ElectronStore } from '../store';
12
12
  * 设置操作
13
13
  */
14
14
  export interface ElectronRemoteServerAction {
15
- connectRemoteServer: (params: { isSelfHosted: boolean; serverUrl?: string }) => Promise<void>;
15
+ connectRemoteServer: (params: DataSyncConfig) => Promise<void>;
16
16
  disconnectRemoteServer: () => Promise<void>;
17
17
  refreshServerConfig: () => Promise<void>;
18
18
  refreshUserData: () => Promise<void>;
19
- useRemoteServerConfig: () => SWRResponse;
19
+ useDataSyncConfig: () => SWRResponse;
20
+ useRefreshDataWhenActive: (active?: boolean) => SWRResponse;
20
21
  }
21
22
 
22
23
  const REMOTE_SERVER_CONFIG_KEY = 'electron:getRemoteServerConfig';
@@ -28,7 +29,7 @@ export const remoteSyncSlice: StateCreator<
28
29
  ElectronRemoteServerAction
29
30
  > = (set, get) => ({
30
31
  connectRemoteServer: async (values) => {
31
- if (!values.serverUrl) return;
32
+ if (!values.remoteServerUrl) return;
32
33
 
33
34
  set({ isConnectingServer: true });
34
35
  try {
@@ -36,12 +37,12 @@ export const remoteSyncSlice: StateCreator<
36
37
  const config = await remoteServerService.getRemoteServerConfig();
37
38
 
38
39
  // 如果已经激活,需要先清除
39
- if (config.active) {
40
- await remoteServerService.clearRemoteServerConfig();
40
+ if (!isEqual(config, values)) {
41
+ await remoteServerService.setRemoteServerConfig({ ...values, active: false });
41
42
  }
42
43
 
43
44
  // 请求授权
44
- const result = await remoteServerService.requestAuthorization(values.serverUrl);
45
+ const result = await remoteServerService.requestAuthorization(values);
45
46
 
46
47
  if (!result.success) {
47
48
  console.error('请求授权失败:', result.error);
@@ -65,9 +66,9 @@ export const remoteSyncSlice: StateCreator<
65
66
  disconnectRemoteServer: async () => {
66
67
  set({ isConnectingServer: false });
67
68
  try {
68
- await remoteServerService.clearRemoteServerConfig();
69
+ await remoteServerService.setRemoteServerConfig({ active: false, storageMode: 'local' });
69
70
  // 更新表单URL为空
70
- set({ remoteServerConfig: initialState.remoteServerConfig });
71
+ set({ dataSyncConfig: initialState.dataSyncConfig });
71
72
  // 刷新状态
72
73
  await get().refreshServerConfig();
73
74
  } catch (error) {
@@ -93,11 +94,10 @@ export const remoteSyncSlice: StateCreator<
93
94
  await getChatStoreState().refreshMessages();
94
95
  await getChatStoreState().refreshTopic();
95
96
  await getUserStoreState().refreshUserState();
96
- getSessionStoreState().switchSession(INBOX_SESSION_ID);
97
97
  },
98
98
 
99
- useRemoteServerConfig: () =>
100
- useSWR<RemoteServerConfig>(
99
+ useDataSyncConfig: () =>
100
+ useSWR<DataSyncConfig>(
101
101
  REMOTE_SERVER_CONFIG_KEY,
102
102
  async () => {
103
103
  try {
@@ -109,13 +109,14 @@ export const remoteSyncSlice: StateCreator<
109
109
  },
110
110
  {
111
111
  onSuccess: (data) => {
112
- console.log('remote server config:', data);
113
- set({ isInitRemoteServerConfig: true, remoteServerConfig: data });
114
-
115
- if (data.active) {
116
- get().refreshUserData();
117
- }
112
+ set({ dataSyncConfig: data, isInitRemoteServerConfig: true });
118
113
  },
119
114
  },
120
115
  ),
116
+ useRefreshDataWhenActive: (active) =>
117
+ useSWR(['refreshDataWhenActive', active], async () => {
118
+ if (!active) return;
119
+
120
+ await get().refreshUserData();
121
+ }),
121
122
  });