@lobehub/chat 1.104.1 → 1.104.3

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,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.104.3](https://github.com/lobehub/lobe-chat/compare/v1.104.2...v1.104.3)
6
+
7
+ <sup>Released on **2025-07-26**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Add Gemini 2.5 Flash-Lite GA model.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Add Gemini 2.5 Flash-Lite GA model, closes [#8539](https://github.com/lobehub/lobe-chat/issues/8539) ([404ac21](https://github.com/lobehub/lobe-chat/commit/404ac21))
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 1.104.2](https://github.com/lobehub/lobe-chat/compare/v1.104.1...v1.104.2)
31
+
32
+ <sup>Released on **2025-07-26**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix update hotkey invalid when input mod in desktop.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix update hotkey invalid when input mod in desktop, closes [#8572](https://github.com/lobehub/lobe-chat/issues/8572) ([07f3e6a](https://github.com/lobehub/lobe-chat/commit/07f3e6a))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.104.1](https://github.com/lobehub/lobe-chat/compare/v1.104.0...v1.104.1)
6
56
 
7
57
  <sup>Released on **2025-07-25**</sup>
@@ -33,6 +33,28 @@ export class ShortcutManager {
33
33
  });
34
34
  }
35
35
 
36
+ /**
37
+ * Convert react-hotkey format to Electron accelerator format
38
+ * @param accelerator The accelerator string from frontend
39
+ * @returns Converted accelerator string for Electron
40
+ */
41
+ private convertAcceleratorFormat(accelerator: string): string {
42
+ return accelerator
43
+ .split('+')
44
+ .map((key) => {
45
+ const trimmedKey = key.trim().toLowerCase();
46
+
47
+ // Convert react-hotkey 'mod' to Electron 'CommandOrControl'
48
+ if (trimmedKey === 'mod') {
49
+ return 'CommandOrControl';
50
+ }
51
+
52
+ // Keep other keys as is, but preserve proper casing
53
+ return key.trim().length === 1 ? key.trim().toUpperCase() : key.trim();
54
+ })
55
+ .join('+');
56
+ }
57
+
36
58
  initialize() {
37
59
  logger.info('Initializing global shortcuts');
38
60
  // Load shortcuts configuration from storage
@@ -67,7 +89,11 @@ export class ShortcutManager {
67
89
  return { errorType: 'INVALID_FORMAT', success: false };
68
90
  }
69
91
 
70
- const cleanAccelerator = accelerator.trim().toLowerCase();
92
+ // 转换前端格式到 Electron 格式
93
+ const convertedAccelerator = this.convertAcceleratorFormat(accelerator.trim());
94
+ const cleanAccelerator = convertedAccelerator.toLowerCase();
95
+
96
+ logger.debug(`Converted accelerator from ${accelerator} to ${convertedAccelerator}`);
71
97
 
72
98
  // 3. 检查是否包含 + 号(修饰键格式)
73
99
  if (!cleanAccelerator.includes('+')) {
@@ -100,17 +126,19 @@ export class ShortcutManager {
100
126
  }
101
127
 
102
128
  // 6. 尝试注册测试(检查是否被系统占用)
103
- const testSuccess = globalShortcut.register(cleanAccelerator, () => {});
129
+ const testSuccess = globalShortcut.register(convertedAccelerator, () => {});
104
130
  if (!testSuccess) {
105
- logger.error(`Shortcut ${cleanAccelerator} is already registered by system or other app`);
131
+ logger.error(
132
+ `Shortcut ${convertedAccelerator} is already registered by system or other app`,
133
+ );
106
134
  return { errorType: 'SYSTEM_OCCUPIED', success: false };
107
135
  } else {
108
136
  // 测试成功,立即取消注册
109
- globalShortcut.unregister(cleanAccelerator);
137
+ globalShortcut.unregister(convertedAccelerator);
110
138
  }
111
139
 
112
140
  // 7. 更新配置
113
- this.shortcutsConfig[id] = cleanAccelerator;
141
+ this.shortcutsConfig[id] = convertedAccelerator;
114
142
 
115
143
  this.saveShortcutsConfig();
116
144
  this.registerConfiguredShortcuts();
@@ -196,7 +224,34 @@ export class ShortcutManager {
196
224
  this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
197
225
  this.saveShortcutsConfig();
198
226
  } else {
199
- this.shortcutsConfig = config;
227
+ // Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
228
+ const filteredConfig: Record<string, string> = {};
229
+ let hasInvalidKeys = false;
230
+
231
+ Object.entries(config).forEach(([id, accelerator]) => {
232
+ if (DEFAULT_SHORTCUTS_CONFIG[id]) {
233
+ filteredConfig[id] = accelerator;
234
+ } else {
235
+ hasInvalidKeys = true;
236
+ logger.debug(`Filtering out invalid shortcut ID: ${id}`);
237
+ }
238
+ });
239
+
240
+ // Ensure all default shortcuts are present
241
+ Object.entries(DEFAULT_SHORTCUTS_CONFIG).forEach(([id, defaultAccelerator]) => {
242
+ if (!(id in filteredConfig)) {
243
+ filteredConfig[id] = defaultAccelerator;
244
+ logger.debug(`Adding missing default shortcut: ${id} = ${defaultAccelerator}`);
245
+ }
246
+ });
247
+
248
+ this.shortcutsConfig = filteredConfig;
249
+
250
+ // Save the filtered configuration back to storage if we removed invalid keys
251
+ if (hasInvalidKeys) {
252
+ logger.debug('Saving filtered shortcuts config to remove invalid keys');
253
+ this.saveShortcutsConfig();
254
+ }
200
255
  }
201
256
 
202
257
  logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
@@ -0,0 +1,539 @@
1
+ import { globalShortcut } from 'electron';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
5
+
6
+ import type { App } from '../../App';
7
+ import { ShortcutManager } from '../ShortcutManager';
8
+
9
+ // Mock electron
10
+ vi.mock('electron', () => ({
11
+ globalShortcut: {
12
+ register: vi.fn(),
13
+ unregister: vi.fn(),
14
+ unregisterAll: vi.fn(),
15
+ isRegistered: vi.fn(),
16
+ },
17
+ }));
18
+
19
+ // Mock Logger
20
+ vi.mock('@/utils/logger', () => ({
21
+ createLogger: () => ({
22
+ debug: vi.fn(),
23
+ info: vi.fn(),
24
+ warn: vi.fn(),
25
+ error: vi.fn(),
26
+ }),
27
+ }));
28
+
29
+ // Mock DEFAULT_SHORTCUTS_CONFIG
30
+ vi.mock('@/shortcuts', () => ({
31
+ DEFAULT_SHORTCUTS_CONFIG: {
32
+ showApp: 'Control+E',
33
+ openSettings: 'CommandOrControl+,',
34
+ },
35
+ }));
36
+
37
+ describe('ShortcutManager', () => {
38
+ let shortcutManager: ShortcutManager;
39
+ let mockApp: App;
40
+ let mockStoreManager: any;
41
+ let mockShortcutMethodMap: Map<string, () => void>;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+
46
+ // Reset all mocks to their default behavior
47
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
48
+ vi.mocked(globalShortcut.unregister).mockReturnValue(undefined);
49
+ vi.mocked(globalShortcut.unregisterAll).mockReturnValue(undefined);
50
+ vi.mocked(globalShortcut.isRegistered).mockReturnValue(false);
51
+
52
+ // Mock store manager
53
+ mockStoreManager = {
54
+ get: vi.fn(),
55
+ set: vi.fn(),
56
+ };
57
+
58
+ // Mock shortcut method map
59
+ mockShortcutMethodMap = new Map();
60
+ const showAppMethod = vi.fn();
61
+ const openSettingsMethod = vi.fn();
62
+ mockShortcutMethodMap.set('showApp', showAppMethod);
63
+ mockShortcutMethodMap.set('openSettings', openSettingsMethod);
64
+
65
+ // Mock App
66
+ mockApp = {
67
+ storeManager: mockStoreManager,
68
+ shortcutMethodMap: mockShortcutMethodMap,
69
+ } as unknown as App;
70
+
71
+ shortcutManager = new ShortcutManager(mockApp);
72
+ });
73
+
74
+ describe('constructor', () => {
75
+ it('should initialize shortcut manager with app', () => {
76
+ expect(shortcutManager).toBeDefined();
77
+ expect(shortcutManager['app']).toBe(mockApp);
78
+ });
79
+
80
+ it('should populate shortcuts map from app shortcut method map', () => {
81
+ expect(shortcutManager['shortcuts'].size).toBe(2);
82
+ expect(shortcutManager['shortcuts'].has('showApp')).toBe(true);
83
+ expect(shortcutManager['shortcuts'].has('openSettings')).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('convertAcceleratorFormat', () => {
88
+ it('should convert mod to CommandOrControl', () => {
89
+ const result = shortcutManager['convertAcceleratorFormat']('mod+e');
90
+ expect(result).toBe('CommandOrControl+E');
91
+ });
92
+
93
+ it('should preserve other keys as is except single characters', () => {
94
+ const result = shortcutManager['convertAcceleratorFormat']('ctrl+alt+f12');
95
+ expect(result).toBe('ctrl+alt+f12');
96
+ });
97
+
98
+ it('should handle single character keys with uppercase', () => {
99
+ const result = shortcutManager['convertAcceleratorFormat']('ctrl + a');
100
+ expect(result).toBe('ctrl+A');
101
+ });
102
+
103
+ it('should handle complex combinations', () => {
104
+ const result = shortcutManager['convertAcceleratorFormat']('mod+shift+delete');
105
+ expect(result).toBe('CommandOrControl+shift+delete');
106
+ });
107
+ });
108
+
109
+ describe('initialize', () => {
110
+ it('should load shortcuts config and register shortcuts', () => {
111
+ // Mock store to return empty config (will use defaults)
112
+ mockStoreManager.get.mockReturnValue({});
113
+
114
+ shortcutManager.initialize();
115
+
116
+ expect(mockStoreManager.get).toHaveBeenCalledWith('shortcuts');
117
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
118
+ expect(globalShortcut.register).toHaveBeenCalledWith('Control+E', expect.any(Function));
119
+ expect(globalShortcut.register).toHaveBeenCalledWith(
120
+ 'CommandOrControl+,',
121
+ expect.any(Function),
122
+ );
123
+ });
124
+
125
+ it('should handle stored config with filtering', () => {
126
+ const storedConfig = {
127
+ showApp: 'Alt+E',
128
+ openSettings: 'Ctrl+Shift+P',
129
+ invalidKey: 'Ctrl+I', // Should be filtered out
130
+ };
131
+ mockStoreManager.get.mockReturnValue(storedConfig);
132
+
133
+ shortcutManager.initialize();
134
+
135
+ const config = shortcutManager.getShortcutsConfig();
136
+ expect(config.showApp).toBe('Alt+E');
137
+ expect(config.openSettings).toBe('Ctrl+Shift+P');
138
+ expect(config.invalidKey).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ describe('getShortcutsConfig', () => {
143
+ it('should return current shortcuts configuration', () => {
144
+ mockStoreManager.get.mockReturnValue({});
145
+ shortcutManager.initialize();
146
+
147
+ const config = shortcutManager.getShortcutsConfig();
148
+ expect(config).toEqual(DEFAULT_SHORTCUTS_CONFIG);
149
+ });
150
+ });
151
+
152
+ describe('updateShortcutConfig', () => {
153
+ beforeEach(() => {
154
+ mockStoreManager.get.mockReturnValue({});
155
+ shortcutManager.initialize();
156
+ });
157
+
158
+ it('should successfully update valid shortcut', () => {
159
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
160
+
161
+ expect(result.success).toBe(true);
162
+ expect(result.errorType).toBeUndefined();
163
+ expect(mockStoreManager.set).toHaveBeenCalledWith(
164
+ 'shortcuts',
165
+ expect.objectContaining({
166
+ showApp: 'Alt+E',
167
+ }),
168
+ );
169
+ });
170
+
171
+ it('should reject invalid shortcut ID', () => {
172
+ const result = shortcutManager.updateShortcutConfig('invalidId', 'Alt+E');
173
+
174
+ expect(result.success).toBe(false);
175
+ expect(result.errorType).toBe('INVALID_ID');
176
+ });
177
+
178
+ it('should reject empty accelerator', () => {
179
+ const result = shortcutManager.updateShortcutConfig('showApp', '');
180
+
181
+ expect(result.success).toBe(false);
182
+ expect(result.errorType).toBe('INVALID_FORMAT');
183
+ });
184
+
185
+ it('should reject accelerator without modifier keys', () => {
186
+ const result = shortcutManager.updateShortcutConfig('showApp', 'E');
187
+
188
+ expect(result.success).toBe(false);
189
+ expect(result.errorType).toBe('INVALID_FORMAT');
190
+ });
191
+
192
+ it('should reject accelerator without proper modifiers', () => {
193
+ const result = shortcutManager.updateShortcutConfig('showApp', 'F1+E');
194
+
195
+ expect(result.success).toBe(false);
196
+ expect(result.errorType).toBe('NO_MODIFIER');
197
+ });
198
+
199
+ it('should detect conflicts with existing shortcuts', () => {
200
+ // First set a shortcut
201
+ shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
202
+
203
+ // Try to set the same accelerator for another shortcut
204
+ const result = shortcutManager.updateShortcutConfig('openSettings', 'Alt+E');
205
+
206
+ expect(result.success).toBe(false);
207
+ expect(result.errorType).toBe('CONFLICT');
208
+ });
209
+
210
+ it('should detect system occupied shortcuts', () => {
211
+ vi.mocked(globalShortcut.register).mockReturnValue(false);
212
+
213
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
214
+
215
+ expect(result.success).toBe(false);
216
+ expect(result.errorType).toBe('SYSTEM_OCCUPIED');
217
+ });
218
+
219
+ it('should handle registration test cleanup', () => {
220
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
221
+
222
+ shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
223
+
224
+ // Should unregister the test registration
225
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+Alt+T');
226
+ });
227
+
228
+ it('should handle conversion from react-hotkey format', () => {
229
+ const result = shortcutManager.updateShortcutConfig('showApp', 'mod+shift+e');
230
+
231
+ expect(result.success).toBe(true);
232
+ const config = shortcutManager.getShortcutsConfig();
233
+ expect(config.showApp).toBe('CommandOrControl+shift+E');
234
+ });
235
+
236
+ it('should handle errors gracefully', () => {
237
+ // Mock globalShortcut.register to throw an error during testing
238
+ vi.mocked(globalShortcut.register).mockImplementation(() => {
239
+ throw new Error('Register error');
240
+ });
241
+
242
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
243
+
244
+ expect(result.success).toBe(false);
245
+ expect(result.errorType).toBe('UNKNOWN');
246
+ });
247
+ });
248
+
249
+ describe('registerShortcut', () => {
250
+ it('should register new shortcut successfully', () => {
251
+ const callback = vi.fn();
252
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
253
+
254
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
255
+
256
+ expect(result).toBe(true);
257
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback);
258
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(true);
259
+ });
260
+
261
+ it('should unregister existing shortcut before registering new one', () => {
262
+ const callback1 = vi.fn();
263
+ const callback2 = vi.fn();
264
+
265
+ // First registration
266
+ shortcutManager['shortcuts'].set('Ctrl+T', callback1);
267
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
268
+
269
+ shortcutManager.registerShortcut('Ctrl+T', callback2);
270
+
271
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
272
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback2);
273
+ });
274
+
275
+ it('should handle registration failure', () => {
276
+ const callback = vi.fn();
277
+ vi.mocked(globalShortcut.register).mockReturnValue(false);
278
+
279
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
280
+
281
+ expect(result).toBe(false);
282
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
283
+ });
284
+
285
+ it('should handle registration errors', () => {
286
+ const callback = vi.fn();
287
+ vi.mocked(globalShortcut.register).mockImplementation(() => {
288
+ throw new Error('Registration error');
289
+ });
290
+
291
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
292
+
293
+ expect(result).toBe(false);
294
+ });
295
+ });
296
+
297
+ describe('unregisterShortcut', () => {
298
+ it('should unregister shortcut successfully', () => {
299
+ const callback = vi.fn();
300
+ shortcutManager['shortcuts'].set('Ctrl+T', callback);
301
+
302
+ shortcutManager.unregisterShortcut('Ctrl+T');
303
+
304
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
305
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
306
+ });
307
+
308
+ it('should handle unregistration errors', () => {
309
+ vi.mocked(globalShortcut.unregister).mockImplementation(() => {
310
+ throw new Error('Unregister error');
311
+ });
312
+
313
+ // Should not throw
314
+ expect(() => shortcutManager.unregisterShortcut('Ctrl+T')).not.toThrow();
315
+ });
316
+ });
317
+
318
+ describe('isRegistered', () => {
319
+ it('should check if shortcut is registered', () => {
320
+ vi.mocked(globalShortcut.isRegistered).mockReturnValue(true);
321
+
322
+ const result = shortcutManager.isRegistered('Ctrl+T');
323
+
324
+ expect(result).toBe(true);
325
+ expect(globalShortcut.isRegistered).toHaveBeenCalledWith('Ctrl+T');
326
+ });
327
+ });
328
+
329
+ describe('unregisterAll', () => {
330
+ it('should unregister all shortcuts', () => {
331
+ shortcutManager.unregisterAll();
332
+
333
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
334
+ });
335
+ });
336
+
337
+ describe('loadShortcutsConfig', () => {
338
+ it('should use defaults when no config exists', () => {
339
+ mockStoreManager.get.mockReturnValue(null);
340
+
341
+ shortcutManager['loadShortcutsConfig']();
342
+
343
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
344
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
345
+ });
346
+
347
+ it('should use defaults when config is empty', () => {
348
+ mockStoreManager.get.mockReturnValue({});
349
+
350
+ shortcutManager['loadShortcutsConfig']();
351
+
352
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
353
+ });
354
+
355
+ it('should filter invalid keys from stored config', () => {
356
+ const storedConfig = {
357
+ showApp: 'Alt+E',
358
+ openSettings: 'Ctrl+P',
359
+ invalidKey1: 'Ctrl+I',
360
+ invalidKey2: 'Ctrl+J',
361
+ };
362
+ mockStoreManager.get.mockReturnValue(storedConfig);
363
+
364
+ shortcutManager['loadShortcutsConfig']();
365
+
366
+ const config = shortcutManager['shortcutsConfig'];
367
+ expect(config.showApp).toBe('Alt+E');
368
+ expect(config.openSettings).toBe('Ctrl+P');
369
+ expect(config.invalidKey1).toBeUndefined();
370
+ expect(config.invalidKey2).toBeUndefined();
371
+
372
+ // Should save filtered config
373
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
374
+ });
375
+
376
+ it('should add missing default shortcuts', () => {
377
+ const incompleteConfig = {
378
+ showApp: 'Alt+E',
379
+ // Missing openSettings
380
+ };
381
+ mockStoreManager.get.mockReturnValue(incompleteConfig);
382
+
383
+ shortcutManager['loadShortcutsConfig']();
384
+
385
+ const config = shortcutManager['shortcutsConfig'];
386
+ expect(config.showApp).toBe('Alt+E');
387
+ expect(config.openSettings).toBe('CommandOrControl+,'); // Default value
388
+ });
389
+
390
+ it('should not save config if no invalid keys were found', () => {
391
+ const validConfig = {
392
+ showApp: 'Alt+E',
393
+ openSettings: 'Ctrl+P',
394
+ };
395
+ mockStoreManager.get.mockReturnValue(validConfig);
396
+
397
+ shortcutManager['loadShortcutsConfig']();
398
+
399
+ // Should not call set since no changes were made
400
+ expect(mockStoreManager.set).not.toHaveBeenCalled();
401
+ });
402
+
403
+ it('should handle store errors gracefully', () => {
404
+ mockStoreManager.get.mockImplementation(() => {
405
+ throw new Error('Store error');
406
+ });
407
+
408
+ shortcutManager['loadShortcutsConfig']();
409
+
410
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
411
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
412
+ });
413
+ });
414
+
415
+ describe('saveShortcutsConfig', () => {
416
+ it('should save shortcuts config to store', () => {
417
+ shortcutManager['shortcutsConfig'] = { showApp: 'Alt+E', openSettings: 'Ctrl+P' };
418
+
419
+ shortcutManager['saveShortcutsConfig']();
420
+
421
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', {
422
+ showApp: 'Alt+E',
423
+ openSettings: 'Ctrl+P',
424
+ });
425
+ });
426
+
427
+ it('should handle save errors gracefully', () => {
428
+ mockStoreManager.set.mockImplementation(() => {
429
+ throw new Error('Save error');
430
+ });
431
+
432
+ // Should not throw
433
+ expect(() => shortcutManager['saveShortcutsConfig']()).not.toThrow();
434
+ });
435
+ });
436
+
437
+ describe('registerConfiguredShortcuts', () => {
438
+ beforeEach(() => {
439
+ shortcutManager['shortcutsConfig'] = {
440
+ showApp: 'Alt+E',
441
+ openSettings: 'Ctrl+P',
442
+ };
443
+ });
444
+
445
+ it('should register all configured shortcuts', () => {
446
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
447
+
448
+ shortcutManager['registerConfiguredShortcuts']();
449
+
450
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
451
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
452
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
453
+ });
454
+
455
+ it('should skip shortcuts not in DEFAULT_SHORTCUTS_CONFIG', () => {
456
+ shortcutManager['shortcutsConfig'] = {
457
+ showApp: 'Alt+E',
458
+ invalidKey: 'Ctrl+I',
459
+ };
460
+
461
+ shortcutManager['registerConfiguredShortcuts']();
462
+
463
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
464
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+I', expect.any(Function));
465
+ });
466
+
467
+ it('should skip shortcuts with empty accelerator', () => {
468
+ shortcutManager['shortcutsConfig'] = {
469
+ showApp: '',
470
+ openSettings: 'Ctrl+P',
471
+ };
472
+
473
+ shortcutManager['registerConfiguredShortcuts']();
474
+
475
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('', expect.any(Function));
476
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
477
+ });
478
+
479
+ it('should skip shortcuts without corresponding methods', () => {
480
+ // Remove method from map
481
+ mockShortcutMethodMap.delete('openSettings');
482
+ shortcutManager = new ShortcutManager(mockApp);
483
+ shortcutManager['shortcutsConfig'] = {
484
+ showApp: 'Alt+E',
485
+ openSettings: 'Ctrl+P',
486
+ };
487
+
488
+ shortcutManager['registerConfiguredShortcuts']();
489
+
490
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
491
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
492
+ });
493
+ });
494
+
495
+ describe('integration tests', () => {
496
+ it('should complete full initialization flow', () => {
497
+ const storedConfig = {
498
+ showApp: 'Alt+E',
499
+ openSettings: 'Ctrl+Shift+P',
500
+ invalidKey: 'Ctrl+I',
501
+ };
502
+ mockStoreManager.get.mockReturnValue(storedConfig);
503
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
504
+
505
+ shortcutManager.initialize();
506
+
507
+ // Should filter config and register valid shortcuts
508
+ const config = shortcutManager.getShortcutsConfig();
509
+ expect(config.showApp).toBe('Alt+E');
510
+ expect(config.openSettings).toBe('Ctrl+Shift+P');
511
+ expect(config.invalidKey).toBeUndefined();
512
+
513
+ expect(globalShortcut.register).toHaveBeenCalledTimes(2);
514
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
515
+ });
516
+
517
+ it('should handle complete update workflow', () => {
518
+ mockStoreManager.get.mockReturnValue({});
519
+ shortcutManager.initialize();
520
+
521
+ // Update a shortcut
522
+ const result = shortcutManager.updateShortcutConfig('showApp', 'mod+alt+e');
523
+
524
+ expect(result.success).toBe(true);
525
+
526
+ // Should convert format and register
527
+ const config = shortcutManager.getShortcutsConfig();
528
+ expect(config.showApp).toBe('CommandOrControl+alt+E');
529
+
530
+ // Should have saved and re-registered shortcuts
531
+ expect(mockStoreManager.set).toHaveBeenCalled();
532
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
533
+ expect(globalShortcut.register).toHaveBeenCalledWith(
534
+ 'CommandOrControl+alt+E',
535
+ expect.any(Function),
536
+ );
537
+ });
538
+ });
539
+ });
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add Gemini 2.5 Flash-Lite GA model."
6
+ ]
7
+ },
8
+ "date": "2025-07-26",
9
+ "version": "1.104.3"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Fix update hotkey invalid when input mod in desktop."
15
+ ]
16
+ },
17
+ "date": "2025-07-26",
18
+ "version": "1.104.2"
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.104.1",
3
+ "version": "1.104.3",
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",
@@ -153,7 +153,7 @@
153
153
  "@lobehub/icons": "^2.17.0",
154
154
  "@lobehub/market-sdk": "^0.22.7",
155
155
  "@lobehub/tts": "^2.0.1",
156
- "@lobehub/ui": "^2.7.4",
156
+ "@lobehub/ui": "^2.7.5",
157
157
  "@modelcontextprotocol/sdk": "^1.16.0",
158
158
  "@neondatabase/serverless": "^1.0.1",
159
159
  "@next/third-parties": "^15.4.3",
@@ -241,7 +241,7 @@
241
241
  "react-fast-marquee": "^1.6.5",
242
242
  "react-hotkeys-hook": "^5.1.0",
243
243
  "react-i18next": "^15.6.1",
244
- "react-layout-kit": "^1.9.2",
244
+ "react-layout-kit": "^2.0.0",
245
245
  "react-lazy-load": "^4.0.1",
246
246
  "react-pdf": "^9.2.1",
247
247
  "react-rnd": "^10.5.2",
@@ -32,6 +32,11 @@ const RootLayout = async ({ children, params, modal }: RootLayoutProps) => {
32
32
 
33
33
  return (
34
34
  <html dir={direction} lang={locale} suppressHydrationWarning>
35
+ <head>
36
+ {process.env.DEBUG_REACT_SCAN === '1' && (
37
+ <script crossOrigin="anonymous" src="https://unpkg.com/react-scan/dist/auto.global.js" />
38
+ )}
39
+ </head>
35
40
  <body>
36
41
  <NuqsAdapter>
37
42
  <GlobalProvider
@@ -17,6 +17,7 @@ const googleChatModels: AIChatModelCard[] = [
17
17
  id: 'gemini-2.5-pro',
18
18
  maxOutput: 65_536,
19
19
  pricing: {
20
+ cachedInput: 0.31, // prompts <= 200k tokens
20
21
  input: 1.25, // prompts <= 200k tokens
21
22
  output: 10, // prompts <= 200k tokens
22
23
  },
@@ -177,6 +178,32 @@ const googleChatModels: AIChatModelCard[] = [
177
178
  },
178
179
  type: 'chat',
179
180
  },
181
+ {
182
+ abilities: {
183
+ functionCall: true,
184
+ reasoning: true,
185
+ search: true,
186
+ vision: true,
187
+ },
188
+ contextWindowTokens: 1_048_576 + 65_536,
189
+ description: 'Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
190
+ displayName: 'Gemini 2.5 Flash-Lite',
191
+ enabled: true,
192
+ id: 'gemini-2.5-flash-lite',
193
+ maxOutput: 65_536,
194
+ pricing: {
195
+ cachedInput: 0.025,
196
+ input: 0.1,
197
+ output: 0.4,
198
+ },
199
+ releasedAt: '2025-07-22',
200
+ settings: {
201
+ extendParams: ['thinkingBudget'],
202
+ searchImpl: 'params',
203
+ searchProvider: 'google',
204
+ },
205
+ type: 'chat',
206
+ },
180
207
  {
181
208
  abilities: {
182
209
  functionCall: true,
@@ -17,6 +17,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
17
17
  id: 'gemini-2.5-pro',
18
18
  maxOutput: 65_536,
19
19
  pricing: {
20
+ cachedInput: 0.31, // prompts <= 200k tokens
20
21
  input: 1.25, // prompts <= 200k tokens
21
22
  output: 10, // prompts <= 200k tokens
22
23
  },
@@ -80,6 +81,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
80
81
  id: 'gemini-2.5-flash',
81
82
  maxOutput: 65_536,
82
83
  pricing: {
84
+ cachedInput: 0.075,
83
85
  input: 0.3,
84
86
  output: 2.5,
85
87
  },
@@ -109,6 +111,31 @@ const vertexaiChatModels: AIChatModelCard[] = [
109
111
  releasedAt: '2025-04-17',
110
112
  type: 'chat',
111
113
  },
114
+ {
115
+ abilities: {
116
+ functionCall: true,
117
+ reasoning: true,
118
+ search: true,
119
+ vision: true,
120
+ },
121
+ contextWindowTokens: 1_000_000 + 64_000,
122
+ description: 'Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。',
123
+ displayName: 'Gemini 2.5 Flash-Lite',
124
+ enabled: true,
125
+ id: 'gemini-2.5-flash-lite',
126
+ maxOutput: 64_000,
127
+ pricing: {
128
+ cachedInput: 0.025,
129
+ input: 0.1,
130
+ output: 0.4,
131
+ },
132
+ releasedAt: '2025-07-22',
133
+ settings: {
134
+ searchImpl: 'params',
135
+ searchProvider: 'google',
136
+ },
137
+ type: 'chat',
138
+ },
112
139
  {
113
140
  abilities: {
114
141
  functionCall: true,
@@ -13,7 +13,6 @@ import AppTheme from './AppTheme';
13
13
  import ImportSettings from './ImportSettings';
14
14
  import Locale from './Locale';
15
15
  import QueryProvider from './Query';
16
- import ReactScan from './ReactScan';
17
16
  import StoreInitialization from './StoreInitialization';
18
17
  import StyleRegistry from './StyleRegistry';
19
18
 
@@ -61,7 +60,6 @@ const GlobalLayout = async ({
61
60
  <StoreInitialization />
62
61
  <Suspense>
63
62
  <ImportSettings />
64
- <ReactScan />
65
63
  {process.env.NODE_ENV === 'development' && <DevPanel />}
66
64
  </Suspense>
67
65
  </ServerConfigStoreProvider>
@@ -1,15 +0,0 @@
1
- 'use client';
2
-
3
- import Script from 'next/script';
4
- import { useQueryState } from 'nuqs';
5
- import React, { memo } from 'react';
6
-
7
- import { withSuspense } from '@/components/withSuspense';
8
-
9
- const ReactScan = memo(() => {
10
- const [debug] = useQueryState('debug', { clearOnDefault: true, defaultValue: '' });
11
-
12
- return !!debug && <Script src="https://unpkg.com/react-scan/dist/auto.global.js" />;
13
- });
14
-
15
- export default withSuspense(ReactScan);