diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d88975b75..34fe13e837 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai
- Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.
- Discord: treat Administrator as full permissions in channel permission checks. Thanks @thewilloftheshadow.
- Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.
+- Discord: add optional gateway proxy support for WebSocket connections via `channels.discord.proxy`. (#10400) Thanks @winter-loo, @thewilloftheshadow.
- Browser: add Chrome launch flag `--disable-blink-features=AutomationControlled` to reduce `navigator.webdriver` automation detection issues on reCAPTCHA-protected sites. (#10735) Thanks @Milofax.
- Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.
- Signal: render mention placeholders as `@uuid`/`@phone` so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.
diff --git a/docs/channels/discord.md b/docs/channels/discord.md
index 358deeac23..e55b03a10f 100644
--- a/docs/channels/discord.md
+++ b/docs/channels/discord.md
@@ -330,6 +330,37 @@ See [Slash commands](/tools/slash-commands) for command catalog and behavior.
+
+ Route Discord gateway WebSocket traffic through an HTTP(S) proxy with `channels.discord.proxy`.
+
+```json5
+{
+ channels: {
+ discord: {
+ proxy: "http://proxy.example:8080",
+ },
+ },
+}
+```
+
+ Per-account override:
+
+```json5
+{
+ channels: {
+ discord: {
+ accounts: {
+ primary: {
+ proxy: "http://proxy.example:8080",
+ },
+ },
+ },
+ },
+}
+```
+
+
+
Enable PluralKit resolution to map proxied messages to system member identity:
diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts
index 222cd7f454..52841428c0 100644
--- a/src/config/schema.help.ts
+++ b/src/config/schema.help.ts
@@ -293,6 +293,8 @@ export const FIELD_HELP: Record = {
"Allow Mattermost to write config in response to channel events/commands (default: true).",
"channels.discord.configWrites":
"Allow Discord to write config in response to channel events/commands (default: true).",
+ "channels.discord.proxy":
+ "Proxy URL for Discord gateway WebSocket connections. Set per account via channels.discord.accounts..proxy.",
"channels.whatsapp.configWrites":
"Allow WhatsApp to write config in response to channel events/commands (default: true).",
"channels.signal.configWrites":
diff --git a/src/discord/monitor/provider.proxy.test.ts b/src/discord/monitor/provider.proxy.test.ts
new file mode 100644
index 0000000000..caed864629
--- /dev/null
+++ b/src/discord/monitor/provider.proxy.test.ts
@@ -0,0 +1,105 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { HttpsProxyAgent, getLastAgent, proxyAgentSpy, resetLastAgent, webSocketSpy } = vi.hoisted(
+ () => {
+ const proxyAgentSpy = vi.fn();
+ const webSocketSpy = vi.fn();
+
+ class HttpsProxyAgent {
+ static lastCreated: HttpsProxyAgent | undefined;
+ proxyUrl: string;
+ constructor(proxyUrl: string) {
+ if (proxyUrl === "bad-proxy") {
+ throw new Error("bad proxy");
+ }
+ this.proxyUrl = proxyUrl;
+ HttpsProxyAgent.lastCreated = this;
+ proxyAgentSpy(proxyUrl);
+ }
+ }
+
+ return {
+ HttpsProxyAgent,
+ getLastAgent: () => HttpsProxyAgent.lastCreated,
+ proxyAgentSpy,
+ resetLastAgent: () => {
+ HttpsProxyAgent.lastCreated = undefined;
+ },
+ webSocketSpy,
+ };
+ },
+);
+
+vi.mock("https-proxy-agent", () => ({
+ HttpsProxyAgent,
+}));
+
+vi.mock("ws", () => ({
+ default: class MockWebSocket {
+ constructor(url: string, options?: { agent?: unknown }) {
+ webSocketSpy(url, options);
+ }
+ },
+}));
+
+describe("createDiscordGatewayPlugin", () => {
+ beforeEach(() => {
+ proxyAgentSpy.mockReset();
+ webSocketSpy.mockReset();
+ resetLastAgent();
+ });
+
+ it("uses proxy agent for gateway WebSocket when configured", async () => {
+ const { __testing } = await import("./provider.js");
+ const { GatewayPlugin } = await import("@buape/carbon/gateway");
+
+ const runtime = {
+ log: vi.fn(),
+ error: vi.fn(),
+ exit: vi.fn(() => {
+ throw new Error("exit");
+ }),
+ };
+
+ const plugin = __testing.createDiscordGatewayPlugin({
+ discordConfig: { proxy: "http://proxy.test:8080" },
+ runtime,
+ });
+
+ expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype);
+
+ const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown })
+ .createWebSocket;
+ createWebSocket("wss://gateway.discord.gg");
+
+ expect(proxyAgentSpy).toHaveBeenCalledWith("http://proxy.test:8080");
+ expect(webSocketSpy).toHaveBeenCalledWith(
+ "wss://gateway.discord.gg",
+ expect.objectContaining({ agent: getLastAgent() }),
+ );
+ expect(runtime.log).toHaveBeenCalledWith("discord: gateway proxy enabled");
+ expect(runtime.error).not.toHaveBeenCalled();
+ });
+
+ it("falls back to the default gateway plugin when proxy is invalid", async () => {
+ const { __testing } = await import("./provider.js");
+ const { GatewayPlugin } = await import("@buape/carbon/gateway");
+
+ const runtime = {
+ log: vi.fn(),
+ error: vi.fn(),
+ exit: vi.fn(() => {
+ throw new Error("exit");
+ }),
+ };
+
+ const plugin = __testing.createDiscordGatewayPlugin({
+ discordConfig: { proxy: "bad-proxy" },
+ runtime,
+ });
+
+ expect(Object.getPrototypeOf(plugin)).toBe(GatewayPlugin.prototype);
+ expect(runtime.error).toHaveBeenCalled();
+ expect(runtime.log).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts
index 7cf384940e..24391c1731 100644
--- a/src/discord/monitor/provider.ts
+++ b/src/discord/monitor/provider.ts
@@ -85,10 +85,7 @@ function createDiscordGatewayPlugin(params: {
this.#proxyAgent = proxyAgent;
}
- createWebSocket(url?: string) {
- if (!url) {
- throw new Error("Gateway URL is required");
- }
+ createWebSocket(url: string) {
return new WebSocket(url, { agent: this.#proxyAgent });
}
}
@@ -753,3 +750,7 @@ async function clearDiscordNativeCommands(params: {
params.runtime.error?.(danger(`discord: failed to clear native commands: ${String(err)}`));
}
}
+
+export const __testing = {
+ createDiscordGatewayPlugin,
+};