feat(Proxy): Implement proxy management system
- Add ProxyManager service to handle system, custom, and no proxy configurations - Integrate proxy support for Gemini, Knowledge, and WebDav services - Add fetch-socks and undici for advanced proxy handling - Enhance proxy configuration with environment variable and session management
This commit is contained in:
parent
89f1de4df4
commit
b9d97e8a35
@ -78,11 +78,13 @@
|
|||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"epub": "^1.3.0",
|
"epub": "^1.3.0",
|
||||||
|
"fetch-socks": "^1.3.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"tokenx": "^0.4.1",
|
"tokenx": "^0.4.1",
|
||||||
|
"undici": "^7.4.0",
|
||||||
"webdav": "4.11.4"
|
"webdav": "4.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
|
||||||
import { MCPServer, Shortcut, ThemeMode } from '@types'
|
import { MCPServer, Shortcut, ThemeMode } from '@types'
|
||||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
|
|
||||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
@ -14,6 +14,7 @@ import FileStorage from './services/FileStorage'
|
|||||||
import { GeminiService } from './services/GeminiService'
|
import { GeminiService } from './services/GeminiService'
|
||||||
import KnowledgeService from './services/KnowledgeService'
|
import KnowledgeService from './services/KnowledgeService'
|
||||||
import MCPService from './services/mcp'
|
import MCPService from './services/mcp'
|
||||||
|
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
@ -41,9 +42,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
||||||
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
const proxyConfig: ProxyConfig =
|
||||||
const proxyConfig: ProxyConfig = proxy === 'system' ? { mode: 'system' } : proxy ? { proxyRules: proxy } : {}
|
proxy === 'system' ? { mode: 'system' } : proxy ? { mode: 'custom', url: proxy } : { mode: 'none' }
|
||||||
await Promise.all(sessions.map((session) => session.setProxy(proxyConfig)))
|
await proxyManager.configureProxy(proxyConfig)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import { FileType } from '@types'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
import { CacheService } from './CacheService'
|
import { CacheService } from './CacheService'
|
||||||
|
import { proxyManager } from './ProxyManager'
|
||||||
|
|
||||||
export class GeminiService {
|
export class GeminiService {
|
||||||
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
||||||
private static readonly CACHE_DURATION = 3000
|
private static readonly CACHE_DURATION = 3000
|
||||||
|
|
||||||
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
|
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
|
||||||
|
proxyManager.setGlobalProxy()
|
||||||
const fileManager = new GoogleAIFileManager(apiKey)
|
const fileManager = new GoogleAIFileManager(apiKey)
|
||||||
const uploadResult = await fileManager.uploadFile(file.path, {
|
const uploadResult = await fileManager.uploadFile(file.path, {
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
@ -29,6 +31,7 @@ export class GeminiService {
|
|||||||
file: FileType,
|
file: FileType,
|
||||||
apiKey: string
|
apiKey: string
|
||||||
): Promise<FileMetadataResponse | undefined> {
|
): Promise<FileMetadataResponse | undefined> {
|
||||||
|
proxyManager.setGlobalProxy()
|
||||||
const fileManager = new GoogleAIFileManager(apiKey)
|
const fileManager = new GoogleAIFileManager(apiKey)
|
||||||
|
|
||||||
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
||||||
@ -52,11 +55,13 @@ export class GeminiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
|
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
|
||||||
|
proxyManager.setGlobalProxy()
|
||||||
const fileManager = new GoogleAIFileManager(apiKey)
|
const fileManager = new GoogleAIFileManager(apiKey)
|
||||||
return await fileManager.listFiles()
|
return await fileManager.listFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
||||||
|
proxyManager.setGlobalProxy()
|
||||||
const fileManager = new GoogleAIFileManager(apiKey)
|
const fileManager = new GoogleAIFileManager(apiKey)
|
||||||
await fileManager.deleteFile(fileId)
|
await fileManager.deleteFile(fileId)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
|
|||||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||||
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||||
import { addFileLoader } from '@main/loader'
|
import { addFileLoader } from '@main/loader'
|
||||||
|
import { proxyManager } from '@main/services/ProxyManager'
|
||||||
import { windowService } from '@main/services/WindowService'
|
import { windowService } from '@main/services/WindowService'
|
||||||
import { getInstanceName } from '@main/utils'
|
import { getInstanceName } from '@main/utils'
|
||||||
import { getAllFiles } from '@main/utils/file'
|
import { getAllFiles } from '@main/utils/file'
|
||||||
@ -123,13 +124,14 @@ class KnowledgeService {
|
|||||||
azureOpenAIApiVersion: apiVersion,
|
azureOpenAIApiVersion: apiVersion,
|
||||||
azureOpenAIApiDeploymentName: model,
|
azureOpenAIApiDeploymentName: model,
|
||||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||||
|
configuration: { httpAgent: proxyManager.getProxyAgent() },
|
||||||
dimensions,
|
dimensions,
|
||||||
batchSize
|
batchSize
|
||||||
})
|
})
|
||||||
: new OpenAiEmbeddings({
|
: new OpenAiEmbeddings({
|
||||||
model,
|
model,
|
||||||
apiKey,
|
apiKey,
|
||||||
configuration: { baseURL },
|
configuration: { baseURL, httpAgent: proxyManager.getProxyAgent() },
|
||||||
dimensions,
|
dimensions,
|
||||||
batchSize
|
batchSize
|
||||||
})
|
})
|
||||||
@ -424,6 +426,7 @@ class KnowledgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
||||||
|
proxyManager.setGlobalProxy()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const { base, item, forceReload = false } = options
|
const { base, item, forceReload = false } = options
|
||||||
const optionsNonNullableAttribute = { base, item, forceReload }
|
const optionsNonNullableAttribute = { base, item, forceReload }
|
||||||
|
|||||||
145
src/main/services/ProxyManager.ts
Normal file
145
src/main/services/ProxyManager.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { ProxyConfig as _ProxyConfig, session } from 'electron'
|
||||||
|
import { socksDispatcher } from 'fetch-socks'
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||||
|
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||||
|
|
||||||
|
type ProxyMode = 'system' | 'custom' | 'none'
|
||||||
|
|
||||||
|
export interface ProxyConfig {
|
||||||
|
mode: ProxyMode
|
||||||
|
url?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProxyManager {
|
||||||
|
private config: ProxyConfig
|
||||||
|
private proxyAgent: HttpsProxyAgent | null = null
|
||||||
|
private proxyUrl: string | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.config = {
|
||||||
|
mode: 'system',
|
||||||
|
url: ''
|
||||||
|
}
|
||||||
|
this.monitorSystemProxy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setSessionsProxy(config: _ProxyConfig): Promise<void> {
|
||||||
|
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||||
|
await Promise.all(sessions.map((session) => session.setProxy(config)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private async monitorSystemProxy(): Promise<void> {
|
||||||
|
setInterval(async () => {
|
||||||
|
await this.setSystemProxy()
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async configureProxy(config: ProxyConfig): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.config = config
|
||||||
|
if (this.config.mode === 'system') {
|
||||||
|
await this.setSystemProxy()
|
||||||
|
} else if (this.config.mode == 'custom') {
|
||||||
|
await this.setCustomProxy()
|
||||||
|
} else {
|
||||||
|
await this.clearProxy()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to config proxy:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setEnvironment(url: string): void {
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
await this.setSessionsProxy({ mode: 'system' })
|
||||||
|
const url = await this.resolveSystemProxy()
|
||||||
|
if (url && url !== this.proxyUrl) {
|
||||||
|
this.proxyUrl = url.toLowerCase()
|
||||||
|
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
|
||||||
|
this.setEnvironment(this.proxyUrl)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set system proxy:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setCustomProxy(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.config.url) {
|
||||||
|
this.proxyUrl = this.config.url.toLowerCase()
|
||||||
|
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
|
||||||
|
this.setEnvironment(this.proxyUrl)
|
||||||
|
await this.setSessionsProxy({ proxyRules: this.proxyUrl })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set custom proxy:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearProxy(): Promise<void> {
|
||||||
|
delete process.env.HTTP_PROXY
|
||||||
|
delete process.env.HTTPS_PROXY
|
||||||
|
await this.setSessionsProxy({})
|
||||||
|
this.config = { mode: 'none' }
|
||||||
|
this.proxyAgent = null
|
||||||
|
this.proxyUrl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveSystemProxy(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await this.resolveElectronProxy()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to resolve system proxy:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveElectronProxy(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
|
||||||
|
const [protocol, address] = proxyString.split(';')[0].split(' ')
|
||||||
|
return protocol === 'PROXY' ? `http://${address}` : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to resolve electron proxy:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxyAgent(): HttpsProxyAgent | null {
|
||||||
|
return this.proxyAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxyUrl(): string | null {
|
||||||
|
return this.proxyUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobalProxy() {
|
||||||
|
const proxyUrl = this.proxyUrl
|
||||||
|
if (proxyUrl) {
|
||||||
|
const [protocol, host, port] = proxyUrl.split(':')
|
||||||
|
if (!protocol.includes('socks')) {
|
||||||
|
setGlobalDispatcher(new ProxyAgent(proxyUrl))
|
||||||
|
} else {
|
||||||
|
const dispatcher = socksDispatcher({
|
||||||
|
port: parseInt(port),
|
||||||
|
type: protocol === 'socks5' ? 5 : 4,
|
||||||
|
host: host
|
||||||
|
})
|
||||||
|
global[Symbol.for('undici.globalDispatcher.1')] = dispatcher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const proxyManager = new ProxyManager()
|
||||||
@ -1,20 +1,24 @@
|
|||||||
|
import { proxyManager } from '@main/services/ProxyManager'
|
||||||
import { WebDavConfig } from '@types'
|
import { WebDavConfig } from '@types'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||||
import Stream from 'stream'
|
import Stream from 'stream'
|
||||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||||
|
|
||||||
export default class WebDav {
|
export default class WebDav {
|
||||||
public instance: WebDAVClient | undefined
|
public instance: WebDAVClient | undefined
|
||||||
private webdavPath: string
|
private webdavPath: string
|
||||||
|
|
||||||
constructor(params: WebDavConfig) {
|
constructor(params: WebDavConfig) {
|
||||||
this.webdavPath = params.webdavPath
|
this.webdavPath = params.webdavPath
|
||||||
|
const url = proxyManager.getProxyUrl()
|
||||||
|
|
||||||
this.instance = createClient(params.webdavHost, {
|
this.instance = createClient(params.webdavHost, {
|
||||||
username: params.webdavUser,
|
username: params.webdavUser,
|
||||||
password: params.webdavPass,
|
password: params.webdavPass,
|
||||||
maxBodyLength: Infinity,
|
maxBodyLength: Infinity,
|
||||||
maxContentLength: Infinity
|
maxContentLength: Infinity,
|
||||||
|
httpAgent: url ? new HttpProxyAgent(url) : undefined,
|
||||||
|
httpsAgent: proxyManager.getProxyAgent()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.putFileContents = this.putFileContents.bind(this)
|
this.putFileContents = this.putFileContents.bind(this)
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@ -3176,6 +3176,7 @@ __metadata:
|
|||||||
eslint-plugin-react-hooks: "npm:^4.6.2"
|
eslint-plugin-react-hooks: "npm:^4.6.2"
|
||||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||||
eslint-plugin-unused-imports: "npm:^4.0.0"
|
eslint-plugin-unused-imports: "npm:^4.0.0"
|
||||||
|
fetch-socks: "npm:^1.3.2"
|
||||||
fs-extra: "npm:^11.2.0"
|
fs-extra: "npm:^11.2.0"
|
||||||
html-to-image: "npm:^1.11.13"
|
html-to-image: "npm:^1.11.13"
|
||||||
i18next: "npm:^23.11.5"
|
i18next: "npm:^23.11.5"
|
||||||
@ -3211,6 +3212,7 @@ __metadata:
|
|||||||
tinycolor2: "npm:^1.6.0"
|
tinycolor2: "npm:^1.6.0"
|
||||||
tokenx: "npm:^0.4.1"
|
tokenx: "npm:^0.4.1"
|
||||||
typescript: "npm:^5.6.2"
|
typescript: "npm:^5.6.2"
|
||||||
|
undici: "npm:^7.4.0"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
vite: "npm:^5.0.12"
|
vite: "npm:^5.0.12"
|
||||||
webdav: "npm:4.11.4"
|
webdav: "npm:4.11.4"
|
||||||
@ -6500,6 +6502,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fflate@npm:0.8.1":
|
||||||
version: 0.8.1
|
version: 0.8.1
|
||||||
resolution: "fflate@npm:0.8.1"
|
resolution: "fflate@npm:0.8.1"
|
||||||
@ -13541,7 +13553,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"socks@npm:^2.6.2, socks@npm:^2.8.3":
|
"socks@npm:^2.6.2, socks@npm:^2.8.2, socks@npm:^2.8.3":
|
||||||
version: 2.8.4
|
version: 2.8.4
|
||||||
resolution: "socks@npm:2.8.4"
|
resolution: "socks@npm:2.8.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -14621,6 +14633,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"undici@npm:>=6, undici@npm:^7.4.0":
|
||||||
|
version: 7.4.0
|
||||||
|
resolution: "undici@npm:7.4.0"
|
||||||
|
checksum: 10c0/0d8d8d627c87e72cf58148d257a79d019ce058b6761363ee5752103aa0ab57d132448fce4ef15171671ee138ef156a695ec1daeb72cd09ae408afa74dee070b5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"unified@npm:^11.0.0":
|
"unified@npm:^11.0.0":
|
||||||
version: 11.0.5
|
version: 11.0.5
|
||||||
resolution: "unified@npm:11.0.5"
|
resolution: "unified@npm:11.0.5"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user