@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 +50 -0
- package/apps/desktop/src/main/core/ui/ShortcutManager.ts +61 -6
- package/apps/desktop/src/main/core/ui/__tests__/ShortcutManager.test.ts +539 -0
- package/changelog/v1.json +18 -0
- package/package.json +3 -3
- package/src/app/[variants]/layout.tsx +5 -0
- package/src/config/aiModels/google.ts +27 -0
- package/src/config/aiModels/vertexai.ts +27 -0
- package/src/layout/GlobalProvider/index.tsx +0 -2
- package/src/layout/GlobalProvider/ReactScan.tsx +0 -15
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
|
+
[](#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
|
+
[](#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
|
-
|
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(
|
129
|
+
const testSuccess = globalShortcut.register(convertedAccelerator, () => {});
|
104
130
|
if (!testSuccess) {
|
105
|
-
logger.error(
|
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(
|
137
|
+
globalShortcut.unregister(convertedAccelerator);
|
110
138
|
}
|
111
139
|
|
112
140
|
// 7. 更新配置
|
113
|
-
this.shortcutsConfig[id] =
|
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
|
-
|
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.
|
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.
|
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": "^
|
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);
|