diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb2028903d..d8ed4db834d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -234,6 +234,7 @@ Docs: https://docs.openclaw.ai - Security/Microsoft Teams: isolate group allowlist and command authorization from DM pairing-store entries to prevent cross-context authorization bleed. (#26111) Thanks @bmendonca3. - Security/SSRF guard: classify IPv6 multicast literals (`ff00::/8`) as blocked/private-internal targets in shared SSRF IP checks, preventing multicast literals from bypassing URL-host preflight and DNS answer validation. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting. - Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman. +- Feishu/WebSocket proxy: pass a proxy agent to Feishu WS clients from standard proxy environment variables and include plugin-local runtime dependency wiring so websocket mode works in proxy-constrained installs. (#26397) Thanks @colin719. ## 2026.2.24 diff --git a/extensions/feishu/package.json b/extensions/feishu/package.json index 36a93566b7c..6fec8e6359b 100644 --- a/extensions/feishu/package.json +++ b/extensions/feishu/package.json @@ -6,6 +6,7 @@ "dependencies": { "@larksuiteoapi/node-sdk": "^1.59.0", "@sinclair/typebox": "0.34.48", + "https-proxy-agent": "^7.0.6", "zod": "^4.3.6" }, "openclaw": { diff --git a/extensions/feishu/src/client.test.ts b/extensions/feishu/src/client.test.ts new file mode 100644 index 00000000000..e293bac8d84 --- /dev/null +++ b/extensions/feishu/src/client.test.ts @@ -0,0 +1,103 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js"; + +const wsClientCtorMock = vi.hoisted(() => + vi.fn(function wsClientCtor() { + return { connected: true }; + }), +); +const httpsProxyAgentCtorMock = vi.hoisted(() => + vi.fn(function httpsProxyAgentCtor(proxyUrl: string) { + return { proxyUrl }; + }), +); + +vi.mock("@larksuiteoapi/node-sdk", () => ({ + AppType: { SelfBuild: "self" }, + Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" }, + LoggerLevel: { info: "info" }, + Client: vi.fn(), + WSClient: wsClientCtorMock, + EventDispatcher: vi.fn(), +})); + +vi.mock("https-proxy-agent", () => ({ + HttpsProxyAgent: httpsProxyAgentCtorMock, +})); + +import { createFeishuWSClient } from "./client.js"; + +const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const; +type ProxyEnvKey = (typeof proxyEnvKeys)[number]; + +let priorProxyEnv: Partial> = {}; + +const baseAccount: ResolvedFeishuAccount = { + accountId: "main", + enabled: true, + configured: true, + appId: "app_123", + appSecret: "secret_123", + domain: "feishu", + config: {} as FeishuConfig, +}; + +function firstWsClientOptions(): { agent?: unknown } { + const calls = wsClientCtorMock.mock.calls as unknown as Array<[options: { agent?: unknown }]>; + return calls[0]?.[0] ?? {}; +} + +beforeEach(() => { + priorProxyEnv = {}; + for (const key of proxyEnvKeys) { + priorProxyEnv[key] = process.env[key]; + delete process.env[key]; + } + vi.clearAllMocks(); +}); + +afterEach(() => { + for (const key of proxyEnvKeys) { + const value = priorProxyEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}); + +describe("createFeishuWSClient proxy handling", () => { + it("does not set a ws proxy agent when proxy env is absent", () => { + createFeishuWSClient(baseAccount); + + expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled(); + const options = firstWsClientOptions(); + expect(options?.agent).toBeUndefined(); + }); + + it("uses proxy env precedence: https_proxy first, then HTTPS_PROXY, then http_proxy/HTTP_PROXY", () => { + process.env.https_proxy = "http://lower-https:8001"; + process.env.HTTPS_PROXY = "http://upper-https:8002"; + process.env.http_proxy = "http://lower-http:8003"; + process.env.HTTP_PROXY = "http://upper-http:8004"; + + createFeishuWSClient(baseAccount); + + expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1); + expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://lower-https:8001"); + const options = firstWsClientOptions(); + expect(options.agent).toEqual({ proxyUrl: "http://lower-https:8001" }); + }); + + it("passes HTTP_PROXY to ws client when https vars are unset", () => { + process.env.HTTP_PROXY = "http://upper-http:8999"; + + createFeishuWSClient(baseAccount); + + expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1); + expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-http:8999"); + const options = firstWsClientOptions(); + expect(options.agent).toEqual({ proxyUrl: "http://upper-http:8999" }); + }); +}); diff --git a/extensions/feishu/src/client.ts b/extensions/feishu/src/client.ts index 3c308907417..569a48313c9 100644 --- a/extensions/feishu/src/client.ts +++ b/extensions/feishu/src/client.ts @@ -1,6 +1,17 @@ import * as Lark from "@larksuiteoapi/node-sdk"; +import { HttpsProxyAgent } from "https-proxy-agent"; import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js"; +function getWsProxyAgent(): HttpsProxyAgent | undefined { + const proxyUrl = + process.env.https_proxy || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.HTTP_PROXY; + if (!proxyUrl) return undefined; + return new HttpsProxyAgent(proxyUrl); +} + // Multi-account client cache const clientCache = new Map< string, @@ -81,11 +92,13 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli throw new Error(`Feishu credentials not configured for account "${accountId}"`); } + const agent = getWsProxyAgent(); return new Lark.WSClient({ appId, appSecret, domain: resolveDomain(domain), loggerLevel: Lark.LoggerLevel.info, + ...(agent ? { agent } : {}), }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3e1adaa249..a170dcc8cc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,6 +317,9 @@ importers: '@sinclair/typebox': specifier: 0.34.48 version: 0.34.48 + https-proxy-agent: + specifier: ^7.0.6 + version: 7.0.6 zod: specifier: ^4.3.6 version: 4.3.6