@lobehub/chat 1.97.17 → 1.98.1

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +8 -5
  3. package/apps/desktop/src/main/const/store.ts +12 -0
  4. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +172 -0
  5. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +401 -0
  6. package/apps/desktop/src/main/core/Browser.ts +2 -0
  7. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +116 -0
  8. package/apps/desktop/src/main/modules/networkProxy/index.ts +6 -0
  9. package/apps/desktop/src/main/modules/networkProxy/tester.ts +163 -0
  10. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +25 -0
  11. package/apps/desktop/src/main/modules/networkProxy/validator.ts +80 -0
  12. package/apps/desktop/src/main/types/store.ts +2 -1
  13. package/apps/desktop/src/main/utils/logger.ts +2 -1
  14. package/changelog/v1.json +18 -0
  15. package/locales/ar/electron.json +39 -0
  16. package/locales/ar/setting.json +1 -0
  17. package/locales/bg-BG/electron.json +39 -0
  18. package/locales/bg-BG/setting.json +1 -0
  19. package/locales/de-DE/electron.json +39 -0
  20. package/locales/de-DE/setting.json +1 -0
  21. package/locales/en-US/electron.json +39 -0
  22. package/locales/en-US/setting.json +1 -0
  23. package/locales/es-ES/electron.json +39 -0
  24. package/locales/es-ES/setting.json +1 -0
  25. package/locales/fa-IR/electron.json +39 -0
  26. package/locales/fa-IR/setting.json +1 -0
  27. package/locales/fr-FR/electron.json +39 -0
  28. package/locales/fr-FR/setting.json +1 -0
  29. package/locales/it-IT/electron.json +39 -0
  30. package/locales/it-IT/setting.json +1 -0
  31. package/locales/ja-JP/electron.json +39 -0
  32. package/locales/ja-JP/setting.json +1 -0
  33. package/locales/ko-KR/electron.json +39 -0
  34. package/locales/ko-KR/setting.json +1 -0
  35. package/locales/nl-NL/electron.json +39 -0
  36. package/locales/nl-NL/setting.json +1 -0
  37. package/locales/pl-PL/electron.json +39 -0
  38. package/locales/pl-PL/setting.json +1 -0
  39. package/locales/pt-BR/electron.json +39 -0
  40. package/locales/pt-BR/setting.json +1 -0
  41. package/locales/ru-RU/electron.json +39 -0
  42. package/locales/ru-RU/setting.json +1 -0
  43. package/locales/tr-TR/electron.json +39 -0
  44. package/locales/tr-TR/setting.json +1 -0
  45. package/locales/vi-VN/electron.json +39 -0
  46. package/locales/vi-VN/setting.json +1 -0
  47. package/locales/zh-CN/electron.json +39 -0
  48. package/locales/zh-CN/setting.json +1 -0
  49. package/locales/zh-TW/electron.json +39 -0
  50. package/locales/zh-TW/setting.json +1 -0
  51. package/package.json +3 -3
  52. package/packages/electron-client-ipc/src/events/index.ts +3 -1
  53. package/packages/electron-client-ipc/src/events/settings.ts +12 -0
  54. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  55. package/packages/electron-client-ipc/src/types/proxy.ts +12 -0
  56. package/src/app/[variants]/(main)/discover/(list)/(home)/page.tsx +4 -4
  57. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +11 -1
  58. package/src/app/[variants]/(main)/settings/proxy/features/ProxyForm.tsx +369 -0
  59. package/src/app/[variants]/(main)/settings/proxy/index.tsx +22 -0
  60. package/src/app/[variants]/(main)/settings/proxy/page.tsx +28 -0
  61. package/src/locales/default/electron.ts +39 -0
  62. package/src/locales/default/setting.ts +1 -0
  63. package/src/services/electron/settings.ts +33 -0
  64. package/src/store/electron/actions/settings.ts +55 -0
  65. package/src/store/electron/initialState.ts +12 -1
  66. package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -1
  67. package/src/store/electron/store.ts +4 -1
  68. package/src/store/global/initialState.ts +1 -0
  69. package/apps/desktop/scripts/pglite-server.ts +0 -14
@@ -0,0 +1,401 @@
1
+ import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App } from '@/core/App';
5
+
6
+ import NetworkProxyCtr from '../NetworkProxyCtr';
7
+
8
+ // 模拟 logger
9
+ vi.mock('@/utils/logger', () => ({
10
+ createLogger: () => ({
11
+ debug: vi.fn(),
12
+ info: vi.fn(),
13
+ warn: vi.fn(),
14
+ error: vi.fn(),
15
+ }),
16
+ }));
17
+
18
+ // 模拟 undici
19
+ vi.mock('undici', () => ({
20
+ fetch: vi.fn(),
21
+ getGlobalDispatcher: vi.fn(),
22
+ setGlobalDispatcher: vi.fn(),
23
+ ProxyAgent: vi.fn(),
24
+ }));
25
+
26
+ // 模拟 defaultProxySettings
27
+ vi.mock('@/const/store', () => ({
28
+ defaultProxySettings: {
29
+ enableProxy: false,
30
+ proxyBypass: 'localhost,127.0.0.1,::1',
31
+ proxyPort: '',
32
+ proxyRequireAuth: false,
33
+ proxyServer: '',
34
+ proxyType: 'http',
35
+ },
36
+ }));
37
+
38
+ // 模拟 fetch
39
+ global.fetch = vi.fn();
40
+
41
+ // 模拟 App 及其依赖项
42
+ const mockStoreManager = {
43
+ get: vi.fn(),
44
+ set: vi.fn(),
45
+ };
46
+
47
+ const mockApp = {
48
+ storeManager: mockStoreManager,
49
+ } as unknown as App;
50
+
51
+ describe('NetworkProxyCtr', () => {
52
+ let networkProxyCtr: NetworkProxyCtr;
53
+
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ networkProxyCtr = new NetworkProxyCtr(mockApp);
57
+
58
+ // 重置全局 fetch mock
59
+ (global.fetch as any).mockReset();
60
+ });
61
+
62
+ describe('ProxyConfigValidator', () => {
63
+ const validConfig: NetworkProxySettings = {
64
+ enableProxy: true,
65
+ proxyType: 'http',
66
+ proxyServer: 'proxy.example.com',
67
+ proxyPort: '8080',
68
+ proxyRequireAuth: false,
69
+ proxyBypass: 'localhost,127.0.0.1,::1',
70
+ };
71
+
72
+ it('should validate enabled proxy config with all required fields', () => {
73
+ // 通过测试公共方法来间接测试验证逻辑
74
+ expect(() => networkProxyCtr.setProxySettings(validConfig)).not.toThrow();
75
+ });
76
+
77
+ it('should validate disabled proxy config', () => {
78
+ const disabledConfig: NetworkProxySettings = {
79
+ ...validConfig,
80
+ enableProxy: false,
81
+ };
82
+
83
+ expect(() => networkProxyCtr.setProxySettings(disabledConfig)).not.toThrow();
84
+ });
85
+
86
+ it('should reject invalid proxy type', async () => {
87
+ const invalidConfig: NetworkProxySettings = {
88
+ ...validConfig,
89
+ proxyType: 'invalid' as any,
90
+ };
91
+
92
+ await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
93
+ });
94
+
95
+ it('should reject missing proxy server', async () => {
96
+ const invalidConfig: NetworkProxySettings = {
97
+ ...validConfig,
98
+ proxyServer: '',
99
+ };
100
+
101
+ await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
102
+ });
103
+
104
+ it('should reject invalid proxy port', async () => {
105
+ const invalidConfig: NetworkProxySettings = {
106
+ ...validConfig,
107
+ proxyPort: 'invalid',
108
+ };
109
+
110
+ await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
111
+ });
112
+
113
+ it('should reject missing auth credentials when auth is required', async () => {
114
+ const invalidConfig: NetworkProxySettings = {
115
+ ...validConfig,
116
+ proxyRequireAuth: true,
117
+ proxyUsername: '',
118
+ proxyPassword: '',
119
+ };
120
+
121
+ await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
122
+ });
123
+ });
124
+
125
+ describe('getDesktopSettings', () => {
126
+ it('should return stored proxy settings', async () => {
127
+ const expectedSettings: NetworkProxySettings = {
128
+ enableProxy: true,
129
+ proxyType: 'http',
130
+ proxyServer: 'proxy.example.com',
131
+ proxyPort: '8080',
132
+ proxyRequireAuth: false,
133
+ proxyBypass: 'localhost,127.0.0.1,::1',
134
+ };
135
+
136
+ mockStoreManager.get.mockReturnValue(expectedSettings);
137
+
138
+ const result = await networkProxyCtr.getDesktopSettings();
139
+
140
+ expect(result).toEqual(expectedSettings);
141
+ expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
142
+ });
143
+
144
+ it('should return default settings when store fails', async () => {
145
+ mockStoreManager.get.mockImplementation(() => {
146
+ throw new Error('Store error');
147
+ });
148
+
149
+ const result = await networkProxyCtr.getDesktopSettings();
150
+
151
+ expect(result).toEqual({
152
+ enableProxy: false,
153
+ proxyBypass: 'localhost,127.0.0.1,::1',
154
+ proxyPort: '',
155
+ proxyRequireAuth: false,
156
+ proxyServer: '',
157
+ proxyType: 'http',
158
+ });
159
+ });
160
+ });
161
+
162
+ describe('setProxySettings', () => {
163
+ const validConfig: NetworkProxySettings = {
164
+ enableProxy: true,
165
+ proxyType: 'http',
166
+ proxyServer: 'proxy.example.com',
167
+ proxyPort: '8080',
168
+ proxyRequireAuth: false,
169
+ proxyBypass: 'localhost,127.0.0.1,::1',
170
+ };
171
+
172
+ it('should save valid proxy settings', async () => {
173
+ mockStoreManager.get.mockReturnValue({
174
+ enableProxy: false,
175
+ proxyType: 'http',
176
+ proxyServer: '',
177
+ proxyPort: '',
178
+ proxyRequireAuth: false,
179
+ proxyBypass: 'localhost,127.0.0.1,::1',
180
+ });
181
+
182
+ await networkProxyCtr.setProxySettings(validConfig);
183
+
184
+ expect(mockStoreManager.set).toHaveBeenCalledWith(
185
+ 'networkProxy',
186
+ expect.objectContaining(validConfig),
187
+ );
188
+ });
189
+
190
+ it('should skip update if settings are unchanged', async () => {
191
+ mockStoreManager.get.mockReturnValue(validConfig);
192
+
193
+ await networkProxyCtr.setProxySettings(validConfig);
194
+
195
+ expect(mockStoreManager.set).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it('should throw error for invalid configuration', async () => {
199
+ const invalidConfig: NetworkProxySettings = {
200
+ ...validConfig,
201
+ proxyServer: '',
202
+ };
203
+
204
+ await expect(networkProxyCtr.setProxySettings(invalidConfig)).rejects.toThrow();
205
+ });
206
+ });
207
+
208
+ describe('testProxyConnection', () => {
209
+ it('should return success for successful connection', async () => {
210
+ const mockResponse = {
211
+ ok: true,
212
+ status: 200,
213
+ statusText: 'OK',
214
+ };
215
+
216
+ (global.fetch as any).mockResolvedValueOnce(mockResponse);
217
+
218
+ const result = await networkProxyCtr.testProxyConnection('https://www.google.com');
219
+
220
+ expect(result).toEqual({ success: true });
221
+ expect(global.fetch).toHaveBeenCalledWith('https://www.google.com', expect.any(Object));
222
+ });
223
+
224
+ it('should throw error for failed connection', async () => {
225
+ const mockResponse = {
226
+ ok: false,
227
+ status: 404,
228
+ statusText: 'Not Found',
229
+ };
230
+
231
+ (global.fetch as any).mockResolvedValueOnce(mockResponse);
232
+
233
+ await expect(networkProxyCtr.testProxyConnection('https://www.google.com')).rejects.toThrow();
234
+ });
235
+
236
+ it('should throw error for network error', async () => {
237
+ (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
238
+
239
+ await expect(networkProxyCtr.testProxyConnection('https://www.google.com')).rejects.toThrow();
240
+ });
241
+ });
242
+
243
+ describe('testProxyConfig', () => {
244
+ const validConfig: NetworkProxySettings = {
245
+ enableProxy: true,
246
+ proxyType: 'http',
247
+ proxyServer: 'proxy.example.com',
248
+ proxyPort: '8080',
249
+ proxyRequireAuth: false,
250
+ proxyBypass: 'localhost,127.0.0.1,::1',
251
+ };
252
+
253
+ it('should return success for valid config and successful connection', async () => {
254
+ const mockResponse = {
255
+ ok: true,
256
+ status: 200,
257
+ statusText: 'OK',
258
+ };
259
+
260
+ (global.fetch as any).mockResolvedValueOnce(mockResponse);
261
+
262
+ const result = await networkProxyCtr.testProxyConfig({ config: validConfig });
263
+
264
+ expect(result.success).toBe(true);
265
+ expect(result.responseTime).toBeGreaterThanOrEqual(0);
266
+ });
267
+
268
+ it('should return failure for invalid config', async () => {
269
+ const invalidConfig: NetworkProxySettings = {
270
+ ...validConfig,
271
+ proxyServer: '',
272
+ };
273
+
274
+ const result = await networkProxyCtr.testProxyConfig({ config: invalidConfig });
275
+
276
+ expect(result.success).toBe(false);
277
+ expect(result.message).toContain('Invalid proxy configuration');
278
+ });
279
+
280
+ it('should test direct connection for disabled proxy', async () => {
281
+ const disabledConfig: NetworkProxySettings = {
282
+ ...validConfig,
283
+ enableProxy: false,
284
+ };
285
+
286
+ const mockResponse = {
287
+ ok: true,
288
+ status: 200,
289
+ statusText: 'OK',
290
+ };
291
+
292
+ (global.fetch as any).mockResolvedValueOnce(mockResponse);
293
+
294
+ const result = await networkProxyCtr.testProxyConfig({ config: disabledConfig });
295
+
296
+ expect(result.success).toBe(true);
297
+ });
298
+
299
+ it('should return failure for connection error', async () => {
300
+ (global.fetch as any).mockRejectedValueOnce(new Error('Connection failed'));
301
+
302
+ const result = await networkProxyCtr.testProxyConfig({ config: validConfig });
303
+
304
+ expect(result.success).toBe(false);
305
+ expect(result.message).toContain('Connection failed');
306
+ });
307
+ });
308
+
309
+ describe('afterAppReady', () => {
310
+ it('should apply stored proxy settings on app ready', async () => {
311
+ const storedConfig: NetworkProxySettings = {
312
+ enableProxy: true,
313
+ proxyType: 'http',
314
+ proxyServer: 'proxy.example.com',
315
+ proxyPort: '8080',
316
+ proxyRequireAuth: false,
317
+ proxyBypass: 'localhost,127.0.0.1,::1',
318
+ };
319
+
320
+ mockStoreManager.get.mockReturnValue(storedConfig);
321
+
322
+ await networkProxyCtr.afterAppReady();
323
+
324
+ expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
325
+ });
326
+
327
+ it('should use default settings if stored config is invalid', async () => {
328
+ const invalidConfig: NetworkProxySettings = {
329
+ enableProxy: true,
330
+ proxyType: 'http',
331
+ proxyServer: '', // 无效的服务器
332
+ proxyPort: '8080',
333
+ proxyRequireAuth: false,
334
+ proxyBypass: 'localhost,127.0.0.1,::1',
335
+ };
336
+
337
+ mockStoreManager.get.mockReturnValue(invalidConfig);
338
+
339
+ await networkProxyCtr.afterAppReady();
340
+
341
+ expect(mockStoreManager.get).toHaveBeenCalledWith('networkProxy', expect.any(Object));
342
+ });
343
+
344
+ it('should handle errors gracefully', async () => {
345
+ mockStoreManager.get.mockImplementation(() => {
346
+ throw new Error('Store error');
347
+ });
348
+
349
+ // 不应该抛出错误
350
+ await expect(networkProxyCtr.afterAppReady()).resolves.not.toThrow();
351
+ });
352
+ });
353
+
354
+ describe('ProxyUrlBuilder', () => {
355
+ it('should build URL without authentication', () => {
356
+ const config: NetworkProxySettings = {
357
+ enableProxy: true,
358
+ proxyType: 'http',
359
+ proxyServer: 'proxy.example.com',
360
+ proxyPort: '8080',
361
+ proxyRequireAuth: false,
362
+ proxyBypass: 'localhost,127.0.0.1,::1',
363
+ };
364
+
365
+ // 通过测试代理设置来间接测试 URL 构建
366
+ expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
367
+ });
368
+
369
+ it('should build URL with authentication', () => {
370
+ const config: NetworkProxySettings = {
371
+ enableProxy: true,
372
+ proxyType: 'http',
373
+ proxyServer: 'proxy.example.com',
374
+ proxyPort: '8080',
375
+ proxyRequireAuth: true,
376
+ proxyUsername: 'user',
377
+ proxyPassword: 'pass',
378
+ proxyBypass: 'localhost,127.0.0.1,::1',
379
+ };
380
+
381
+ // 通过测试代理设置来间接测试 URL 构建
382
+ expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
383
+ });
384
+
385
+ it('should handle special characters in credentials', () => {
386
+ const config: NetworkProxySettings = {
387
+ enableProxy: true,
388
+ proxyType: 'http',
389
+ proxyServer: 'proxy.example.com',
390
+ proxyPort: '8080',
391
+ proxyRequireAuth: true,
392
+ proxyUsername: 'user@domain',
393
+ proxyPassword: 'pass:word',
394
+ proxyBypass: 'localhost,127.0.0.1,::1',
395
+ };
396
+
397
+ // 通过测试代理设置来间接测试 URL 构建
398
+ expect(() => networkProxyCtr.setProxySettings(config)).not.toThrow();
399
+ });
400
+ });
401
+ });
@@ -381,6 +381,8 @@ export default class Browser {
381
381
  }
382
382
 
383
383
  broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
384
+ if (this._browserWindow.isDestroyed()) return;
385
+
384
386
  logger.debug(`Broadcasting to window ${this.identifier}, channel: ${channel}`);
385
387
  this._browserWindow.webContents.send(channel, data);
386
388
  };
@@ -0,0 +1,116 @@
1
+ import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
2
+ import { Agent, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
3
+
4
+ import { createLogger } from '@/utils/logger';
5
+
6
+ import { ProxyUrlBuilder } from './urlBuilder';
7
+
8
+ // Create logger
9
+ const logger = createLogger('modules:networkProxy:dispatcher');
10
+
11
+ /**
12
+ * 代理管理器
13
+ */
14
+ export class ProxyDispatcherManager {
15
+ private static isChanging = false;
16
+ private static changeQueue: Array<() => Promise<void>> = [];
17
+
18
+ /**
19
+ * 应用代理设置(带并发控制)
20
+ */
21
+ static async applyProxySettings(config: NetworkProxySettings): Promise<void> {
22
+ return new Promise((resolve, reject) => {
23
+ const operation = async () => {
24
+ try {
25
+ await this.doApplyProxySettings(config);
26
+ resolve();
27
+ } catch (error) {
28
+ reject(error);
29
+ }
30
+ };
31
+
32
+ if (this.isChanging) {
33
+ // 如果正在切换,加入队列
34
+ this.changeQueue.push(operation);
35
+ } else {
36
+ // 立即执行
37
+ operation();
38
+ }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * 执行代理设置应用
44
+ */
45
+ private static async doApplyProxySettings(config: NetworkProxySettings): Promise<void> {
46
+ this.isChanging = true;
47
+
48
+ try {
49
+ const currentDispatcher = getGlobalDispatcher();
50
+
51
+ // 禁用代理,恢复默认连接
52
+ if (!config.enableProxy) {
53
+ await this.safeDestroyDispatcher(currentDispatcher);
54
+ // 创建一个新的默认 Agent 来替代代理
55
+ setGlobalDispatcher(new Agent());
56
+ logger.debug('Proxy disabled, reset to direct connection mode');
57
+ return;
58
+ }
59
+
60
+ // 构建代理 URL
61
+ const proxyUrl = ProxyUrlBuilder.build(config);
62
+
63
+ // 创建代理 agent
64
+ const agent = this.createProxyAgent(config.proxyType, proxyUrl);
65
+
66
+ // 切换代理前销毁旧 dispatcher
67
+ await this.safeDestroyDispatcher(currentDispatcher);
68
+ setGlobalDispatcher(agent);
69
+
70
+ logger.info(
71
+ `Proxy settings applied: ${config.proxyType}://${config.proxyServer}:${config.proxyPort}`,
72
+ );
73
+ logger.debug(
74
+ 'Global request proxy set, all Node.js network requests will go through this proxy',
75
+ );
76
+ } finally {
77
+ this.isChanging = false;
78
+
79
+ // 处理队列中的下一个操作
80
+ if (this.changeQueue.length > 0) {
81
+ const nextOperation = this.changeQueue.shift();
82
+ if (nextOperation) {
83
+ setTimeout(() => nextOperation(), 0);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 创建代理 agent
91
+ */
92
+ static createProxyAgent(proxyType: string, proxyUrl: string) {
93
+ try {
94
+ // undici 的 ProxyAgent 支持 http, https 和 socks5
95
+ return new ProxyAgent({ uri: proxyUrl });
96
+ } catch (error) {
97
+ logger.error(`Failed to create proxy agent for ${proxyType}:`, error);
98
+ throw new Error(
99
+ `Failed to create proxy agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 安全销毁 dispatcher
106
+ */
107
+ private static async safeDestroyDispatcher(dispatcher: any): Promise<void> {
108
+ try {
109
+ if (dispatcher && typeof dispatcher.destroy === 'function') {
110
+ await dispatcher.destroy();
111
+ }
112
+ } catch (error) {
113
+ logger.warn('Failed to destroy dispatcher:', error);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,6 @@
1
+ export { ProxyDispatcherManager } from './dispatcher';
2
+ export type { ProxyTestResult } from './tester';
3
+ export { ProxyConnectionTester } from './tester';
4
+ export { ProxyUrlBuilder } from './urlBuilder';
5
+ export type { ProxyValidationResult } from './validator';
6
+ export { ProxyConfigValidator } from './validator';
@@ -0,0 +1,163 @@
1
+ import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
2
+ import { fetch, getGlobalDispatcher, setGlobalDispatcher } from 'undici';
3
+
4
+ import { createLogger } from '@/utils/logger';
5
+
6
+ import { ProxyDispatcherManager } from './dispatcher';
7
+ import { ProxyUrlBuilder } from './urlBuilder';
8
+ import { ProxyConfigValidator } from './validator';
9
+
10
+ // Create logger
11
+ const logger = createLogger('modules:networkProxy:tester');
12
+
13
+ /**
14
+ * 代理连接测试结果
15
+ */
16
+ export interface ProxyTestResult {
17
+ message?: string;
18
+ responseTime?: number;
19
+ success: boolean;
20
+ }
21
+
22
+ /**
23
+ * 代理连接测试器
24
+ */
25
+ export class ProxyConnectionTester {
26
+ private static readonly DEFAULT_TIMEOUT = 10_000; // 10秒超时
27
+ private static readonly DEFAULT_TEST_URL = 'https://www.google.com';
28
+
29
+ /**
30
+ * 测试代理连接
31
+ */
32
+ static async testConnection(
33
+ url: string = this.DEFAULT_TEST_URL,
34
+ timeout: number = this.DEFAULT_TIMEOUT,
35
+ ): Promise<ProxyTestResult> {
36
+ const startTime = Date.now();
37
+
38
+ try {
39
+ logger.info(`Testing proxy connection with URL: ${url}`);
40
+
41
+ const controller = new AbortController();
42
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
43
+
44
+ const response = await fetch(url, {
45
+ headers: {
46
+ 'User-Agent': 'LobeChat-Desktop/1.0.0',
47
+ },
48
+ signal: controller.signal,
49
+ });
50
+
51
+ clearTimeout(timeoutId);
52
+
53
+ if (!response.ok) {
54
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
55
+ }
56
+
57
+ const responseTime = Date.now() - startTime;
58
+
59
+ logger.info(`Proxy connection test successful, response time: ${responseTime}ms`);
60
+
61
+ return {
62
+ responseTime,
63
+ success: true,
64
+ };
65
+ } catch (error) {
66
+ const responseTime = Date.now() - startTime;
67
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
68
+
69
+ logger.error(`Proxy connection test failed after ${responseTime}ms:`, errorMessage);
70
+
71
+ return {
72
+ message: errorMessage,
73
+ responseTime,
74
+ success: false,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 测试指定代理配置的连接
81
+ */
82
+ static async testProxyConfig(
83
+ config: NetworkProxySettings,
84
+ testUrl: string = this.DEFAULT_TEST_URL,
85
+ ): Promise<ProxyTestResult> {
86
+ // 验证配置
87
+ const validation = ProxyConfigValidator.validate(config);
88
+ if (!validation.isValid) {
89
+ return {
90
+ message: `Invalid proxy configuration: ${validation.errors.join(', ')}`,
91
+ success: false,
92
+ };
93
+ }
94
+
95
+ // 如果未启用代理,直接测试
96
+ if (!config.enableProxy) {
97
+ return this.testConnection(testUrl);
98
+ }
99
+
100
+ // 创建临时代理 agent 进行测试
101
+ try {
102
+ const proxyUrl = ProxyUrlBuilder.build(config);
103
+ logger.debug(`Testing proxy with URL: ${proxyUrl}`);
104
+
105
+ const agent = ProxyDispatcherManager.createProxyAgent(config.proxyType, proxyUrl);
106
+
107
+ const startTime = Date.now();
108
+ const controller = new AbortController();
109
+ const timeoutId = setTimeout(() => controller.abort(), this.DEFAULT_TIMEOUT);
110
+
111
+ // 临时设置代理进行测试
112
+ const originalDispatcher = getGlobalDispatcher();
113
+ setGlobalDispatcher(agent);
114
+
115
+ try {
116
+ const response = await fetch(testUrl, {
117
+ dispatcher: agent,
118
+ headers: {
119
+ 'User-Agent': 'LobeChat-Desktop/1.0.0',
120
+ },
121
+ signal: controller.signal,
122
+ });
123
+
124
+ clearTimeout(timeoutId);
125
+
126
+ if (!response.ok) {
127
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
128
+ }
129
+
130
+ const responseTime = Date.now() - startTime;
131
+ logger.info(`Proxy test successful, response time: ${responseTime}ms`);
132
+
133
+ return {
134
+ responseTime,
135
+ success: true,
136
+ };
137
+ } catch (fetchError) {
138
+ clearTimeout(timeoutId);
139
+ throw fetchError;
140
+ } finally {
141
+ // 恢复原来的 dispatcher
142
+ setGlobalDispatcher(originalDispatcher);
143
+ // 清理临时创建的代理 agent
144
+ if (agent && typeof agent.destroy === 'function') {
145
+ try {
146
+ await agent.destroy();
147
+ } catch (error) {
148
+ logger.warn('Failed to destroy test agent:', error);
149
+ }
150
+ }
151
+ }
152
+ } catch (error) {
153
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
154
+
155
+ logger.error(`Proxy test failed: ${errorMessage}`, error);
156
+
157
+ return {
158
+ message: `Proxy test failed: ${errorMessage}`,
159
+ success: false,
160
+ };
161
+ }
162
+ }
163
+ }