@lobehub/lobehub 2.0.0-next.141 → 2.0.0-next.142
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 +25 -0
- package/Dockerfile +2 -0
- package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
- package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
- package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
- package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
- package/src/config/modelProviders/vertexai.ts +1 -1
- package/src/envs/llm.ts +4 -0
- package/src/server/modules/ModelRuntime/index.ts +4 -4
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { autoUpdater } from 'electron-updater';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { App as AppCore } from '../../App';
|
|
5
|
+
import { UpdaterManager } from '../UpdaterManager';
|
|
6
|
+
|
|
7
|
+
// Use vi.hoisted to ensure mocks work with require()
|
|
8
|
+
const { mockGetAllWindows, mockReleaseSingleInstanceLock } = vi.hoisted(() => ({
|
|
9
|
+
mockGetAllWindows: vi.fn().mockReturnValue([]),
|
|
10
|
+
mockReleaseSingleInstanceLock: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock electron-log
|
|
14
|
+
vi.mock('electron-log', () => ({
|
|
15
|
+
default: {
|
|
16
|
+
transports: {
|
|
17
|
+
file: {
|
|
18
|
+
level: 'info',
|
|
19
|
+
getFile: vi.fn().mockReturnValue({ path: '/mock/log/path' }),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock electron-updater
|
|
26
|
+
vi.mock('electron-updater', () => ({
|
|
27
|
+
autoUpdater: {
|
|
28
|
+
allowDowngrade: false,
|
|
29
|
+
allowPrerelease: false,
|
|
30
|
+
autoDownload: false,
|
|
31
|
+
autoInstallOnAppQuit: false,
|
|
32
|
+
channel: 'stable',
|
|
33
|
+
checkForUpdates: vi.fn(),
|
|
34
|
+
downloadUpdate: vi.fn(),
|
|
35
|
+
forceDevUpdateConfig: false,
|
|
36
|
+
logger: null as any,
|
|
37
|
+
on: vi.fn(),
|
|
38
|
+
quitAndInstall: vi.fn(),
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// Mock electron - uses hoisted functions for require() compatibility
|
|
43
|
+
vi.mock('electron', () => ({
|
|
44
|
+
BrowserWindow: {
|
|
45
|
+
getAllWindows: mockGetAllWindows,
|
|
46
|
+
},
|
|
47
|
+
app: {
|
|
48
|
+
releaseSingleInstanceLock: mockReleaseSingleInstanceLock,
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Mock logger
|
|
53
|
+
vi.mock('@/utils/logger', () => ({
|
|
54
|
+
createLogger: () => ({
|
|
55
|
+
debug: vi.fn(),
|
|
56
|
+
error: vi.fn(),
|
|
57
|
+
info: vi.fn(),
|
|
58
|
+
warn: vi.fn(),
|
|
59
|
+
}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Mock updater configs
|
|
63
|
+
vi.mock('@/modules/updater/configs', () => ({
|
|
64
|
+
UPDATE_CHANNEL: 'stable',
|
|
65
|
+
updaterConfig: {
|
|
66
|
+
app: {
|
|
67
|
+
autoCheckUpdate: false,
|
|
68
|
+
autoDownloadUpdate: true,
|
|
69
|
+
checkUpdateInterval: 60 * 60 * 1000,
|
|
70
|
+
},
|
|
71
|
+
enableAppUpdate: true,
|
|
72
|
+
enableRenderHotUpdate: true,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// Mock isDev
|
|
77
|
+
vi.mock('@/const/env', () => ({
|
|
78
|
+
isDev: false,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
describe('UpdaterManager', () => {
|
|
82
|
+
let updaterManager: UpdaterManager;
|
|
83
|
+
let mockApp: AppCore;
|
|
84
|
+
let mockBroadcast: ReturnType<typeof vi.fn>;
|
|
85
|
+
let registeredEvents: Map<string, (...args: any[]) => void>;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.clearAllMocks();
|
|
89
|
+
vi.useFakeTimers();
|
|
90
|
+
|
|
91
|
+
// Reset autoUpdater state
|
|
92
|
+
(autoUpdater as any).autoDownload = false;
|
|
93
|
+
(autoUpdater as any).autoInstallOnAppQuit = false;
|
|
94
|
+
(autoUpdater as any).channel = 'stable';
|
|
95
|
+
(autoUpdater as any).allowPrerelease = false;
|
|
96
|
+
(autoUpdater as any).allowDowngrade = false;
|
|
97
|
+
(autoUpdater as any).forceDevUpdateConfig = false;
|
|
98
|
+
|
|
99
|
+
// Capture registered events
|
|
100
|
+
registeredEvents = new Map();
|
|
101
|
+
vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
|
|
102
|
+
registeredEvents.set(event, handler);
|
|
103
|
+
return autoUpdater;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Mock broadcast function
|
|
107
|
+
mockBroadcast = vi.fn();
|
|
108
|
+
|
|
109
|
+
// Create mock App
|
|
110
|
+
mockApp = {
|
|
111
|
+
browserManager: {
|
|
112
|
+
getMainWindow: vi.fn().mockReturnValue({
|
|
113
|
+
broadcast: mockBroadcast,
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
isQuiting: false,
|
|
117
|
+
} as unknown as AppCore;
|
|
118
|
+
|
|
119
|
+
updaterManager = new UpdaterManager(mockApp);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
vi.useRealTimers();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('constructor', () => {
|
|
127
|
+
it('should set up electron-log for autoUpdater', () => {
|
|
128
|
+
expect(autoUpdater.logger).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('initialize', () => {
|
|
133
|
+
it('should configure autoUpdater properties', async () => {
|
|
134
|
+
await updaterManager.initialize();
|
|
135
|
+
|
|
136
|
+
expect(autoUpdater.autoDownload).toBe(false);
|
|
137
|
+
expect(autoUpdater.autoInstallOnAppQuit).toBe(false);
|
|
138
|
+
expect(autoUpdater.channel).toBe('stable');
|
|
139
|
+
expect(autoUpdater.allowPrerelease).toBe(false);
|
|
140
|
+
expect(autoUpdater.allowDowngrade).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should register all event listeners', async () => {
|
|
144
|
+
await updaterManager.initialize();
|
|
145
|
+
|
|
146
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('checking-for-update', expect.any(Function));
|
|
147
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('update-available', expect.any(Function));
|
|
148
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('update-not-available', expect.any(Function));
|
|
149
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
150
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('download-progress', expect.any(Function));
|
|
151
|
+
expect(autoUpdater.on).toHaveBeenCalledWith('update-downloaded', expect.any(Function));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('checkForUpdates', () => {
|
|
156
|
+
beforeEach(async () => {
|
|
157
|
+
await updaterManager.initialize();
|
|
158
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should call autoUpdater.checkForUpdates', async () => {
|
|
162
|
+
await updaterManager.checkForUpdates();
|
|
163
|
+
|
|
164
|
+
expect(autoUpdater.checkForUpdates).toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should broadcast manualUpdateCheckStart when manual check', async () => {
|
|
168
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
169
|
+
|
|
170
|
+
expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateCheckStart');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should not broadcast when auto check', async () => {
|
|
174
|
+
await updaterManager.checkForUpdates({ manual: false });
|
|
175
|
+
|
|
176
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith('manualUpdateCheckStart');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should ignore duplicate check requests while checking', async () => {
|
|
180
|
+
// Start first check but don't resolve
|
|
181
|
+
vi.mocked(autoUpdater.checkForUpdates).mockImplementation(
|
|
182
|
+
() => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const firstCheck = updaterManager.checkForUpdates();
|
|
186
|
+
const secondCheck = updaterManager.checkForUpdates();
|
|
187
|
+
|
|
188
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
189
|
+
await Promise.all([firstCheck, secondCheck]);
|
|
190
|
+
|
|
191
|
+
expect(autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should broadcast updateError when check fails during manual check', async () => {
|
|
195
|
+
const error = new Error('Network error');
|
|
196
|
+
vi.mocked(autoUpdater.checkForUpdates).mockRejectedValue(error);
|
|
197
|
+
|
|
198
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
199
|
+
|
|
200
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Network error');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('downloadUpdate', () => {
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
await updaterManager.initialize();
|
|
207
|
+
vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
|
|
208
|
+
|
|
209
|
+
// Simulate update available
|
|
210
|
+
const updateAvailableHandler = registeredEvents.get('update-available');
|
|
211
|
+
updateAvailableHandler?.({ version: '2.0.0' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should call autoUpdater.downloadUpdate', async () => {
|
|
215
|
+
await updaterManager.downloadUpdate();
|
|
216
|
+
|
|
217
|
+
expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should ignore download request when no update available', async () => {
|
|
221
|
+
// Create fresh manager without update available
|
|
222
|
+
const freshManager = new UpdaterManager(mockApp);
|
|
223
|
+
await freshManager.initialize();
|
|
224
|
+
|
|
225
|
+
await freshManager.downloadUpdate();
|
|
226
|
+
|
|
227
|
+
// Reset call count since downloadUpdate might have been called in beforeEach
|
|
228
|
+
vi.mocked(autoUpdater.downloadUpdate).mockClear();
|
|
229
|
+
await freshManager.downloadUpdate();
|
|
230
|
+
|
|
231
|
+
// downloadUpdate should not be called on autoUpdater for fresh manager
|
|
232
|
+
expect(autoUpdater.downloadUpdate).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should ignore duplicate download requests while downloading', async () => {
|
|
236
|
+
vi.mocked(autoUpdater.downloadUpdate).mockImplementation(
|
|
237
|
+
() => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const firstDownload = updaterManager.downloadUpdate();
|
|
241
|
+
const secondDownload = updaterManager.downloadUpdate();
|
|
242
|
+
|
|
243
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
244
|
+
await Promise.all([firstDownload, secondDownload]);
|
|
245
|
+
|
|
246
|
+
expect(autoUpdater.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should broadcast updateDownloadStart when isManualCheck is true', async () => {
|
|
250
|
+
// Create a fresh manager to avoid state pollution from beforeEach
|
|
251
|
+
const freshManager = new UpdaterManager(mockApp);
|
|
252
|
+
|
|
253
|
+
// Setup fresh event capture
|
|
254
|
+
const freshEvents = new Map<string, (...args: any[]) => void>();
|
|
255
|
+
vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
|
|
256
|
+
freshEvents.set(event, handler);
|
|
257
|
+
return autoUpdater;
|
|
258
|
+
});
|
|
259
|
+
await freshManager.initialize();
|
|
260
|
+
|
|
261
|
+
// Trigger a manual check to set isManualCheck = true
|
|
262
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
263
|
+
await freshManager.checkForUpdates({ manual: true });
|
|
264
|
+
|
|
265
|
+
// Manually set updateAvailable without triggering auto-download
|
|
266
|
+
// Access private property to set state
|
|
267
|
+
(freshManager as any).updateAvailable = true;
|
|
268
|
+
|
|
269
|
+
// Clear previous broadcast calls
|
|
270
|
+
mockBroadcast.mockClear();
|
|
271
|
+
|
|
272
|
+
// Now download should broadcast updateDownloadStart because isManualCheck is true
|
|
273
|
+
vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
|
|
274
|
+
await freshManager.downloadUpdate();
|
|
275
|
+
|
|
276
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadStart');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should broadcast updateError when download fails with isManualCheck true', async () => {
|
|
280
|
+
// Create a fresh manager to avoid state pollution from beforeEach
|
|
281
|
+
const freshManager = new UpdaterManager(mockApp);
|
|
282
|
+
|
|
283
|
+
// Setup fresh event capture
|
|
284
|
+
const freshEvents = new Map<string, (...args: any[]) => void>();
|
|
285
|
+
vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
|
|
286
|
+
freshEvents.set(event, handler);
|
|
287
|
+
return autoUpdater;
|
|
288
|
+
});
|
|
289
|
+
await freshManager.initialize();
|
|
290
|
+
|
|
291
|
+
// Trigger a manual check to set isManualCheck = true
|
|
292
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
293
|
+
await freshManager.checkForUpdates({ manual: true });
|
|
294
|
+
|
|
295
|
+
// Manually set updateAvailable without triggering auto-download
|
|
296
|
+
(freshManager as any).updateAvailable = true;
|
|
297
|
+
|
|
298
|
+
// Clear previous broadcast calls
|
|
299
|
+
mockBroadcast.mockClear();
|
|
300
|
+
|
|
301
|
+
// Setup error
|
|
302
|
+
const error = new Error('Download failed');
|
|
303
|
+
vi.mocked(autoUpdater.downloadUpdate).mockRejectedValue(error);
|
|
304
|
+
|
|
305
|
+
await freshManager.downloadUpdate();
|
|
306
|
+
|
|
307
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Download failed');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('installNow', () => {
|
|
312
|
+
// Note: installNow uses require('electron') which is difficult to mock in vitest.
|
|
313
|
+
// These tests are skipped because vi.mock doesn't work with dynamic require().
|
|
314
|
+
// The functionality should be tested in integration tests or E2E tests.
|
|
315
|
+
|
|
316
|
+
it.skip('should set app.isQuiting to true', () => {
|
|
317
|
+
updaterManager.installNow();
|
|
318
|
+
expect(mockApp.isQuiting).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it.skip('should close all windows', () => {
|
|
322
|
+
const mockWindow1 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
|
|
323
|
+
const mockWindow2 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
|
|
324
|
+
mockGetAllWindows.mockReturnValue([mockWindow1, mockWindow2]);
|
|
325
|
+
updaterManager.installNow();
|
|
326
|
+
expect(mockWindow1.close).toHaveBeenCalled();
|
|
327
|
+
expect(mockWindow2.close).toHaveBeenCalled();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it.skip('should not close destroyed windows', () => {
|
|
331
|
+
const mockWindow = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(true) };
|
|
332
|
+
mockGetAllWindows.mockReturnValue([mockWindow]);
|
|
333
|
+
updaterManager.installNow();
|
|
334
|
+
expect(mockWindow.close).not.toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it.skip('should release single instance lock', () => {
|
|
338
|
+
updaterManager.installNow();
|
|
339
|
+
expect(mockReleaseSingleInstanceLock).toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it.skip('should call quitAndInstall with correct parameters after delay', async () => {
|
|
343
|
+
updaterManager.installNow();
|
|
344
|
+
expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled();
|
|
345
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
346
|
+
expect(autoUpdater.quitAndInstall).toHaveBeenCalledWith(true, true);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('installLater', () => {
|
|
351
|
+
it('should set autoInstallOnAppQuit to true', () => {
|
|
352
|
+
updaterManager.installLater();
|
|
353
|
+
|
|
354
|
+
expect(autoUpdater.autoInstallOnAppQuit).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should broadcast updateWillInstallLater', () => {
|
|
358
|
+
updaterManager.installLater();
|
|
359
|
+
|
|
360
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateWillInstallLater');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('event handlers', () => {
|
|
365
|
+
beforeEach(async () => {
|
|
366
|
+
await updaterManager.initialize();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('update-available', () => {
|
|
370
|
+
it('should broadcast manualUpdateAvailable when manual check', async () => {
|
|
371
|
+
// Trigger manual check first
|
|
372
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
373
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
374
|
+
|
|
375
|
+
const updateInfo = { version: '2.0.0' };
|
|
376
|
+
const handler = registeredEvents.get('update-available');
|
|
377
|
+
handler?.(updateInfo);
|
|
378
|
+
|
|
379
|
+
expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateAvailable', updateInfo);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should auto download when auto check finds update', async () => {
|
|
383
|
+
// Trigger auto check first
|
|
384
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
385
|
+
await updaterManager.checkForUpdates({ manual: false });
|
|
386
|
+
|
|
387
|
+
vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
|
|
388
|
+
|
|
389
|
+
const handler = registeredEvents.get('update-available');
|
|
390
|
+
handler?.({ version: '2.0.0' });
|
|
391
|
+
|
|
392
|
+
expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('update-not-available', () => {
|
|
397
|
+
it('should broadcast manualUpdateNotAvailable when manual check', async () => {
|
|
398
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
399
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
400
|
+
|
|
401
|
+
const info = { version: '1.0.0' };
|
|
402
|
+
const handler = registeredEvents.get('update-not-available');
|
|
403
|
+
handler?.(info);
|
|
404
|
+
|
|
405
|
+
expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateNotAvailable', info);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should not broadcast when auto check', async () => {
|
|
409
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
410
|
+
await updaterManager.checkForUpdates({ manual: false });
|
|
411
|
+
|
|
412
|
+
const handler = registeredEvents.get('update-not-available');
|
|
413
|
+
handler?.({ version: '1.0.0' });
|
|
414
|
+
|
|
415
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith(
|
|
416
|
+
'manualUpdateNotAvailable',
|
|
417
|
+
expect.anything(),
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('download-progress', () => {
|
|
423
|
+
it('should broadcast progress when manual check', async () => {
|
|
424
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
425
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
426
|
+
|
|
427
|
+
const progressObj = {
|
|
428
|
+
bytesPerSecond: 1024,
|
|
429
|
+
percent: 50,
|
|
430
|
+
total: 1024 * 1024,
|
|
431
|
+
transferred: 512 * 1024,
|
|
432
|
+
};
|
|
433
|
+
const handler = registeredEvents.get('download-progress');
|
|
434
|
+
handler?.(progressObj);
|
|
435
|
+
|
|
436
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadProgress', progressObj);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe('update-downloaded', () => {
|
|
441
|
+
it('should broadcast updateDownloaded', async () => {
|
|
442
|
+
await updaterManager.initialize();
|
|
443
|
+
|
|
444
|
+
const info = { version: '2.0.0' };
|
|
445
|
+
const handler = registeredEvents.get('update-downloaded');
|
|
446
|
+
handler?.(info);
|
|
447
|
+
|
|
448
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateDownloaded', info);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('error', () => {
|
|
453
|
+
it('should broadcast updateError when manual check', async () => {
|
|
454
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
455
|
+
await updaterManager.checkForUpdates({ manual: true });
|
|
456
|
+
|
|
457
|
+
const error = new Error('Update error');
|
|
458
|
+
const handler = registeredEvents.get('error');
|
|
459
|
+
handler?.(error);
|
|
460
|
+
|
|
461
|
+
expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Update error');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should not broadcast when auto check', async () => {
|
|
465
|
+
vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
|
|
466
|
+
await updaterManager.checkForUpdates({ manual: false });
|
|
467
|
+
|
|
468
|
+
const error = new Error('Update error');
|
|
469
|
+
const handler = registeredEvents.get('error');
|
|
470
|
+
handler?.(error);
|
|
471
|
+
|
|
472
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('simulation methods (dev mode)', () => {
|
|
478
|
+
it('simulateUpdateAvailable should do nothing when not in dev mode', () => {
|
|
479
|
+
// Current mock has isDev = false
|
|
480
|
+
updaterManager.simulateUpdateAvailable();
|
|
481
|
+
|
|
482
|
+
// Should not broadcast anything since isDev is false
|
|
483
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith(
|
|
484
|
+
'manualUpdateAvailable',
|
|
485
|
+
expect.objectContaining({ version: '1.0.0' }),
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('simulateUpdateDownloaded should do nothing when not in dev mode', () => {
|
|
490
|
+
updaterManager.simulateUpdateDownloaded();
|
|
491
|
+
|
|
492
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith(
|
|
493
|
+
'updateDownloaded',
|
|
494
|
+
expect.objectContaining({ version: '1.0.0' }),
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('simulateDownloadProgress should do nothing when not in dev mode', () => {
|
|
499
|
+
updaterManager.simulateDownloadProgress();
|
|
500
|
+
|
|
501
|
+
expect(mockBroadcast).not.toHaveBeenCalledWith('updateDownloadStart');
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe('mainWindow getter', () => {
|
|
506
|
+
it('should return main window from browserManager', () => {
|
|
507
|
+
const mainWindow = updaterManager['mainWindow'];
|
|
508
|
+
|
|
509
|
+
expect(mockApp.browserManager.getMainWindow).toHaveBeenCalled();
|
|
510
|
+
expect(mainWindow.broadcast).toBe(mockBroadcast);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|
package/changelog/v1.json
CHANGED
|
@@ -121,6 +121,37 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
|
|
|
121
121
|
- Default: `-`
|
|
122
122
|
- Example: `-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
|
|
123
123
|
|
|
124
|
+
## Vertex AI
|
|
125
|
+
|
|
126
|
+
### `VERTEXAI_CREDENTIALS`
|
|
127
|
+
|
|
128
|
+
- Type: Required
|
|
129
|
+
- Description: A JSON string of your Google Cloud service account key, you can get the key from [here](/docs/usage/providers/vertexai).
|
|
130
|
+
- Default: -
|
|
131
|
+
- Example: `{"type": "service_account", "project_id": "your-gcp-project-id", ...}`
|
|
132
|
+
|
|
133
|
+
### `VERTEXAI_PROJECT`
|
|
134
|
+
|
|
135
|
+
- Type: Optional
|
|
136
|
+
- Description: Your Google Cloud project ID. If not set, it will be obtained from the `project_id` field in `VERTEXAI_CREDENTIALS`.
|
|
137
|
+
- Default: -
|
|
138
|
+
- Example: `your-gcp-project-id`
|
|
139
|
+
|
|
140
|
+
### `VERTEXAI_LOCATION`
|
|
141
|
+
|
|
142
|
+
- Type: Optional
|
|
143
|
+
- Description: The region where your Vertex AI model is located.
|
|
144
|
+
- Default: `global`
|
|
145
|
+
- Example: `us-central1`
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
### `VERTEXAI_MODEL_LIST`
|
|
149
|
+
|
|
150
|
+
- Type: Optional
|
|
151
|
+
- Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
|
|
152
|
+
- Default: `-`
|
|
153
|
+
- Example: `-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
|
|
154
|
+
|
|
124
155
|
## Anthropic AI
|
|
125
156
|
|
|
126
157
|
### `ANTHROPIC_API_KEY`
|
|
@@ -119,6 +119,36 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
|
|
119
119
|
- 默认值:`-`
|
|
120
120
|
- 示例:`-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
|
|
121
121
|
|
|
122
|
+
## Vertex AI
|
|
123
|
+
|
|
124
|
+
### `VERTEXAI_CREDENTIALS`
|
|
125
|
+
|
|
126
|
+
- 类型:必选
|
|
127
|
+
- 描述:Google Cloud 服务账号密钥的 JSON 字符串。用于认证和授权访问 Vertex AI 服务,获取方法请参考 [这里](/zh/docs/usage/providers/vertexai)
|
|
128
|
+
- 默认值:-
|
|
129
|
+
- 示例:`{"type": "service_account", "project_id": "your-gcp-project-id", ...}`
|
|
130
|
+
|
|
131
|
+
### `VERTEXAI_PROJECT`
|
|
132
|
+
|
|
133
|
+
- 类型:可选
|
|
134
|
+
- 描述:你的 Google Cloud 项目 ID。如果未设置,将从 `VERTEXAI_CREDENTIALS` 中的 `project_id` 字段获取。
|
|
135
|
+
- 默认值:-
|
|
136
|
+
- 示例:`your-gcp-project-id`
|
|
137
|
+
|
|
138
|
+
### `VERTEXAI_LOCATION`
|
|
139
|
+
|
|
140
|
+
- 类型:可选
|
|
141
|
+
- 描述:你的 Vertex AI 模型所在的区域。
|
|
142
|
+
- 默认值:`global`
|
|
143
|
+
- 示例:`us-central1`
|
|
144
|
+
|
|
145
|
+
### `VERTEXAI_MODEL_LIST`
|
|
146
|
+
|
|
147
|
+
- 类型:可选
|
|
148
|
+
- 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
|
|
149
|
+
- 默认值:`-`
|
|
150
|
+
- 示例:`-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
|
|
151
|
+
|
|
122
152
|
## Anthropic AI
|
|
123
153
|
|
|
124
154
|
### `ANTHROPIC_API_KEY`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.142",
|
|
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",
|
|
@@ -368,8 +368,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
368
368
|
this,
|
|
369
369
|
) as any;
|
|
370
370
|
} else {
|
|
371
|
+
// Remove internal apiMode parameter before sending to API
|
|
372
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
373
|
+
const { apiMode: _, ...cleanedPayload } = postPayload as any;
|
|
371
374
|
const finalPayload = {
|
|
372
|
-
...
|
|
375
|
+
...cleanedPayload,
|
|
373
376
|
messages,
|
|
374
377
|
...(chatCompletion?.noUserId ? {} : { user: options?.user }),
|
|
375
378
|
stream_options:
|
|
@@ -385,11 +388,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
|
385
388
|
console.log(JSON.stringify(finalPayload), '\n');
|
|
386
389
|
}
|
|
387
390
|
|
|
388
|
-
response = await this.client.chat.completions.create(finalPayload, {
|
|
391
|
+
response = (await this.client.chat.completions.create(finalPayload, {
|
|
389
392
|
// https://github.com/lobehub/lobe-chat/pull/318
|
|
390
393
|
headers: { Accept: '*/*', ...options?.requestHeaders },
|
|
391
394
|
signal: options?.signal,
|
|
392
|
-
})
|
|
395
|
+
})) as unknown as Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
if (postPayload.stream) {
|
|
@@ -3,7 +3,7 @@ import { ModelProviderCard } from '@/types/llm';
|
|
|
3
3
|
// ref: https://ai.google.dev/gemini-api/docs/models/gemini
|
|
4
4
|
const VertexAI: ModelProviderCard = {
|
|
5
5
|
chatModels: [],
|
|
6
|
-
checkModel: 'gemini-
|
|
6
|
+
checkModel: 'gemini-2.0-flash',
|
|
7
7
|
description:
|
|
8
8
|
'Google 的 Gemini 系列是其最先进、通用的 AI模型,由 Google DeepMind 打造,专为多模态设计,支持文本、代码、图像、音频和视频的无缝理解与处理。适用于从数据中心到移动设备的多种环境,极大提升了AI模型的效率与应用广泛性。',
|
|
9
9
|
id: 'vertexai',
|
package/src/envs/llm.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const getLLMConfig = () => {
|
|
|
28
28
|
ENABLED_GOOGLE: z.boolean(),
|
|
29
29
|
GOOGLE_API_KEY: z.string().optional(),
|
|
30
30
|
|
|
31
|
+
ENABLED_VERTEXAI: z.boolean(),
|
|
32
|
+
|
|
31
33
|
ENABLED_MOONSHOT: z.boolean(),
|
|
32
34
|
MOONSHOT_API_KEY: z.string().optional(),
|
|
33
35
|
|
|
@@ -237,6 +239,8 @@ export const getLLMConfig = () => {
|
|
|
237
239
|
ENABLED_GOOGLE: !!process.env.GOOGLE_API_KEY,
|
|
238
240
|
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
|
239
241
|
|
|
242
|
+
ENABLED_VERTEXAI: !!process.env.VERTEXAI_CREDENTIALS,
|
|
243
|
+
|
|
240
244
|
ENABLED_VOLCENGINE: !!process.env.VOLCENGINE_API_KEY,
|
|
241
245
|
VOLCENGINE_API_KEY: process.env.VOLCENGINE_API_KEY,
|
|
242
246
|
|
|
@@ -162,18 +162,18 @@ const buildVertexOptions = (
|
|
|
162
162
|
payload: ClientSecretPayload,
|
|
163
163
|
params: Partial<GoogleGenAIOptions> = {},
|
|
164
164
|
): GoogleGenAIOptions => {
|
|
165
|
-
const rawCredentials = payload.apiKey
|
|
165
|
+
const rawCredentials = payload.apiKey || process.env.VERTEXAI_CREDENTIALS || '';
|
|
166
166
|
const credentials = safeParseJSON<Record<string, string>>(rawCredentials);
|
|
167
167
|
|
|
168
168
|
const projectFromParams = params.project as string | undefined;
|
|
169
169
|
const projectFromCredentials = credentials?.project_id;
|
|
170
170
|
const projectFromEnv = process.env.VERTEXAI_PROJECT;
|
|
171
171
|
|
|
172
|
-
const project = projectFromParams
|
|
172
|
+
const project = projectFromParams || projectFromCredentials || projectFromEnv;
|
|
173
173
|
const location =
|
|
174
|
-
(params.location as string | undefined)
|
|
174
|
+
(params.location as string | undefined) || payload.vertexAIRegion || process.env.VERTEXAI_LOCATION || undefined;
|
|
175
175
|
|
|
176
|
-
const googleAuthOptions = params.googleAuthOptions
|
|
176
|
+
const googleAuthOptions = params.googleAuthOptions || (credentials ? { credentials } : undefined);
|
|
177
177
|
|
|
178
178
|
const options: GoogleGenAIOptions = {
|
|
179
179
|
...params,
|