diff --git a/package.json b/package.json index b7204d48f5..70abb10b54 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,7 @@ "eslint-plugin-unused-imports": "^4.1.4", "fast-diff": "^1.3.0", "fast-xml-parser": "^5.2.0", + "fetch-socks": "1.3.2", "franc-min": "^6.2.0", "fs-extra": "^11.2.0", "google-auth-library": "^9.15.1", @@ -229,6 +230,7 @@ "tiny-pinyin": "^1.3.2", "tokenx": "^1.1.0", "typescript": "^5.6.2", + "undici": "7.10.0", "unified": "^11.0.5", "uuid": "^10.0.0", "vite": "6.2.6", diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 88c038a1fa..37abf38dff 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom' import { UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types' -import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webContents } from 'electron' +import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' import log from 'electron-log' import { Notification } from 'src/renderer/src/types/notification' @@ -27,7 +27,7 @@ import MemoryService from './services/memory/MemoryService' import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' -import { ProxyConfig, proxyManager } from './services/ProxyManager' +import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' import { FileServiceManager } from './services/remotefile/FileServiceManager' import { searchService } from './services/SearchService' @@ -78,9 +78,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { if (proxy === 'system') { proxyConfig = { mode: 'system' } } else if (proxy) { - proxyConfig = { mode: 'custom', url: proxy } + proxyConfig = { mode: 'fixed_servers', proxyRules: proxy } } else { - proxyConfig = { mode: 'none' } + proxyConfig = { mode: 'direct' } } await proxyManager.configureProxy(proxyConfig) diff --git a/src/main/knowledge/reranker/GeneralReranker.ts b/src/main/knowledge/reranker/GeneralReranker.ts index 185e2132c7..1252ecad57 100644 --- a/src/main/knowledge/reranker/GeneralReranker.ts +++ b/src/main/knowledge/reranker/GeneralReranker.ts @@ -1,6 +1,6 @@ import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' -import AxiosProxy from '@main/services/AxiosProxy' import { KnowledgeBaseParams } from '@types' +import axios from 'axios' import BaseReranker from './BaseReranker' @@ -15,7 +15,7 @@ export default class GeneralReranker extends BaseReranker { const requestBody = this.getRerankRequestBody(query, searchResults) try { - const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() }) + const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) const rerankResults = this.extractRerankResult(data) return this.getRerankResult(searchResults, rerankResults) diff --git a/src/main/services/AxiosProxy.ts b/src/main/services/AxiosProxy.ts deleted file mode 100644 index 6f767bd3a2..0000000000 --- a/src/main/services/AxiosProxy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AxiosInstance, default as axios_ } from 'axios' -import { ProxyAgent } from 'proxy-agent' - -import { proxyManager } from './ProxyManager' - -class AxiosProxy { - private cacheAxios: AxiosInstance | null = null - private proxyAgent: ProxyAgent | null = null - - get axios(): AxiosInstance { - const currentProxyAgent = proxyManager.getProxyAgent() - - // 如果代理发生变化或尚未初始化,则重新创建 axios 实例 - if (this.cacheAxios === null || (currentProxyAgent !== null && this.proxyAgent !== currentProxyAgent)) { - this.proxyAgent = currentProxyAgent - - // 创建带有代理配置的 axios 实例 - this.cacheAxios = axios_.create({ - proxy: false, - httpAgent: currentProxyAgent || undefined, - httpsAgent: currentProxyAgent || undefined - }) - } - - return this.cacheAxios - } -} - -export default new AxiosProxy() diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index a10e7521eb..cf3185ad70 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -25,7 +25,8 @@ export enum ConfigKeys { SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize', SelectionAssistantFilterMode = 'selectionAssistantFilterMode', SelectionAssistantFilterList = 'selectionAssistantFilterList', - DisableHardwareAcceleration = 'disableHardwareAcceleration' + DisableHardwareAcceleration = 'disableHardwareAcceleration', + Proxy = 'proxy' } export class ConfigManager { diff --git a/src/main/services/CopilotService.ts b/src/main/services/CopilotService.ts index 0be9ee8a5e..bc331a4468 100644 --- a/src/main/services/CopilotService.ts +++ b/src/main/services/CopilotService.ts @@ -1,11 +1,10 @@ import { AxiosRequestConfig } from 'axios' +import axios from 'axios' import { app, safeStorage } from 'electron' import Logger from 'electron-log' import fs from 'fs/promises' import path from 'path' -import aoxisProxy from './AxiosProxy' - // 配置常量,集中管理 const CONFIG = { GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98', @@ -96,7 +95,7 @@ class CopilotService { } } - const response = await aoxisProxy.axios.get(CONFIG.API_URLS.GITHUB_USER, config) + const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config) return { login: response.data.login, avatar: response.data.avatar_url @@ -117,7 +116,7 @@ class CopilotService { try { this.updateHeaders(headers) - const response = await aoxisProxy.axios.post( + const response = await axios.post( CONFIG.API_URLS.GITHUB_DEVICE_CODE, { client_id: CONFIG.GITHUB_CLIENT_ID, @@ -149,7 +148,7 @@ class CopilotService { await this.delay(currentDelay) try { - const response = await aoxisProxy.axios.post( + const response = await axios.post( CONFIG.API_URLS.GITHUB_ACCESS_TOKEN, { client_id: CONFIG.GITHUB_CLIENT_ID, @@ -211,7 +210,7 @@ class CopilotService { } } - const response = await aoxisProxy.axios.get(CONFIG.API_URLS.COPILOT_TOKEN, config) + const response = await axios.get(CONFIG.API_URLS.COPILOT_TOKEN, config) return response.data } catch (error) { diff --git a/src/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts index 3a4aa09438..a2936e37dc 100644 --- a/src/main/services/ProxyManager.ts +++ b/src/main/services/ProxyManager.ts @@ -1,38 +1,54 @@ -import { ProxyConfig as _ProxyConfig, session } from 'electron' +import axios from 'axios' +import { app, ProxyConfig, session } from 'electron' +import Logger from 'electron-log' +import { socksDispatcher } from 'fetch-socks' +import http from 'http' +import https from 'https' import { getSystemProxy } from 'os-proxy-config' -import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent' -// import { ProxyAgent, setGlobalDispatcher } from 'undici' - -type ProxyMode = 'system' | 'custom' | 'none' - -export interface ProxyConfig { - mode: ProxyMode - url?: string -} +import { ProxyAgent } from 'proxy-agent' +import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici' export class ProxyManager { - private config: ProxyConfig - private proxyAgent: GeneralProxyAgent | null = null + private config: ProxyConfig = { mode: 'direct' } private systemProxyInterval: NodeJS.Timeout | null = null + private isSettingProxy = false + + private originalGlobalDispatcher: Dispatcher + private originalSocksDispatcher: Dispatcher + // for http and https + private originalHttpGet: typeof http.get + private originalHttpRequest: typeof http.request + private originalHttpsGet: typeof https.get + private originalHttpsRequest: typeof https.request constructor() { - this.config = { - mode: 'none' - } - } - - private async setSessionsProxy(config: _ProxyConfig): Promise { - const sessions = [session.defaultSession, session.fromPartition('persist:webview')] - await Promise.all(sessions.map((session) => session.setProxy(config))) + this.originalGlobalDispatcher = getGlobalDispatcher() + this.originalSocksDispatcher = global[Symbol.for('undici.globalDispatcher.1')] + this.originalHttpGet = http.get + this.originalHttpRequest = http.request + this.originalHttpsGet = https.get + this.originalHttpsRequest = https.request } private async monitorSystemProxy(): Promise { // Clear any existing interval first this.clearSystemProxyMonitor() // Set new interval - this.systemProxyInterval = setInterval(async () => { - await this.setSystemProxy() - }, 10000) + this.systemProxyInterval = setInterval( + async () => { + const currentProxy = await getSystemProxy() + if (currentProxy && currentProxy.proxyUrl.toLowerCase() === this.config.proxyRules) { + return + } + + await this.configureProxy({ + mode: 'system', + proxyRules: currentProxy?.proxyUrl.toLowerCase() + }) + }, + // 1 minutes + 1000 * 60 + ) } private clearSystemProxyMonitor(): void { @@ -43,99 +59,182 @@ export class ProxyManager { } async configureProxy(config: ProxyConfig): Promise { + Logger.info('configureProxy', config.mode, config.proxyRules) + if (this.isSettingProxy) { + return + } + + this.isSettingProxy = true + try { + if (config?.mode === this.config?.mode && config?.proxyRules === this.config?.proxyRules) { + Logger.info('proxy config is the same, skip configure') + return + } + this.config = config this.clearSystemProxyMonitor() - if (this.config.mode === 'system') { - await this.setSystemProxy() - this.monitorSystemProxy() - } else if (this.config.mode === 'custom') { - await this.setCustomProxy() - } else { - await this.clearProxy() + if (config.mode === 'system') { + const currentProxy = await getSystemProxy() + if (currentProxy) { + Logger.info('current system proxy', currentProxy.proxyUrl) + this.config.proxyRules = currentProxy.proxyUrl.toLowerCase() + this.monitorSystemProxy() + } else { + // no system proxy, use direct mode + this.config.mode = 'direct' + } } + + this.setGlobalProxy() } catch (error) { - console.error('Failed to config proxy:', error) + Logger.error('Failed to config proxy:', error) throw error + } finally { + this.isSettingProxy = false } } private setEnvironment(url: string): void { + if (url === '') { + delete process.env.HTTP_PROXY + delete process.env.HTTPS_PROXY + delete process.env.grpc_proxy + delete process.env.http_proxy + delete process.env.https_proxy + + delete process.env.SOCKS_PROXY + delete process.env.ALL_PROXY + return + } + process.env.grpc_proxy = url process.env.HTTP_PROXY = url process.env.HTTPS_PROXY = url process.env.http_proxy = url process.env.https_proxy = url - } - private async setSystemProxy(): Promise { - try { - const currentProxy = await getSystemProxy() - if (!currentProxy || currentProxy.proxyUrl === this.config.url) { - return - } - await this.setSessionsProxy({ mode: 'system' }) - this.config.url = currentProxy.proxyUrl.toLowerCase() - this.setEnvironment(this.config.url) - this.proxyAgent = new GeneralProxyAgent() - } catch (error) { - console.error('Failed to set system proxy:', error) - throw error + if (url.startsWith('socks')) { + process.env.SOCKS_PROXY = url + process.env.ALL_PROXY = url } } - private async setCustomProxy(): Promise { - try { - if (this.config.url) { - this.setEnvironment(this.config.url) - this.proxyAgent = new GeneralProxyAgent() - await this.setSessionsProxy({ proxyRules: this.config.url }) + private setGlobalProxy() { + this.setEnvironment(this.config.proxyRules || '') + this.setGlobalFetchProxy(this.config) + this.setSessionsProxy(this.config) + + this.setGlobalHttpProxy(this.config) + } + + private setGlobalHttpProxy(config: ProxyConfig) { + const proxyUrl = config.proxyRules + if (config.mode === 'direct' || !proxyUrl) { + http.get = this.originalHttpGet + http.request = this.originalHttpRequest + https.get = this.originalHttpsGet + https.request = this.originalHttpsRequest + + axios.defaults.proxy = undefined + axios.defaults.httpAgent = undefined + axios.defaults.httpsAgent = undefined + return + } + + // ProxyAgent 从环境变量读取代理配置 + const agent = new ProxyAgent() + + // axios 使用代理 + axios.defaults.proxy = false + axios.defaults.httpAgent = agent + axios.defaults.httpsAgent = agent + + http.get = this.bindHttpMethod(this.originalHttpGet, agent) + http.request = this.bindHttpMethod(this.originalHttpRequest, agent) + + https.get = this.bindHttpMethod(this.originalHttpsGet, agent) + https.request = this.bindHttpMethod(this.originalHttpsRequest, agent) + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + private bindHttpMethod(originalMethod: Function, agent: http.Agent | https.Agent) { + return (...args: any[]) => { + let url: string | URL | undefined + let options: http.RequestOptions | https.RequestOptions + let callback: (res: http.IncomingMessage) => void + + if (typeof args[0] === 'string' || args[0] instanceof URL) { + url = args[0] + if (typeof args[1] === 'function') { + options = {} + callback = args[1] + } else { + options = { + ...args[1] + } + callback = args[2] + } + } else { + options = { + ...args[0] + } + callback = args[1] } - } catch (error) { - console.error('Failed to set custom proxy:', error) - throw error + + // for webdav https self-signed certificate + if (options.agent instanceof https.Agent) { + ;(agent as https.Agent).options.rejectUnauthorized = options.agent.options.rejectUnauthorized + } + + // 确保只设置 agent,不修改其他网络选项 + if (!options.agent) { + options.agent = agent + } + + if (url) { + return originalMethod(url, options, callback) + } + return originalMethod(options, callback) } } - private clearEnvironment(): void { - delete process.env.HTTP_PROXY - delete process.env.HTTPS_PROXY - delete process.env.grpc_proxy - delete process.env.http_proxy - delete process.env.https_proxy + private setGlobalFetchProxy(config: ProxyConfig) { + const proxyUrl = config.proxyRules + if (config.mode === 'direct' || !proxyUrl) { + setGlobalDispatcher(this.originalGlobalDispatcher) + global[Symbol.for('undici.globalDispatcher.1')] = this.originalSocksDispatcher + return + } + + const url = new URL(proxyUrl) + if (url.protocol === 'http:' || url.protocol === 'https:') { + setGlobalDispatcher(new EnvHttpProxyAgent()) + return + } + + global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({ + port: parseInt(url.port), + type: url.protocol === 'socks4:' ? 4 : 5, + host: url.hostname, + userId: url.username || undefined, + password: url.password || undefined + }) } - private async clearProxy(): Promise { - this.clearEnvironment() - await this.setSessionsProxy({ mode: 'direct' }) - this.config = { mode: 'none' } - this.proxyAgent = null - } + private async setSessionsProxy(config: ProxyConfig): Promise { + let c = config - getProxyAgent(): GeneralProxyAgent | null { - return this.proxyAgent - } + if (config.mode === 'direct' || !config.proxyRules) { + c = { mode: 'direct' } + } - getProxyUrl(): string { - return this.config.url || '' - } + const sessions = [session.defaultSession, session.fromPartition('persist:webview')] + await Promise.all(sessions.map((session) => session.setProxy(c))) - // setGlobalProxy() { - // const proxyUrl = this.config.url - // if (proxyUrl) { - // const [protocol, address] = proxyUrl.split('://') - // const [host, port] = address.split(':') - // if (!protocol.includes('socks')) { - // setGlobalDispatcher(new ProxyAgent(proxyUrl)) - // } else { - // global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({ - // port: parseInt(port), - // type: protocol === 'socks5' ? 5 : 4, - // host: host - // }) - // } - // } - // } + // set proxy for electron + app.setProxy(c) + } } export const proxyManager = new ProxyManager() diff --git a/src/main/services/WebDav.ts b/src/main/services/WebDav.ts index fae0e2da38..76996140e0 100644 --- a/src/main/services/WebDav.ts +++ b/src/main/services/WebDav.ts @@ -23,7 +23,9 @@ export default class WebDav { password: params.webdavPass, maxBodyLength: Infinity, maxContentLength: Infinity, - httpsAgent: new https.Agent({ rejectUnauthorized: false }) + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }) }) this.putFileContents = this.putFileContents.bind(this) diff --git a/yarn.lock b/yarn.lock index 6b32f3cabe..158586d777 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7182,6 +7182,7 @@ __metadata: eslint-plugin-unused-imports: "npm:^4.1.4" fast-diff: "npm:^1.3.0" fast-xml-parser: "npm:^5.2.0" + fetch-socks: "npm:1.3.2" franc-min: "npm:^6.2.0" fs-extra: "npm:^11.2.0" google-auth-library: "npm:^9.15.1" @@ -7245,6 +7246,7 @@ __metadata: tokenx: "npm:^1.1.0" turndown: "npm:7.2.0" typescript: "npm:^5.6.2" + undici: "npm:7.10.0" unified: "npm:^11.0.5" uuid: "npm:^10.0.0" vite: "npm:6.2.6" @@ -11312,6 +11314,16 @@ __metadata: languageName: node linkType: hard +"fetch-socks@npm:1.3.2": + version: 1.3.2 + resolution: "fetch-socks@npm:1.3.2" + dependencies: + socks: "npm:^2.8.2" + undici: "npm:>=6" + checksum: 10c0/6a3f20142c82d3eaef0bfe6b53a0af61381ffbe8bfeb1fdfe5c285c863f9648159ba5ab9b771fac6d3c726e0b894ba52e1069947de0ec97dc287645b40e5d24c + languageName: node + linkType: hard + "fflate@npm:0.8.1": version: 0.8.1 resolution: "fflate@npm:0.8.1" @@ -18578,6 +18590,16 @@ __metadata: languageName: node linkType: hard +"socks@npm:^2.8.2": + version: 2.8.6 + resolution: "socks@npm:2.8.6" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/15b95db4caa359c80bfa880ff3e58f3191b9ffa4313570e501a60ee7575f51e4be664a296f4ee5c2c40544da179db6140be53433ce41ec745f9d51f342557514 + languageName: node + linkType: hard + "source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -19597,6 +19619,20 @@ __metadata: languageName: node linkType: hard +"undici@npm:7.10.0": + version: 7.10.0 + resolution: "undici@npm:7.10.0" + checksum: 10c0/756ac876a8df845bc89eb8348c35d33a0ff63c17eb45b664075c961a7fbd4a398f94f9dce438262f55fe66e4bbb0a46aa63a3fd58ce51361c616aff11a270450 + languageName: node + linkType: hard + +"undici@npm:>=6": + version: 7.11.0 + resolution: "undici@npm:7.11.0" + checksum: 10c0/e5dd3cc2acae9c8333f97a78d4e91108957367fa7e69918e3a5cbd84702cb453cf7de3f8c2a33bcf808850d78ead70f3bd62900a70d969912e9fed8842bbfc11 + languageName: node + linkType: hard + "unified@npm:^11.0.0, unified@npm:^11.0.5": version: 11.0.5 resolution: "unified@npm:11.0.5"