@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,82 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { App } from '@/core/App';
4
+
5
+ // 模拟 logger
6
+ vi.mock('@/utils/logger', () => ({
7
+ createLogger: () => ({
8
+ info: vi.fn(),
9
+ }),
10
+ }));
11
+
12
+ import UpdaterCtr from '../UpdaterCtr';
13
+
14
+ // 模拟 App 及其依赖项
15
+ const mockCheckForUpdates = vi.fn();
16
+ const mockDownloadUpdate = vi.fn();
17
+ const mockInstallNow = vi.fn();
18
+ const mockInstallLater = vi.fn();
19
+
20
+ const mockApp = {
21
+ updaterManager: {
22
+ checkForUpdates: mockCheckForUpdates,
23
+ downloadUpdate: mockDownloadUpdate,
24
+ installNow: mockInstallNow,
25
+ installLater: mockInstallLater,
26
+ },
27
+ } as unknown as App;
28
+
29
+ describe('UpdaterCtr', () => {
30
+ let updaterCtr: UpdaterCtr;
31
+
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ updaterCtr = new UpdaterCtr(mockApp);
35
+ });
36
+
37
+ describe('checkForUpdates', () => {
38
+ it('should call updaterManager.checkForUpdates', async () => {
39
+ await updaterCtr.checkForUpdates();
40
+ expect(mockCheckForUpdates).toHaveBeenCalled();
41
+ });
42
+ });
43
+
44
+ describe('downloadUpdate', () => {
45
+ it('should call updaterManager.downloadUpdate', async () => {
46
+ await updaterCtr.downloadUpdate();
47
+ expect(mockDownloadUpdate).toHaveBeenCalled();
48
+ });
49
+ });
50
+
51
+ describe('quitAndInstallUpdate', () => {
52
+ it('should call updaterManager.installNow', () => {
53
+ updaterCtr.quitAndInstallUpdate();
54
+ expect(mockInstallNow).toHaveBeenCalled();
55
+ });
56
+ });
57
+
58
+ describe('installLater', () => {
59
+ it('should call updaterManager.installLater', () => {
60
+ updaterCtr.installLater();
61
+ expect(mockInstallLater).toHaveBeenCalled();
62
+ });
63
+ });
64
+
65
+ // 测试错误处理
66
+ describe('error handling', () => {
67
+ it('should handle errors when checking for updates', async () => {
68
+ const error = new Error('Network error');
69
+ mockCheckForUpdates.mockRejectedValueOnce(error);
70
+
71
+ // 由于控制器并未明确处理并返回错误,这里我们只验证调用发生且错误正确冒泡
72
+ await expect(updaterCtr.checkForUpdates()).rejects.toThrow(error);
73
+ });
74
+
75
+ it('should handle errors when downloading updates', async () => {
76
+ const error = new Error('Download failed');
77
+ mockDownloadUpdate.mockRejectedValueOnce(error);
78
+
79
+ await expect(updaterCtr.downloadUpdate()).rejects.toThrow(error);
80
+ });
81
+ });
82
+ });
@@ -6,12 +6,16 @@ import { promisify } from 'node:util';
6
6
 
7
7
  import { FILE_STORAGE_DIR } from '@/const/dir';
8
8
  import { makeSureDirExist } from '@/utils/file-system';
9
+ import { createLogger } from '@/utils/logger';
9
10
 
10
11
  import { ServiceModule } from './index';
11
12
 
12
13
  const readFilePromise = promisify(fs.readFile);
13
14
  const unlinkPromise = promisify(fs.unlink);
14
15
 
16
+ // Create logger
17
+ const logger = createLogger('services:FileService');
18
+
15
19
  interface UploadFileParams {
16
20
  content: ArrayBuffer;
17
21
  filename: string;
@@ -35,8 +39,10 @@ export default class FileService extends ServiceModule {
35
39
  constructor(app) {
36
40
  super(app);
37
41
 
38
- // 初始化文件存储目录
42
+ // Initialize file storage directory
43
+ logger.info('Initializing file storage directory');
39
44
  makeSureDirExist(this.UPLOADS_DIR);
45
+ logger.debug(`Upload directory created: ${this.UPLOADS_DIR}`);
40
46
  }
41
47
 
42
48
  /**
@@ -48,19 +54,23 @@ export default class FileService extends ServiceModule {
48
54
  hash,
49
55
  type,
50
56
  }: UploadFileParams): Promise<{ metadata: FileMetadata; success: boolean }> {
57
+ logger.info(`Starting to upload file: ${filename}, hash: ${hash}`);
51
58
  try {
52
59
  // 创建时间戳目录
53
60
  const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
54
61
  const dirname = join(this.UPLOADS_DIR, date);
62
+ logger.debug(`Creating timestamp directory: ${dirname}`);
55
63
  makeSureDirExist(dirname);
56
64
 
57
65
  // 生成文件保存路径
58
66
  const fileExt = filename.split('.').pop() || '';
59
67
  const savedFilename = `${hash}${fileExt ? `.${fileExt}` : ''}`;
60
68
  const savedPath = join(dirname, savedFilename);
69
+ logger.debug(`Generated file save path: ${savedPath}`);
61
70
 
62
71
  // 写入文件内容
63
72
  const buffer = Buffer.from(content);
73
+ logger.debug(`Writing file content, size: ${buffer.length} bytes`);
64
74
  await writeFile(savedPath, buffer);
65
75
 
66
76
  // 写入元数据文件
@@ -72,10 +82,12 @@ export default class FileService extends ServiceModule {
72
82
  size: buffer.length,
73
83
  type,
74
84
  };
85
+ logger.debug(`Writing metadata file: ${metaFilePath}`);
75
86
  await writeFile(metaFilePath, JSON.stringify(metadata, null, 2));
76
87
 
77
88
  // 返回与S3兼容的元数据格式
78
89
  const desktopPath = `desktop://${date}/${savedFilename}`;
90
+ logger.info(`File upload successful: ${desktopPath}`);
79
91
 
80
92
  return {
81
93
  metadata: {
@@ -87,7 +99,7 @@ export default class FileService extends ServiceModule {
87
99
  success: true,
88
100
  };
89
101
  } catch (error) {
90
- console.error('File upload failed:', error);
102
+ logger.error(`File upload failed:`, error);
91
103
  throw new Error(`File upload failed: ${(error as Error).message}`);
92
104
  }
93
105
  }
@@ -96,35 +108,41 @@ export default class FileService extends ServiceModule {
96
108
  * 获取文件内容
97
109
  */
98
110
  async getFile(path: string): Promise<{ content: ArrayBuffer; mimeType: string }> {
111
+ logger.info(`Getting file content: ${path}`);
99
112
  try {
100
113
  // 处理desktop://路径
101
114
  if (!path.startsWith('desktop://')) {
115
+ logger.error(`Invalid desktop file path: ${path}`);
102
116
  throw new Error(`Invalid desktop file path: ${path}`);
103
117
  }
104
118
 
105
119
  // 标准化路径格式
106
120
  // 可能收到的格式: desktop:/12345/file.png 或 desktop://12345/file.png
107
121
  const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
122
+ logger.debug(`Normalized path: ${normalizedPath}`);
108
123
 
109
124
  // 解析路径
110
125
  const relativePath = normalizedPath.replace('desktop://', '');
111
126
  const filePath = join(this.UPLOADS_DIR, relativePath);
112
-
113
- console.log('Reading file from:', filePath);
127
+ logger.debug(`Reading file from path: ${filePath}`);
114
128
 
115
129
  // 读取文件内容
130
+ logger.debug(`Starting to read file content`);
116
131
  const content = await readFilePromise(filePath);
132
+ logger.debug(`File content read complete, size: ${content.length} bytes`);
117
133
 
118
134
  // 读取元数据获取MIME类型
119
135
  const metaFilePath = `${filePath}.meta`;
120
136
  let mimeType = 'application/octet-stream'; // 默认MIME类型
137
+ logger.debug(`Attempting to read metadata file: ${metaFilePath}`);
121
138
 
122
139
  try {
123
140
  const metaContent = await readFilePromise(metaFilePath, 'utf8');
124
141
  const metadata = JSON.parse(metaContent);
125
142
  mimeType = metadata.type || mimeType;
143
+ logger.debug(`Got MIME type from metadata: ${mimeType}`);
126
144
  } catch (metaError) {
127
- console.warn(`Failed to read metadata file: ${metaError.message}, using default MIME type`);
145
+ logger.warn(`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`);
128
146
  // 如果元数据文件不存在,尝试从文件扩展名猜测MIME类型
129
147
  const ext = path.split('.').pop()?.toLowerCase();
130
148
  if (ext) {
@@ -155,15 +173,17 @@ export default class FileService extends ServiceModule {
155
173
  break;
156
174
  }
157
175
  }
176
+ logger.debug(`Set MIME type based on file extension: ${mimeType}`);
158
177
  }
159
178
  }
160
179
 
180
+ logger.info(`File retrieval successful: ${path}`);
161
181
  return {
162
182
  content: content.buffer as ArrayBuffer,
163
183
  mimeType,
164
184
  };
165
185
  } catch (error) {
166
- console.error('File retrieval failed:', error);
186
+ logger.error(`File retrieval failed:`, error);
167
187
  throw new Error(`File retrieval failed: ${(error as Error).message}`);
168
188
  }
169
189
  }
@@ -172,29 +192,37 @@ export default class FileService extends ServiceModule {
172
192
  * 删除文件
173
193
  */
174
194
  async deleteFile(path: string): Promise<{ success: boolean }> {
195
+ logger.info(`Deleting file: ${path}`);
175
196
  try {
176
197
  // 处理desktop://路径
177
198
  if (!path.startsWith('desktop://')) {
199
+ logger.error(`Invalid desktop file path: ${path}`);
178
200
  throw new Error(`Invalid desktop file path: ${path}`);
179
201
  }
180
202
 
181
203
  // 解析路径
182
204
  const relativePath = path.replace('desktop://', '');
183
205
  const filePath = join(this.UPLOADS_DIR, relativePath);
206
+ logger.debug(`File deletion path: ${filePath}`);
184
207
 
185
208
  // 删除文件及其元数据
209
+ logger.debug(`Starting file deletion`);
186
210
  await unlinkPromise(filePath);
211
+ logger.debug(`File deletion successful`);
187
212
 
188
213
  // 尝试删除元数据文件,但不强制要求存在
189
214
  try {
215
+ logger.debug(`Attempting to delete metadata file`);
190
216
  await unlinkPromise(`${filePath}.meta`);
217
+ logger.debug(`Metadata file deletion successful`);
191
218
  } catch (error) {
192
- console.warn(`Failed to delete metadata file: ${(error as Error).message}`);
219
+ logger.warn(`Failed to delete metadata file: ${(error as Error).message}`);
193
220
  }
194
221
 
222
+ logger.info(`File deletion operation complete: ${path}`);
195
223
  return { success: true };
196
224
  } catch (error) {
197
- console.error('File deletion failed:', error);
225
+ logger.error(`File deletion failed:`, error);
198
226
  throw new Error(`File deletion failed: ${(error as Error).message}`);
199
227
  }
200
228
  }
@@ -203,15 +231,18 @@ export default class FileService extends ServiceModule {
203
231
  * 批量删除文件
204
232
  */
205
233
  async deleteFiles(paths: string[]): Promise<DeleteFilesResponse> {
234
+ logger.info(`Batch deleting files, count: ${paths.length}`);
206
235
  const errors: { message: string; path: string }[] = [];
207
236
 
208
237
  // 并行处理所有删除请求
238
+ logger.debug(`Starting parallel deletion requests`);
209
239
  const results = await Promise.allSettled(
210
240
  paths.map(async (path) => {
211
241
  try {
212
242
  await this.deleteFile(path);
213
243
  return { path, success: true };
214
244
  } catch (error) {
245
+ logger.warn(`Failed to delete file: ${path}, error: ${(error as Error).message}`);
215
246
  return {
216
247
  error: (error as Error).message,
217
248
  path,
@@ -222,8 +253,10 @@ export default class FileService extends ServiceModule {
222
253
  );
223
254
 
224
255
  // 处理结果
256
+ logger.debug(`Processing batch deletion results`);
225
257
  results.forEach((result) => {
226
258
  if (result.status === 'rejected') {
259
+ logger.error(`Unexpected error: ${result.reason}`);
227
260
  errors.push({
228
261
  message: `Unexpected error: ${result.reason}`,
229
262
  path: 'unknown',
@@ -236,20 +269,26 @@ export default class FileService extends ServiceModule {
236
269
  }
237
270
  });
238
271
 
272
+ const success = errors.length === 0;
273
+ logger.info(`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`);
239
274
  return {
240
- success: errors.length === 0,
275
+ success,
241
276
  ...(errors.length > 0 && { errors }),
242
277
  };
243
278
  }
244
279
 
245
280
  async getFilePath(path: string): Promise<string> {
281
+ logger.debug(`Getting filesystem path: ${path}`);
246
282
  // 处理desktop://路径
247
283
  if (!path.startsWith('desktop://')) {
284
+ logger.error(`Invalid desktop file path: ${path}`);
248
285
  throw new Error(`Invalid desktop file path: ${path}`);
249
286
  }
250
287
 
251
288
  // 解析路径
252
289
  const relativePath = path.replace('desktop://', '');
253
- return join(this.UPLOADS_DIR, relativePath);
290
+ const fullPath = join(this.UPLOADS_DIR, relativePath);
291
+ logger.debug(`Resolved filesystem path: ${fullPath}`);
292
+ return fullPath;
254
293
  }
255
294
  }
@@ -0,0 +1,17 @@
1
+ import { resolve } from 'node:path';
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ alias: {
7
+ '@': resolve(__dirname, './src/main'),
8
+ },
9
+ coverage: {
10
+ all: false,
11
+ provider: 'v8',
12
+ reporter: ['text', 'json', 'lcov', 'text-summary'],
13
+ reportsDirectory: './coverage/app',
14
+ },
15
+ environment: 'node',
16
+ },
17
+ });
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add qwen3 for ollama."
6
+ ]
7
+ },
8
+ "date": "2025-05-08",
9
+ "version": "1.84.26"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fix desktop upload image on macOS."
15
+ ]
16
+ },
17
+ "date": "2025-05-08",
18
+ "version": "1.84.25"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.84.24",
3
+ "version": "1.84.26",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -121,7 +121,7 @@
121
121
  "dependencies": {
122
122
  "@ant-design/icons": "^5.6.1",
123
123
  "@ant-design/pro-components": "^2.8.7",
124
- "@anthropic-ai/sdk": "^0.39.0",
124
+ "@anthropic-ai/sdk": "^0.40.1",
125
125
  "@auth/core": "^0.38.0",
126
126
  "@aws-sdk/client-bedrock-runtime": "^3.779.0",
127
127
  "@aws-sdk/client-s3": "^3.779.0",
@@ -199,7 +199,7 @@
199
199
  "langfuse": "^3.37.1",
200
200
  "langfuse-core": "^3.37.1",
201
201
  "lodash-es": "^4.17.21",
202
- "lucide-react": "^0.503.0",
202
+ "lucide-react": "^0.508.0",
203
203
  "mammoth": "^1.9.0",
204
204
  "mdast-util-to-markdown": "^2.1.2",
205
205
  "modern-screenshot": "^4.6.0",
@@ -4,6 +4,9 @@
4
4
  "private": true,
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
+ "scripts": {
8
+ "test": "vitest"
9
+ },
7
10
  "dependencies": {
8
11
  "debug": "^4.3.4"
9
12
  },
@@ -192,39 +192,76 @@ export class ElectronIpcClient {
192
192
 
193
193
  log('Sending request: %s %o', method, params);
194
194
  return new Promise<T>((resolve, reject) => {
195
- try {
196
- const id = Math.random().toString(36).slice(2, 15);
197
- const request = { id, method, params };
198
- log('Created request with ID: %s', id);
199
-
200
- // 将请求添加到队列
201
- this.requestQueue.set(id, { reject, resolve });
202
- log('Added request to queue, current queue size: %d', this.requestQueue.size);
203
-
204
- // 设置超时
205
- const timeout = setTimeout(() => {
206
- this.requestQueue.delete(id);
207
- const errorMsg = `Request timed out, method: ${method}`;
195
+ const id = Math.random().toString(36).slice(2, 15);
196
+ const request = { id, method, params };
197
+ log('Created request with ID: %s', id);
198
+
199
+ // eslint-disable-next-line no-undef
200
+ let requestTimeoutId: NodeJS.Timeout;
201
+
202
+ const cleanupAndResolve = (value: T) => {
203
+ clearTimeout(requestTimeoutId);
204
+ this.requestQueue.delete(id);
205
+ resolve(value);
206
+ };
207
+
208
+ const cleanupAndReject = (error: any) => {
209
+ clearTimeout(requestTimeoutId);
210
+ this.requestQueue.delete(id);
211
+ // 保留超时错误的 console.error 日志
212
+ if (
213
+ error &&
214
+ error.message &&
215
+ typeof error.message === 'string' &&
216
+ error.message.startsWith('Request timed out')
217
+ ) {
208
218
  console.error('Request timed out, ID: %s, method: %s', id, method);
209
- reject(new Error(errorMsg));
210
- }, 5000);
219
+ }
220
+ reject(error);
221
+ };
222
+
223
+ this.requestQueue.set(id, { reject: cleanupAndReject, resolve: cleanupAndResolve });
224
+ log('Added request to queue, current queue size: %d', this.requestQueue.size);
225
+
226
+ requestTimeoutId = setTimeout(() => {
227
+ const pendingRequest = this.requestQueue.get(id);
228
+ if (pendingRequest) {
229
+ // 请求仍在队列中,表示超时
230
+ // 调用其专用的 reject处理器 (cleanupAndReject)
231
+ const errorMsg = `Request timed out, method: ${method}`;
232
+ // console.error 移至 cleanupAndReject 中处理
233
+ pendingRequest.reject(new Error(errorMsg));
234
+ }
235
+ // 如果 pendingRequest 不存在, 表示请求已被处理,其超时已清除
236
+ }, 5000);
211
237
 
238
+ try {
212
239
  // 发送请求
213
- const requestJson = JSON.stringify(request);
240
+ const requestJson = JSON.stringify(request) + '\n';
214
241
  log('Writing request to socket, size: %d bytes', requestJson.length);
215
242
  this.socket!.write(requestJson, (err) => {
216
243
  if (err) {
217
- clearTimeout(timeout);
218
- this.requestQueue.delete(id);
244
+ // 写入失败,请求应被视为失败
245
+ // 调用其 reject 处理器 (cleanupAndReject)
246
+ const pending = this.requestQueue.get(id);
247
+ if (pending) {
248
+ pending.reject(err); // 这会调用 cleanupAndReject
249
+ } else {
250
+ // 理论上不应发生,因为写入失败通常很快
251
+ // 但为安全起见,确保原始 promise 被 reject
252
+ cleanupAndReject(err);
253
+ }
219
254
  console.error('Failed to write request to socket: %o', err);
220
- reject(err);
221
255
  } else {
222
256
  log('Request successfully written to socket, ID: %s', id);
223
257
  }
224
258
  });
225
259
  } catch (err) {
226
- console.error('Error sending request: %o', err);
227
- reject(err);
260
+ console.error('Error sending request (during setup/write phase): %o', err);
261
+ // 发生在此处的错误意味着请求甚至没有机会进入队列或设置超时
262
+ // 直接调用 cleanupAndReject 以确保一致性,尽管此时 requestTimeoutId 可能未定义
263
+ // (clearTimeout(undefined)是安全的)
264
+ cleanupAndReject(err);
228
265
  }
229
266
  });
230
267
  }