mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 13:00:21 +00:00
refactor: share ambient proxy agent helpers
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
9d1bfce6aa8b2c7d0dc9ff05f33ac02b783897ab2dabb64308cd1fce71c4b7d8 plugin-sdk-api-baseline.json
|
||||
a51809a3007d31764d773e077c394a4e58c0cdc32bc47c9552aa4180bfa6e544 plugin-sdk-api-baseline.jsonl
|
||||
23bfae10a189a7d0548bc7213a9180841bbb1125e97ce1d2d0b7a765773a92fd plugin-sdk-api-baseline.json
|
||||
6c64b352b19368015c867b4c16225d676110544943497238c2f78602ad2fb519 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -145,6 +145,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
|
||||
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
|
||||
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
||||
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
|
||||
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
|
||||
|
||||
## Access control and activation
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ Current bundled provider examples:
|
||||
| `plugin-sdk/allowlist-config-edit` | Allowlist config helpers | Allowlist config edit/read helpers |
|
||||
| `plugin-sdk/group-access` | Group access helpers | Shared group-access decision helpers |
|
||||
| `plugin-sdk/direct-dm` | Direct-DM helpers | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/extension-shared` | Shared extension helpers | Passive-channel/status helper primitives |
|
||||
| `plugin-sdk/extension-shared` | Shared extension helpers | Passive-channel/status and ambient proxy helper primitives |
|
||||
| `plugin-sdk/webhook-targets` | Webhook target helpers | Webhook target registry and route-install helpers |
|
||||
| `plugin-sdk/webhook-path` | Webhook path helpers | Webhook path normalization helpers |
|
||||
| `plugin-sdk/web-media` | Shared web media helpers | Remote/local media loading helpers |
|
||||
|
||||
@@ -203,7 +203,7 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/boolean-param` | Loose boolean param reader |
|
||||
| `plugin-sdk/dangerous-name-runtime` | Dangerous-name matching resolution helpers |
|
||||
| `plugin-sdk/device-bootstrap` | Device bootstrap and pairing token helpers |
|
||||
| `plugin-sdk/extension-shared` | Shared passive-channel and status helper primitives |
|
||||
| `plugin-sdk/extension-shared` | Shared passive-channel, status, and ambient proxy helper primitives |
|
||||
| `plugin-sdk/models-provider-runtime` | `/models` command/provider reply helpers |
|
||||
| `plugin-sdk/skill-commands-runtime` | Skill command listing helpers |
|
||||
| `plugin-sdk/native-command-registry` | Native command registry/build/serialize helpers |
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@sinclair/typebox": "0.34.49",
|
||||
"https-proxy-agent": "^9.0.0"
|
||||
"@sinclair/typebox": "0.34.49"
|
||||
},
|
||||
"devDependencies": {
|
||||
"openclaw": "workspace:*"
|
||||
|
||||
@@ -17,9 +17,9 @@ const wsClientCtorMock = vi.hoisted(() =>
|
||||
return { connected: true };
|
||||
}),
|
||||
);
|
||||
const httpsProxyAgentCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function httpsProxyAgentCtor(proxyUrl: string) {
|
||||
return { proxyUrl };
|
||||
const proxyAgentCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function proxyAgentCtor() {
|
||||
return { proxied: true };
|
||||
}),
|
||||
);
|
||||
const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
@@ -139,8 +139,8 @@ beforeAll(async () => {
|
||||
EventDispatcher: vi.fn(),
|
||||
defaultHttpInstance: mockBaseHttpInstance,
|
||||
}));
|
||||
vi.doMock("https-proxy-agent", () => ({
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock,
|
||||
vi.doMock("proxy-agent", () => ({
|
||||
ProxyAgent: proxyAgentCtorMock,
|
||||
}));
|
||||
|
||||
({
|
||||
@@ -177,7 +177,6 @@ beforeEach(() => {
|
||||
EventDispatcher: vi.fn() as never,
|
||||
defaultHttpInstance: mockBaseHttpInstance as never,
|
||||
},
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock as never,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -351,68 +350,41 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
});
|
||||
|
||||
describe("createFeishuWSClient proxy handling", () => {
|
||||
it("does not set a ws proxy agent when proxy env is absent", () => {
|
||||
createFeishuWSClient(baseAccount);
|
||||
it("does not set a ws proxy agent when proxy env is absent", async () => {
|
||||
await createFeishuWSClient(baseAccount);
|
||||
|
||||
expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled();
|
||||
expect(proxyAgentCtorMock).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", () => {
|
||||
// NOTE: On Windows, environment variables are case-insensitive, so it's not
|
||||
// possible to set both https_proxy and HTTPS_PROXY to different values.
|
||||
// Keep this test cross-platform by asserting precedence via mutually-exclusive
|
||||
// setups.
|
||||
process.env.https_proxy = "http://lower-https:8001";
|
||||
process.env.http_proxy = "http://lower-http:8003";
|
||||
process.env.HTTP_PROXY = "http://upper-http:8004";
|
||||
|
||||
createFeishuWSClient(baseAccount);
|
||||
|
||||
// On Windows env keys are case-insensitive, so setting HTTPS_PROXY may
|
||||
// overwrite https_proxy. We assert https proxies still win over http.
|
||||
const expectedProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
|
||||
expect(expectedProxy).toBeTruthy();
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedProxy);
|
||||
const options = firstWsClientOptions();
|
||||
expect(options.agent).toEqual({ proxyUrl: expectedProxy });
|
||||
});
|
||||
|
||||
it("accepts lowercase https_proxy when it is the configured HTTPS proxy var", () => {
|
||||
it("creates a ws proxy agent when lowercase https_proxy is set", async () => {
|
||||
process.env.https_proxy = "http://lower-https:8001";
|
||||
|
||||
createFeishuWSClient(baseAccount);
|
||||
await createFeishuWSClient(baseAccount);
|
||||
|
||||
const expectedHttpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(expectedHttpsProxy).toBeTruthy();
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith(expectedHttpsProxy);
|
||||
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
const options = firstWsClientOptions();
|
||||
expect(options.agent).toEqual({ proxyUrl: expectedHttpsProxy });
|
||||
expect(options.agent).toEqual({ proxied: true });
|
||||
});
|
||||
|
||||
it("uses HTTPS_PROXY when https_proxy is unset", () => {
|
||||
it("creates a ws proxy agent when uppercase HTTPS_PROXY is set", async () => {
|
||||
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
||||
process.env.http_proxy = "http://lower-http:8003";
|
||||
|
||||
createFeishuWSClient(baseAccount);
|
||||
await createFeishuWSClient(baseAccount);
|
||||
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-https:8002");
|
||||
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
const options = firstWsClientOptions();
|
||||
expect(options.agent).toEqual({ proxyUrl: "http://upper-https:8002" });
|
||||
expect(options.agent).toEqual({ proxied: true });
|
||||
});
|
||||
|
||||
it("passes HTTP_PROXY to ws client when https vars are unset", () => {
|
||||
it("falls back to HTTP_PROXY for ws proxy agent creation", async () => {
|
||||
process.env.HTTP_PROXY = "http://upper-http:8999";
|
||||
|
||||
createFeishuWSClient(baseAccount);
|
||||
await createFeishuWSClient(baseAccount);
|
||||
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-http:8999");
|
||||
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
||||
const options = firstWsClientOptions();
|
||||
expect(options.agent).toEqual({ proxyUrl: "http://upper-http:8999" });
|
||||
expect(options.agent).toEqual({ proxied: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Agent } from "node:https";
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
|
||||
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
type FeishuClientSdk = Pick<
|
||||
@@ -24,7 +25,6 @@ const defaultFeishuClientSdk: FeishuClientSdk = {
|
||||
};
|
||||
|
||||
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
|
||||
let httpsProxyAgentCtor: typeof HttpsProxyAgent = HttpsProxyAgent;
|
||||
|
||||
/** Default HTTP timeout for Feishu API requests (30 seconds). */
|
||||
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
|
||||
@@ -36,16 +36,8 @@ type FeishuHttpInstanceLike = Pick<
|
||||
"request" | "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
|
||||
>;
|
||||
|
||||
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 httpsProxyAgentCtor(proxyUrl);
|
||||
async function getWsProxyAgent(): Promise<Agent | undefined> {
|
||||
return resolveAmbientNodeProxyAgent<Agent>();
|
||||
}
|
||||
|
||||
// Multi-account client cache
|
||||
@@ -181,14 +173,14 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
||||
* Create a Feishu WebSocket client for an account.
|
||||
* Note: WSClient is not cached since each call creates a new connection.
|
||||
*/
|
||||
export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSClient {
|
||||
export async function createFeishuWSClient(account: ResolvedFeishuAccount): Promise<Lark.WSClient> {
|
||||
const { accountId, appId, appSecret, domain } = account;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
||||
}
|
||||
|
||||
const agent = getWsProxyAgent();
|
||||
const agent = await getWsProxyAgent();
|
||||
return new feishuClientSdk.WSClient({
|
||||
appId,
|
||||
appSecret,
|
||||
@@ -228,10 +220,8 @@ export function clearClientCache(accountId?: string): void {
|
||||
|
||||
export function setFeishuClientRuntimeForTest(overrides?: {
|
||||
sdk?: Partial<FeishuClientSdk>;
|
||||
HttpsProxyAgent?: typeof HttpsProxyAgent;
|
||||
}): void {
|
||||
feishuClientSdk = overrides?.sdk
|
||||
? { ...defaultFeishuClientSdk, ...overrides.sdk }
|
||||
: defaultFeishuClientSdk;
|
||||
httpsProxyAgentCtor = overrides?.HttpsProxyAgent ?? HttpsProxyAgent;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function monitorWebSocket({
|
||||
const error = runtime?.error ?? console.error;
|
||||
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
||||
|
||||
const wsClient = createFeishuWSClient(account);
|
||||
const wsClient = await createFeishuWSClient(account);
|
||||
wsClients.set(accountId, wsClient);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -74,6 +74,7 @@ describe("web session", () => {
|
||||
await waitForCredsSaveQueue();
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
vi.unstubAllEnvs();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -95,6 +96,30 @@ describe("web session", () => {
|
||||
expect(saveCreds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
|
||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
|
||||
|
||||
await createWaSocket(false, false);
|
||||
|
||||
const passed = (baileys.makeWASocket as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as {
|
||||
agent?: unknown;
|
||||
fetchAgent?: unknown;
|
||||
};
|
||||
expect(passed.agent).toBeDefined();
|
||||
expect(passed.fetchAgent).toBe(passed.agent);
|
||||
});
|
||||
|
||||
it("does not create a proxy agent when no env proxy is configured", async () => {
|
||||
await createWaSocket(false, false);
|
||||
|
||||
const passed = (baileys.makeWASocket as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as {
|
||||
agent?: unknown;
|
||||
fetchAgent?: unknown;
|
||||
};
|
||||
expect(passed.agent).toBeUndefined();
|
||||
expect(passed.fetchAgent).toBeUndefined();
|
||||
});
|
||||
|
||||
it("waits for connection open", async () => {
|
||||
const ev = new EventEmitter();
|
||||
const promise = waitForWaConnection({ ev } as unknown as ReturnType<
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import type { Agent } from "node:https";
|
||||
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { VERSION } from "openclaw/plugin-sdk/cli-runtime";
|
||||
import { resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
|
||||
import { danger, success } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { getChildLogger, toPinoLikeLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { ensureDir, resolveUserPath } from "openclaw/plugin-sdk/text-runtime";
|
||||
@@ -118,6 +120,7 @@ export async function createWaSocket(
|
||||
maybeRestoreCredsFromBackup(authDir);
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
const agent = await resolveEnvProxyAgent(sessionLogger);
|
||||
const sock = makeWASocket({
|
||||
auth: {
|
||||
creds: state.creds,
|
||||
@@ -129,6 +132,8 @@ export async function createWaSocket(
|
||||
browser: ["openclaw", "cli", VERSION],
|
||||
syncFullHistory: false,
|
||||
markOnlineOnConnect: false,
|
||||
agent,
|
||||
fetchAgent: agent,
|
||||
});
|
||||
|
||||
sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, sessionLogger));
|
||||
@@ -173,6 +178,22 @@ export async function createWaSocket(
|
||||
return sock;
|
||||
}
|
||||
|
||||
async function resolveEnvProxyAgent(
|
||||
logger: ReturnType<typeof getChildLogger>,
|
||||
): Promise<Agent | undefined> {
|
||||
return resolveAmbientNodeProxyAgent<Agent>({
|
||||
onError: (err) => {
|
||||
logger.warn(
|
||||
{ error: String(err) },
|
||||
"Failed to initialize env proxy agent for WhatsApp WebSocket connection",
|
||||
);
|
||||
},
|
||||
onUsingProxy: () => {
|
||||
logger.info("Using ambient env proxy for WhatsApp WebSocket connection");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForWaConnection(sock: ReturnType<typeof makeWASocket>) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
type OffCapable = {
|
||||
|
||||
@@ -1240,6 +1240,7 @@
|
||||
"osc-progress": "^0.3.0",
|
||||
"pdfjs-dist": "^5.6.205",
|
||||
"playwright-core": "1.59.1",
|
||||
"proxy-agent": "^8.0.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sqlite-vec": "0.1.9",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { z } from "zod";
|
||||
import { hasEnvHttpProxyConfigured } from "../infra/net/proxy-env.js";
|
||||
import { runPassiveAccountLifecycle } from "./channel-lifecycle.core.js";
|
||||
import { createLoggerBackedRuntime } from "./runtime-logger.js";
|
||||
export { safeParseJsonWithSchema, safeParseWithSchema } from "../utils/zod-parse.js";
|
||||
@@ -134,3 +135,21 @@ export function createDeferred<T>() {
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
export async function resolveAmbientNodeProxyAgent<TAgent>(params?: {
|
||||
onError?: (error: unknown) => void;
|
||||
onUsingProxy?: () => void;
|
||||
protocol?: "http" | "https";
|
||||
}): Promise<TAgent | undefined> {
|
||||
if (!hasEnvHttpProxyConfigured(params?.protocol ?? "https")) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const { ProxyAgent } = await import("proxy-agent");
|
||||
params?.onUsingProxy?.();
|
||||
return new ProxyAgent() as TAgent;
|
||||
} catch (error) {
|
||||
params?.onError?.(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user