@lobehub/chat 1.84.24 → 1.84.26

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 (27) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +195 -0
  3. package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +44 -0
  4. package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +82 -0
  5. package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +64 -0
  6. package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +256 -0
  7. package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +82 -0
  8. package/apps/desktop/src/main/services/fileSrv.ts +49 -10
  9. package/apps/desktop/vitest.config.ts +17 -0
  10. package/changelog/v1.json +18 -0
  11. package/package.json +3 -3
  12. package/packages/electron-server-ipc/package.json +3 -0
  13. package/packages/electron-server-ipc/src/ipcClient.ts +58 -21
  14. package/packages/electron-server-ipc/src/ipcServer.test.ts +417 -0
  15. package/packages/electron-server-ipc/src/ipcServer.ts +21 -16
  16. package/src/config/aiModels/ollama.ts +12 -2
  17. package/src/libs/langchain/loaders/epub/index.ts +4 -2
  18. package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +3 -1
  19. package/src/server/routers/async/file.ts +1 -1
  20. package/src/server/routers/lambda/file.ts +1 -1
  21. package/src/server/routers/lambda/importer.ts +4 -2
  22. package/src/server/routers/lambda/message.ts +2 -2
  23. package/src/server/routers/lambda/ragEval.ts +1 -1
  24. package/src/server/routers/lambda/user.ts +2 -2
  25. package/src/server/services/file/index.ts +46 -0
  26. package/src/server/utils/tempFileManager.ts +5 -8
  27. package/vitest.config.ts +3 -2
@@ -0,0 +1,417 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { ElectronIPCServer } from './ipcServer';
8
+
9
+ // Mock node modules
10
+ vi.mock('node:fs');
11
+ vi.mock('node:net');
12
+ vi.mock('node:os');
13
+ vi.mock('node:path');
14
+
15
+ const appId = 'lobehub';
16
+
17
+ describe('ElectronIPCServer', () => {
18
+ // Mock data
19
+ const mockTempDir = '/mock/temp/dir';
20
+ const mockSocketPath = '/mock/temp/dir/lobehub-electron-ipc.sock';
21
+ const mockSocketInfoPath = '/mock/temp/dir/lobehub-electron-ipc-info.json';
22
+
23
+ // Mock server and socket
24
+ const mockServer = {
25
+ on: vi.fn(),
26
+ listen: vi.fn(),
27
+ close: vi.fn(),
28
+ };
29
+
30
+ const mockSocket = {
31
+ on: vi.fn(),
32
+ write: vi.fn(),
33
+ };
34
+
35
+ // Mock event handler
36
+ const mockEventHandler = {
37
+ testMethod: vi.fn(),
38
+ getStaticFilePath: vi.fn(),
39
+ };
40
+
41
+ beforeEach(() => {
42
+ // Reset all mocks
43
+ vi.resetAllMocks();
44
+
45
+ // 使用模拟定时器
46
+ vi.useFakeTimers();
47
+
48
+ // Setup common mocks
49
+ vi.mocked(os.tmpdir).mockReturnValue(mockTempDir);
50
+ vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
51
+ vi.mocked(net.createServer).mockReturnValue(mockServer as unknown as net.Server);
52
+
53
+ // Mock socket path for different platforms
54
+ const originalPlatform = process.platform;
55
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
56
+
57
+ // Mock fs functions
58
+ vi.mocked(fs.existsSync).mockReturnValue(false);
59
+ vi.mocked(fs.unlinkSync).mockReturnValue(undefined);
60
+ vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
61
+
62
+ // Mock console methods
63
+ vi.spyOn(console, 'error').mockImplementation(() => {});
64
+ vi.spyOn(console, 'log').mockImplementation(() => {});
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.restoreAllMocks();
69
+ vi.useRealTimers();
70
+ });
71
+
72
+ describe('server initialization and start', () => {
73
+ it('should create server and start listening', async () => {
74
+ // Setup
75
+ mockServer.listen.mockImplementation((path, callback) => {
76
+ callback?.();
77
+ return mockServer;
78
+ });
79
+
80
+ // Execute
81
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
82
+ await server.start();
83
+
84
+ // Verify
85
+ expect(net.createServer).toHaveBeenCalled();
86
+ expect(mockServer.listen).toHaveBeenCalledWith(mockSocketPath, expect.any(Function));
87
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
88
+ mockSocketInfoPath,
89
+ JSON.stringify({ socketPath: mockSocketPath }),
90
+ 'utf8',
91
+ );
92
+ });
93
+
94
+ it('should remove existing socket file if it exists', async () => {
95
+ // Setup
96
+ vi.mocked(fs.existsSync).mockReturnValue(true);
97
+ mockServer.listen.mockImplementation((path, callback) => {
98
+ callback?.();
99
+ return mockServer;
100
+ });
101
+
102
+ // Execute
103
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
104
+
105
+ // Verify
106
+ expect(fs.existsSync).toHaveBeenCalledWith(mockSocketPath);
107
+ expect(fs.unlinkSync).toHaveBeenCalledWith(mockSocketPath);
108
+ });
109
+
110
+ it('should handle server start error', async () => {
111
+ // Setup
112
+ const mockError = new Error('Server start error');
113
+ mockServer.on.mockImplementation((event, callback) => {
114
+ if (event === 'error') {
115
+ callback(mockError);
116
+ }
117
+ return mockServer;
118
+ });
119
+
120
+ // Execute and verify
121
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
122
+ await expect(server.start()).rejects.toThrow('Server start error');
123
+ });
124
+ });
125
+
126
+ describe('connection and message handling', () => {
127
+ let server: ElectronIPCServer;
128
+ let connectionHandler: Function;
129
+
130
+ beforeEach(() => {
131
+ // Setup connection handler capture
132
+ mockServer.on.mockReset();
133
+ mockSocket.on.mockReset();
134
+ mockSocket.write.mockReset();
135
+
136
+ vi.mocked(net.createServer).mockImplementation((handler) => {
137
+ connectionHandler = handler as any;
138
+ return mockServer as unknown as net.Server;
139
+ });
140
+
141
+ mockServer.listen.mockImplementation((path, callback) => {
142
+ callback?.();
143
+ return mockServer;
144
+ });
145
+
146
+ // Create server
147
+ server = new ElectronIPCServer(appId, mockEventHandler as any);
148
+ });
149
+
150
+ it('should handle client connection and setup data listeners', async () => {
151
+ // Start server
152
+ await server.start();
153
+
154
+ // Simulate connection
155
+ connectionHandler(mockSocket);
156
+
157
+ // Verify socket listeners setup
158
+ expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function));
159
+ expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
160
+ expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
161
+ });
162
+
163
+ it('should parse messages with \n separator and execute handler', async () => {
164
+ // Setup mock handler
165
+ mockEventHandler.testMethod.mockResolvedValue('success');
166
+
167
+ // Start server
168
+ await server.start();
169
+
170
+ // Simulate connection
171
+ connectionHandler(mockSocket);
172
+
173
+ // Get data handler
174
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
175
+ expect(dataHandlerCall).toBeDefined();
176
+ const dataHandler = dataHandlerCall![1];
177
+
178
+ // Create test message
179
+ const message =
180
+ JSON.stringify({
181
+ id: 'test-id',
182
+ method: 'testMethod',
183
+ params: { key: 'value' },
184
+ }) + '\n';
185
+
186
+ // Send message
187
+ await dataHandler(Buffer.from(message));
188
+
189
+ // 确保异步处理完成
190
+ await vi.runAllTimersAsync();
191
+
192
+ // Verify handler execution
193
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
194
+ { key: 'value' },
195
+ expect.objectContaining({
196
+ id: 'test-id',
197
+ method: 'testMethod',
198
+ socket: mockSocket,
199
+ }),
200
+ );
201
+
202
+ // 触发服务器端处理程序执行
203
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
204
+ await pendingHandlerPromise;
205
+
206
+ // Verify response format with \n\n separator
207
+ expect(mockSocket.write).toHaveBeenCalledWith(
208
+ JSON.stringify({ id: 'test-id', result: 'success' }) + '\n\n',
209
+ );
210
+ });
211
+
212
+ it('should handle multiple messages in single data chunk', async () => {
213
+ // Setup mock handlers with resolved values
214
+ mockEventHandler.testMethod.mockResolvedValue('success1');
215
+ mockEventHandler.getStaticFilePath.mockResolvedValue('path/to/file');
216
+
217
+ // Start server
218
+ await server.start();
219
+
220
+ // Simulate connection
221
+ connectionHandler(mockSocket);
222
+
223
+ // Get data handler
224
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
225
+ expect(dataHandlerCall).toBeDefined();
226
+ const dataHandler = dataHandlerCall![1];
227
+
228
+ // Create multiple messages in one chunk
229
+ const message1 =
230
+ JSON.stringify({
231
+ id: 'id1',
232
+ method: 'testMethod',
233
+ params: { key1: 'value1' },
234
+ }) + '\n\n';
235
+
236
+ const message2 =
237
+ JSON.stringify({
238
+ id: 'id2',
239
+ method: 'getStaticFilePath',
240
+ params: 'path/param',
241
+ }) + '\n\n';
242
+
243
+ // Send combined message
244
+ await dataHandler(Buffer.from(message1 + message2));
245
+
246
+ // 确保异步处理完成
247
+ await vi.runAllTimersAsync();
248
+
249
+ // Verify both handlers were executed
250
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
251
+ { key1: 'value1' },
252
+ expect.objectContaining({ id: 'id1', method: 'testMethod' }),
253
+ );
254
+
255
+ expect(mockEventHandler.getStaticFilePath).toHaveBeenCalledWith(
256
+ 'path/param',
257
+ expect.objectContaining({ id: 'id2', method: 'getStaticFilePath' }),
258
+ );
259
+
260
+ // 等待处理程序完成
261
+ const promise1 = mockEventHandler.testMethod.mock.results[0].value;
262
+ const promise2 = mockEventHandler.getStaticFilePath.mock.results[0].value;
263
+ await Promise.all([promise1, promise2]);
264
+
265
+ // Verify responses
266
+ expect(mockSocket.write).toHaveBeenCalledTimes(2);
267
+ expect(mockSocket.write).toHaveBeenCalledWith(
268
+ JSON.stringify({ id: 'id1', result: 'success1' }) + '\n\n',
269
+ );
270
+ expect(mockSocket.write).toHaveBeenCalledWith(
271
+ JSON.stringify({ id: 'id2', result: 'path/to/file' }) + '\n\n',
272
+ );
273
+ });
274
+
275
+ it('should handle partial messages and buffer them', async () => {
276
+ // Setup mock handler
277
+ mockEventHandler.testMethod.mockResolvedValue('success');
278
+
279
+ // Start server
280
+ await server.start();
281
+
282
+ // Simulate connection
283
+ connectionHandler(mockSocket);
284
+
285
+ // Get data handler
286
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
287
+ expect(dataHandlerCall).toBeDefined();
288
+ const dataHandler = dataHandlerCall![1];
289
+
290
+ // Create partial message (first half)
291
+ const fullMessage =
292
+ JSON.stringify({
293
+ id: 'test-id',
294
+ method: 'testMethod',
295
+ params: { data: 'test' },
296
+ }) + '\n\n';
297
+
298
+ const firstHalf = fullMessage.substring(0, 20);
299
+ await dataHandler(Buffer.from(firstHalf));
300
+
301
+ // 确保异步处理完成
302
+ await vi.runAllTimersAsync();
303
+
304
+ // Verify no handler calls yet
305
+ expect(mockEventHandler.testMethod).not.toHaveBeenCalled();
306
+
307
+ // Send second half
308
+ const secondHalf = fullMessage.substring(20);
309
+ await dataHandler(Buffer.from(secondHalf));
310
+
311
+ // 确保异步处理完成
312
+ await vi.runAllTimersAsync();
313
+
314
+ // Now handler should be called
315
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
316
+ { data: 'test' },
317
+ expect.objectContaining({ id: 'test-id' }),
318
+ );
319
+
320
+ // 等待处理程序完成
321
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
322
+ await pendingHandlerPromise;
323
+
324
+ // 验证响应发送
325
+ expect(mockSocket.write).toHaveBeenCalledWith(
326
+ JSON.stringify({ id: 'test-id', result: 'success' }) + '\n\n',
327
+ );
328
+ });
329
+
330
+ it('should handle errors from method handlers', async () => {
331
+ // Setup mock handler to throw error
332
+ const mockError = new Error('Handler error');
333
+ mockEventHandler.testMethod.mockRejectedValue(mockError);
334
+
335
+ // Start server
336
+ await server.start();
337
+
338
+ // Simulate connection
339
+ connectionHandler(mockSocket);
340
+
341
+ // Get data handler
342
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
343
+ expect(dataHandlerCall).toBeDefined();
344
+ const dataHandler = dataHandlerCall![1];
345
+
346
+ // Create test message
347
+ const message =
348
+ JSON.stringify({
349
+ id: 'test-id',
350
+ method: 'testMethod',
351
+ params: {},
352
+ }) + '\n\n';
353
+
354
+ // Send message
355
+ await dataHandler(Buffer.from(message));
356
+
357
+ // 确保异步处理完成
358
+ await vi.runAllTimersAsync();
359
+
360
+ // 等待Promise被拒绝
361
+ try {
362
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
363
+ await pendingHandlerPromise;
364
+ } catch (error) {
365
+ // 错误预期会被捕获
366
+ }
367
+
368
+ // Verify error response
369
+ expect(mockSocket.write).toHaveBeenCalledWith(
370
+ expect.stringContaining(
371
+ '{"error":"Failed to handle method(testMethod): Handler error","id":"test-id"}\n\n',
372
+ ),
373
+ );
374
+ });
375
+ });
376
+
377
+ describe('server close', () => {
378
+ it('should close server and clean up socket file', async () => {
379
+ // Setup
380
+ mockServer.listen.mockImplementation((path, callback) => {
381
+ callback?.();
382
+ return mockServer;
383
+ });
384
+
385
+ // 明确模拟关闭回调
386
+ mockServer.close.mockImplementation((callback) => {
387
+ if (callback) {
388
+ setTimeout(() => callback(), 0);
389
+ }
390
+ return mockServer;
391
+ });
392
+
393
+ // 为非Windows环境设置平台
394
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
395
+
396
+ // 模拟文件存在
397
+ vi.mocked(fs.existsSync).mockReturnValue(true);
398
+
399
+ // Execute
400
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
401
+ await server.start();
402
+
403
+ // 调用关闭方法
404
+ const closePromise = server.close();
405
+
406
+ // 运行所有计时器使关闭回调触发
407
+ await vi.runAllTimersAsync();
408
+
409
+ // 等待关闭完成
410
+ await closePromise;
411
+
412
+ // Verify
413
+ expect(mockServer.close).toHaveBeenCalled();
414
+ expect(fs.unlinkSync).toHaveBeenCalledWith(mockSocketPath);
415
+ });
416
+ });
417
+ });
@@ -70,20 +70,25 @@ export class ElectronIPCServer {
70
70
  log('Received data chunk, size: %d bytes', chunk.length);
71
71
  dataBuffer += chunk;
72
72
 
73
- try {
74
- // 尝试解析 JSON 消息
75
- const message = JSON.parse(dataBuffer);
76
- log('Successfully parsed JSON message: %o', {
77
- id: message.id,
78
- method: message.method,
79
- });
80
- dataBuffer = ''; // 重置缓冲区
81
-
82
- // 处理请求
83
- this.handleRequest(socket, message);
84
- } catch {
85
- // 如果不是有效的 JSON,可能是消息不完整,继续等待
86
- log('Incomplete or invalid JSON, buffering for more data');
73
+ // 按 \n\n 分割消息
74
+ const messages = dataBuffer.split('\n');
75
+ // 保留最后一个可能不完整的消息
76
+ dataBuffer = messages.pop() || '';
77
+
78
+ // 处理每个完整的消息
79
+ for (const message of messages) {
80
+ if (!message.trim()) continue;
81
+
82
+ try {
83
+ const parsedMessage = JSON.parse(message);
84
+ log('Successfully parsed JSON message: %o', {
85
+ id: parsedMessage.id,
86
+ method: parsedMessage.method,
87
+ });
88
+ this.handleRequest(socket, parsedMessage);
89
+ } catch (err) {
90
+ console.error('Failed to parse message: %s', err);
91
+ }
87
92
  }
88
93
  });
89
94
 
@@ -123,14 +128,14 @@ export class ElectronIPCServer {
123
128
 
124
129
  // 发送结果
125
130
  private sendResult(socket: net.Socket, id: string, result: any): void {
126
- const response = JSON.stringify({ id, result }) + '\n';
131
+ const response = JSON.stringify({ id, result }) + '\n\n';
127
132
  log('Sending success response for ID: %s, size: %d bytes', id, response.length);
128
133
  socket.write(response);
129
134
  }
130
135
 
131
136
  // 发送错误
132
137
  private sendError(socket: net.Socket, id: string, error: string): void {
133
- const response = JSON.stringify({ error, id }) + '\n';
138
+ const response = JSON.stringify({ error, id }) + '\n\n';
134
139
  log('Sending error response for ID: %s: %s', id, error);
135
140
  socket.write(response);
136
141
  }
@@ -89,11 +89,22 @@ const ollamaChatModels: AIChatModelCard[] = [
89
89
  description:
90
90
  'QwQ 是 Qwen 系列的推理模型。与传统的指令调优模型相比,QwQ 具备思考和推理的能力,能够在下游任务中,尤其是困难问题上,显著提升性能。QwQ-32B 是中型推理模型,能够在与最先进的推理模型(如 DeepSeek-R1、o1-mini)竞争时取得可观的表现。',
91
91
  displayName: 'QwQ 32B',
92
- enabled: true,
93
92
  id: 'qwq',
94
93
  releasedAt: '2024-11-28',
95
94
  type: 'chat',
96
95
  },
96
+ {
97
+ abilities: {
98
+ functionCall: true,
99
+ },
100
+ contextWindowTokens: 65_536,
101
+ description: 'Qwen3 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
102
+ displayName: 'Qwen3 7B',
103
+ enabled: true,
104
+ id: 'qwen3',
105
+ type: 'chat',
106
+ },
107
+
97
108
  {
98
109
  contextWindowTokens: 128_000,
99
110
  description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
@@ -115,7 +126,6 @@ const ollamaChatModels: AIChatModelCard[] = [
115
126
  contextWindowTokens: 128_000,
116
127
  description: 'Qwen2.5 是阿里巴巴的新一代大规模语言模型,以优异的性能支持多元化的应用需求。',
117
128
  displayName: 'Qwen2.5 7B',
118
- enabled: true,
119
129
  id: 'qwen2.5',
120
130
  type: 'chat',
121
131
  },
@@ -2,13 +2,15 @@ import { EPubLoader as Loader } from '@langchain/community/document_loaders/fs/e
2
2
  import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
3
3
 
4
4
  import { TempFileManager } from '@/server/utils/tempFileManager';
5
+ import { nanoid } from '@/utils/uuid';
5
6
 
6
7
  import { loaderConfig } from '../config';
7
8
 
8
9
  export const EPubLoader = async (content: Uint8Array) => {
9
- const tempManager = new TempFileManager();
10
+ const tempManager = new TempFileManager('epub-');
11
+
10
12
  try {
11
- const tempPath = await tempManager.writeTempFile(content);
13
+ const tempPath = await tempManager.writeTempFile(content, `${nanoid()}.epub`);
12
14
  const loader = new Loader(tempPath);
13
15
  const documents = await loader.load();
14
16
 
@@ -21,9 +21,11 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
21
21
  "name": "echo",
22
22
  },
23
23
  {
24
- "annotations": {},
25
24
  "description": "Lists all available tools and methods",
26
25
  "inputSchema": {
26
+ "$schema": "http://json-schema.org/draft-07/schema#",
27
+ "additionalProperties": false,
28
+ "properties": {},
27
29
  "type": "object",
28
30
  },
29
31
  "name": "debug",
@@ -35,7 +35,7 @@ const fileProcedure = asyncAuthedProcedure.use(async (opts) => {
35
35
  chunkService: new ChunkService(ctx.userId),
36
36
  embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId),
37
37
  fileModel: new FileModel(ctx.serverDB, ctx.userId),
38
- fileService: new FileService(),
38
+ fileService: new FileService(ctx.serverDB, ctx.userId),
39
39
  },
40
40
  });
41
41
  });
@@ -19,7 +19,7 @@ const fileProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
19
19
  asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
20
20
  chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
21
21
  fileModel: new FileModel(ctx.serverDB, ctx.userId),
22
- fileService: new FileService(),
22
+ fileService: new FileService(ctx.serverDB, ctx.userId),
23
23
  },
24
24
  });
25
25
  });
@@ -10,10 +10,12 @@ import { ImportResultData, ImporterEntryData } from '@/types/importer';
10
10
 
11
11
  const importProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
12
12
  const { ctx } = opts;
13
- const dataImporterService = new DataImporterRepos(ctx.serverDB, ctx.userId);
14
13
 
15
14
  return opts.next({
16
- ctx: { dataImporterService, fileService: new FileService() },
15
+ ctx: {
16
+ dataImporterService: new DataImporterRepos(ctx.serverDB, ctx.userId),
17
+ fileService: new FileService(ctx.serverDB, ctx.userId),
18
+ },
17
19
  });
18
20
  });
19
21
 
@@ -16,7 +16,7 @@ const messageProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
16
16
 
17
17
  return opts.next({
18
18
  ctx: {
19
- fileService: new FileService(),
19
+ fileService: new FileService(ctx.serverDB, ctx.userId),
20
20
  messageModel: new MessageModel(ctx.serverDB, ctx.userId),
21
21
  },
22
22
  });
@@ -102,7 +102,7 @@ export const messageRouter = router({
102
102
  const serverDB = await getServerDB();
103
103
 
104
104
  const messageModel = new MessageModel(serverDB, ctx.userId);
105
- const fileService = new FileService();
105
+ const fileService = new FileService(serverDB, ctx.userId);
106
106
 
107
107
  return messageModel.query(input, {
108
108
  postProcessUrl: (path) => fileService.getFullFileUrl(path),
@@ -40,7 +40,7 @@ const ragEvalProcedure = authedProcedure
40
40
  datasetRecordModel: new EvalDatasetRecordModel(ctx.userId),
41
41
  evaluationModel: new EvalEvaluationModel(ctx.userId),
42
42
  evaluationRecordModel: new EvaluationRecordModel(ctx.userId),
43
- fileService: new FileService(),
43
+ fileService: new FileService(ctx.serverDB, ctx.userId),
44
44
  },
45
45
  });
46
46
  });
@@ -1,6 +1,6 @@
1
1
  import { UserJSON } from '@clerk/backend';
2
+ import { v4 as uuidv4 } from 'uuid';
2
3
  import { z } from 'zod';
3
- import { v4 as uuidv4 } from 'uuid'; // 需要添加此导入
4
4
 
5
5
  import { enableClerk } from '@/const/auth';
6
6
  import { isDesktop } from '@/const/version';
@@ -28,7 +28,7 @@ const userProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next
28
28
  return next({
29
29
  ctx: {
30
30
  clerkAuth: new ClerkAuth(),
31
- fileService: new FileService(),
31
+ fileService: new FileService(ctx.serverDB, ctx.userId),
32
32
  nextAuthDbAdapter: LobeNextAuthDbAdapter(ctx.serverDB),
33
33
  userModel: new UserModel(ctx.serverDB, ctx.userId),
34
34
  },
@@ -1,3 +1,12 @@
1
+ import { TRPCError } from '@trpc/server';
2
+
3
+ import { serverDBEnv } from '@/config/db';
4
+ import { FileModel } from '@/database/models/file';
5
+ import { FileItem } from '@/database/schemas';
6
+ import { LobeChatDatabase } from '@/database/type';
7
+ import { TempFileManager } from '@/server/utils/tempFileManager';
8
+ import { nanoid } from '@/utils/uuid';
9
+
1
10
  import { FileServiceImpl, createFileServiceModule } from './impls';
2
11
 
3
12
  /**
@@ -5,8 +14,16 @@ import { FileServiceImpl, createFileServiceModule } from './impls';
5
14
  * 使用模块化实现方式,提供文件操作服务
6
15
  */
7
16
  export class FileService {
17
+ private userId: string;
18
+ private fileModel: FileModel;
19
+
8
20
  private impl: FileServiceImpl = createFileServiceModule();
9
21
 
22
+ constructor(db: LobeChatDatabase, userId: string) {
23
+ this.userId = userId;
24
+ this.fileModel = new FileModel(db, userId);
25
+ }
26
+
10
27
  /**
11
28
  * 删除文件
12
29
  */
@@ -62,4 +79,33 @@ export class FileService {
62
79
  public async getFullFileUrl(url?: string | null, expiresIn?: number): Promise<string> {
63
80
  return this.impl.getFullFileUrl(url, expiresIn);
64
81
  }
82
+
83
+ async downloadFileToLocal(
84
+ fileId: string,
85
+ ): Promise<{ cleanup: () => void; file: FileItem; filePath: string }> {
86
+ const file = await this.fileModel.findById(fileId);
87
+ if (!file) {
88
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
89
+ }
90
+
91
+ let content: Uint8Array | undefined;
92
+ try {
93
+ content = await this.getFileByteArray(file.url);
94
+ } catch (e) {
95
+ console.error(e);
96
+ // if file not found, delete it from db
97
+ if ((e as any).Code === 'NoSuchKey') {
98
+ await this.fileModel.delete(fileId, serverDBEnv.REMOVE_GLOBAL_FILE);
99
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
100
+ }
101
+ }
102
+
103
+ if (!content) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File content is empty' });
104
+
105
+ const dir = nanoid();
106
+ const tempManager = new TempFileManager(dir);
107
+
108
+ const filePath = await tempManager.writeTempFile(content, file.name);
109
+ return { cleanup: () => tempManager.cleanup(), file, filePath };
110
+ }
65
111
  }