diff --git a/electron/main/index.ts b/electron/main/index.ts index 7539482..93af293 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -5,6 +5,7 @@ import * as fs from 'fs/promises' import { checkUpdate } from './update' import mainIpcMain from './ipcMain' import { initAnalytics, trackDailyActive } from './analytics' +import { initProxy } from './network/proxy' class MainProcess { mainWindow: BrowserWindow | null @@ -46,6 +47,7 @@ class MainProcess { // 初始化程序 async init() { initAnalytics() + initProxy() // 初始化代理配置 // 注册应用协议 app.setAsDefaultProtocolClient('chatlab') diff --git a/electron/main/ipc/network.ts b/electron/main/ipc/network.ts new file mode 100644 index 0000000..1daec17 --- /dev/null +++ b/electron/main/ipc/network.ts @@ -0,0 +1,65 @@ +/** + * 网络设置 IPC 处理器 + * 处理代理配置的读取、保存和测试 + */ + +import { ipcMain } from 'electron' +import type { IpcContext } from './types' +import { + loadProxyConfig, + saveProxyConfig, + testProxyConnection, + validateProxyUrl, + type ProxyConfig, +} from '../network/proxy' + +/** + * 注册网络设置相关的 IPC 处理器 + */ +export function registerNetworkHandlers(_context: IpcContext): void { + console.log('[IpcMain] Registering network handlers...') + + /** + * 获取代理配置 + */ + ipcMain.handle('network:getProxyConfig', (): ProxyConfig => { + return loadProxyConfig() + }) + + /** + * 保存代理配置 + */ + ipcMain.handle( + 'network:saveProxyConfig', + (_event, config: ProxyConfig): { success: boolean; error?: string } => { + try { + // 如果启用了代理,验证 URL 格式 + if (config.enabled && config.url) { + const validation = validateProxyUrl(config.url) + if (!validation.valid) { + return { success: false, error: validation.error } + } + } + + saveProxyConfig(config) + return { success: true } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { success: false, error: `保存配置失败: ${errorMessage}` } + } + } + ) + + /** + * 测试代理连接 + */ + ipcMain.handle( + 'network:testProxyConnection', + async (_event, proxyUrl: string): Promise<{ success: boolean; error?: string }> => { + return testProxyConnection(proxyUrl) + } + ) + + console.log('[IpcMain] Network handlers registered') +} + diff --git a/electron/main/ipcMain.ts b/electron/main/ipcMain.ts index a4de436..4463c39 100644 --- a/electron/main/ipcMain.ts +++ b/electron/main/ipcMain.ts @@ -12,6 +12,7 @@ import { registerMergeHandlers, initMergeModule } from './ipc/merge' import { registerAIHandlers } from './ipc/ai' import { registerMessagesHandlers } from './ipc/messages' import { registerCacheHandlers } from './ipc/cache' +import { registerNetworkHandlers } from './ipc/network' import { registerAnalyticsHandlers } from './analytics' // 导入 Worker 模块(用于异步分析查询和流式导入) import * as worker from './worker/workerManager' @@ -43,6 +44,7 @@ const mainIpcMain = (win: BrowserWindow) => { registerAIHandlers(context) registerMessagesHandlers(context) registerCacheHandlers(context) + registerNetworkHandlers(context) registerAnalyticsHandlers() console.log('[IpcMain] All IPC handlers registered successfully') diff --git a/electron/main/network/index.ts b/electron/main/network/index.ts new file mode 100644 index 0000000..20b1af3 --- /dev/null +++ b/electron/main/network/index.ts @@ -0,0 +1,6 @@ +/** + * 网络模块入口 + * 导出代理配置功能 + */ + +export * from './proxy' diff --git a/electron/main/network/proxy.ts b/electron/main/network/proxy.ts new file mode 100644 index 0000000..8bb8be6 --- /dev/null +++ b/electron/main/network/proxy.ts @@ -0,0 +1,252 @@ +/** + * 代理配置管理模块 + * 提供 HTTP/HTTPS 代理的配置存储、读取和连接测试 + */ + +import * as fs from 'fs' +import * as path from 'path' +import { app, session } from 'electron' + +// 代理配置接口 +export interface ProxyConfig { + enabled: boolean + url: string // 完整的代理 URL,如 http://127.0.0.1:7890 +} + +// 默认配置 +const DEFAULT_CONFIG: ProxyConfig = { + enabled: false, + url: '', +} + +// 配置文件路径 +let CONFIG_PATH: string | null = null + +function getConfigPath(): string { + if (CONFIG_PATH) return CONFIG_PATH + + try { + const docPath = app.getPath('documents') + CONFIG_PATH = path.join(docPath, 'ChatLab', 'settings', 'proxy.json') + } catch { + CONFIG_PATH = path.join(process.cwd(), 'settings', 'proxy.json') + } + + return CONFIG_PATH +} + +/** + * 加载代理配置 + */ +export function loadProxyConfig(): ProxyConfig { + const configPath = getConfigPath() + + if (!fs.existsSync(configPath)) { + return { ...DEFAULT_CONFIG } + } + + try { + const content = fs.readFileSync(configPath, 'utf-8') + const data = JSON.parse(content) + return { + enabled: Boolean(data.enabled), + url: String(data.url || ''), + } + } catch { + return { ...DEFAULT_CONFIG } + } +} + +/** + * 保存代理配置 + */ +export function saveProxyConfig(config: ProxyConfig): void { + const configPath = getConfigPath() + const dir = path.dirname(configPath) + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8') + + // 保存后立即应用代理设置到 Electron session + applyProxyToSession() +} + +/** + * 验证代理 URL 格式 + */ +export function validateProxyUrl(url: string): { valid: boolean; error?: string } { + if (!url) { + return { valid: false, error: '代理地址不能为空' } + } + + try { + const parsed = new URL(url) + if (!['http:', 'https:'].includes(parsed.protocol)) { + return { valid: false, error: '仅支持 http:// 或 https:// 协议' } + } + if (!parsed.hostname) { + return { valid: false, error: '代理地址格式无效' } + } + return { valid: true } + } catch { + return { valid: false, error: '代理地址格式无效,请使用 http://host:port 格式' } + } +} + +/** + * 将代理设置应用到 Electron session + * 这会影响所有通过 Electron 发起的网络请求(包括主进程的 fetch) + */ +export async function applyProxyToSession(): Promise { + const config = loadProxyConfig() + + try { + if (config.enabled && config.url) { + const validation = validateProxyUrl(config.url) + if (validation.valid) { + // 设置代理规则 + await session.defaultSession.setProxy({ + proxyRules: config.url, + }) + console.log(`[Proxy] 代理已启用: ${config.url}`) + } + } else { + // 清除代理设置 + await session.defaultSession.setProxy({ + proxyRules: '', + }) + console.log('[Proxy] 代理已禁用') + } + } catch (error) { + console.error('[Proxy] 设置代理失败:', error) + } +} + +/** + * 测试代理连接 + * 通过代理请求一个可靠的 HTTPS 地址来验证代理是否可用 + */ +export async function testProxyConnection(proxyUrl: string): Promise<{ success: boolean; error?: string }> { + // 先验证格式 + const validation = validateProxyUrl(proxyUrl) + if (!validation.valid) { + return { success: false, error: validation.error } + } + + // 测试 URL 列表(按优先级) + const testUrls = [ + 'https://www.google.com', + 'https://www.cloudflare.com', + 'https://api.deepseek.com', + ] + + try { + // 临时设置代理 + await session.defaultSession.setProxy({ + proxyRules: proxyUrl, + }) + + // 使用 Electron 的 net 模块测试连接 + const { net } = await import('electron') + + let lastError: string = '' + + for (const testUrl of testUrls) { + try { + const result = await new Promise<{ success: boolean; error?: string }>((resolve) => { + const request = net.request({ + method: 'HEAD', + url: testUrl, + }) + + const timeout = setTimeout(() => { + request.abort() + resolve({ success: false, error: '连接超时' }) + }, 10000) + + request.on('response', (response) => { + clearTimeout(timeout) + // 任何响应都说明代理可用 + if (response.statusCode < 500) { + resolve({ success: true }) + } else { + resolve({ success: false, error: `HTTP ${response.statusCode}` }) + } + }) + + request.on('error', (error) => { + clearTimeout(timeout) + resolve({ success: false, error: error.message }) + }) + + request.end() + }) + + if (result.success) { + // 恢复之前的代理设置 + await applyProxyToSession() + return { success: true } + } + + lastError = result.error || '' + } catch (e) { + lastError = e instanceof Error ? e.message : String(e) + } + } + + // 恢复之前的代理设置 + await applyProxyToSession() + return { success: false, error: lastError || '无法通过代理连接到测试服务器' } + } catch (error) { + // 恢复之前的代理设置 + await applyProxyToSession() + + const errorMessage = error instanceof Error ? error.message : String(error) + + // 友好的错误提示 + if (errorMessage.includes('ECONNREFUSED')) { + return { success: false, error: '连接被拒绝,请检查代理服务器是否运行中' } + } + if (errorMessage.includes('ETIMEDOUT') || errorMessage.includes('timeout')) { + return { success: false, error: '连接超时,请检查代理地址和端口' } + } + if (errorMessage.includes('ENOTFOUND')) { + return { success: false, error: '无法解析代理服务器地址' } + } + + return { success: false, error: `代理连接失败: ${errorMessage}` } + } +} + +/** + * 获取当前有效的代理 URL + * 如果代理已启用且 URL 有效,返回代理 URL,否则返回 undefined + */ +export function getActiveProxyUrl(): string | undefined { + const config = loadProxyConfig() + if (config.enabled && config.url) { + const validation = validateProxyUrl(config.url) + if (validation.valid) { + return config.url + } + } + return undefined +} + +/** + * 初始化代理模块 + * 应用启动时调用,加载并应用代理配置 + */ +export function initProxy(): void { + // 延迟执行,确保 app ready + if (app.isReady()) { + applyProxyToSession() + } else { + app.whenReady().then(() => { + applyProxyToSession() + }) + } +} diff --git a/electron/main/update.ts b/electron/main/update.ts index d228128..30965b5 100644 --- a/electron/main/update.ts +++ b/electron/main/update.ts @@ -2,9 +2,32 @@ import { dialog, app } from 'electron' import { autoUpdater } from 'electron-updater' import { platform } from '@electron-toolkit/utils' import { logger } from './logger' +import { getActiveProxyUrl } from './network/proxy' + +/** + * 配置自动更新的代理设置 + * electron-updater 通过环境变量读取代理配置 + */ +function configureUpdateProxy(): void { + const proxyUrl = getActiveProxyUrl() + + if (proxyUrl) { + // 设置环境变量,electron-updater 会自动读取 + process.env.HTTPS_PROXY = proxyUrl + process.env.HTTP_PROXY = proxyUrl + logger.info(`[Update] 使用代理: ${proxyUrl}`) + } else { + // 清除代理环境变量 + delete process.env.HTTPS_PROXY + delete process.env.HTTP_PROXY + } +} let isFirstShow = true const checkUpdate = (win) => { + // 配置代理 + configureUpdateProxy() + autoUpdater.autoDownload = false // 自动下载 autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装 diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 655bab0..ce457b2 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -382,6 +382,18 @@ interface CacheApi { ) => Promise<{ success: boolean; filePath?: string; error?: string }> } +// Network API 类型 - 网络代理配置 +interface ProxyConfig { + enabled: boolean + url: string +} + +interface NetworkApi { + getProxyConfig: () => Promise + saveProxyConfig: (config: ProxyConfig) => Promise<{ success: boolean; error?: string }> + testProxyConnection: (proxyUrl: string) => Promise<{ success: boolean; error?: string }> +} + declare global { interface Window { electron: ElectronAPI @@ -392,6 +404,7 @@ declare global { llmApi: LlmApi agentApi: AgentApi cacheApi: CacheApi + networkApi: NetworkApi } } @@ -403,11 +416,12 @@ export { LlmApi, AgentApi, CacheApi, + NetworkApi, + ProxyConfig, SearchMessageResult, AIConversation, AIMessage, LLMProviderInfo, - LLMConfig, AIServiceConfigDisplay, LLMChatMessage, LLMChatOptions, diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 53a461f..2a3f22b 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -935,6 +935,35 @@ const agentApi = { }, } +// Network API - 网络设置 +interface ProxyConfig { + enabled: boolean + url: string +} + +const networkApi = { + /** + * 获取代理配置 + */ + getProxyConfig: (): Promise => { + return ipcRenderer.invoke('network:getProxyConfig') + }, + + /** + * 保存代理配置 + */ + saveProxyConfig: (config: ProxyConfig): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('network:saveProxyConfig', config) + }, + + /** + * 测试代理连接 + */ + testProxyConnection: (proxyUrl: string): Promise<{ success: boolean; error?: string }> => { + return ipcRenderer.invoke('network:testProxyConnection', proxyUrl) + }, +} + // Cache API - 缓存管理 interface CacheDirectoryInfo { id: string @@ -1057,6 +1086,7 @@ if (process.contextIsolated) { contextBridge.exposeInMainWorld('llmApi', llmApi) contextBridge.exposeInMainWorld('agentApi', agentApi) contextBridge.exposeInMainWorld('cacheApi', cacheApi) + contextBridge.exposeInMainWorld('networkApi', networkApi) } catch (error) { console.error(error) } @@ -1077,4 +1107,6 @@ if (process.contextIsolated) { window.agentApi = agentApi // @ts-ignore (define in dts) window.cacheApi = cacheApi + // @ts-ignore (define in dts) + window.networkApi = networkApi } diff --git a/src/components/common/settings/AboutTab.vue b/src/components/common/settings/AboutTab.vue index a5532f1..b10c02d 100644 --- a/src/components/common/settings/AboutTab.vue +++ b/src/components/common/settings/AboutTab.vue @@ -89,15 +89,6 @@ onMounted(() => { - - diff --git a/src/components/common/settings/BasicSettingsTab.vue b/src/components/common/settings/BasicSettingsTab.vue index cc59905..758d334 100644 --- a/src/components/common/settings/BasicSettingsTab.vue +++ b/src/components/common/settings/BasicSettingsTab.vue @@ -2,6 +2,7 @@ import { storeToRefs } from 'pinia' import { useLayoutStore } from '@/stores/layout' import { useColorMode } from '@vueuse/core' +import NetworkSettingsSection from './NetworkSettingsSection.vue' // Store const layoutStore = useLayoutStore() @@ -56,5 +57,8 @@ const colorModeOptions = [ + + + diff --git a/src/components/common/settings/NetworkSettingsSection.vue b/src/components/common/settings/NetworkSettingsSection.vue new file mode 100644 index 0000000..50e2325 --- /dev/null +++ b/src/components/common/settings/NetworkSettingsSection.vue @@ -0,0 +1,210 @@ + + + +