From 56701d823c492372c310116f4090006524a3c2b8 Mon Sep 17 00:00:00 2001 From: Lingxuan Zuo Date: Sat, 21 Mar 2026 17:09:08 +0800 Subject: [PATCH] fix: preserve telegram proxy on gateway restart after doctor (#546) Co-authored-by: zuolingxuan --- README.ja-JP.md | 2 + README.md | 2 + README.zh-CN.md | 2 + electron/api/routes/settings.ts | 2 + electron/gateway/config-sync.ts | 2 +- electron/main/ipc-handlers.ts | 3 ++ electron/utils/openclaw-proxy.ts | 19 ++++++- src/i18n/locales/en/settings.json | 2 +- src/i18n/locales/ja/settings.json | 2 +- src/i18n/locales/zh/settings.json | 2 +- tests/unit/openclaw-proxy.test.ts | 89 +++++++++++++++++++++++++++++++ 11 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/unit/openclaw-proxy.test.ts diff --git a/README.ja-JP.md b/README.ja-JP.md index eedcde87a..8ffdb964c 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -190,6 +190,8 @@ ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネ - 高度なプロキシフィールドが空の場合、ClawXは`プロキシサーバー`にフォールバックします。 - プロキシ設定を保存すると、Electronのネットワーク設定が即座に再適用され、ゲートウェイが自動的に再起動されます。 - ClawXはTelegramが有効な場合、プロキシをOpenClawのTelegramチャネル設定にも同期します。 +- ClawXのプロキシが無効な状態では、Gatewayの通常再起動時に既存のTelegramチャネルプロキシ設定を保持します。 +- OpenClaw設定のTelegramプロキシを明示的に消したい場合は、プロキシ無効の状態で一度「保存」を実行してください。 - **設定 → 詳細 → 開発者** では **OpenClaw Doctor** を実行でき、`openclaw doctor --json` の診断出力をアプリ内で確認できます。 - Windows のパッケージ版では、同梱された `openclaw` CLI/TUI は端末入力を安定させるため、同梱の `node.exe` エントリーポイント経由で実行されます。 diff --git a/README.md b/README.md index 0e839d7e2..ef9bffc0b 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ Notes: - If advanced proxy fields are left empty, ClawX falls back to `Proxy Server`. - Saving proxy settings reapplies Electron networking immediately and restarts the Gateway automatically. - ClawX also syncs the proxy to OpenClaw's Telegram channel config when Telegram is enabled. +- Gateway restarts preserve an existing Telegram channel proxy if ClawX proxy is currently disabled. +- To explicitly clear Telegram channel proxy from OpenClaw config, save proxy settings with proxy disabled. - In **Settings → Advanced → Developer**, you can run **OpenClaw Doctor** to execute `openclaw doctor --json` and inspect the diagnostic output without leaving the app. - On packaged Windows builds, the bundled `openclaw` CLI/TUI runs via the shipped `node.exe` entrypoint to keep terminal input behavior stable. diff --git a/README.zh-CN.md b/README.zh-CN.md index c772586e3..0377a9701 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -194,6 +194,8 @@ ClawX 内置了代理设置,适用于需要通过本地代理客户端访问 - 高级代理项留空时,会自动回退到“代理服务器”。 - 保存代理设置后,Electron 网络层会立即重新应用代理,并自动重启 Gateway。 - 如果启用了 Telegram,ClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。 +- 当 ClawX 代理处于关闭状态时,Gateway 的常规重启会保留已有的 Telegram 频道代理配置。 +- 如果你要明确清空 OpenClaw 中的 Telegram 代理,请在关闭代理后点一次“保存代理设置”。 - 在 **设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。 - 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。 diff --git a/electron/api/routes/settings.ts b/electron/api/routes/settings.ts index ddb552188..c3e948e70 100644 --- a/electron/api/routes/settings.ts +++ b/electron/api/routes/settings.ts @@ -1,12 +1,14 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { applyProxySettings } from '../../main/proxy'; import { syncLaunchAtStartupSettingFromStore } from '../../main/launch-at-startup'; +import { syncProxyConfigToOpenClaw } from '../../utils/openclaw-proxy'; import { getAllSettings, getSetting, resetSettings, setSetting, type AppSettings } from '../../utils/store'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; async function handleProxySettingsChange(ctx: HostApiContext): Promise { const settings = await getAllSettings(); + await syncProxyConfigToOpenClaw(settings, { preserveExistingWhenDisabled: false }); await applyProxySettings(settings); if (ctx.gatewayManager.getStatus().state === 'running') { await ctx.gatewayManager.restart(); diff --git a/electron/gateway/config-sync.ts b/electron/gateway/config-sync.ts index ee82ce03a..a866164b1 100644 --- a/electron/gateway/config-sync.ts +++ b/electron/gateway/config-sync.ts @@ -124,7 +124,7 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void { export async function syncGatewayConfigBeforeLaunch( appSettings: Awaited>, ): Promise { - await syncProxyConfigToOpenClaw(appSettings); + await syncProxyConfigToOpenClaw(appSettings, { preserveExistingWhenDisabled: true }); try { await sanitizeOpenClawConfig(); diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 3b5e26529..2b50aa920 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -19,6 +19,7 @@ import { saveProviderKeyToOpenClaw, removeProviderFromOpenClaw, } from '../utils/openclaw-auth'; +import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy'; import { buildOpenClawControlUiUrl } from '../utils/openclaw-control-ui'; import { logger } from '../utils/logger'; import { @@ -240,6 +241,7 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void { const providerService = getProviderService(); const handleProxySettingsChange = async () => { const settings = await getAllSettings(); + await syncProxyConfigToOpenClaw(settings, { preserveExistingWhenDisabled: false }); await applyProxySettings(settings); if (gatewayManager.getStatus().state === 'running') { await gatewayManager.restart(); @@ -2121,6 +2123,7 @@ function registerAppHandlers(): void { function registerSettingsHandlers(gatewayManager: GatewayManager): void { const handleProxySettingsChange = async () => { const settings = await getAllSettings(); + await syncProxyConfigToOpenClaw(settings, { preserveExistingWhenDisabled: false }); await applyProxySettings(settings); if (gatewayManager.getStatus().state === 'running') { await gatewayManager.restart(); diff --git a/electron/utils/openclaw-proxy.ts b/electron/utils/openclaw-proxy.ts index 1922348fc..c4b283573 100644 --- a/electron/utils/openclaw-proxy.ts +++ b/electron/utils/openclaw-proxy.ts @@ -3,11 +3,22 @@ import { resolveProxySettings, type ProxySettings } from './proxy'; import { logger } from './logger'; import { withConfigLock } from './config-mutex'; +interface SyncProxyOptions { + /** + * When true, keep an existing channels.telegram.proxy value if proxy is + * currently disabled in ClawX settings. + */ + preserveExistingWhenDisabled?: boolean; +} + /** * 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 { +export async function syncProxyConfigToOpenClaw( + settings: ProxySettings, + options: SyncProxyOptions = {}, +): Promise { return withConfigLock(async () => { const config = await readOpenClawConfig(); const telegramConfig = config.channels?.telegram; @@ -17,11 +28,17 @@ export async function syncProxyConfigToOpenClaw(settings: ProxySettings): Promis } const resolved = resolveProxySettings(settings); + const preserveExistingWhenDisabled = options.preserveExistingWhenDisabled !== false; const nextProxy = settings.proxyEnabled ? (resolved.allProxy || resolved.httpsProxy || resolved.httpProxy) : ''; const currentProxy = typeof telegramConfig.proxy === 'string' ? telegramConfig.proxy : ''; + if (!settings.proxyEnabled && preserveExistingWhenDisabled && currentProxy) { + logger.info('Skipped Telegram proxy sync because ClawX proxy is disabled and preserve mode is enabled'); + return; + } + if (!nextProxy && !currentProxy) { return; } diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 5933b3463..d6bf22911 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -141,7 +141,7 @@ "hideAdvancedProxy": "Hide advanced proxy fields", "proxyBypass": "Bypass Rules", "proxyBypassHelp": "Semicolon, comma, or newline separated hosts that should connect directly.", - "proxyRestartNote": "Saving reapplies Electron networking and restarts the Gateway immediately.", + "proxyRestartNote": "Saving reapplies Electron networking and restarts the Gateway immediately. Regular Gateway restarts keep existing Telegram channel proxy values when proxy is disabled; save while disabled to clear them.", "proxySaved": "Proxy settings saved", "proxySaveFailed": "Failed to save proxy settings" }, diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index 62afd976a..cdf4623f2 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -140,7 +140,7 @@ "hideAdvancedProxy": "高度なプロキシ項目を非表示", "proxyBypass": "バイパスルール", "proxyBypassHelp": "直接接続するホストをセミコロン、カンマ、または改行で区切って指定します。", - "proxyRestartNote": "保存すると Electron のネットワーク設定を再適用し、Gateway をすぐ再起動します。", + "proxyRestartNote": "保存すると Electron のネットワーク設定を再適用し、Gateway をすぐ再起動します。プロキシ無効時の通常再起動では Telegram チャネルの既存プロキシ設定を保持し、無効状態で保存すると明示的に削除されます。", "proxySaved": "プロキシ設定を保存しました", "proxySaveFailed": "プロキシ設定の保存に失敗しました" }, diff --git a/src/i18n/locales/zh/settings.json b/src/i18n/locales/zh/settings.json index 62750b86a..475ea5461 100644 --- a/src/i18n/locales/zh/settings.json +++ b/src/i18n/locales/zh/settings.json @@ -141,7 +141,7 @@ "hideAdvancedProxy": "隐藏高级代理字段", "proxyBypass": "绕过规则", "proxyBypassHelp": "使用分号、逗号或换行分隔需要直连的主机。", - "proxyRestartNote": "保存后会立即重新应用 Electron 网络代理,并自动重启 Gateway。", + "proxyRestartNote": "保存后会立即重新应用 Electron 网络代理,并自动重启 Gateway。若代理已关闭,Gateway 常规重启会保留 Telegram 频道已有代理;如需清空,请在关闭代理后手动保存一次。", "proxySaved": "代理设置已保存", "proxySaveFailed": "保存代理设置失败" }, diff --git a/tests/unit/openclaw-proxy.test.ts b/tests/unit/openclaw-proxy.test.ts new file mode 100644 index 000000000..5ca7963aa --- /dev/null +++ b/tests/unit/openclaw-proxy.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { + readOpenClawConfigMock, + writeOpenClawConfigMock, + withConfigLockMock, +} = vi.hoisted(() => ({ + readOpenClawConfigMock: vi.fn(), + writeOpenClawConfigMock: vi.fn(), + withConfigLockMock: vi.fn(async (fn: () => Promise) => await fn()), +})); + +vi.mock('@electron/utils/channel-config', () => ({ + readOpenClawConfig: readOpenClawConfigMock, + writeOpenClawConfig: writeOpenClawConfigMock, +})); + +vi.mock('@electron/utils/config-mutex', () => ({ + withConfigLock: withConfigLockMock, +})); + +vi.mock('@electron/utils/logger', () => ({ + logger: { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, +})); + +describe('syncProxyConfigToOpenClaw', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('preserves existing telegram proxy on startup-style sync when proxy is disabled', async () => { + readOpenClawConfigMock.mockResolvedValue({ + channels: { + telegram: { + botToken: 'token', + proxy: 'socks5://127.0.0.1:7891', + }, + }, + }); + + const { syncProxyConfigToOpenClaw } = await import('@electron/utils/openclaw-proxy'); + + await syncProxyConfigToOpenClaw({ + proxyEnabled: false, + proxyServer: '', + proxyHttpServer: '', + proxyHttpsServer: '', + proxyAllServer: '', + proxyBypassRules: '', + }); + + expect(writeOpenClawConfigMock).not.toHaveBeenCalled(); + }); + + it('clears telegram proxy when explicitly requested while proxy is disabled', async () => { + readOpenClawConfigMock.mockResolvedValue({ + channels: { + telegram: { + botToken: 'token', + proxy: 'socks5://127.0.0.1:7891', + }, + }, + }); + + const { syncProxyConfigToOpenClaw } = await import('@electron/utils/openclaw-proxy'); + + await syncProxyConfigToOpenClaw({ + proxyEnabled: false, + proxyServer: '', + proxyHttpServer: '', + proxyHttpsServer: '', + proxyAllServer: '', + proxyBypassRules: '', + }, { + preserveExistingWhenDisabled: false, + }); + + expect(writeOpenClawConfigMock).toHaveBeenCalledTimes(1); + const updatedConfig = writeOpenClawConfigMock.mock.calls[0][0] as { + channels: { telegram: Record }; + }; + expect(updatedConfig.channels.telegram.proxy).toBeUndefined(); + }); +});