diff --git a/extensions/whatsapp/src/session.test.ts b/extensions/whatsapp/src/session.test.ts index 92613cf19f51..f2e0fa872ea8 100644 --- a/extensions/whatsapp/src/session.test.ts +++ b/extensions/whatsapp/src/session.test.ts @@ -41,6 +41,7 @@ const useMultiFileAuthStateMock = vi.mocked(baileys.useMultiFileAuthState); let createWaSocket: typeof import("./session.js").createWaSocket; let formatError: typeof import("./session.js").formatError; let logWebSelfId: typeof import("./session.js").logWebSelfId; +let OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV: typeof import("./session.js").OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV; let renderQrTerminalMock: ReturnType; let waitForWaConnection: typeof import("./session.js").waitForWaConnection; let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue; @@ -153,6 +154,7 @@ function readLastSocketOptions(): { fetchAgent?: unknown; keepAliveIntervalMs?: number; printQRInTerminal?: boolean; + waWebSocketUrl?: string | URL; logger?: { level?: string; trace?: unknown }; } { const [options] = firstMockCall( @@ -169,6 +171,7 @@ function readLastSocketOptions(): { fetchAgent?: unknown; keepAliveIntervalMs?: number; printQRInTerminal?: boolean; + waWebSocketUrl?: string | URL; logger?: { level?: string; trace?: unknown }; }; } @@ -224,6 +227,7 @@ describe("web session", () => { createWaSocket, formatError, logWebSelfId, + OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, waitForWaConnection, waitForCredsSaveQueue, writeCredsJsonAtomically, @@ -411,6 +415,40 @@ describe("web session", () => { expect(passed.defaultQueryTimeoutMs).toBe(120_000); }); + it("passes explicit Baileys WebSocket URL overrides", async () => { + await createWaSocket(false, false, { + waWebSocketUrl: " ws://127.0.0.1:49152/ws/chat ", + }); + + expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49152/ws/chat"); + }); + + it("uses OPENCLAW_WHATSAPP_WEB_SOCKET_URL as the default Baileys WebSocket URL", async () => { + vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " ws://127.0.0.1:49153/ws/chat "); + + await createWaSocket(false, false); + + expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49153/ws/chat"); + }); + + it("preserves explicit Baileys WebSocket URL options over environment", async () => { + vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, "ws://127.0.0.1:49153/ws/chat"); + + await createWaSocket(false, false, { + waWebSocketUrl: "ws://127.0.0.1:49154/ws/chat", + }); + + expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49154/ws/chat"); + }); + + it("ignores blank Baileys WebSocket URL environment overrides", async () => { + vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " "); + + await createWaSocket(false, false); + + expect(readLastSocketOptions().waWebSocketUrl).toBeUndefined(); + }); + it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => { vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080"); diff --git a/extensions/whatsapp/src/session.ts b/extensions/whatsapp/src/session.ts index 5c4a06cb80b4..1a90dfa0c564 100644 --- a/extensions/whatsapp/src/session.ts +++ b/extensions/whatsapp/src/session.ts @@ -68,6 +68,7 @@ const LOGGED_OUT_STATUS = 401; const WHATSAPP_WEBSOCKET_PROXY_TARGET = "https://mmg.whatsapp.net/"; const CREDS_FLUSH_TIMEOUT_MESSAGE = "Queued WhatsApp creds save did not finish before auth bootstrap; skipping repair and continuing with primary creds."; +export const OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV = "OPENCLAW_WHATSAPP_WEB_SOCKET_URL"; async function rejectUnsafeWebCredsPath(authDir: string): Promise { await assertWebCredsPathRegularFileOrMissing(resolveWebCredsPath(authDir)); @@ -125,6 +126,13 @@ async function printTerminalQr(qr: string): Promise { process.stdout.write(output.endsWith("\n") ? output : `${output}\n`); } +function resolveWaWebSocketUrl(value: string | URL | undefined): string | URL | undefined { + if (typeof value !== "string") { + return value; + } + return value.trim() || undefined; +} + /** * Create a Baileys socket backed by the multi-file auth store we keep on disk. * Consumers can opt into QR printing for interactive login flows. @@ -137,6 +145,7 @@ export async function createWaSocket( onQr?: (qr: string) => void; getMessage?: (key: WAMessageKey) => Promise; cachedGroupMetadata?: (jid: string) => Promise; + waWebSocketUrl?: string | URL; } & WhatsAppSocketTimingOptions = {}, ): Promise> { const baseLogger = getChildLogger( @@ -172,6 +181,9 @@ export async function createWaSocket( defaultQueryTimeoutMs: opts.defaultQueryTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs, }; + const waWebSocketUrl = + resolveWaWebSocketUrl(opts.waWebSocketUrl) ?? + resolveWaWebSocketUrl(process.env[OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV]); const sock = makeWASocket({ auth: { creds: state.creds, @@ -188,6 +200,7 @@ export async function createWaSocket( // Baileys types still model `fetchAgent` as a Node agent even though the // runtime path accepts an undici dispatcher for upload fetches. fetchAgent: fetchAgent as Agent | undefined, + ...(waWebSocketUrl ? { waWebSocketUrl } : {}), ...(opts.getMessage ? { getMessage: opts.getMessage } : {}), ...(opts.cachedGroupMetadata ? { cachedGroupMetadata: opts.cachedGroupMetadata } : {}), }); diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 742507f80cfb..2abbe4c2370a 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -921,6 +921,7 @@ describe("workspace .env blocklist completeness", () => { "OPENCLAW_DISABLE_BUNDLED_PLUGINS", "OPENCLAW_ALLOW_INSECURE_PRIVATE_WS", "OPENCLAW_BROWSER_EXECUTABLE_PATH", + "OPENCLAW_WHATSAPP_WEB_SOCKET_URL", "EXAMPLE_API_HOST", "HOMEBREW_BREW_FILE", "HOMEBREW_PREFIX",