@lobehub/chat 1.97.17 → 1.98.0
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/apps/desktop/package.json +8 -5
- package/apps/desktop/src/main/const/store.ts +12 -0
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +172 -0
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +401 -0
- package/apps/desktop/src/main/core/Browser.ts +2 -0
- package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +116 -0
- package/apps/desktop/src/main/modules/networkProxy/index.ts +6 -0
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +163 -0
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +25 -0
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +80 -0
- package/apps/desktop/src/main/types/store.ts +2 -1
- package/apps/desktop/src/main/utils/logger.ts +2 -1
- package/changelog/v1.json +9 -0
- package/locales/ar/electron.json +39 -0
- package/locales/ar/setting.json +1 -0
- package/locales/bg-BG/electron.json +39 -0
- package/locales/bg-BG/setting.json +1 -0
- package/locales/de-DE/electron.json +39 -0
- package/locales/de-DE/setting.json +1 -0
- package/locales/en-US/electron.json +39 -0
- package/locales/en-US/setting.json +1 -0
- package/locales/es-ES/electron.json +39 -0
- package/locales/es-ES/setting.json +1 -0
- package/locales/fa-IR/electron.json +39 -0
- package/locales/fa-IR/setting.json +1 -0
- package/locales/fr-FR/electron.json +39 -0
- package/locales/fr-FR/setting.json +1 -0
- package/locales/it-IT/electron.json +39 -0
- package/locales/it-IT/setting.json +1 -0
- package/locales/ja-JP/electron.json +39 -0
- package/locales/ja-JP/setting.json +1 -0
- package/locales/ko-KR/electron.json +39 -0
- package/locales/ko-KR/setting.json +1 -0
- package/locales/nl-NL/electron.json +39 -0
- package/locales/nl-NL/setting.json +1 -0
- package/locales/pl-PL/electron.json +39 -0
- package/locales/pl-PL/setting.json +1 -0
- package/locales/pt-BR/electron.json +39 -0
- package/locales/pt-BR/setting.json +1 -0
- package/locales/ru-RU/electron.json +39 -0
- package/locales/ru-RU/setting.json +1 -0
- package/locales/tr-TR/electron.json +39 -0
- package/locales/tr-TR/setting.json +1 -0
- package/locales/vi-VN/electron.json +39 -0
- package/locales/vi-VN/setting.json +1 -0
- package/locales/zh-CN/electron.json +39 -0
- package/locales/zh-CN/setting.json +1 -0
- package/locales/zh-TW/electron.json +39 -0
- package/locales/zh-TW/setting.json +1 -0
- package/package.json +3 -3
- package/packages/electron-client-ipc/src/events/index.ts +3 -1
- package/packages/electron-client-ipc/src/events/settings.ts +12 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/proxy.ts +12 -0
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +11 -1
- package/src/app/[variants]/(main)/settings/proxy/features/ProxyForm.tsx +369 -0
- package/src/app/[variants]/(main)/settings/proxy/index.tsx +22 -0
- package/src/app/[variants]/(main)/settings/proxy/page.tsx +28 -0
- package/src/locales/default/electron.ts +39 -0
- package/src/locales/default/setting.ts +1 -0
- package/src/services/electron/settings.ts +33 -0
- package/src/store/electron/actions/settings.ts +55 -0
- package/src/store/electron/initialState.ts +12 -1
- package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -1
- package/src/store/electron/store.ts +4 -1
- package/src/store/global/initialState.ts +1 -0
- package/apps/desktop/scripts/pglite-server.ts +0 -14
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
## [Version 1.98.0](https://github.com/lobehub/lobe-chat/compare/v1.97.17...v1.98.0)
|
6
|
+
|
7
|
+
<sup>Released on **2025-07-13**</sup>
|
8
|
+
|
9
|
+
#### ✨ Features
|
10
|
+
|
11
|
+
- **misc**: Add network proxy for desktop.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's improved
|
19
|
+
|
20
|
+
- **misc**: Add network proxy for desktop, closes [#7848](https://github.com/lobehub/lobe-chat/issues/7848) ([46d2509](https://github.com/lobehub/lobe-chat/commit/46d2509))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.97.17](https://github.com/lobehub/lobe-chat/compare/v1.97.16...v1.97.17)
|
6
31
|
|
7
32
|
<sup>Released on **2025-07-13**</sup>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "lobehub-desktop-dev",
|
3
|
-
"version": "0.0.
|
3
|
+
"version": "0.0.0",
|
4
4
|
"description": "LobeHub Desktop Application",
|
5
5
|
"homepage": "https://lobehub.com",
|
6
6
|
"repository": {
|
@@ -24,6 +24,7 @@
|
|
24
24
|
"lint": "eslint --cache ",
|
25
25
|
"pg-server": "bun run scripts/pglite-server.ts",
|
26
26
|
"start": "electron-vite preview",
|
27
|
+
"test": "vite --run",
|
27
28
|
"typecheck": "tsgo --noEmit -p tsconfig.json"
|
28
29
|
},
|
29
30
|
"dependencies": {
|
@@ -45,10 +46,10 @@
|
|
45
46
|
"@types/resolve": "^1.20.6",
|
46
47
|
"@types/semver": "^7.7.0",
|
47
48
|
"@types/set-cookie-parser": "^2.4.10",
|
48
|
-
"@typescript/native-preview": "
|
49
|
+
"@typescript/native-preview": "7.0.0-dev.20250711.1",
|
49
50
|
"consola": "^3.1.0",
|
50
51
|
"cookie": "^1.0.2",
|
51
|
-
"electron": "
|
52
|
+
"electron": "~37.1.0",
|
52
53
|
"electron-builder": "^26.0.12",
|
53
54
|
"electron-is": "^3.0.0",
|
54
55
|
"electron-log": "^5.3.3",
|
@@ -58,17 +59,19 @@
|
|
58
59
|
"fix-path": "^4.0.0",
|
59
60
|
"just-diff": "^6.0.2",
|
60
61
|
"lodash": "^4.17.21",
|
61
|
-
"
|
62
|
+
"lodash-es": "^4.17.21",
|
62
63
|
"resolve": "^1.22.8",
|
63
64
|
"semver": "^7.5.4",
|
64
65
|
"set-cookie-parser": "^2.7.1",
|
65
66
|
"tsx": "^4.19.3",
|
66
67
|
"typescript": "^5.7.3",
|
68
|
+
"undici": "^7.9.0",
|
67
69
|
"vite": "^6.2.5"
|
68
70
|
},
|
69
71
|
"pnpm": {
|
70
72
|
"onlyBuiltDependencies": [
|
71
|
-
"electron"
|
73
|
+
"electron",
|
74
|
+
"electron-builder"
|
72
75
|
]
|
73
76
|
}
|
74
77
|
}
|
@@ -1,6 +1,8 @@
|
|
1
1
|
/**
|
2
2
|
* 应用设置存储相关常量
|
3
3
|
*/
|
4
|
+
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
|
5
|
+
|
4
6
|
import { appStorageDir } from '@/const/dir';
|
5
7
|
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
|
6
8
|
import { ElectronMainStore } from '@/types/store';
|
@@ -10,6 +12,15 @@ import { ElectronMainStore } from '@/types/store';
|
|
10
12
|
*/
|
11
13
|
export const STORE_NAME = 'lobehub-settings';
|
12
14
|
|
15
|
+
export const defaultProxySettings: NetworkProxySettings = {
|
16
|
+
enableProxy: false,
|
17
|
+
proxyBypass: 'localhost, 127.0.0.1, ::1',
|
18
|
+
proxyPort: '',
|
19
|
+
proxyRequireAuth: false,
|
20
|
+
proxyServer: '',
|
21
|
+
proxyType: 'http',
|
22
|
+
};
|
23
|
+
|
13
24
|
/**
|
14
25
|
* 存储默认值
|
15
26
|
*/
|
@@ -17,6 +28,7 @@ export const STORE_DEFAULTS: ElectronMainStore = {
|
|
17
28
|
dataSyncConfig: { storageMode: 'local' },
|
18
29
|
encryptedTokens: {},
|
19
30
|
locale: 'auto',
|
31
|
+
networkProxy: defaultProxySettings,
|
20
32
|
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
21
33
|
storagePath: appStorageDir,
|
22
34
|
};
|
@@ -0,0 +1,172 @@
|
|
1
|
+
import { NetworkProxySettings } from '@lobechat/electron-client-ipc';
|
2
|
+
import { merge } from 'lodash';
|
3
|
+
import { isEqual } from 'lodash-es';
|
4
|
+
|
5
|
+
import { defaultProxySettings } from '@/const/store';
|
6
|
+
import { createLogger } from '@/utils/logger';
|
7
|
+
|
8
|
+
import {
|
9
|
+
ProxyConfigValidator,
|
10
|
+
ProxyConnectionTester,
|
11
|
+
ProxyDispatcherManager,
|
12
|
+
ProxyTestResult,
|
13
|
+
} from '../modules/networkProxy';
|
14
|
+
import { ControllerModule, ipcClientEvent } from './index';
|
15
|
+
|
16
|
+
// Create logger
|
17
|
+
const logger = createLogger('controllers:NetworkProxyCtr');
|
18
|
+
|
19
|
+
/**
|
20
|
+
* 网络代理控制器
|
21
|
+
* 处理桌面应用的网络代理相关功能
|
22
|
+
*/
|
23
|
+
export default class NetworkProxyCtr extends ControllerModule {
|
24
|
+
/**
|
25
|
+
* 获取代理设置
|
26
|
+
*/
|
27
|
+
@ipcClientEvent('getProxySettings')
|
28
|
+
async getDesktopSettings(): Promise<NetworkProxySettings> {
|
29
|
+
try {
|
30
|
+
const settings = this.app.storeManager.get(
|
31
|
+
'networkProxy',
|
32
|
+
defaultProxySettings,
|
33
|
+
) as NetworkProxySettings;
|
34
|
+
logger.debug('Retrieved proxy settings:', {
|
35
|
+
enableProxy: settings.enableProxy,
|
36
|
+
proxyType: settings.proxyType,
|
37
|
+
});
|
38
|
+
return settings;
|
39
|
+
} catch (error) {
|
40
|
+
logger.error('Failed to get proxy settings:', error);
|
41
|
+
return defaultProxySettings;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* 设置代理配置
|
47
|
+
*/
|
48
|
+
@ipcClientEvent('setProxySettings')
|
49
|
+
async setProxySettings(config: NetworkProxySettings): Promise<void> {
|
50
|
+
try {
|
51
|
+
// 验证配置
|
52
|
+
const validation = ProxyConfigValidator.validate(config);
|
53
|
+
if (!validation.isValid) {
|
54
|
+
const errorMessage = `Invalid proxy configuration: ${validation.errors.join(', ')}`;
|
55
|
+
logger.error(errorMessage);
|
56
|
+
throw new Error(errorMessage);
|
57
|
+
}
|
58
|
+
|
59
|
+
// 获取当前配置
|
60
|
+
const currentConfig = this.app.storeManager.get(
|
61
|
+
'networkProxy',
|
62
|
+
defaultProxySettings,
|
63
|
+
) as NetworkProxySettings;
|
64
|
+
|
65
|
+
// 检查是否有变化
|
66
|
+
if (isEqual(currentConfig, config)) {
|
67
|
+
logger.debug('Proxy settings unchanged, skipping update');
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
|
71
|
+
// 合并配置
|
72
|
+
const newConfig = merge({}, currentConfig, config);
|
73
|
+
|
74
|
+
// 应用代理设置
|
75
|
+
await ProxyDispatcherManager.applyProxySettings(newConfig);
|
76
|
+
|
77
|
+
// 保存到存储
|
78
|
+
this.app.storeManager.set('networkProxy', newConfig);
|
79
|
+
|
80
|
+
logger.info('Proxy settings updated successfully', {
|
81
|
+
enableProxy: newConfig.enableProxy,
|
82
|
+
proxyPort: newConfig.proxyPort,
|
83
|
+
proxyServer: newConfig.proxyServer,
|
84
|
+
proxyType: newConfig.proxyType,
|
85
|
+
});
|
86
|
+
} catch (error) {
|
87
|
+
logger.error('Failed to update proxy settings:', error);
|
88
|
+
throw error;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* 测试代理连接
|
94
|
+
*/
|
95
|
+
@ipcClientEvent('testProxyConnection')
|
96
|
+
async testProxyConnection(url: string): Promise<{ message?: string; success: boolean }> {
|
97
|
+
try {
|
98
|
+
const result = await ProxyConnectionTester.testConnection(url);
|
99
|
+
|
100
|
+
if (result.success) {
|
101
|
+
return { success: true };
|
102
|
+
} else {
|
103
|
+
throw new Error(result.message || 'Connection test failed');
|
104
|
+
}
|
105
|
+
} catch (error) {
|
106
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
107
|
+
logger.error('Proxy connection test failed:', errorMessage);
|
108
|
+
throw new Error(`Connection failed: ${errorMessage}`);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* 测试指定代理配置
|
114
|
+
*/
|
115
|
+
@ipcClientEvent('testProxyConfig')
|
116
|
+
async testProxyConfig({
|
117
|
+
config,
|
118
|
+
testUrl,
|
119
|
+
}: {
|
120
|
+
config: NetworkProxySettings;
|
121
|
+
testUrl?: string;
|
122
|
+
}): Promise<ProxyTestResult> {
|
123
|
+
try {
|
124
|
+
return await ProxyConnectionTester.testProxyConfig(config, testUrl);
|
125
|
+
} catch (error) {
|
126
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
127
|
+
logger.error('Proxy config test failed:', errorMessage);
|
128
|
+
return {
|
129
|
+
message: `Proxy config test failed: ${errorMessage}`,
|
130
|
+
success: false,
|
131
|
+
};
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
/**
|
136
|
+
* 应用初始代理设置
|
137
|
+
*/
|
138
|
+
async afterAppReady(): Promise<void> {
|
139
|
+
try {
|
140
|
+
// 获取存储的代理设置
|
141
|
+
const networkProxy = this.app.storeManager.get(
|
142
|
+
'networkProxy',
|
143
|
+
defaultProxySettings,
|
144
|
+
) as NetworkProxySettings;
|
145
|
+
|
146
|
+
// 验证配置
|
147
|
+
const validation = ProxyConfigValidator.validate(networkProxy);
|
148
|
+
if (!validation.isValid) {
|
149
|
+
logger.warn('Invalid stored proxy configuration, using defaults:', validation.errors);
|
150
|
+
await ProxyDispatcherManager.applyProxySettings(defaultProxySettings);
|
151
|
+
return;
|
152
|
+
}
|
153
|
+
|
154
|
+
// 应用代理设置
|
155
|
+
await ProxyDispatcherManager.applyProxySettings(networkProxy);
|
156
|
+
|
157
|
+
logger.info('Initial proxy settings applied successfully', {
|
158
|
+
enableProxy: networkProxy.enableProxy,
|
159
|
+
proxyType: networkProxy.proxyType,
|
160
|
+
});
|
161
|
+
} catch (error) {
|
162
|
+
logger.error('Failed to apply initial proxy settings:', error);
|
163
|
+
// 出错时使用默认设置
|
164
|
+
try {
|
165
|
+
await ProxyDispatcherManager.applyProxySettings(defaultProxySettings);
|
166
|
+
logger.info('Fallback to default proxy settings');
|
167
|
+
} catch (fallbackError) {
|
168
|
+
logger.error('Failed to apply fallback proxy settings:', fallbackError);
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|
@@ -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
|
};
|