@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.
- package/CHANGELOG.md +50 -0
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +195 -0
- package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +44 -0
- package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +82 -0
- package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +64 -0
- package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +256 -0
- package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +82 -0
- package/apps/desktop/src/main/services/fileSrv.ts +49 -10
- package/apps/desktop/vitest.config.ts +17 -0
- package/changelog/v1.json +18 -0
- package/package.json +3 -3
- package/packages/electron-server-ipc/package.json +3 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +58 -21
- package/packages/electron-server-ipc/src/ipcServer.test.ts +417 -0
- package/packages/electron-server-ipc/src/ipcServer.ts +21 -16
- package/src/config/aiModels/ollama.ts +12 -2
- package/src/libs/langchain/loaders/epub/index.ts +4 -2
- package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +3 -1
- package/src/server/routers/async/file.ts +1 -1
- package/src/server/routers/lambda/file.ts +1 -1
- package/src/server/routers/lambda/importer.ts +4 -2
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/routers/lambda/ragEval.ts +1 -1
- package/src/server/routers/lambda/user.ts +2 -2
- package/src/server/services/file/index.ts +46 -0
- package/src/server/utils/tempFileManager.ts +5 -8
- 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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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: {
|
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
|
}
|