@lobehub/chat 1.84.23 → 1.84.25
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/locales/ar/hotkey.json +4 -0
- package/locales/ar/models.json +55 -13
- package/locales/ar/providers.json +0 -3
- package/locales/bg-BG/hotkey.json +4 -0
- package/locales/bg-BG/models.json +55 -13
- package/locales/bg-BG/providers.json +0 -3
- package/locales/de-DE/hotkey.json +4 -0
- package/locales/de-DE/models.json +55 -13
- package/locales/de-DE/providers.json +0 -3
- package/locales/en-US/hotkey.json +4 -0
- package/locales/en-US/models.json +55 -13
- package/locales/en-US/providers.json +0 -3
- package/locales/es-ES/hotkey.json +4 -0
- package/locales/es-ES/models.json +55 -13
- package/locales/es-ES/providers.json +0 -3
- package/locales/fa-IR/hotkey.json +4 -0
- package/locales/fa-IR/models.json +55 -13
- package/locales/fa-IR/providers.json +0 -3
- package/locales/fr-FR/hotkey.json +4 -0
- package/locales/fr-FR/models.json +55 -13
- package/locales/fr-FR/providers.json +0 -3
- package/locales/it-IT/hotkey.json +4 -0
- package/locales/it-IT/models.json +55 -13
- package/locales/it-IT/providers.json +0 -3
- package/locales/ja-JP/hotkey.json +4 -0
- package/locales/ja-JP/models.json +55 -13
- package/locales/ja-JP/providers.json +0 -3
- package/locales/ko-KR/hotkey.json +4 -0
- package/locales/ko-KR/models.json +55 -13
- package/locales/ko-KR/providers.json +0 -3
- package/locales/nl-NL/hotkey.json +4 -0
- package/locales/nl-NL/models.json +55 -13
- package/locales/nl-NL/providers.json +0 -3
- package/locales/pl-PL/hotkey.json +4 -0
- package/locales/pl-PL/models.json +55 -13
- package/locales/pl-PL/providers.json +0 -3
- package/locales/pt-BR/hotkey.json +4 -0
- package/locales/pt-BR/models.json +55 -13
- package/locales/pt-BR/providers.json +0 -3
- package/locales/ru-RU/hotkey.json +4 -0
- package/locales/ru-RU/models.json +55 -13
- package/locales/ru-RU/providers.json +0 -3
- package/locales/tr-TR/hotkey.json +4 -0
- package/locales/tr-TR/models.json +55 -13
- package/locales/tr-TR/providers.json +0 -3
- package/locales/vi-VN/hotkey.json +4 -0
- package/locales/vi-VN/models.json +55 -13
- package/locales/vi-VN/providers.json +0 -3
- package/locales/zh-CN/hotkey.json +4 -0
- package/locales/zh-CN/models.json +55 -13
- package/locales/zh-CN/providers.json +0 -3
- package/locales/zh-TW/hotkey.json +4 -0
- package/locales/zh-TW/models.json +55 -13
- package/locales/zh-TW/providers.json +0 -3
- package/package.json +1 -1
- 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/const/hotkeys.ts +7 -0
- package/src/const/url.ts +1 -1
- package/src/features/User/UserPanel/useMenu.tsx +2 -1
- package/src/locales/default/hotkey.ts +4 -0
- package/src/services/__tests__/_url.test.ts +23 -0
- package/src/types/hotkey.ts +1 -0
- 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
|
}
|
package/src/const/hotkeys.ts
CHANGED
@@ -11,6 +11,13 @@ const combineKeys = (keys: string[]) => keys.join('+');
|
|
11
11
|
// mod 在 Mac 上是 command 键,alt 在 Win 上是 ctrl 键
|
12
12
|
export const HOTKEYS_REGISTRATION: HotkeyRegistration = [
|
13
13
|
// basic
|
14
|
+
{
|
15
|
+
group: HotkeyGroupEnum.Essential,
|
16
|
+
id: HotkeyEnum.ShowApp,
|
17
|
+
keys: combineKeys([KeyEnum.Mod, 'e']),
|
18
|
+
nonEditable: true,
|
19
|
+
scopes: [HotkeyScopeEnum.Global],
|
20
|
+
},
|
14
21
|
{
|
15
22
|
group: HotkeyGroupEnum.Essential,
|
16
23
|
id: HotkeyEnum.Search,
|
package/src/const/url.ts
CHANGED
@@ -17,7 +17,7 @@ export const OG_URL = '/og/cover.png?v=1';
|
|
17
17
|
|
18
18
|
export const GITHUB = pkg.homepage;
|
19
19
|
export const GITHUB_ISSUES = urlJoin(GITHUB, 'issues/new/choose');
|
20
|
-
export const CHANGELOG =
|
20
|
+
export const CHANGELOG = 'https://lobehub.com/changelog';
|
21
21
|
export const DOCKER_IMAGE = 'https://hub.docker.com/r/lobehub/lobe-chat';
|
22
22
|
|
23
23
|
export const DOCUMENTS = urlJoin(OFFICIAL_SITE, '/docs');
|
@@ -25,6 +25,7 @@ import { enableAuth } from '@/const/auth';
|
|
25
25
|
import { LOBE_CHAT_CLOUD } from '@/const/branding';
|
26
26
|
import { DEFAULT_HOTKEY_CONFIG } from '@/const/settings';
|
27
27
|
import {
|
28
|
+
CHANGELOG,
|
28
29
|
DISCORD,
|
29
30
|
DOCUMENTS_REFER_URL,
|
30
31
|
EMAIL_SUPPORT,
|
@@ -144,7 +145,7 @@ export const useMenu = () => {
|
|
144
145
|
{
|
145
146
|
icon: <Icon icon={FileClockIcon} />,
|
146
147
|
key: 'changelog',
|
147
|
-
label: <Link href={'/changelog/modal'}>{t('changelog')}</Link>,
|
148
|
+
label: <Link href={isDesktop ? CHANGELOG : '/changelog/modal'}>{t('changelog')}</Link>,
|
148
149
|
},
|
149
150
|
{
|
150
151
|
children: [
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { API_ENDPOINTS } from '../_url';
|
4
|
+
|
5
|
+
describe('API_ENDPOINTS', () => {
|
6
|
+
it('should return correct basePath URLs', () => {
|
7
|
+
expect(API_ENDPOINTS.oauth).toBe('/api/auth');
|
8
|
+
expect(API_ENDPOINTS.proxy).toBe('/webapi/proxy');
|
9
|
+
expect(API_ENDPOINTS.gateway).toBe('/webapi/plugin/gateway');
|
10
|
+
expect(API_ENDPOINTS.trace).toBe('/webapi/trace');
|
11
|
+
expect(API_ENDPOINTS.stt).toBe('/webapi/stt/openai');
|
12
|
+
expect(API_ENDPOINTS.tts).toBe('/webapi/tts/openai');
|
13
|
+
expect(API_ENDPOINTS.edge).toBe('/webapi/tts/edge');
|
14
|
+
expect(API_ENDPOINTS.microsoft).toBe('/webapi/tts/microsoft');
|
15
|
+
});
|
16
|
+
|
17
|
+
it('should return correct dynamic URLs', () => {
|
18
|
+
expect(API_ENDPOINTS.chat('openai')).toBe('/webapi/chat/openai');
|
19
|
+
expect(API_ENDPOINTS.models('anthropic')).toBe('/webapi/models/anthropic');
|
20
|
+
expect(API_ENDPOINTS.modelPull('azure')).toBe('/webapi/models/azure/pull');
|
21
|
+
expect(API_ENDPOINTS.images('dalle')).toBe('/webapi/text-to-image/dalle');
|
22
|
+
});
|
23
|
+
});
|
package/src/types/hotkey.ts
CHANGED
package/vitest.config.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { resolve } from 'node:path';
|
1
|
+
import { join, resolve } from 'node:path';
|
2
2
|
import { coverageConfigDefaults, defineConfig } from 'vitest/config';
|
3
3
|
|
4
4
|
export default defineConfig({
|
@@ -32,6 +32,7 @@ export default defineConfig({
|
|
32
32
|
'**/node_modules/**',
|
33
33
|
'**/dist/**',
|
34
34
|
'**/build/**',
|
35
|
+
'**/apps/desktop/**',
|
35
36
|
'src/database/server/**/**',
|
36
37
|
'src/database/repositories/dataImporter/deprecated/**/**',
|
37
38
|
],
|
@@ -41,6 +42,6 @@ export default defineConfig({
|
|
41
42
|
inline: ['vitest-canvas-mock'],
|
42
43
|
},
|
43
44
|
},
|
44
|
-
setupFiles: './tests/setup.ts',
|
45
|
+
setupFiles: join(__dirname, './tests/setup.ts'),
|
45
46
|
},
|
46
47
|
});
|