@lobehub/lobehub 2.0.0-next.30 → 2.0.0-next.32

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 CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.32](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.31...v2.0.0-next.32)
6
+
7
+ <sup>Released on **2025-11-05**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Should install new version after quit this instance.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Should install new version after quit this instance, closes [#10064](https://github.com/lobehub/lobe-chat/issues/10064) ([9ab77b2](https://github.com/lobehub/lobe-chat/commit/9ab77b2))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.31](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.30...v2.0.0-next.31)
31
+
32
+ <sup>Released on **2025-11-05**</sup>
33
+
34
+ <br/>
35
+
36
+ <details>
37
+ <summary><kbd>Improvements and Fixes</kbd></summary>
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ## [Version 2.0.0-next.30](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.29...v2.0.0-next.30)
6
48
 
7
49
  <sup>Released on **2025-11-05**</sup>
@@ -141,8 +141,29 @@ export class UpdaterManager {
141
141
  // Mark application for exit
142
142
  this.app.isQuiting = true;
143
143
 
144
- // Delay installation by 1 second to ensure window is closed
145
- autoUpdater.quitAndInstall();
144
+ // Close all windows first to ensure clean exit
145
+ logger.info('Closing all windows before update installation...');
146
+ const { BrowserWindow, app } = require('electron');
147
+ const allWindows = BrowserWindow.getAllWindows();
148
+ allWindows.forEach((window) => {
149
+ if (!window.isDestroyed()) {
150
+ window.close();
151
+ }
152
+ });
153
+
154
+ // Release single instance lock before quitting
155
+ // This ensures the new instance can acquire the lock
156
+ logger.info('Releasing single instance lock...');
157
+ app.releaseSingleInstanceLock();
158
+
159
+ // Small delay to ensure windows are closed and lock is released
160
+ setTimeout(() => {
161
+ // quitAndInstall parameters:
162
+ // - isSilent: true (don't show installation UI)
163
+ // - isForceRunAfter: true (force start app after installation)
164
+ logger.info('Calling autoUpdater.quitAndInstall...');
165
+ autoUpdater.quitAndInstall(true, true);
166
+ }, 100);
146
167
  };
147
168
 
148
169
  /**
package/changelog/v1.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Should install new version after quit this instance."
6
+ ]
7
+ },
8
+ "date": "2025-11-05",
9
+ "version": "2.0.0-next.32"
10
+ },
11
+ {
12
+ "children": {},
13
+ "date": "2025-11-05",
14
+ "version": "2.0.0-next.31"
15
+ },
2
16
  {
3
17
  "children": {
4
18
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.30",
3
+ "version": "2.0.0-next.32",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent 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",
@@ -360,7 +360,7 @@
360
360
  "glob": "^11.0.3",
361
361
  "happy-dom": "^20.0.10",
362
362
  "husky": "^9.1.7",
363
- "import-in-the-middle": "^1.15.0",
363
+ "import-in-the-middle": "^2.0.0",
364
364
  "just-diff": "^6.0.2",
365
365
  "lint-staged": "^16.2.6",
366
366
  "lodash": "^4.17.21",
@@ -376,7 +376,7 @@
376
376
  "remark-frontmatter": "^5.0.0",
377
377
  "remark-mdx": "^3.1.1",
378
378
  "remark-parse": "^11.0.0",
379
- "require-in-the-middle": "^7.5.2",
379
+ "require-in-the-middle": "^8.0.1",
380
380
  "semantic-release": "^21.1.2",
381
381
  "serwist": "^9.2.1",
382
382
  "stylelint": "^15.11.0",
@@ -18,8 +18,6 @@
18
18
  "@opentelemetry/sdk-node": "^0.207.0",
19
19
  "@opentelemetry/sdk-trace-node": "^2.0.1",
20
20
  "@opentelemetry/semantic-conventions": "^1.36.0",
21
- "@vercel/otel": "^1.13.0",
22
- "import-in-the-middle": "^1.14.2",
23
- "require-in-the-middle": "^7.5.2"
21
+ "@vercel/otel": "^1.13.0"
24
22
  }
25
23
  }
@@ -9,9 +9,7 @@ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
9
9
  import { NodeSDK } from '@opentelemetry/sdk-node';
10
10
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
11
11
 
12
-
13
-
14
- export function register(options?: { debug?: true | DiagLogLevel, version?: string; }) {
12
+ export function register(options?: { debug?: true | DiagLogLevel; version?: string }) {
15
13
  const attributes: Record<string, string> = {
16
14
  [ATTR_SERVICE_NAME]: 'lobe-chat',
17
15
  };
@@ -31,9 +29,7 @@ export function register(options?: { debug?: true | DiagLogLevel, version?: stri
31
29
  new HttpInstrumentation(),
32
30
  getNodeAutoInstrumentations(),
33
31
  ],
34
- metricReader: new PeriodicExportingMetricReader({
35
- exporter: new OTLPMetricExporter(),
36
- }),
32
+ metricReaders: [new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter() })],
37
33
  resource: resourceFromAttributes(attributes),
38
34
  traceExporter: new OTLPTraceExporter(),
39
35
  });
@@ -41,4 +37,4 @@ export function register(options?: { debug?: true | DiagLogLevel, version?: stri
41
37
  sdk.start();
42
38
  }
43
39
 
44
- export {DiagLogLevel} from '@opentelemetry/api';
40
+ export { DiagLogLevel } from '@opentelemetry/api';
@@ -0,0 +1,379 @@
1
+ // @vitest-environment node
2
+ import {
3
+ DeleteObjectCommand,
4
+ DeleteObjectsCommand,
5
+ GetObjectCommand,
6
+ PutObjectCommand,
7
+ S3Client,
8
+ } from '@aws-sdk/client-s3';
9
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
+
12
+ import { S3 } from './index';
13
+
14
+ // Mock AWS SDK
15
+ vi.mock('@aws-sdk/client-s3');
16
+ vi.mock('@aws-sdk/s3-request-presigner');
17
+
18
+ // Mock environment variables
19
+ vi.mock('@/envs/file', () => ({
20
+ fileEnv: {
21
+ S3_ACCESS_KEY_ID: 'test-access-key',
22
+ S3_BUCKET: 'test-bucket',
23
+ S3_ENABLE_PATH_STYLE: false,
24
+ S3_ENDPOINT: 'https://s3.amazonaws.com',
25
+ S3_PREVIEW_URL_EXPIRE_IN: 7200,
26
+ S3_REGION: 'us-east-1',
27
+ S3_SECRET_ACCESS_KEY: 'test-secret-key',
28
+ S3_SET_ACL: true,
29
+ },
30
+ }));
31
+
32
+ // Mock utilities
33
+ vi.mock('@/utils/url', () => ({
34
+ inferContentTypeFromImageUrl: vi.fn((key: string) => {
35
+ if (key.endsWith('.jpg') || key.endsWith('.jpeg')) return 'image/jpeg';
36
+ if (key.endsWith('.png')) return 'image/png';
37
+ if (key.endsWith('.gif')) return 'image/gif';
38
+ return 'application/octet-stream';
39
+ }),
40
+ }));
41
+
42
+ describe('S3', () => {
43
+ let mockS3ClientSend: ReturnType<typeof vi.fn>;
44
+ let mockGetSignedUrl: ReturnType<typeof vi.fn>;
45
+
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+
49
+ // Setup S3Client mock
50
+ mockS3ClientSend = vi.fn();
51
+ (S3Client as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
52
+ send: mockS3ClientSend,
53
+ }));
54
+
55
+ // Setup getSignedUrl mock
56
+ mockGetSignedUrl = vi.fn().mockResolvedValue('https://presigned-url.example.com');
57
+ (getSignedUrl as unknown as ReturnType<typeof vi.fn>).mockImplementation(mockGetSignedUrl);
58
+ });
59
+
60
+ describe('constructor', () => {
61
+ it('should initialize S3 client with correct configuration', () => {
62
+ new S3();
63
+
64
+ expect(S3Client).toHaveBeenCalledWith({
65
+ credentials: {
66
+ accessKeyId: 'test-access-key',
67
+ secretAccessKey: 'test-secret-key',
68
+ },
69
+ endpoint: 'https://s3.amazonaws.com',
70
+ forcePathStyle: false,
71
+ region: 'us-east-1',
72
+ requestChecksumCalculation: 'WHEN_REQUIRED',
73
+ responseChecksumValidation: 'WHEN_REQUIRED',
74
+ });
75
+ });
76
+
77
+ it('should use default region when S3_REGION is not set', () => {
78
+ vi.doMock('@/envs/file', () => ({
79
+ fileEnv: {
80
+ S3_ACCESS_KEY_ID: 'test-access-key',
81
+ S3_BUCKET: 'test-bucket',
82
+ S3_ENABLE_PATH_STYLE: false,
83
+ S3_ENDPOINT: 'https://s3.amazonaws.com',
84
+ S3_PREVIEW_URL_EXPIRE_IN: 7200,
85
+ S3_REGION: '',
86
+ S3_SECRET_ACCESS_KEY: 'test-secret-key',
87
+ S3_SET_ACL: true,
88
+ },
89
+ }));
90
+
91
+ new S3();
92
+
93
+ expect(S3Client).toHaveBeenCalledWith(
94
+ expect.objectContaining({
95
+ region: 'us-east-1',
96
+ }),
97
+ );
98
+ });
99
+ });
100
+
101
+ describe('deleteFile', () => {
102
+ it('should delete a file with the correct parameters', async () => {
103
+ const s3 = new S3();
104
+ mockS3ClientSend.mockResolvedValue({});
105
+
106
+ await s3.deleteFile('test-key.txt');
107
+
108
+ expect(DeleteObjectCommand).toHaveBeenCalledWith({
109
+ Bucket: 'test-bucket',
110
+ Key: 'test-key.txt',
111
+ });
112
+ expect(mockS3ClientSend).toHaveBeenCalled();
113
+ });
114
+
115
+ it('should handle deletion errors', async () => {
116
+ const s3 = new S3();
117
+ const error = new Error('Delete failed');
118
+ mockS3ClientSend.mockRejectedValue(error);
119
+
120
+ await expect(s3.deleteFile('test-key.txt')).rejects.toThrow('Delete failed');
121
+ });
122
+ });
123
+
124
+ describe('deleteFiles', () => {
125
+ it('should delete multiple files with correct parameters', async () => {
126
+ const s3 = new S3();
127
+ mockS3ClientSend.mockResolvedValue({});
128
+
129
+ const keys = ['file1.txt', 'file2.txt', 'file3.txt'];
130
+ await s3.deleteFiles(keys);
131
+
132
+ expect(DeleteObjectsCommand).toHaveBeenCalledWith({
133
+ Bucket: 'test-bucket',
134
+ Delete: {
135
+ Objects: [{ Key: 'file1.txt' }, { Key: 'file2.txt' }, { Key: 'file3.txt' }],
136
+ },
137
+ });
138
+ expect(mockS3ClientSend).toHaveBeenCalled();
139
+ });
140
+
141
+ it('should handle empty array', async () => {
142
+ const s3 = new S3();
143
+ mockS3ClientSend.mockResolvedValue({});
144
+
145
+ await s3.deleteFiles([]);
146
+
147
+ expect(DeleteObjectsCommand).toHaveBeenCalledWith({
148
+ Bucket: 'test-bucket',
149
+ Delete: {
150
+ Objects: [],
151
+ },
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('getFileContent', () => {
157
+ it('should retrieve file content as string', async () => {
158
+ const s3 = new S3();
159
+ const mockContent = 'Hello, World!';
160
+ mockS3ClientSend.mockResolvedValue({
161
+ Body: {
162
+ transformToString: vi.fn().mockResolvedValue(mockContent),
163
+ },
164
+ });
165
+
166
+ const result = await s3.getFileContent('test-file.txt');
167
+
168
+ expect(GetObjectCommand).toHaveBeenCalledWith({
169
+ Bucket: 'test-bucket',
170
+ Key: 'test-file.txt',
171
+ });
172
+ expect(result).toBe(mockContent);
173
+ });
174
+
175
+ it('should throw error when response body is missing', async () => {
176
+ const s3 = new S3();
177
+ mockS3ClientSend.mockResolvedValue({
178
+ Body: undefined,
179
+ });
180
+
181
+ await expect(s3.getFileContent('test-file.txt')).rejects.toThrow(
182
+ 'No body in response with test-file.txt',
183
+ );
184
+ });
185
+ });
186
+
187
+ describe('getFileByteArray', () => {
188
+ it('should retrieve file content as byte array', async () => {
189
+ const s3 = new S3();
190
+ const mockBytes = new Uint8Array([1, 2, 3, 4, 5]);
191
+ mockS3ClientSend.mockResolvedValue({
192
+ Body: {
193
+ transformToByteArray: vi.fn().mockResolvedValue(mockBytes),
194
+ },
195
+ });
196
+
197
+ const result = await s3.getFileByteArray('test-file.bin');
198
+
199
+ expect(GetObjectCommand).toHaveBeenCalledWith({
200
+ Bucket: 'test-bucket',
201
+ Key: 'test-file.bin',
202
+ });
203
+ expect(result).toEqual(mockBytes);
204
+ });
205
+
206
+ it('should throw error when response body is missing', async () => {
207
+ const s3 = new S3();
208
+ mockS3ClientSend.mockResolvedValue({
209
+ Body: undefined,
210
+ });
211
+
212
+ await expect(s3.getFileByteArray('test-file.bin')).rejects.toThrow(
213
+ 'No body in response with test-file.bin',
214
+ );
215
+ });
216
+ });
217
+
218
+ describe('createPreSignedUrl', () => {
219
+ it('should create presigned URL for upload with ACL', async () => {
220
+ const s3 = new S3();
221
+
222
+ const result = await s3.createPreSignedUrl('upload-file.txt');
223
+
224
+ expect(PutObjectCommand).toHaveBeenCalledWith({
225
+ ACL: 'public-read',
226
+ Bucket: 'test-bucket',
227
+ Key: 'upload-file.txt',
228
+ });
229
+ expect(mockGetSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
230
+ expiresIn: 3600,
231
+ });
232
+ expect(result).toBe('https://presigned-url.example.com');
233
+ });
234
+ });
235
+
236
+ describe('createPreSignedUrlForPreview', () => {
237
+ it('should create presigned URL for preview with default expiration', async () => {
238
+ const s3 = new S3();
239
+
240
+ const result = await s3.createPreSignedUrlForPreview('preview-file.jpg');
241
+
242
+ expect(GetObjectCommand).toHaveBeenCalledWith({
243
+ Bucket: 'test-bucket',
244
+ Key: 'preview-file.jpg',
245
+ });
246
+ expect(mockGetSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
247
+ expiresIn: 7200,
248
+ });
249
+ expect(result).toBe('https://presigned-url.example.com');
250
+ });
251
+
252
+ it('should create presigned URL for preview with custom expiration', async () => {
253
+ const s3 = new S3();
254
+
255
+ await s3.createPreSignedUrlForPreview('preview-file.jpg', 1800);
256
+
257
+ expect(mockGetSignedUrl).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
258
+ expiresIn: 1800,
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('uploadBuffer', () => {
264
+ it('should upload buffer with correct parameters', async () => {
265
+ const s3 = new S3();
266
+ mockS3ClientSend.mockResolvedValue({});
267
+
268
+ const buffer = Buffer.from('test data');
269
+ await s3.uploadBuffer('test-file.bin', buffer, 'application/octet-stream');
270
+
271
+ expect(PutObjectCommand).toHaveBeenCalledWith({
272
+ ACL: 'public-read',
273
+ Body: buffer,
274
+ Bucket: 'test-bucket',
275
+ ContentType: 'application/octet-stream',
276
+ Key: 'test-file.bin',
277
+ });
278
+ expect(mockS3ClientSend).toHaveBeenCalled();
279
+ });
280
+
281
+ it('should upload buffer without content type', async () => {
282
+ const s3 = new S3();
283
+ mockS3ClientSend.mockResolvedValue({});
284
+
285
+ const buffer = Buffer.from('test data');
286
+ await s3.uploadBuffer('test-file.bin', buffer);
287
+
288
+ expect(PutObjectCommand).toHaveBeenCalledWith({
289
+ ACL: 'public-read',
290
+ Body: buffer,
291
+ Bucket: 'test-bucket',
292
+ ContentType: undefined,
293
+ Key: 'test-file.bin',
294
+ });
295
+ });
296
+ });
297
+
298
+ describe('uploadContent', () => {
299
+ it('should upload string content with correct parameters', async () => {
300
+ const s3 = new S3();
301
+ mockS3ClientSend.mockResolvedValue({});
302
+
303
+ const content = 'Hello, World!';
304
+ await s3.uploadContent('test-file.txt', content);
305
+
306
+ expect(PutObjectCommand).toHaveBeenCalledWith({
307
+ ACL: 'public-read',
308
+ Body: content,
309
+ Bucket: 'test-bucket',
310
+ Key: 'test-file.txt',
311
+ });
312
+ expect(mockS3ClientSend).toHaveBeenCalled();
313
+ });
314
+
315
+ it('should handle empty content', async () => {
316
+ const s3 = new S3();
317
+ mockS3ClientSend.mockResolvedValue({});
318
+
319
+ await s3.uploadContent('empty.txt', '');
320
+
321
+ expect(PutObjectCommand).toHaveBeenCalledWith({
322
+ ACL: 'public-read',
323
+ Body: '',
324
+ Bucket: 'test-bucket',
325
+ Key: 'empty.txt',
326
+ });
327
+ });
328
+ });
329
+
330
+ describe('uploadMedia', () => {
331
+ it('should upload media with correct content type and cache control for JPEG', async () => {
332
+ const s3 = new S3();
333
+ mockS3ClientSend.mockResolvedValue({});
334
+
335
+ const buffer = Buffer.from('fake image data');
336
+ await s3.uploadMedia('image.jpg', buffer);
337
+
338
+ expect(PutObjectCommand).toHaveBeenCalledWith({
339
+ ACL: 'public-read',
340
+ Body: buffer,
341
+ Bucket: 'test-bucket',
342
+ CacheControl: expect.stringContaining('public, max-age='),
343
+ ContentType: 'image/jpeg',
344
+ Key: 'image.jpg',
345
+ });
346
+ expect(mockS3ClientSend).toHaveBeenCalled();
347
+ });
348
+
349
+ it('should upload media with correct content type for PNG', async () => {
350
+ const s3 = new S3();
351
+ mockS3ClientSend.mockResolvedValue({});
352
+
353
+ const buffer = Buffer.from('fake image data');
354
+ await s3.uploadMedia('image.png', buffer);
355
+
356
+ expect(PutObjectCommand).toHaveBeenCalledWith(
357
+ expect.objectContaining({
358
+ ContentType: 'image/png',
359
+ Key: 'image.png',
360
+ }),
361
+ );
362
+ });
363
+
364
+ it('should upload media with correct content type for GIF', async () => {
365
+ const s3 = new S3();
366
+ mockS3ClientSend.mockResolvedValue({});
367
+
368
+ const buffer = Buffer.from('fake image data');
369
+ await s3.uploadMedia('animation.gif', buffer);
370
+
371
+ expect(PutObjectCommand).toHaveBeenCalledWith(
372
+ expect.objectContaining({
373
+ ContentType: 'image/gif',
374
+ Key: 'animation.gif',
375
+ }),
376
+ );
377
+ });
378
+ });
379
+ });