diff --git a/.env.development b/.env.development index 2614ca2cc..81b3be797 100644 --- a/.env.development +++ b/.env.development @@ -11,8 +11,3 @@ VITE_USE_LOCAL_PROXY=false VITE_STACK_PROJECT_ID=dummy_project_id VITE_STACK_PUBLISHABLE_CLIENT_KEY=dummy_publishable_key VITE_STACK_SECRET_SERVER_KEY=dummy_secret_server_key - -# HTTP proxy settings for local development -# HTTP_PROXY=http://127.0.0.1:9090 -# HTTPS_PROXY=https://127.0.0.1:9090 -# NO_PROXY=localhost,127.0.0.1 \ No newline at end of file diff --git a/electron/main/index.ts b/electron/main/index.ts index 8b3ab4392..b2001bbf0 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -52,7 +52,6 @@ import { getEmailFolderPath, getEnvPath, maskProxyUrl, - readEnvValueWithPriority, readGlobalEnvKey, removeEnvKey, updateEnvBlock, @@ -139,31 +138,11 @@ app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction'); app.commandLine.appendSwitch('renderer-process-limit', '8'); // ==================== Proxy configuration ==================== -// Read proxy from multiple sources with priority: -// 1. Process environment (inline: SET HTTP_PROXY=... && eigent.exe) -// 2. .env.development (development mode) -// 3. Global ~/.eigent/.env file -// Check both HTTP_PROXY and HTTPS_PROXY (with lowercase variants) -const httpProxy = - readEnvValueWithPriority('HTTP_PROXY') || - readEnvValueWithPriority('http_proxy'); -const httpsProxy = - readEnvValueWithPriority('HTTPS_PROXY') || - readEnvValueWithPriority('https_proxy'); - -// Prefer HTTPS proxy if available, fallback to HTTP proxy -proxyUrl = httpsProxy || httpProxy; - +// Read proxy from global .env file on startup +proxyUrl = readGlobalEnvKey('HTTP_PROXY'); if (proxyUrl) { log.info(`[PROXY] Applying proxy configuration: ${maskProxyUrl(proxyUrl)}`); app.commandLine.appendSwitch('proxy-server', proxyUrl); - - // Log which proxy type is being used - if (httpsProxy) { - log.info('[PROXY] Using HTTPS_PROXY configuration'); - } else if (httpProxy) { - log.info('[PROXY] Using HTTP_PROXY configuration'); - } } else { log.info('[PROXY] No proxy configured'); } @@ -1180,39 +1159,17 @@ function registerIpcHandlers() { log.error('global env-remove error:', error); } - // Also remove from .env.development file (remove from anywhere in file, not just MCP block) - const DEV_ENV_PATH = path.join(process.cwd(), '.env.development'); - try { - if (fs.existsSync(DEV_ENV_PATH)) { - let devContent = fs.readFileSync(DEV_ENV_PATH, 'utf-8'); - let devLines = devContent.split(/\r?\n/); - // Remove key from anywhere in the file (not limited to MCP block) - devLines = devLines.filter((line) => !line.trim().startsWith(key + '=')); - fs.writeFileSync(DEV_ENV_PATH, devLines.join('\n'), 'utf-8'); - log.info(`env-remove: removed ${key} from .env.development`); - } - } catch (error) { - log.error('.env.development env-remove error:', error); - } - return { success: true }; }); // ==================== read global env handler ==================== - const ALLOWED_GLOBAL_ENV_KEYS = new Set([ - 'HTTP_PROXY', - 'HTTPS_PROXY', - 'NO_PROXY', - 'http_proxy', - 'https_proxy', - 'no_proxy', - ]); + const ALLOWED_GLOBAL_ENV_KEYS = new Set(['HTTP_PROXY', 'HTTPS_PROXY']); ipcMain.handle('read-global-env', async (_event, key: string) => { if (!ALLOWED_GLOBAL_ENV_KEYS.has(key)) { log.warn(`[ENV] Blocked read of disallowed global env key: ${key}`); return { value: null }; } - return { value: readEnvValueWithPriority(key) }; + return { value: readGlobalEnvKey(key) }; }); // ==================== new window handler ==================== diff --git a/electron/main/init.ts b/electron/main/init.ts index b60ce45ed..57fb26c87 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -22,7 +22,7 @@ import os from 'os'; import path from 'path'; import { promisify } from 'util'; import { PromiseReturnType } from './install-deps'; -import { maskProxyUrl, readEnvValueWithPriority } from './utils/envUtil'; +import { maskProxyUrl, readGlobalEnvKey } from './utils/envUtil'; import { ensureTerminalVenvAtUserPath, findNodejsWheelBinPath, @@ -42,10 +42,7 @@ const execAsync = promisify(exec); const DEFAULT_SERVER_URL = 'https://dev.eigent.ai/api'; -export function readEnvValue( - filePath: string, - key: string -): string | undefined { +function readEnvValue(filePath: string, key: string): string | undefined { try { if (!fs.existsSync(filePath)) return undefined; const content = fs.readFileSync(filePath, 'utf-8'); @@ -239,22 +236,22 @@ export async function startBackend( const uvEnv = getUvEnv(currentVersion); const globalEnvPath = path.join(os.homedir(), '.eigent', '.env'); - // Build proxy env vars if configured - const proxyEnv = { - HTTP_PROXY: readEnvValueWithPriority('HTTP_PROXY'), - HTTPS_PROXY: readEnvValueWithPriority('HTTPS_PROXY'), - http_proxy: readEnvValueWithPriority('http_proxy'), - https_proxy: readEnvValueWithPriority('https_proxy'), - // Ensure local connections bypass proxy - NO_PROXY: - readEnvValueWithPriority('NO_PROXY') || 'localhost,127.0.0.1,.local', - no_proxy: - readEnvValueWithPriority('no_proxy') || 'localhost,127.0.0.1,.local', - }; + // Load proxy configuration from global .env file + const proxyUrl = readGlobalEnvKey('HTTP_PROXY'); - if (proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) { + // Build proxy env vars if configured + const proxyEnv = proxyUrl + ? { + HTTP_PROXY: proxyUrl, + HTTPS_PROXY: proxyUrl, + http_proxy: proxyUrl, + https_proxy: proxyUrl, + } + : {}; + + if (proxyUrl) { log.info( - `[BACKEND] Proxy configured for backend: ${maskProxyUrl((proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) as string)}` + `[BACKEND] Proxy configured for backend: ${maskProxyUrl(proxyUrl)}` ); } diff --git a/electron/main/utils/envUtil.ts b/electron/main/utils/envUtil.ts index c7e89b593..565c8e017 100644 --- a/electron/main/utils/envUtil.ts +++ b/electron/main/utils/envUtil.ts @@ -15,7 +15,6 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { readEnvValue } from '../init'; export const ENV_START = '# === MCP INTEGRATION ENV START ==='; export const ENV_END = '# === MCP INTEGRATION ENV END ==='; @@ -117,34 +116,6 @@ export function readGlobalEnvKey(key: string): string | null { return null; } -/** - * Read environment variable value with priority system. - * - * Priority order (highest to lowest): - * 1. Process environment variables (inline/system) - * 2. .env.development file (development mode only) - * 3. Global ~/.eigent/.env file - * - * @param key - The environment variable key to read - * @returns The value if found, null otherwise - */ -export function readEnvValueWithPriority(key: string): string | null { - // Priority 1: Process environment variables (highest priority) - if (process.env[key]) { - return process.env[key]!; - } - - // Priority 2: .env.development file (development mode only) - if (process.env.NODE_ENV === 'development') { - const devEnvPath = path.join(process.cwd(), '.env.development'); - const value = readEnvValue(devEnvPath, key); - if (value) return value; - } - - // Priority 3: Global ~/.eigent/.env file - return readGlobalEnvKey(key); -} - /** * Mask credentials in a proxy URL for safe logging. * e.g. "http://user:pass@host:port" → "http://***:***@host:port" diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index 96d865a5c..70fd11921 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -18,7 +18,6 @@ import log from 'electron-log'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import { maskProxyUrl, readEnvValueWithPriority } from './envUtil'; export function getResourcePath() { return path.join(app.getAppPath(), 'resources'); @@ -34,69 +33,6 @@ export function getBackendPath() { } } -/** - * Get proxy environment variables with priority: - * 1. Process environment variables (inline/system) - * 2. .env.development file (development mode only) - * 3. Global ~/.eigent/.env config - * - * Returns an object with HTTP_PROXY, HTTPS_PROXY, NO_PROXY and lowercase variants - * if a proxy is configured, or an empty object if not. - * Supports separate HTTP and HTTPS proxy configurations. - */ -function getProxyEnvVars(): Record { - // Check both uppercase and lowercase variants - const httpProxy = - readEnvValueWithPriority('HTTP_PROXY') || - readEnvValueWithPriority('http_proxy'); - - const httpsProxy = - readEnvValueWithPriority('HTTPS_PROXY') || - readEnvValueWithPriority('https_proxy'); - - // Return empty object if no proxy configured - if (!httpProxy && !httpsProxy) { - return {}; - } - - // Log configured proxies - if (httpProxy) { - log.info( - `[INSTALL SCRIPT] HTTP Proxy configured: ${maskProxyUrl(httpProxy)}` - ); - } - if (httpsProxy) { - log.info( - `[INSTALL SCRIPT] HTTPS Proxy configured: ${maskProxyUrl(httpsProxy)}` - ); - } - - // Get NO_PROXY configuration (with default for local connections) - const noProxy = readEnvValueWithPriority('NO_PROXY') || - readEnvValueWithPriority('no_proxy') || - 'localhost,127.0.0.1,.local'; - - // Return all variants (some tools need uppercase, others lowercase) - // Filter out undefined values - const result: Record = {}; - - if (httpProxy) { - result.HTTP_PROXY = httpProxy; - result.http_proxy = httpProxy; - } - - if (httpsProxy) { - result.HTTPS_PROXY = httpsProxy; - result.https_proxy = httpsProxy; - } - - // Always set NO_PROXY when proxy is configured to avoid issues with local connections - result.NO_PROXY = noProxy; - result.no_proxy = noProxy; - - return result; -} - export function runInstallScript(scriptPath: string): Promise { return new Promise((resolve, reject) => { const installScriptPath = path.join( @@ -106,15 +42,8 @@ export function runInstallScript(scriptPath: string): Promise { ); log.info(`Running script at: ${installScriptPath}`); - // Get proxy configuration from global .env file - const proxyEnv = getProxyEnvVars(); - const nodeProcess = spawn(process.execPath, [installScriptPath], { - env: { - ...process.env, - ...proxyEnv, - ELECTRON_RUN_AS_NODE: '1', - }, + env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, }); let stderrOutput = ''; diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 5b61b1e5c..b527c6c8f 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -12,250 +12,13 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -/* global console, process */ +/* global console, setTimeout, clearTimeout, require */ // @ts-check import fs from 'fs'; -import http from 'http'; import https from 'https'; -import tls from 'tls'; -import { URL } from 'url'; /** - * Check if the target URL should bypass proxy based on NO_PROXY env var. - * @param {string} targetUrl - The target URL - * @returns {boolean} True if proxy should be bypassed - */ -function shouldBypassProxy(targetUrl) { - const noProxy = process.env.NO_PROXY || process.env.no_proxy; - if (!noProxy) return false; - - try { - const targetHost = new URL(targetUrl).hostname.toLowerCase(); - const noProxyList = noProxy.split(',').map((s) => s.trim().toLowerCase()); - - for (const pattern of noProxyList) { - if (!pattern) continue; - if (pattern === '*') return true; - if (targetHost === pattern) return true; - if (pattern.startsWith('.') && targetHost.endsWith(pattern)) return true; - if (targetHost.endsWith('.' + pattern)) return true; - } - } catch (error) { - console.warn(`Warning: Failed to parse NO_PROXY: ${error.message}`); - } - return false; -} - -/** - * Get proxy URL from environment variables. - * @param {string} targetUrl - The target URL to determine which proxy to use - * @returns {string | null} The proxy URL or null if not configured - */ -function getProxyUrl(targetUrl) { - // Check NO_PROXY first - if (shouldBypassProxy(targetUrl)) { - return null; - } - - const isHttps = targetUrl.startsWith('https://'); - - // Priority order for proxy env vars (check both uppercase and lowercase) - const envVars = isHttps - ? ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'] - : ['HTTP_PROXY', 'http_proxy']; - - for (const envVar of envVars) { - const value = process.env[envVar]; - if (value && value.trim()) { - return value.trim(); - } - } - - return null; -} - -/** - * Mask credentials in a proxy URL for safe logging. - * @param {string} url - The URL to mask - * @returns {string} The masked URL - */ -function maskProxyUrl(url) { - try { - const parsed = new URL(url); - if (parsed.username || parsed.password) { - parsed.username = parsed.username ? '***' : ''; - parsed.password = parsed.password ? '***' : ''; - return parsed.toString(); - } - } catch (error) { - // Not a valid URL, return as-is - console.warn(`Warning: Failed to parse proxy URL: ${error.message}`); - } - return url; -} - -/** - * Make an HTTP GET request with optional proxy support. - * For HTTPS URLs through HTTP proxy, uses CONNECT tunnel with TLS. - * @param {string} url - The URL to request - * @param {(response: http.IncomingMessage) => void} callback - Response callback - * @param {(error: Error) => void} onError - Error callback - */ -function makeRequest(url, callback, onError) { - const proxyUrl = getProxyUrl(url); - const isHttps = url.startsWith('https://'); - const targetUrl = new URL(url); - const targetPort = parseInt(targetUrl.port, 10) || (isHttps ? 443 : 80); - - if (!proxyUrl) { - // Direct connection (no proxy) - const httpModule = isHttps ? https : http; - const req = httpModule.get(url, callback); - req.on('error', onError); - return; - } - - console.log(`Using proxy: ${maskProxyUrl(proxyUrl)}`); - - const proxy = new URL(proxyUrl); - const proxyPort = parseInt(proxy.port, 10) || 80; - - // Build proxy auth header if credentials provided - const proxyAuthHeader = - proxy.username || proxy.password - ? { - 'Proxy-Authorization': `Basic ${Buffer.from( - `${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(proxy.password || '')}` - ).toString('base64')}`, - } - : {}; - - if (isHttps) { - // HTTPS through HTTP proxy: Use CONNECT tunnel - const connectReq = http.request({ - host: proxy.hostname, - port: proxyPort, - method: 'CONNECT', - path: `${targetUrl.hostname}:${targetPort}`, - headers: { - Host: `${targetUrl.hostname}:${targetPort}`, - ...proxyAuthHeader, - }, - }); - - // Track resources for cleanup - let tlsSocket = null; - let httpsReq = null; - - // Cleanup function to destroy all connections - const cleanup = () => { - if (httpsReq && !httpsReq.destroyed) { - httpsReq.destroy(); - } - if (tlsSocket && !tlsSocket.destroyed) { - tlsSocket.destroy(); - } - if (connectReq && !connectReq.destroyed) { - connectReq.destroy(); - } - }; - - connectReq.on('connect', (res, socket) => { - if (res.statusCode !== 200) { - socket.destroy(); - cleanup(); - onError( - new Error( - `Proxy CONNECT failed with status ${res.statusCode}: ${res.statusMessage}` - ) - ); - return; - } - - // Upgrade socket to TLS - tlsSocket = tls.connect( - { - host: targetUrl.hostname, - port: targetPort, - socket: socket, - servername: targetUrl.hostname, // SNI - }, - () => { - // Make HTTPS request over TLS socket - httpsReq = https.request( - { - hostname: targetUrl.hostname, - port: targetPort, - path: targetUrl.pathname + targetUrl.search, - method: 'GET', - headers: { Host: targetUrl.host }, - agent: false, - // Use createConnection to provide the pre-established TLS socket - createConnection: () => tlsSocket, - }, - (response) => { - // Cleanup connections when response ends - response.on('end', cleanup); - response.on('close', cleanup); - callback(response); - } - ); - - httpsReq.on('error', (err) => { - cleanup(); - onError(new Error(`HTTPS request error: ${err.message}`)); - }); - - httpsReq.end(); - } - ); - - tlsSocket.on('error', (err) => { - cleanup(); - onError(new Error(`TLS connection error: ${err.message}`)); - }); - }); - - connectReq.on('error', (err) => { - cleanup(); - onError(new Error(`Proxy connection error: ${err.message}`)); - }); - - connectReq.setTimeout(30000, () => { - cleanup(); - onError(new Error('Proxy connection timeout after 30 seconds')); - }); - - connectReq.end(); - } else { - // HTTP through HTTP proxy: Use proxy as target with full URL as path - const req = http.request( - { - host: proxy.hostname, - port: proxyPort, - path: url, // Full URL for HTTP proxy - method: 'GET', - headers: { - Host: targetUrl.host, - ...proxyAuthHeader, - }, - }, - callback - ); - req.on('error', (err) => { - onError(new Error(`HTTP proxy request error: ${err.message}`)); - }); - req.end(); - } -} - -/** - * Downloads a file from a URL with redirect handling and proxy support. - * Proxy is automatically detected from environment variables: - * - HTTPS_PROXY / https_proxy (for HTTPS URLs) - * - HTTP_PROXY / http_proxy (for HTTP URLs, or as fallback for HTTPS) - * - NO_PROXY / no_proxy (to bypass proxy for specific hosts) - * + * Downloads a file from a URL with redirect handling * @param {string} url The URL to download from * @param {string} destinationPath The path to save the file to * @returns {Promise} Promise that resolves when download is complete @@ -263,7 +26,9 @@ function makeRequest(url, callback, onError) { export async function downloadWithRedirects(url, destinationPath) { return new Promise((resolve, reject) => { const timeoutMs = 10 * 60 * 1000; // 10 minutes - let timeoutId = null; + const timeout = setTimeout(() => { + reject(new Error(`timeout(${timeoutMs / 1000} seconds)`)); + }, timeoutMs); // Use flag to prevent multiple resolve/reject calls let settled = false; @@ -271,7 +36,7 @@ export async function downloadWithRedirects(url, destinationPath) { const safeReject = (error) => { if (!settled) { settled = true; - if (timeoutId) clearTimeout(timeoutId); + clearTimeout(timeout); reject(error); } }; @@ -279,19 +44,17 @@ export async function downloadWithRedirects(url, destinationPath) { const safeResolve = () => { if (!settled) { settled = true; - if (timeoutId) clearTimeout(timeoutId); + clearTimeout(timeout); resolve(); } }; - timeoutId = setTimeout(() => { - safeReject(new Error(`Download timeout after ${timeoutMs / 1000} seconds`)); - }, timeoutMs); + const request = (url) => { + // Support both http and https + const httpModule = url.startsWith('https://') ? https : require('http'); - const request = (requestUrl) => { - makeRequest( - requestUrl, - (response) => { + httpModule + .get(url, (response) => { const statusCode = response.statusCode || 0; // Handle redirects (301, 302, 307, 308) @@ -300,28 +63,15 @@ export async function downloadWithRedirects(url, destinationPath) { statusCode <= 308 && response.headers.location ) { - let redirectUrl = response.headers.location; - - // Handle relative redirects - if (redirectUrl.startsWith('/')) { - try { - const originalUrl = new URL(requestUrl); - redirectUrl = `${originalUrl.protocol}//${originalUrl.host}${redirectUrl}`; - } catch (error) { - safeReject(new Error(`Failed to parse redirect URL: ${error.message}`)); - return; - } - } - + const redirectUrl = response.headers.location; console.log(`Following redirect to: ${redirectUrl}`); request(redirectUrl); return; } - if (statusCode !== 200) { safeReject( new Error( - `Download failed with status ${statusCode}: ${response.statusMessage || 'Unknown error'}` + `Download failed: ${statusCode} ${response.statusMessage || 'Unknown error'}` ) ); return; @@ -330,8 +80,7 @@ export async function downloadWithRedirects(url, destinationPath) { const file = fs.createWriteStream(destinationPath); let downloadedBytes = 0; const expectedBytes = parseInt( - response.headers['content-length'] || '0', - 10 + response.headers['content-length'] || '0' ); const startTime = Date.now(); let lastProgressTime = Date.now(); @@ -382,10 +131,8 @@ export async function downloadWithRedirects(url, destinationPath) { if (fs.existsSync(destinationPath)) { fs.unlinkSync(destinationPath); } - } catch (cleanupError) { - console.warn( - `Warning: Failed to delete incomplete file: ${cleanupError.message}` - ); + } catch (err) { + console.error('Failed to delete incomplete file:', err); } safeReject( new Error( @@ -408,9 +155,9 @@ export async function downloadWithRedirects(url, destinationPath) { } else { safeReject(new Error('Downloaded file does not exist')); } - } catch (verifyError) { + } catch (err) { safeReject( - new Error(`Failed to verify download: ${verifyError.message}`) + new Error(`Failed to verify download: ${err.message}`) ); } }); @@ -421,18 +168,16 @@ export async function downloadWithRedirects(url, destinationPath) { if (fs.existsSync(destinationPath)) { fs.unlinkSync(destinationPath); } - } catch (cleanupError) { - console.warn( - `Warning: Failed to delete file after error: ${cleanupError.message}` - ); + } catch (deleteErr) { + console.error('Failed to delete file after error:', deleteErr); } - safeReject(new Error(`File write error: ${err.message}`)); + safeReject(err); }); - }, - safeReject - ); + }) + .on('error', (err) => { + safeReject(err); + }); }; - request(url); }); } diff --git a/src/pages/Setting/General.tsx b/src/pages/Setting/General.tsx index 02cd199c5..435133d41 100644 --- a/src/pages/Setting/General.tsx +++ b/src/pages/Setting/General.tsx @@ -77,7 +77,6 @@ export default function SettingGeneral() { // Proxy configuration state const [proxyUrl, setProxyUrl] = useState(''); - const [loadedProxyUrl, setLoadedProxyUrl] = useState(''); // Track the initially loaded value const [isProxySaving, setIsProxySaving] = useState(false); const [proxyNeedsRestart, setProxyNeedsRestart] = useState(false); @@ -159,20 +158,16 @@ export default function SettingGeneral() { ]; useEffect(() => { - // Load proxy configuration from env (with priority system) + // Load proxy configuration from global env const loadProxyConfig = async () => { if (window.electronAPI?.readGlobalEnv) { try { - const result = - (await window.electronAPI.readGlobalEnv('HTTPS_PROXY')) || - (await window.electronAPI.readGlobalEnv('HTTP_PROXY')); - const value = result?.value || ''; - setProxyUrl(value); - setLoadedProxyUrl(value); // Remember the loaded value + const result = await window.electronAPI.readGlobalEnv('HTTP_PROXY'); + if (result?.value) { + setProxyUrl(result.value); + } } catch (_error) { console.log('No proxy configured'); - setProxyUrl(''); - setLoadedProxyUrl(''); } } }; @@ -234,9 +229,6 @@ export default function SettingGeneral() { } }; - // Check if proxy value has changed from loaded value - const hasProxyChanged = proxyUrl.trim() !== loadedProxyUrl.trim(); - if (!chatStore) { return
Loading...
; } @@ -380,24 +372,22 @@ export default function SettingGeneral() { size="default" note={proxyNeedsRestart ? t('setting.proxy-restart-hint') : undefined} trailingButton={ - proxyNeedsRestart ? ( - - ) : hasProxyChanged ? ( - - ) : undefined + } />