diff --git a/package.json b/package.json index 36c25a221b..bd2cba2361 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "express": "^5.2.1", "file-type": "^21.3.0", "grammy": "^1.40.0", + "https-proxy-agent": "^7.0.6", "jiti": "^2.6.1", "json5": "^2.2.3", "jszip": "^3.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c20d53d9b9..c85cf9c574 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ importers: grammy: specifier: ^1.40.0 version: 1.40.0 + https-proxy-agent: + specifier: ^7.0.6 + version: 7.0.6 jiti: specifier: ^2.6.1 version: 2.6.1 diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index b01f455321..73a84383ff 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -123,6 +123,8 @@ export type DiscordAccountConfig = { /** If false, do not start this Discord account. Default: true. */ enabled?: boolean; token?: string; + /** HTTP(S) proxy URL for Discord gateway WebSocket connections. */ + proxy?: string; /** Allow bot-authored messages to trigger replies (default: false). */ allowBots?: boolean; /** diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 590accc9c6..7e2c4bd0f4 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -266,6 +266,7 @@ export const DiscordAccountSchema = z commands: ProviderCommandsSchema, configWrites: z.boolean().optional(), token: z.string().optional().register(sensitive), + proxy: z.string().optional(), allowBots: z.boolean().optional(), groupPolicy: GroupPolicySchema.optional().default("allowlist"), historyLimit: z.number().int().min(0).optional(), diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index 28e1079ec1..4f791faa08 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -1,9 +1,12 @@ import { Client, type BaseMessageInteractiveComponent } from "@buape/carbon"; import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway"; import { Routes } from "discord-api-types/v10"; +import { HttpsProxyAgent } from "https-proxy-agent"; import { inspect } from "node:util"; +import WebSocket from "ws"; import type { HistoryEntry } from "../../auto-reply/reply/history.js"; import type { OpenClawConfig, ReplyToMode } from "../../config/config.js"; +import type { DiscordAccountConfig } from "../../config/types.js"; import type { RuntimeEnv } from "../../runtime.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js"; @@ -53,6 +56,51 @@ export type MonitorDiscordOpts = { replyToMode?: ReplyToMode; }; +function createDiscordGatewayPlugin(params: { + discordConfig: DiscordAccountConfig; + runtime: RuntimeEnv; +}): GatewayPlugin { + const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents); + const proxy = params.discordConfig?.proxy?.trim(); + const options = { + reconnect: { maxAttempts: Number.POSITIVE_INFINITY }, + intents, + autoInteractions: true, + }; + + if (!proxy) { + return new GatewayPlugin(options); + } + + let agent: HttpsProxyAgent | undefined; + try { + agent = new HttpsProxyAgent(proxy); + } catch (err) { + params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`)); + return new GatewayPlugin(options); + } + + params.runtime.log?.("discord: gateway proxy enabled"); + + class ProxyGatewayPlugin extends GatewayPlugin { + #proxyAgent: HttpsProxyAgent; + + constructor(proxyAgent: HttpsProxyAgent) { + super(options); + this.#proxyAgent = proxyAgent; + } + + createWebSocket(url?: string) { + if (!url) { + throw new Error("Gateway URL is required"); + } + return new WebSocket(url, { agent: this.#proxyAgent }); + } + } + + return new ProxyGatewayPlugin(agent); +} + function summarizeAllowList(list?: Array) { if (!list || list.length === 0) { return "any"; @@ -527,15 +575,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { listeners: [], components, }, - [ - new GatewayPlugin({ - reconnect: { - maxAttempts: 50, - }, - intents: resolveDiscordGatewayIntents(discordCfg.intents), - autoInteractions: true, - }), - ], + [createDiscordGatewayPlugin({ discordConfig: discordCfg, runtime })], ); await deployDiscordCommands({ client, runtime, enabled: nativeEnabled });