mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(feishu): pass proxy agent to WSClient for proxy environments (#26397)
* fix(feishu): pass proxy agent to WSClient for environments behind HTTPS proxy The Lark SDK WSClient uses the `ws` library which does not automatically respect https_proxy/HTTP_PROXY environment variables. This causes WebSocket connection failures in proxy environments (e.g. WSL2 with a local proxy). Detect proxy env vars and pass an HttpsProxyAgent to WSClient via the existing `agent` constructor option. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(feishu): add generic type parameter to HttpsProxyAgent return type Fix TS2314: `HttpsProxyAgent<Uri>` requires a type argument. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(feishu): wire ws proxy dependency and coverage * chore(lockfile): resolve axios peer lock entry after rebase --------- Co-authored-by: lirui <lirui@fxiaoke.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
103
extensions/feishu/src/client.test.ts
Normal file
103
extensions/feishu/src/client.test.ts
Normal file
@@ -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<Record<ProxyEnvKey, string | undefined>> = {};
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
@@ -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<string> | 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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user