mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-28 03:30:06 +00:00
fix: enable proxy support to resolve (#1125)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com> Co-authored-by: Ahmed Awelkair A <108264625+a7m-1st@users.noreply.github.com>
This commit is contained in:
parent
ad44e59485
commit
53d88308df
7 changed files with 483 additions and 67 deletions
|
|
@ -11,3 +11,8 @@ 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
|
||||
|
|
@ -52,6 +52,7 @@ import {
|
|||
getEmailFolderPath,
|
||||
getEnvPath,
|
||||
maskProxyUrl,
|
||||
readEnvValueWithPriority,
|
||||
readGlobalEnvKey,
|
||||
removeEnvKey,
|
||||
updateEnvBlock,
|
||||
|
|
@ -138,11 +139,31 @@ app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
|
|||
app.commandLine.appendSwitch('renderer-process-limit', '8');
|
||||
|
||||
// ==================== Proxy configuration ====================
|
||||
// Read proxy from global .env file on startup
|
||||
proxyUrl = readGlobalEnvKey('HTTP_PROXY');
|
||||
// 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;
|
||||
|
||||
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');
|
||||
}
|
||||
|
|
@ -1159,17 +1180,39 @@ 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']);
|
||||
const ALLOWED_GLOBAL_ENV_KEYS = new Set([
|
||||
'HTTP_PROXY',
|
||||
'HTTPS_PROXY',
|
||||
'NO_PROXY',
|
||||
'http_proxy',
|
||||
'https_proxy',
|
||||
'no_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: readGlobalEnvKey(key) };
|
||||
return { value: readEnvValueWithPriority(key) };
|
||||
});
|
||||
|
||||
// ==================== new window handler ====================
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import os from 'os';
|
|||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { PromiseReturnType } from './install-deps';
|
||||
import { maskProxyUrl, readGlobalEnvKey } from './utils/envUtil';
|
||||
import { maskProxyUrl, readEnvValueWithPriority } from './utils/envUtil';
|
||||
import {
|
||||
ensureTerminalVenvAtUserPath,
|
||||
findNodejsWheelBinPath,
|
||||
|
|
@ -42,7 +42,10 @@ const execAsync = promisify(exec);
|
|||
|
||||
const DEFAULT_SERVER_URL = 'https://dev.eigent.ai/api';
|
||||
|
||||
function readEnvValue(filePath: string, key: string): string | undefined {
|
||||
export function readEnvValue(
|
||||
filePath: string,
|
||||
key: string
|
||||
): string | undefined {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return undefined;
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
|
@ -236,22 +239,22 @@ export async function startBackend(
|
|||
const uvEnv = getUvEnv(currentVersion);
|
||||
const globalEnvPath = path.join(os.homedir(), '.eigent', '.env');
|
||||
|
||||
// Load proxy configuration from global .env file
|
||||
const proxyUrl = readGlobalEnvKey('HTTP_PROXY');
|
||||
|
||||
// Build proxy env vars if configured
|
||||
const proxyEnv = proxyUrl
|
||||
? {
|
||||
HTTP_PROXY: proxyUrl,
|
||||
HTTPS_PROXY: proxyUrl,
|
||||
http_proxy: proxyUrl,
|
||||
https_proxy: proxyUrl,
|
||||
}
|
||||
: {};
|
||||
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',
|
||||
};
|
||||
|
||||
if (proxyUrl) {
|
||||
if (proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) {
|
||||
log.info(
|
||||
`[BACKEND] Proxy configured for backend: ${maskProxyUrl(proxyUrl)}`
|
||||
`[BACKEND] Proxy configured for backend: ${maskProxyUrl((proxyEnv.HTTP_PROXY || proxyEnv.HTTPS_PROXY) as string)}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
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 ===';
|
||||
|
|
@ -116,6 +117,34 @@ 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"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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');
|
||||
|
|
@ -33,6 +34,69 @@ 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<string, string> {
|
||||
// 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<string, string> = {};
|
||||
|
||||
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<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const installScriptPath = path.join(
|
||||
|
|
@ -42,8 +106,15 @@ export function runInstallScript(scriptPath: string): Promise<boolean> {
|
|||
);
|
||||
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, ELECTRON_RUN_AS_NODE: '1' },
|
||||
env: {
|
||||
...process.env,
|
||||
...proxyEnv,
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
},
|
||||
});
|
||||
|
||||
let stderrOutput = '';
|
||||
|
|
|
|||
|
|
@ -12,13 +12,250 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
/* global console, setTimeout, clearTimeout, require */
|
||||
/* global console, process */
|
||||
// @ts-check
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import tls from 'tls';
|
||||
import { URL } from 'url';
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL with redirect handling
|
||||
* 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)
|
||||
*
|
||||
* @param {string} url The URL to download from
|
||||
* @param {string} destinationPath The path to save the file to
|
||||
* @returns {Promise<void>} Promise that resolves when download is complete
|
||||
|
|
@ -26,9 +263,7 @@ import https from 'https';
|
|||
export async function downloadWithRedirects(url, destinationPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutMs = 10 * 60 * 1000; // 10 minutes
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error(`timeout(${timeoutMs / 1000} seconds)`));
|
||||
}, timeoutMs);
|
||||
let timeoutId = null;
|
||||
|
||||
// Use flag to prevent multiple resolve/reject calls
|
||||
let settled = false;
|
||||
|
|
@ -36,7 +271,7 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
const safeReject = (error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -44,17 +279,19 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
const safeResolve = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const request = (url) => {
|
||||
// Support both http and https
|
||||
const httpModule = url.startsWith('https://') ? https : require('http');
|
||||
timeoutId = setTimeout(() => {
|
||||
safeReject(new Error(`Download timeout after ${timeoutMs / 1000} seconds`));
|
||||
}, timeoutMs);
|
||||
|
||||
httpModule
|
||||
.get(url, (response) => {
|
||||
const request = (requestUrl) => {
|
||||
makeRequest(
|
||||
requestUrl,
|
||||
(response) => {
|
||||
const statusCode = response.statusCode || 0;
|
||||
|
||||
// Handle redirects (301, 302, 307, 308)
|
||||
|
|
@ -63,15 +300,28 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
statusCode <= 308 &&
|
||||
response.headers.location
|
||||
) {
|
||||
const redirectUrl = 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;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Following redirect to: ${redirectUrl}`);
|
||||
request(redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
safeReject(
|
||||
new Error(
|
||||
`Download failed: ${statusCode} ${response.statusMessage || 'Unknown error'}`
|
||||
`Download failed with status ${statusCode}: ${response.statusMessage || 'Unknown error'}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
|
|
@ -80,7 +330,8 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
const file = fs.createWriteStream(destinationPath);
|
||||
let downloadedBytes = 0;
|
||||
const expectedBytes = parseInt(
|
||||
response.headers['content-length'] || '0'
|
||||
response.headers['content-length'] || '0',
|
||||
10
|
||||
);
|
||||
const startTime = Date.now();
|
||||
let lastProgressTime = Date.now();
|
||||
|
|
@ -131,8 +382,10 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
if (fs.existsSync(destinationPath)) {
|
||||
fs.unlinkSync(destinationPath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete incomplete file:', err);
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
`Warning: Failed to delete incomplete file: ${cleanupError.message}`
|
||||
);
|
||||
}
|
||||
safeReject(
|
||||
new Error(
|
||||
|
|
@ -155,9 +408,9 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
} else {
|
||||
safeReject(new Error('Downloaded file does not exist'));
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (verifyError) {
|
||||
safeReject(
|
||||
new Error(`Failed to verify download: ${err.message}`)
|
||||
new Error(`Failed to verify download: ${verifyError.message}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -168,16 +421,18 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
if (fs.existsSync(destinationPath)) {
|
||||
fs.unlinkSync(destinationPath);
|
||||
}
|
||||
} catch (deleteErr) {
|
||||
console.error('Failed to delete file after error:', deleteErr);
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
`Warning: Failed to delete file after error: ${cleanupError.message}`
|
||||
);
|
||||
}
|
||||
safeReject(err);
|
||||
safeReject(new Error(`File write error: ${err.message}`));
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
safeReject(err);
|
||||
});
|
||||
},
|
||||
safeReject
|
||||
);
|
||||
};
|
||||
|
||||
request(url);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ 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);
|
||||
|
||||
|
|
@ -158,16 +159,20 @@ export default function SettingGeneral() {
|
|||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Load proxy configuration from global env
|
||||
// Load proxy configuration from env (with priority system)
|
||||
const loadProxyConfig = async () => {
|
||||
if (window.electronAPI?.readGlobalEnv) {
|
||||
try {
|
||||
const result = await window.electronAPI.readGlobalEnv('HTTP_PROXY');
|
||||
if (result?.value) {
|
||||
setProxyUrl(result.value);
|
||||
}
|
||||
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
|
||||
} catch (_error) {
|
||||
console.log('No proxy configured');
|
||||
setProxyUrl('');
|
||||
setLoadedProxyUrl('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -229,6 +234,9 @@ export default function SettingGeneral() {
|
|||
}
|
||||
};
|
||||
|
||||
// Check if proxy value has changed from loaded value
|
||||
const hasProxyChanged = proxyUrl.trim() !== loadedProxyUrl.trim();
|
||||
|
||||
if (!chatStore) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
|
@ -372,22 +380,24 @@ export default function SettingGeneral() {
|
|||
size="default"
|
||||
note={proxyNeedsRestart ? t('setting.proxy-restart-hint') : undefined}
|
||||
trailingButton={
|
||||
<Button
|
||||
variant={proxyNeedsRestart ? 'outline' : 'primary'}
|
||||
size="sm"
|
||||
onClick={
|
||||
proxyNeedsRestart
|
||||
? () => window.electronAPI?.restartApp()
|
||||
: handleSaveProxy
|
||||
}
|
||||
disabled={!proxyNeedsRestart && isProxySaving}
|
||||
>
|
||||
{proxyNeedsRestart
|
||||
? t('setting.restart-to-apply')
|
||||
: isProxySaving
|
||||
? t('setting.saving')
|
||||
: t('setting.save')}
|
||||
</Button>
|
||||
proxyNeedsRestart ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.electronAPI?.restartApp()}
|
||||
>
|
||||
{t('setting.restart-to-apply')}
|
||||
</Button>
|
||||
) : hasProxyChanged ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleSaveProxy}
|
||||
disabled={isProxySaving}
|
||||
>
|
||||
{isProxySaving ? t('setting.saving') : t('setting.save')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue