Add built-in proxy settings for Electron and Gateway (#239)

Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-03-02 17:33:06 +08:00
committed by GitHub
Unverified
parent c09b45832b
commit e40f4b2163
20 changed files with 758 additions and 25 deletions

View File

@@ -10,6 +10,7 @@ import { join } from 'path';
import { homedir } from 'os';
import { getOpenClawResolvedDir } from './paths';
import * as logger from './logger';
import { proxyAwareFetch } from './proxy-fetch';
const OPENCLAW_DIR = join(homedir(), '.openclaw');
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
@@ -497,7 +498,7 @@ async function validateTelegramCredentials(
if (!allowedUsers) return { valid: false, errors: ['At least one allowed user ID is required'], warnings: [] };
try {
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
const response = await proxyAwareFetch(`https://api.telegram.org/bot${botToken}/getMe`);
const data = (await response.json()) as { ok?: boolean; description?: string; result?: { username?: string } };
if (data.ok) {
return { valid: true, errors: [], warnings: [], details: { botUsername: data.result?.username || 'Unknown' } };

View File

@@ -0,0 +1,43 @@
import { readOpenClawConfig, writeOpenClawConfig } from './channel-config';
import { resolveProxySettings, type ProxySettings } from './proxy';
import { logger } from './logger';
/**
* Sync ClawX global proxy settings into OpenClaw channel config where the
* upstream runtime expects an explicit per-channel proxy knob.
*/
export async function syncProxyConfigToOpenClaw(settings: ProxySettings): Promise<void> {
const config = await readOpenClawConfig();
const telegramConfig = config.channels?.telegram;
if (!telegramConfig) {
return;
}
const resolved = resolveProxySettings(settings);
const nextProxy = settings.proxyEnabled
? (resolved.allProxy || resolved.httpsProxy || resolved.httpProxy)
: '';
const currentProxy = typeof telegramConfig.proxy === 'string' ? telegramConfig.proxy : '';
if (!nextProxy && !currentProxy) {
return;
}
if (!config.channels) {
config.channels = {};
}
config.channels.telegram = {
...telegramConfig,
};
if (nextProxy) {
config.channels.telegram.proxy = nextProxy;
} else {
delete config.channels.telegram.proxy;
}
await writeOpenClawConfig(config);
logger.info(`Synced Telegram proxy to OpenClaw config (${nextProxy || 'disabled'})`);
}

View File

@@ -0,0 +1,21 @@
/**
* Use Electron's network stack when available so requests honor
* session.defaultSession.setProxy(...). Fall back to the Node global fetch
* for non-Electron test environments.
*/
export async function proxyAwareFetch(
input: string | URL,
init?: RequestInit
): Promise<Response> {
if (process.versions.electron) {
try {
const { net } = await import('electron');
return await net.fetch(input, init);
} catch {
// Fall through to the global fetch.
}
}
return await fetch(input, init);
}

122
electron/utils/proxy.ts Normal file
View File

@@ -0,0 +1,122 @@
/**
* Proxy helpers shared by the Electron main process and Gateway launcher.
*/
export interface ProxySettings {
proxyEnabled: boolean;
proxyServer: string;
proxyHttpServer: string;
proxyHttpsServer: string;
proxyAllServer: string;
proxyBypassRules: string;
}
export interface ResolvedProxySettings {
httpProxy: string;
httpsProxy: string;
allProxy: string;
bypassRules: string;
}
export interface ElectronProxyConfig {
mode: 'direct' | 'fixed_servers';
proxyRules?: string;
proxyBypassRules?: string;
}
function trimValue(value: string | undefined | null): string {
return typeof value === 'string' ? value.trim() : '';
}
/**
* Accept bare host:port values from users and normalize them to a valid URL.
* Electron accepts scheme-less proxy rules in some cases, but child-process
* env vars are more reliable when they are full URLs.
*/
export function normalizeProxyServer(proxyServer: string): string {
const value = trimValue(proxyServer);
if (!value) return '';
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value;
return `http://${value}`;
}
export function resolveProxySettings(settings: ProxySettings): ResolvedProxySettings {
const legacyProxy = normalizeProxyServer(settings.proxyServer);
const allProxy = normalizeProxyServer(settings.proxyAllServer);
const httpProxy = normalizeProxyServer(settings.proxyHttpServer) || legacyProxy || allProxy;
const httpsProxy = normalizeProxyServer(settings.proxyHttpsServer) || legacyProxy || allProxy;
return {
httpProxy,
httpsProxy,
allProxy: allProxy || legacyProxy,
bypassRules: trimValue(settings.proxyBypassRules),
};
}
export function buildElectronProxyConfig(settings: ProxySettings): ElectronProxyConfig {
if (!settings.proxyEnabled) {
return { mode: 'direct' };
}
const resolved = resolveProxySettings(settings);
const rules: string[] = [];
if (resolved.httpProxy) {
rules.push(`http=${resolved.httpProxy}`);
}
if (resolved.httpsProxy) {
rules.push(`https=${resolved.httpsProxy}`);
}
// Fallback rule for protocols like ws/wss or when users only configured ALL_PROXY.
const fallbackProxy = resolved.allProxy || resolved.httpsProxy || resolved.httpProxy;
if (fallbackProxy) {
rules.push(fallbackProxy);
}
if (rules.length === 0) {
return { mode: 'direct' };
}
return {
mode: 'fixed_servers',
proxyRules: rules.join(';'),
...(resolved.bypassRules ? { proxyBypassRules: resolved.bypassRules } : {}),
};
}
export function buildProxyEnv(settings: ProxySettings): Record<string, string> {
const blank = {
HTTP_PROXY: '',
HTTPS_PROXY: '',
ALL_PROXY: '',
http_proxy: '',
https_proxy: '',
all_proxy: '',
NO_PROXY: '',
no_proxy: '',
};
if (!settings.proxyEnabled) {
return blank;
}
const resolved = resolveProxySettings(settings);
const noProxy = resolved.bypassRules
.split(/[,\n;]/)
.map((rule) => rule.trim())
.filter(Boolean)
.join(',');
return {
HTTP_PROXY: resolved.httpProxy,
HTTPS_PROXY: resolved.httpsProxy,
ALL_PROXY: resolved.allProxy,
http_proxy: resolved.httpProxy,
https_proxy: resolved.httpsProxy,
all_proxy: resolved.allProxy,
NO_PROXY: noProxy,
no_proxy: noProxy,
};
}

View File

@@ -30,6 +30,12 @@ export interface AppSettings {
gatewayAutoStart: boolean;
gatewayPort: number;
gatewayToken: string;
proxyEnabled: boolean;
proxyServer: string;
proxyHttpServer: string;
proxyHttpsServer: string;
proxyAllServer: string;
proxyBypassRules: string;
// Update
updateChannel: 'stable' | 'beta' | 'dev';
@@ -61,6 +67,12 @@ const defaults: AppSettings = {
gatewayAutoStart: true,
gatewayPort: 18789,
gatewayToken: generateToken(),
proxyEnabled: false,
proxyServer: '',
proxyHttpServer: '',
proxyHttpsServer: '',
proxyAllServer: '',
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
// Update
updateChannel: 'stable',