import type { Dispatcher } from "undici"; import { ProxyAgent, fetch as undiciFetch } from "undici"; type ProxyDispatcher = Dispatcher; const proxyDispatchers = new Map(); /** * **getProxyDispatcher (获取代理 Dispatcher)** * * 缓存并复用 ProxyAgent,避免重复创建连接池。 */ function getProxyDispatcher(proxyUrl: string): ProxyDispatcher { const existing = proxyDispatchers.get(proxyUrl); if (existing) return existing; const created = new ProxyAgent(proxyUrl); proxyDispatchers.set(proxyUrl, created); return created; } function mergeAbortSignal(params: { signal?: AbortSignal; timeoutMs?: number; }): AbortSignal | undefined { const signals: AbortSignal[] = []; if (params.signal) signals.push(params.signal); if (params.timeoutMs && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0) { signals.push(AbortSignal.timeout(params.timeoutMs)); } if (!signals.length) return undefined; if (signals.length === 1) return signals[0]; return AbortSignal.any(signals); } /** * **WecomHttpOptions (HTTP 选项)** * * @property proxyUrl 代理服务器地址 * @property timeoutMs 请求超时时间 (毫秒) * @property signal AbortSignal 信号 */ export type WecomHttpOptions = { proxyUrl?: string; timeoutMs?: number; signal?: AbortSignal; }; /** * **wecomFetch (统一 HTTP 请求)** * * 基于 `undici` 的 fetch 封装,自动处理 ProxyAgent 和 Timeout。 * 所有对企业微信 API 的调用都应经过此函数。 */ export async function wecomFetch(input: string | URL, init?: RequestInit, opts?: WecomHttpOptions): Promise { const proxyUrl = opts?.proxyUrl?.trim() ?? ""; const dispatcher = proxyUrl ? getProxyDispatcher(proxyUrl) : undefined; const initSignal = init?.signal ?? undefined; const signal = mergeAbortSignal({ signal: opts?.signal ?? initSignal, timeoutMs: opts?.timeoutMs }); const headers = new Headers(init?.headers ?? {}); if (!headers.has("User-Agent")) { headers.set("User-Agent", "OpenClaw/2.0 (WeCom-Agent)"); } const nextInit: RequestInit & { dispatcher?: Dispatcher } = { ...(init ?? {}), ...(signal ? { signal } : {}), ...(dispatcher ? { dispatcher } : {}), headers, }; try { return await undiciFetch(input, nextInit as Parameters[1]) as unknown as Response; } catch (err: unknown) { if (err instanceof Error && err.name === "TypeError" && err.message === "fetch failed") { const cause = (err as any).cause; console.error(`[wecom-http] fetch failed: ${input} (proxy: ${proxyUrl || "none"})${cause ? ` - cause: ${String(cause)}` : ""}`); } throw err; } } /** * **readResponseBodyAsBuffer (读取响应 Body)** * * 将 Response Body 读取为 Buffer,支持最大字节限制以防止内存溢出。 * 适用于下载媒体文件等场景。 */ export async function readResponseBodyAsBuffer(res: Response, maxBytes?: number): Promise { if (!res.body) return Buffer.alloc(0); const limit = maxBytes && Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : undefined; const chunks: Uint8Array[] = []; let total = 0; const reader = res.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; if (!value) continue; total += value.byteLength; if (limit && total > limit) { try { await reader.cancel("body too large"); } catch { // ignore } throw new Error(`response body too large (>${limit} bytes)`); } chunks.push(value); } return Buffer.concat(chunks.map((c) => Buffer.from(c))); }