feat: 支持配置代理(resolve #7)

This commit is contained in:
digua
2025-12-28 16:43:47 +08:00
parent 59b09a3dce
commit 1c638b1f10
11 changed files with 611 additions and 10 deletions
+2
View File
@@ -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')
+65
View File
@@ -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')
}
+2
View File
@@ -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')
+6
View File
@@ -0,0 +1,6 @@
/**
* 网络模块入口
* 导出代理配置功能
*/
export * from './proxy'
+252
View File
@@ -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<void> {
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()
})
}
}
+23
View File
@@ -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 // 应用退出后自动安装