@lobehub/chat 1.82.0 → 1.82.2

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 (102) hide show
  1. package/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
  2. package/.env.desktop +2 -1
  3. package/.github/scripts/pr-comment.js +4 -9
  4. package/CHANGELOG.md +51 -0
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/electron.json +38 -2
  7. package/locales/ar/plugin.json +51 -31
  8. package/locales/bg-BG/electron.json +38 -2
  9. package/locales/bg-BG/plugin.json +51 -31
  10. package/locales/de-DE/electron.json +38 -2
  11. package/locales/de-DE/plugin.json +29 -9
  12. package/locales/en-US/electron.json +38 -2
  13. package/locales/en-US/plugin.json +29 -9
  14. package/locales/es-ES/electron.json +38 -2
  15. package/locales/es-ES/plugin.json +51 -31
  16. package/locales/fa-IR/electron.json +38 -2
  17. package/locales/fa-IR/plugin.json +51 -31
  18. package/locales/fr-FR/electron.json +38 -2
  19. package/locales/fr-FR/plugin.json +51 -31
  20. package/locales/it-IT/electron.json +38 -2
  21. package/locales/it-IT/plugin.json +51 -31
  22. package/locales/ja-JP/electron.json +38 -2
  23. package/locales/ja-JP/plugin.json +51 -31
  24. package/locales/ko-KR/electron.json +38 -2
  25. package/locales/ko-KR/plugin.json +29 -9
  26. package/locales/nl-NL/electron.json +38 -2
  27. package/locales/nl-NL/plugin.json +51 -31
  28. package/locales/pl-PL/electron.json +38 -2
  29. package/locales/pl-PL/plugin.json +29 -9
  30. package/locales/pt-BR/electron.json +38 -2
  31. package/locales/pt-BR/plugin.json +51 -31
  32. package/locales/ru-RU/electron.json +38 -2
  33. package/locales/ru-RU/plugin.json +51 -31
  34. package/locales/tr-TR/electron.json +38 -2
  35. package/locales/tr-TR/plugin.json +51 -31
  36. package/locales/vi-VN/electron.json +38 -2
  37. package/locales/vi-VN/plugin.json +29 -9
  38. package/locales/zh-CN/electron.json +38 -2
  39. package/locales/zh-CN/plugin.json +30 -10
  40. package/locales/zh-TW/electron.json +38 -2
  41. package/locales/zh-TW/plugin.json +51 -31
  42. package/package.json +1 -1
  43. package/packages/electron-client-ipc/src/events/update.ts +3 -3
  44. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
  45. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
  46. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
  47. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
  48. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
  49. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
  50. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
  51. package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
  52. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
  53. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  54. package/src/app/[variants]/layout.tsx +2 -1
  55. package/src/config/aiModels/openrouter.ts +6 -6
  56. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
  57. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
  58. package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
  59. package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
  60. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
  61. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
  62. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
  63. package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
  64. package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
  65. package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
  66. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +289 -0
  67. package/src/features/PluginDevModal/MCPManifestForm/utils.test.ts +262 -0
  68. package/src/features/PluginDevModal/MCPManifestForm/utils.ts +151 -0
  69. package/src/features/PluginDevModal/index.tsx +31 -22
  70. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
  71. package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +0 -56
  72. package/src/locales/default/electron.ts +38 -2
  73. package/src/locales/default/plugin.ts +28 -8
  74. package/src/server/modules/ElectronIPCClient/index.ts +36 -0
  75. package/src/server/routers/lambda/session.ts +2 -6
  76. package/src/server/routers/tools/mcp.ts +6 -0
  77. package/src/server/services/file/impls/index.ts +9 -1
  78. package/src/server/services/file/impls/local.test.ts +299 -0
  79. package/src/server/services/file/impls/local.ts +183 -0
  80. package/src/server/services/mcp/index.ts +26 -0
  81. package/src/services/aiModel/index.ts +5 -1
  82. package/src/services/aiProvider/index.ts +5 -1
  83. package/src/services/electron/autoUpdate.ts +4 -0
  84. package/src/services/file/index.ts +5 -1
  85. package/src/services/mcp.ts +13 -2
  86. package/src/services/message/index.ts +5 -1
  87. package/src/services/plugin/index.ts +5 -1
  88. package/src/services/session/index.ts +5 -1
  89. package/src/services/tableViewer/desktop.ts +15 -0
  90. package/src/services/tableViewer/index.ts +4 -1
  91. package/src/services/thread/index.ts +5 -1
  92. package/src/services/topic/index.ts +5 -1
  93. package/src/services/user/index.ts +5 -1
  94. package/src/store/electron/actions/app.ts +59 -0
  95. package/src/store/electron/actions/sync.ts +5 -1
  96. package/src/store/electron/initialState.ts +3 -1
  97. package/src/store/electron/store.ts +6 -1
  98. package/src/store/tool/slices/customPlugin/action.ts +16 -4
  99. package/src/utils/client/GlobalAgentContextManager.ts +85 -0
  100. package/src/utils/promptTemplate.test.ts +78 -0
  101. package/src/utils/promptTemplate.ts +17 -0
  102. package/src/features/PluginDevModal/MCPManifestForm.tsx +0 -164
@@ -0,0 +1,299 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
6
+
7
+ import { DesktopLocalFileImpl } from './local';
8
+
9
+ // 模拟依赖项
10
+ vi.mock('node:fs', async (importOriginal) => ({
11
+ ...((await importOriginal()) as any),
12
+ existsSync: vi.fn(),
13
+ readFileSync: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/server/modules/ElectronIPCClient', () => ({
17
+ electronIpcClient: {
18
+ getFilePathById: vi.fn(),
19
+ deleteFiles: vi.fn(),
20
+ },
21
+ }));
22
+
23
+ describe('DesktopLocalFileImpl', () => {
24
+ let service: DesktopLocalFileImpl;
25
+ const testFilePath = '/path/to/file.txt';
26
+ const testFileKey = 'desktop://file.txt';
27
+ const testFileContent = 'test file content';
28
+ const testFileBuffer = Buffer.from(testFileContent);
29
+
30
+ beforeEach(() => {
31
+ service = new DesktopLocalFileImpl();
32
+
33
+ // 重置所有模拟
34
+ vi.resetAllMocks();
35
+
36
+ // 设置默认模拟行为
37
+ vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue(testFilePath);
38
+ vi.mocked(existsSync).mockReturnValue(true);
39
+ vi.mocked(readFileSync).mockReturnValueOnce(testFileBuffer);
40
+ });
41
+
42
+ describe('getLocalFileUrl', () => {
43
+ it.skip('应该正确获取本地文件URL并转换为data URL', async () => {
44
+ // 准备: readFileSync在第一次被调用时返回文件内容
45
+ vi.mocked(readFileSync).mockReturnValueOnce(testFileBuffer);
46
+
47
+ // 使用私有方法进行测试,通过原型访问
48
+ const result = await (service as any).getLocalFileUrl(testFileKey);
49
+
50
+ // 验证
51
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
52
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
53
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath);
54
+
55
+ // 验证返回的data URL格式正确
56
+ expect(result).toContain('data:text/plain;base64,');
57
+ expect(result).toContain(testFileBuffer.toString('base64'));
58
+ });
59
+
60
+ it('当文件不存在时应返回原始键', async () => {
61
+ // 准备: 文件不存在
62
+ vi.mocked(existsSync).mockReturnValueOnce(false);
63
+
64
+ // 使用私有方法进行测试
65
+ const result = await (service as any).getLocalFileUrl(testFileKey);
66
+
67
+ // 验证
68
+ expect(result).toBe(testFileKey);
69
+ });
70
+
71
+ it('当发生错误时应返回空字符串', async () => {
72
+ // 准备: 模拟错误
73
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
74
+
75
+ // 使用私有方法进行测试
76
+ const result = await (service as any).getLocalFileUrl(testFileKey);
77
+
78
+ // 验证
79
+ expect(result).toBe('');
80
+ });
81
+ });
82
+
83
+ describe('getMimeTypeFromPath', () => {
84
+ it('应该返回正确的MIME类型', () => {
85
+ // 使用私有方法进行测试
86
+ const jpgResult = (service as any).getMimeTypeFromPath('test.jpg');
87
+ const pngResult = (service as any).getMimeTypeFromPath('test.png');
88
+ const unknownResult = (service as any).getMimeTypeFromPath('test.unknown');
89
+
90
+ // 验证
91
+ expect(jpgResult).toBe('image/jpeg');
92
+ expect(pngResult).toBe('image/png');
93
+ expect(unknownResult).toBe('application/octet-stream');
94
+ });
95
+ });
96
+
97
+ describe('createPreSignedUrl', () => {
98
+ it('应该返回原始键', async () => {
99
+ const result = await service.createPreSignedUrl(testFileKey);
100
+
101
+ expect(result).toBe(testFileKey);
102
+ });
103
+ });
104
+
105
+ describe('createPreSignedUrlForPreview', () => {
106
+ it('应该调用getLocalFileUrl获取预览URL', async () => {
107
+ // 准备
108
+ const getLocalFileUrlSpy = vi.spyOn(service as any, 'getLocalFileUrl');
109
+ getLocalFileUrlSpy.mockResolvedValueOnce('data:text/plain;base64,dGVzdA==');
110
+
111
+ // 执行
112
+ const result = await service.createPreSignedUrlForPreview(testFileKey);
113
+
114
+ // 验证
115
+ expect(getLocalFileUrlSpy).toHaveBeenCalledWith(testFileKey);
116
+ expect(result).toBe('data:text/plain;base64,dGVzdA==');
117
+ });
118
+ });
119
+
120
+ describe('deleteFile', () => {
121
+ it('应该调用deleteFiles方法删除单个文件', async () => {
122
+ // 准备
123
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValueOnce({ success: true });
124
+ const deleteFilesSpy = vi.spyOn(service, 'deleteFiles');
125
+
126
+ // 执行
127
+ await service.deleteFile(testFileKey);
128
+
129
+ // 验证
130
+ expect(deleteFilesSpy).toHaveBeenCalledWith([testFileKey]);
131
+ });
132
+ });
133
+
134
+ describe('deleteFiles', () => {
135
+ it('应该成功删除有效的文件', async () => {
136
+ // 准备
137
+ const keys = ['desktop://file1.txt', 'desktop://file2.png'];
138
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValueOnce({ success: true });
139
+
140
+ // 执行
141
+ const result = await service.deleteFiles(keys);
142
+
143
+ // 验证
144
+ expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(keys);
145
+ expect(result).toEqual({ success: true });
146
+ });
147
+
148
+ it('当提供无效键时应返回错误', async () => {
149
+ // 准备: 包含无效的文件路径
150
+ const keys = ['invalid://file1.txt', 'desktop://file2.png'];
151
+
152
+ // 执行
153
+ const result = await service.deleteFiles(keys);
154
+
155
+ // 验证
156
+ expect(electronIpcClient.deleteFiles).not.toHaveBeenCalled();
157
+ expect(result.success).toBe(false);
158
+ expect(result.errors).toBeDefined();
159
+ expect(result.errors!.length).toBe(1);
160
+ expect(result.errors![0].path).toBe('invalid://file1.txt');
161
+ });
162
+
163
+ it('当未提供键时应返回成功', async () => {
164
+ // 执行
165
+ const result = await service.deleteFiles([]);
166
+
167
+ // 验证
168
+ expect(electronIpcClient.deleteFiles).not.toHaveBeenCalled();
169
+ expect(result).toEqual({ success: true });
170
+ });
171
+
172
+ it('当删除过程中出现错误时应正确处理', async () => {
173
+ // 准备
174
+ const keys = ['desktop://file1.txt'];
175
+ vi.mocked(electronIpcClient.deleteFiles).mockRejectedValueOnce(new Error('删除错误'));
176
+
177
+ // 执行
178
+ const result = await service.deleteFiles(keys);
179
+
180
+ // 验证
181
+ expect(result.success).toBe(false);
182
+ expect(result.errors).toBeDefined();
183
+ expect(result.errors![0].message).toContain('删除错误');
184
+ });
185
+ });
186
+
187
+ describe.skip('getFileByteArray', () => {
188
+ it('应该返回文件的字节数组', async () => {
189
+ // 准备
190
+ vi.mocked(readFileSync).mockReturnValueOnce(Buffer.from('测试内容'));
191
+
192
+ // 执行
193
+ const result = await service.getFileByteArray(testFileKey);
194
+
195
+ // 验证
196
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
197
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
198
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath);
199
+ expect(result).toBeInstanceOf(Uint8Array);
200
+ expect(Buffer.from(result).toString()).toBe('测试内容');
201
+ });
202
+
203
+ it('当文件不存在时应返回空数组', async () => {
204
+ // 准备
205
+ vi.mocked(existsSync).mockReturnValueOnce(false);
206
+
207
+ // 执行
208
+ const result = await service.getFileByteArray(testFileKey);
209
+
210
+ // 验证
211
+ expect(result).toBeInstanceOf(Uint8Array);
212
+ expect(result.length).toBe(0);
213
+ });
214
+
215
+ it('当发生错误时应返回空数组', async () => {
216
+ // 准备
217
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
218
+
219
+ // 执行
220
+ const result = await service.getFileByteArray(testFileKey);
221
+
222
+ // 验证
223
+ expect(result).toBeInstanceOf(Uint8Array);
224
+ expect(result.length).toBe(0);
225
+ });
226
+ });
227
+
228
+ describe.skip('getFileContent', () => {
229
+ it('应该返回文件内容', async () => {
230
+ // 准备
231
+ vi.mocked(readFileSync).mockReturnValueOnce('文件内容');
232
+
233
+ // 执行
234
+ const result = await service.getFileContent(testFileKey);
235
+
236
+ // 验证
237
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
238
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
239
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath, 'utf8');
240
+ expect(result).toBe('文件内容');
241
+ });
242
+
243
+ it('当文件不存在时应返回空字符串', async () => {
244
+ // 准备
245
+ vi.mocked(existsSync).mockReturnValueOnce(false);
246
+
247
+ // 执行
248
+ const result = await service.getFileContent(testFileKey);
249
+
250
+ // 验证
251
+ expect(result).toBe('');
252
+ });
253
+
254
+ it('当发生错误时应返回空字符串', async () => {
255
+ // 准备
256
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
257
+
258
+ // 执行
259
+ const result = await service.getFileContent(testFileKey);
260
+
261
+ // 验证
262
+ expect(result).toBe('');
263
+ });
264
+ });
265
+
266
+ describe('getFullFileUrl', () => {
267
+ it('应该调用getLocalFileUrl获取完整URL', async () => {
268
+ // 准备
269
+ const getLocalFileUrlSpy = vi.spyOn(service as any, 'getLocalFileUrl');
270
+ getLocalFileUrlSpy.mockResolvedValueOnce('data:image/png;base64,test');
271
+
272
+ // 执行
273
+ const result = await service.getFullFileUrl(testFileKey);
274
+
275
+ // 验证
276
+ expect(getLocalFileUrlSpy).toHaveBeenCalledWith(testFileKey);
277
+ expect(result).toBe('data:image/png;base64,test');
278
+ });
279
+
280
+ it('当url为空时应返回空字符串', async () => {
281
+ // 执行
282
+ const result = await service.getFullFileUrl(null);
283
+
284
+ // 验证
285
+ expect(result).toBe('');
286
+ });
287
+ });
288
+
289
+ describe('uploadContent', () => {
290
+ it('应该正确处理上传内容的请求', async () => {
291
+ // 目前这个方法未实现,仅验证调用不会导致错误
292
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
293
+
294
+ await service.uploadContent('path/to/file', 'content');
295
+
296
+ expect(consoleSpy).toHaveBeenCalled();
297
+ });
298
+ });
299
+ });
@@ -0,0 +1,183 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
5
+
6
+ import { FileServiceImpl } from './type';
7
+
8
+ /**
9
+ * 桌面应用本地文件服务实现
10
+ */
11
+ export class DesktopLocalFileImpl implements FileServiceImpl {
12
+ /**
13
+ * 获取本地文件的URL
14
+ * Electron返回文件的绝对路径,然后在服务端将文件转为base64
15
+ */
16
+ private async getLocalFileUrl(key: string): Promise<string> {
17
+ try {
18
+ // 从Electron获取文件的绝对路径
19
+ const filePath = await electronIpcClient.getFilePathById(key);
20
+
21
+ // 检查文件是否存在
22
+ if (!existsSync(filePath)) {
23
+ console.error(`File not found: ${filePath}`);
24
+ return key;
25
+ }
26
+
27
+ // 读取文件内容
28
+ const fileContent = readFileSync(filePath);
29
+
30
+ // 确定文件的MIME类型
31
+ const mimeType = this.getMimeTypeFromPath(filePath);
32
+
33
+ // 转换为base64并返回data URL
34
+ const base64 = fileContent.toString('base64');
35
+ return `data:${mimeType};base64,${base64}`;
36
+ } catch (e) {
37
+ console.error('[DesktopLocalFileImpl] Failed to process file from Electron IPC:', e);
38
+ return '';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 根据文件路径获取MIME类型
44
+ */
45
+ private getMimeTypeFromPath(filePath: string): string {
46
+ const extension = path.extname(filePath).toLowerCase();
47
+
48
+ // 常见文件类型的MIME映射
49
+ const mimeTypes: Record<string, string> = {
50
+ '.css': 'text/css',
51
+ '.gif': 'image/gif',
52
+ '.html': 'text/html',
53
+ '.jpeg': 'image/jpeg',
54
+ '.jpg': 'image/jpeg',
55
+ '.js': 'application/javascript',
56
+ '.json': 'application/json',
57
+ '.pdf': 'application/pdf',
58
+ '.png': 'image/png',
59
+ '.svg': 'image/svg+xml',
60
+ '.txt': 'text/plain',
61
+ '.webp': 'image/webp',
62
+ };
63
+
64
+ return mimeTypes[extension] || 'application/octet-stream';
65
+ }
66
+
67
+ /**
68
+ * 创建预签名上传URL(本地版实际上是直接返回文件路径,可能需要进一步扩展)
69
+ */
70
+ async createPreSignedUrl(key: string): Promise<string> {
71
+ // 在桌面应用本地文件实现中,不需要预签名URL
72
+ // 直接返回文件路径
73
+ return key;
74
+ }
75
+
76
+ /**
77
+ * 创建预签名预览URL(本地版是通过Electron获取本地文件URL)
78
+ */
79
+ async createPreSignedUrlForPreview(key: string): Promise<string> {
80
+ return this.getLocalFileUrl(key);
81
+ }
82
+
83
+ async deleteFile(key: string): Promise<any> {
84
+ return await this.deleteFiles([key]);
85
+ }
86
+
87
+ /**
88
+ * 批量删除文件
89
+ */
90
+ async deleteFiles(keys: string[]): Promise<any> {
91
+ try {
92
+ if (!keys || keys.length === 0) return { success: true };
93
+
94
+ // 确保所有路径都是合法的desktop://路径
95
+ const invalidKeys = keys.filter((key) => !key.startsWith('desktop://'));
96
+ if (invalidKeys.length > 0) {
97
+ console.error('Invalid desktop file paths:', invalidKeys);
98
+ return {
99
+ errors: invalidKeys.map((key) => ({ message: 'Invalid desktop file path', path: key })),
100
+ success: false,
101
+ };
102
+ }
103
+
104
+ // 使用electronIpcClient的专用方法
105
+ return await electronIpcClient.deleteFiles(keys);
106
+ } catch (error) {
107
+ console.error('Failed to delete files:', error);
108
+ return {
109
+ errors: [
110
+ {
111
+ message: `Batch delete failed: ${(error as Error).message}`,
112
+ path: 'batch',
113
+ },
114
+ ],
115
+ success: false,
116
+ };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 获取文件字节数组
122
+ */
123
+ async getFileByteArray(key: string): Promise<Uint8Array> {
124
+ try {
125
+ // 从Electron获取文件的绝对路径
126
+ const filePath = await electronIpcClient.getFilePathById(key);
127
+
128
+ // 检查文件是否存在
129
+ if (!existsSync(filePath)) {
130
+ console.error(`File not found: ${filePath}`);
131
+ return new Uint8Array();
132
+ }
133
+
134
+ // 读取文件内容并转换为Uint8Array
135
+ const buffer = readFileSync(filePath);
136
+ return new Uint8Array(buffer);
137
+ } catch (e) {
138
+ console.error('Failed to get file byte array:', e);
139
+ return new Uint8Array();
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 获取文件内容
145
+ */
146
+ async getFileContent(key: string): Promise<string> {
147
+ try {
148
+ // 从Electron获取文件的绝对路径
149
+ const filePath = await electronIpcClient.getFilePathById(key);
150
+
151
+ // 检查文件是否存在
152
+ if (!existsSync(filePath)) {
153
+ console.error(`File not found: ${filePath}`);
154
+ return '';
155
+ }
156
+
157
+ // 读取文件内容并转换为字符串
158
+ return readFileSync(filePath, 'utf8');
159
+ } catch (e) {
160
+ console.error('Failed to get file content:', e);
161
+ return '';
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 获取完整文件URL
167
+ */
168
+ async getFullFileUrl(url?: string | null): Promise<string> {
169
+ if (!url) return '';
170
+ return this.getLocalFileUrl(url);
171
+ }
172
+
173
+ /**
174
+ * 上传内容
175
+ * 注意:这个功能可能需要扩展Electron IPC接口
176
+ */
177
+ async uploadContent(filePath: string, content: string): Promise<any> {
178
+ // 这里需要扩展electronIpcClient以支持上传文件内容
179
+ // 例如: return electronIpcClient.uploadContent(filePath, content);
180
+ console.warn('uploadContent not implemented for Desktop local file service', filePath, content);
181
+ return;
182
+ }
183
+ }
@@ -1,4 +1,5 @@
1
1
  import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
2
+ import { McpError } from '@modelcontextprotocol/sdk/types.js';
2
3
  import { TRPCError } from '@trpc/server';
3
4
  import debug from 'debug';
4
5
 
@@ -61,6 +62,12 @@ class MCPService {
61
62
 
62
63
  return result;
63
64
  } catch (error) {
65
+ if (error instanceof McpError) {
66
+ const mcpError = error as McpError;
67
+
68
+ return mcpError.message;
69
+ }
70
+
64
71
  console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
65
72
  // Propagate a TRPCError
66
73
  throw new TRPCError({
@@ -139,6 +146,25 @@ class MCPService {
139
146
  ): Promise<LobeChatPluginManifest> {
140
147
  const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
141
148
 
149
+ return {
150
+ api: tools,
151
+ identifier,
152
+ meta: {
153
+ avatar: 'MCP_AVATAR',
154
+ description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
155
+ title: identifier,
156
+ },
157
+ // TODO: temporary
158
+ type: 'mcp' as any,
159
+ };
160
+ }
161
+ async getStdioMcpServerManifest(
162
+ identifier: string,
163
+ command: string,
164
+ args: string[],
165
+ ): Promise<LobeChatPluginManifest> {
166
+ const tools = await this.listTools({ args, command, name: identifier, type: 'stdio' }); // Get client using params
167
+
142
168
  return {
143
169
  api: tools,
144
170
  identifier,
@@ -1,5 +1,9 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService } from './client';
2
4
  import { ServerService } from './server';
3
5
 
4
6
  export const aiModelService =
5
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
7
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
8
+ ? new ServerService()
9
+ : new ClientService();
@@ -1,5 +1,9 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService } from './client';
2
4
  import { ServerService } from './server';
3
5
 
4
6
  export const aiProviderService =
5
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
7
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
8
+ ? new ServerService()
9
+ : new ClientService();
@@ -12,6 +12,10 @@ class AutoUpdateService {
12
12
  installLater = async () => {
13
13
  return dispatch('installLater');
14
14
  };
15
+
16
+ downloadUpdate() {
17
+ return dispatch('downloadUpdate');
18
+ }
15
19
  }
16
20
 
17
21
  export const autoUpdateService = new AutoUpdateService();
@@ -1,3 +1,5 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService as DeprecatedService } from './_deprecated';
2
4
  import { ClientService } from './client';
3
5
  import { ServerService } from './server';
@@ -6,4 +8,6 @@ const clientService =
6
8
  process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
9
 
8
10
  export const fileService =
9
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
11
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
12
+ ? new ServerService()
13
+ : clientService;
@@ -1,12 +1,14 @@
1
1
  import { toolsClient } from '@/libs/trpc/client';
2
- import { getToolStoreState } from '@/store/tool';
3
- import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
4
2
  import { ChatToolPayload } from '@/types/message';
5
3
 
6
4
  class MCPService {
7
5
  async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
6
+ const { pluginSelectors } = await import('@/store/tool/selectors');
7
+ const { getToolStoreState } = await import('@/store/tool/store');
8
+
8
9
  const s = getToolStoreState();
9
10
  const { identifier, arguments: args, apiName } = payload;
11
+
10
12
  const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
11
13
 
12
14
  if (!plugin) return;
@@ -20,6 +22,15 @@ class MCPService {
20
22
  async getStreamableMcpServerManifest(identifier: string, url: string) {
21
23
  return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
22
24
  }
25
+
26
+ async getStdioMcpServerManifest(identifier: string, command: string, args?: string[]) {
27
+ return toolsClient.mcp.getStdioMcpServerManifest.query({
28
+ args: args,
29
+ command,
30
+ name: identifier,
31
+ type: 'stdio',
32
+ });
33
+ }
23
34
  }
24
35
 
25
36
  export const mcpService = new MCPService();
@@ -1,3 +1,5 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService as DeprecatedService } from './_deprecated';
2
4
  import { ClientService } from './client';
3
5
  import { ServerService } from './server';
@@ -6,4 +8,6 @@ const clientService =
6
8
  process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
9
 
8
10
  export const messageService =
9
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
11
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
12
+ ? new ServerService()
13
+ : clientService;
@@ -1,3 +1,5 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService as DeprecatedService } from './_deprecated';
2
4
  import { ClientService } from './client';
3
5
  import { ServerService } from './server';
@@ -6,4 +8,6 @@ const clientService =
6
8
  process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
9
 
8
10
  export const pluginService =
9
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
11
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
12
+ ? new ServerService()
13
+ : clientService;
@@ -1,3 +1,5 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService as DeprecatedService } from './_deprecated';
2
4
  import { ClientService } from './client';
3
5
  import { ServerService } from './server';
@@ -6,4 +8,6 @@ const clientService =
6
8
  process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
9
 
8
10
  export const sessionService =
9
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
11
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
12
+ ? new ServerService()
13
+ : clientService;
@@ -0,0 +1,15 @@
1
+ import { desktopClient } from '@/libs/trpc/client/desktop';
2
+
3
+ export class DesktopService {
4
+ getAllTables = async () => {
5
+ return desktopClient.pgTable.getAllTables.query();
6
+ };
7
+
8
+ getTableDetails = async (tableName: string) => {
9
+ return desktopClient.pgTable.getTableDetails.query({ tableName });
10
+ };
11
+
12
+ getTableData = async (tableName: string) => {
13
+ return desktopClient.pgTable.getTableData.query({ page: 1, pageSize: 300, tableName });
14
+ };
15
+ }
@@ -1,3 +1,6 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService } from './client';
4
+ import { DesktopService } from './desktop';
2
5
 
3
- export const tableViewerService = new ClientService();
6
+ export const tableViewerService = isDesktop ? new DesktopService() : new ClientService();
@@ -1,5 +1,9 @@
1
+ import { isDesktop } from '@/const/version';
2
+
1
3
  import { ClientService } from './client';
2
4
  import { ServerService } from './server';
3
5
 
4
6
  export const threadService =
5
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
7
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
8
+ ? new ServerService()
9
+ : new ClientService();