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

@@ -87,11 +87,16 @@ class ErrorBoundary extends Component<
function App() {
const navigate = useNavigate();
const location = useLocation();
const initSettings = useSettingsStore((state) => state.init);
const theme = useSettingsStore((state) => state.theme);
const language = useSettingsStore((state) => state.language);
const setupComplete = useSettingsStore((state) => state.setupComplete);
const initGateway = useGatewayStore((state) => state.init);
useEffect(() => {
initSettings();
}, [initSettings]);
// Sync i18n language with persisted settings on mount
useEffect(() => {
if (language && language !== i18n.language) {

View File

@@ -85,7 +85,22 @@
"appLogs": "Application Logs",
"openFolder": "Open Folder",
"autoStart": "Auto-start Gateway",
"autoStartDesc": "Start Gateway when ClawX launches"
"autoStartDesc": "Start Gateway when ClawX launches",
"proxyTitle": "Proxy",
"proxyDesc": "Route Electron and Gateway traffic through your local proxy client.",
"proxyServer": "Proxy Server",
"proxyServerHelp": "The default proxy for all requests. Bare host:port values default to HTTP.",
"proxyHttpServer": "HTTP Proxy",
"proxyHttpServerHelp": "Advanced override for HTTP requests. Leave blank to use Proxy Server.",
"proxyHttpsServer": "HTTPS Proxy",
"proxyHttpsServerHelp": "Advanced override for HTTPS requests. Leave blank to use Proxy Server.",
"proxyAllServer": "ALL_PROXY / SOCKS",
"proxyAllServerHelp": "Advanced fallback for SOCKS-capable clients and protocols such as Telegram. Leave blank to use Proxy Server.",
"proxyBypass": "Bypass Rules",
"proxyBypassHelp": "Semicolon, comma, or newline separated hosts that should connect directly.",
"proxyRestartNote": "Saving reapplies Electron networking and restarts the Gateway immediately.",
"proxySaved": "Proxy settings saved",
"proxySaveFailed": "Failed to save proxy settings"
},
"updates": {
"title": "Updates",
@@ -155,4 +170,4 @@
"docs": "Website",
"github": "GitHub"
}
}
}

View File

@@ -83,7 +83,22 @@
"appLogs": "アプリケーションログ",
"openFolder": "フォルダーを開く",
"autoStart": "ゲートウェイ自動起動",
"autoStartDesc": "ClawX 起動時にゲートウェイを自動起動"
"autoStartDesc": "ClawX 起動時にゲートウェイを自動起動",
"proxyTitle": "プロキシ",
"proxyDesc": "Electron と Gateway の通信をローカルプロキシ経由にします。",
"proxyServer": "プロキシサーバー",
"proxyServerHelp": "すべてのリクエストで使う基本プロキシです。host:port のみの場合は HTTP 扱いです。",
"proxyHttpServer": "HTTP プロキシ",
"proxyHttpServerHelp": "HTTP リクエスト用の高度な上書き設定です。空欄の場合はプロキシサーバーを使用します。",
"proxyHttpsServer": "HTTPS プロキシ",
"proxyHttpsServerHelp": "HTTPS リクエスト用の高度な上書き設定です。空欄の場合はプロキシサーバーを使用します。",
"proxyAllServer": "ALL_PROXY / SOCKS",
"proxyAllServerHelp": "SOCKS 対応クライアントや Telegram など向けの高度なフォールバックです。空欄の場合はプロキシサーバーを使用します。",
"proxyBypass": "バイパスルール",
"proxyBypassHelp": "直接接続するホストをセミコロン、カンマ、または改行で区切って指定します。",
"proxyRestartNote": "保存すると Electron のネットワーク設定を再適用し、Gateway をすぐ再起動します。",
"proxySaved": "プロキシ設定を保存しました",
"proxySaveFailed": "プロキシ設定の保存に失敗しました"
},
"updates": {
"title": "アップデート",
@@ -153,4 +168,4 @@
"docs": "公式サイト",
"github": "GitHub"
}
}
}

View File

@@ -85,7 +85,22 @@
"appLogs": "应用日志",
"openFolder": "打开文件夹",
"autoStart": "自动启动网关",
"autoStartDesc": "ClawX 启动时自动启动网关"
"autoStartDesc": "ClawX 启动时自动启动网关",
"proxyTitle": "代理",
"proxyDesc": "让 Electron 和 Gateway 的网络请求都走本地代理客户端。",
"proxyServer": "代理服务器",
"proxyServerHelp": "所有请求默认使用的代理。只填 host:port 时默认按 HTTP 处理。",
"proxyHttpServer": "HTTP 代理",
"proxyHttpServerHelp": "HTTP 请求的高级覆盖项。留空时使用“代理服务器”。",
"proxyHttpsServer": "HTTPS 代理",
"proxyHttpsServerHelp": "HTTPS 请求的高级覆盖项。留空时使用“代理服务器”。",
"proxyAllServer": "ALL_PROXY / SOCKS",
"proxyAllServerHelp": "支持 SOCKS 的客户端和 Telegram 等协议的高级兜底代理。留空时使用“代理服务器”。",
"proxyBypass": "绕过规则",
"proxyBypassHelp": "使用分号、逗号或换行分隔需要直连的主机。",
"proxyRestartNote": "保存后会立即重新应用 Electron 网络代理,并自动重启 Gateway。",
"proxySaved": "代理设置已保存",
"proxySaveFailed": "保存代理设置失败"
},
"updates": {
"title": "更新",
@@ -155,4 +170,4 @@
"docs": "官网",
"github": "GitHub"
}
}
}

View File

@@ -45,6 +45,18 @@ export function Settings() {
setLanguage,
gatewayAutoStart,
setGatewayAutoStart,
proxyEnabled,
proxyServer,
proxyHttpServer,
proxyHttpsServer,
proxyAllServer,
proxyBypassRules,
setProxyEnabled,
setProxyServer,
setProxyHttpServer,
setProxyHttpsServer,
setProxyAllServer,
setProxyBypassRules,
autoCheckUpdate,
setAutoCheckUpdate,
autoDownloadUpdate,
@@ -59,6 +71,13 @@ export function Settings() {
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
const [proxyServerDraft, setProxyServerDraft] = useState('');
const [proxyHttpServerDraft, setProxyHttpServerDraft] = useState('');
const [proxyHttpsServerDraft, setProxyHttpsServerDraft] = useState('');
const [proxyAllServerDraft, setProxyAllServerDraft] = useState('');
const [proxyBypassRulesDraft, setProxyBypassRulesDraft] = useState('');
const [proxyEnabledDraft, setProxyEnabledDraft] = useState(false);
const [savingProxy, setSavingProxy] = useState(false);
const isWindows = window.electron.platform === 'win32';
const showCliTools = true;
@@ -184,6 +203,62 @@ export function Settings() {
return () => { unsubscribe?.(); };
}, []);
useEffect(() => {
setProxyEnabledDraft(proxyEnabled);
}, [proxyEnabled]);
useEffect(() => {
setProxyServerDraft(proxyServer);
}, [proxyServer]);
useEffect(() => {
setProxyHttpServerDraft(proxyHttpServer);
}, [proxyHttpServer]);
useEffect(() => {
setProxyHttpsServerDraft(proxyHttpsServer);
}, [proxyHttpsServer]);
useEffect(() => {
setProxyAllServerDraft(proxyAllServer);
}, [proxyAllServer]);
useEffect(() => {
setProxyBypassRulesDraft(proxyBypassRules);
}, [proxyBypassRules]);
const handleSaveProxySettings = async () => {
setSavingProxy(true);
try {
const normalizedProxyServer = proxyServerDraft.trim();
const normalizedHttpServer = proxyHttpServerDraft.trim();
const normalizedHttpsServer = proxyHttpsServerDraft.trim();
const normalizedAllServer = proxyAllServerDraft.trim();
const normalizedBypassRules = proxyBypassRulesDraft.trim();
await window.electron.ipcRenderer.invoke('settings:setMany', {
proxyEnabled: proxyEnabledDraft,
proxyServer: normalizedProxyServer,
proxyHttpServer: normalizedHttpServer,
proxyHttpsServer: normalizedHttpsServer,
proxyAllServer: normalizedAllServer,
proxyBypassRules: normalizedBypassRules,
});
setProxyServer(normalizedProxyServer);
setProxyHttpServer(normalizedHttpServer);
setProxyHttpsServer(normalizedHttpsServer);
setProxyAllServer(normalizedAllServer);
setProxyBypassRules(normalizedBypassRules);
setProxyEnabled(proxyEnabledDraft);
toast.success(t('gateway.proxySaved'));
} catch (error) {
toast.error(`${t('gateway.proxySaveFailed')}: ${String(error)}`);
} finally {
setSavingProxy(false);
}
};
return (
<div className="space-y-6 p-6">
<div>
@@ -332,6 +407,106 @@ export function Settings() {
onCheckedChange={setGatewayAutoStart}
/>
</div>
<Separator />
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>{t('gateway.proxyTitle')}</Label>
<p className="text-sm text-muted-foreground">
{t('gateway.proxyDesc')}
</p>
</div>
<Switch
checked={proxyEnabledDraft}
onCheckedChange={setProxyEnabledDraft}
/>
</div>
<div className="space-y-2">
<Label htmlFor="proxy-server">{t('gateway.proxyServer')}</Label>
<Input
id="proxy-server"
value={proxyServerDraft}
onChange={(event) => setProxyServerDraft(event.target.value)}
placeholder="http://127.0.0.1:7890"
/>
<p className="text-xs text-muted-foreground">
{t('gateway.proxyServerHelp')}
</p>
</div>
{devModeUnlocked && (
<>
<div className="space-y-2">
<Label htmlFor="proxy-http-server">{t('gateway.proxyHttpServer')}</Label>
<Input
id="proxy-http-server"
value={proxyHttpServerDraft}
onChange={(event) => setProxyHttpServerDraft(event.target.value)}
placeholder={proxyServerDraft || 'http://127.0.0.1:7890'}
/>
<p className="text-xs text-muted-foreground">
{t('gateway.proxyHttpServerHelp')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="proxy-https-server">{t('gateway.proxyHttpsServer')}</Label>
<Input
id="proxy-https-server"
value={proxyHttpsServerDraft}
onChange={(event) => setProxyHttpsServerDraft(event.target.value)}
placeholder={proxyServerDraft || 'http://127.0.0.1:7890'}
/>
<p className="text-xs text-muted-foreground">
{t('gateway.proxyHttpsServerHelp')}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="proxy-all-server">{t('gateway.proxyAllServer')}</Label>
<Input
id="proxy-all-server"
value={proxyAllServerDraft}
onChange={(event) => setProxyAllServerDraft(event.target.value)}
placeholder={proxyServerDraft || 'socks5://127.0.0.1:7891'}
/>
<p className="text-xs text-muted-foreground">
{t('gateway.proxyAllServerHelp')}
</p>
</div>
</>
)}
<div className="space-y-2">
<Label htmlFor="proxy-bypass">{t('gateway.proxyBypass')}</Label>
<Input
id="proxy-bypass"
value={proxyBypassRulesDraft}
onChange={(event) => setProxyBypassRulesDraft(event.target.value)}
placeholder="<local>;localhost;127.0.0.1;::1"
/>
<p className="text-xs text-muted-foreground">
{t('gateway.proxyBypassHelp')}
</p>
</div>
<div className="flex items-center justify-between gap-3 rounded-lg border border-border/60 bg-background/40 p-3">
<p className="text-sm text-muted-foreground">
{t('gateway.proxyRestartNote')}
</p>
<Button
variant="outline"
onClick={handleSaveProxySettings}
disabled={savingProxy}
>
<RefreshCw className={`h-4 w-4 mr-2${savingProxy ? ' animate-spin' : ''}`} />
{savingProxy ? t('common:status.saving') : t('common:actions.save')}
</Button>
</div>
</div>
</CardContent>
</Card>

View File

@@ -19,6 +19,12 @@ interface SettingsState {
// Gateway
gatewayAutoStart: boolean;
gatewayPort: number;
proxyEnabled: boolean;
proxyServer: string;
proxyHttpServer: string;
proxyHttpsServer: string;
proxyAllServer: string;
proxyBypassRules: string;
// Update
updateChannel: UpdateChannel;
@@ -33,12 +39,19 @@ interface SettingsState {
setupComplete: boolean;
// Actions
init: () => Promise<void>;
setTheme: (theme: Theme) => void;
setLanguage: (language: string) => void;
setStartMinimized: (value: boolean) => void;
setLaunchAtStartup: (value: boolean) => void;
setGatewayAutoStart: (value: boolean) => void;
setGatewayPort: (port: number) => void;
setProxyEnabled: (value: boolean) => void;
setProxyServer: (value: string) => void;
setProxyHttpServer: (value: string) => void;
setProxyHttpsServer: (value: string) => void;
setProxyAllServer: (value: string) => void;
setProxyBypassRules: (value: string) => void;
setUpdateChannel: (channel: UpdateChannel) => void;
setAutoCheckUpdate: (value: boolean) => void;
setAutoDownloadUpdate: (value: boolean) => void;
@@ -60,6 +73,12 @@ const defaultSettings = {
launchAtStartup: false,
gatewayAutoStart: true,
gatewayPort: 18789,
proxyEnabled: false,
proxyServer: '',
proxyHttpServer: '',
proxyHttpsServer: '',
proxyAllServer: '',
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
updateChannel: 'stable' as UpdateChannel,
autoCheckUpdate: true,
autoDownloadUpdate: false,
@@ -73,12 +92,31 @@ export const useSettingsStore = create<SettingsState>()(
(set) => ({
...defaultSettings,
init: async () => {
try {
const settings = await window.electron.ipcRenderer.invoke('settings:getAll') as Partial<typeof defaultSettings>;
set((state) => ({ ...state, ...settings }));
if (settings.language) {
i18n.changeLanguage(settings.language);
}
} catch {
// Keep renderer-persisted settings as a fallback when the main
// process store is not reachable.
}
},
setTheme: (theme) => set({ theme }),
setLanguage: (language) => { i18n.changeLanguage(language); set({ language }); },
setLanguage: (language) => { i18n.changeLanguage(language); set({ language }); void window.electron.ipcRenderer.invoke('settings:set', 'language', language).catch(() => {}); },
setStartMinimized: (startMinimized) => set({ startMinimized }),
setLaunchAtStartup: (launchAtStartup) => set({ launchAtStartup }),
setGatewayAutoStart: (gatewayAutoStart) => set({ gatewayAutoStart }),
setGatewayPort: (gatewayPort) => set({ gatewayPort }),
setGatewayAutoStart: (gatewayAutoStart) => { set({ gatewayAutoStart }); void window.electron.ipcRenderer.invoke('settings:set', 'gatewayAutoStart', gatewayAutoStart).catch(() => {}); },
setGatewayPort: (gatewayPort) => { set({ gatewayPort }); void window.electron.ipcRenderer.invoke('settings:set', 'gatewayPort', gatewayPort).catch(() => {}); },
setProxyEnabled: (proxyEnabled) => set({ proxyEnabled }),
setProxyServer: (proxyServer) => set({ proxyServer }),
setProxyHttpServer: (proxyHttpServer) => set({ proxyHttpServer }),
setProxyHttpsServer: (proxyHttpsServer) => set({ proxyHttpsServer }),
setProxyAllServer: (proxyAllServer) => set({ proxyAllServer }),
setProxyBypassRules: (proxyBypassRules) => set({ proxyBypassRules }),
setUpdateChannel: (updateChannel) => set({ updateChannel }),
setAutoCheckUpdate: (autoCheckUpdate) => set({ autoCheckUpdate }),
setAutoDownloadUpdate: (autoDownloadUpdate) => set({ autoDownloadUpdate }),