@lobehub/lobehub 2.0.0-next.140 → 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 +50 -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 +18 -0
- package/docs/development/database-schema.dbml +1 -2
- 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/database/migrations/0055_rename_phone_number_to_phone.sql +4 -0
- package/packages/database/migrations/meta/0055_snapshot.json +8396 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +11 -0
- package/packages/database/src/schemas/user.ts +1 -2
- 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
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Remove internal apiMode param from chat completion API requests."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2025-12-01",
|
|
9
|
+
"version": "2.0.0-next.142"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {
|
|
13
|
+
"fixes": [
|
|
14
|
+
"Drop user.phoneNumber and reuse user.phone."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"date": "2025-12-01",
|
|
18
|
+
"version": "2.0.0-next.141"
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"children": {
|
|
4
22
|
"features": [
|
|
@@ -1044,7 +1044,7 @@ table users {
|
|
|
1044
1044
|
username text [unique]
|
|
1045
1045
|
email text [unique]
|
|
1046
1046
|
avatar text
|
|
1047
|
-
phone text
|
|
1047
|
+
phone text [unique]
|
|
1048
1048
|
first_name text
|
|
1049
1049
|
last_name text
|
|
1050
1050
|
full_name text
|
|
@@ -1058,7 +1058,6 @@ table users {
|
|
|
1058
1058
|
ban_reason text
|
|
1059
1059
|
ban_expires "timestamp with time zone"
|
|
1060
1060
|
two_factor_enabled boolean [default: false]
|
|
1061
|
-
phone_number text [unique]
|
|
1062
1061
|
phone_number_verified boolean
|
|
1063
1062
|
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
|
1064
1063
|
created_at "timestamp with time zone" [not null, default: `now()`]
|
|
@@ -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",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "users_phone_number_unique";--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "users" DROP COLUMN IF EXISTS "phone_number";--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "users_phone_unique";--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "users" ADD CONSTRAINT "users_phone_unique" UNIQUE("phone");
|