fix: preserve telegram proxy on gateway restart after doctor (#546)

Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
Lingxuan Zuo
2026-03-21 17:09:08 +08:00
committed by GitHub
Unverified
parent e10ff3a1fb
commit 56701d823c
11 changed files with 122 additions and 5 deletions

View File

@@ -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` エントリーポイント経由で実行されます。

View File

@@ -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.

View File

@@ -194,6 +194,8 @@ ClawX 内置了代理设置,适用于需要通过本地代理客户端访问
- 高级代理项留空时,会自动回退到“代理服务器”。
- 保存代理设置后Electron 网络层会立即重新应用代理,并自动重启 Gateway。
- 如果启用了 TelegramClawX 还会把代理同步到 OpenClaw 的 Telegram 频道配置中。
- 当 ClawX 代理处于关闭状态时Gateway 的常规重启会保留已有的 Telegram 频道代理配置。
- 如果你要明确清空 OpenClaw 中的 Telegram 代理,请在关闭代理后点一次“保存代理设置”。
-**设置 → 高级 → 开发者** 中,可以直接运行 **OpenClaw Doctor**,执行 `openclaw doctor --json` 并在应用内查看诊断输出。
- 在 Windows 打包版本中,内置的 `openclaw` CLI/TUI 会通过随包分发的 `node.exe` 入口运行,以保证终端输入行为稳定。

View File

@@ -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<void> {
const settings = await getAllSettings();
await syncProxyConfigToOpenClaw(settings, { preserveExistingWhenDisabled: false });
await applyProxySettings(settings);
if (ctx.gatewayManager.getStatus().state === 'running') {
await ctx.gatewayManager.restart();

View File

@@ -124,7 +124,7 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
export async function syncGatewayConfigBeforeLaunch(
appSettings: Awaited<ReturnType<typeof getAllSettings>>,
): Promise<void> {
await syncProxyConfigToOpenClaw(appSettings);
await syncProxyConfigToOpenClaw(appSettings, { preserveExistingWhenDisabled: true });
try {
await sanitizeOpenClawConfig();

View File

@@ -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();

View File

@@ -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<void> {
export async function syncProxyConfigToOpenClaw(
settings: ProxySettings,
options: SyncProxyOptions = {},
): Promise<void> {
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;
}

View File

@@ -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"
},

View File

@@ -140,7 +140,7 @@
"hideAdvancedProxy": "高度なプロキシ項目を非表示",
"proxyBypass": "バイパスルール",
"proxyBypassHelp": "直接接続するホストをセミコロン、カンマ、または改行で区切って指定します。",
"proxyRestartNote": "保存すると Electron のネットワーク設定を再適用し、Gateway をすぐ再起動します。",
"proxyRestartNote": "保存すると Electron のネットワーク設定を再適用し、Gateway をすぐ再起動します。プロキシ無効時の通常再起動では Telegram チャネルの既存プロキシ設定を保持し、無効状態で保存すると明示的に削除されます。",
"proxySaved": "プロキシ設定を保存しました",
"proxySaveFailed": "プロキシ設定の保存に失敗しました"
},

View File

@@ -141,7 +141,7 @@
"hideAdvancedProxy": "隐藏高级代理字段",
"proxyBypass": "绕过规则",
"proxyBypassHelp": "使用分号、逗号或换行分隔需要直连的主机。",
"proxyRestartNote": "保存后会立即重新应用 Electron 网络代理,并自动重启 Gateway。",
"proxyRestartNote": "保存后会立即重新应用 Electron 网络代理,并自动重启 Gateway。若代理已关闭Gateway 常规重启会保留 Telegram 频道已有代理;如需清空,请在关闭代理后手动保存一次。",
"proxySaved": "代理设置已保存",
"proxySaveFailed": "保存代理设置失败"
},

View File

@@ -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<unknown>) => 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<string, unknown> };
};
expect(updatedConfig.channels.telegram.proxy).toBeUndefined();
});
});