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 summarizeHttpTarget(input: string | URL): string { try { const url = typeof input === "string" ? new URL(input) : input; return `${url.origin}${url.pathname}`; } catch { return String(input); } } 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 startedAt = Date.now(); const method = (init?.method ?? "GET").toUpperCase(); const target = summarizeHttpTarget(input); 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 { console.log( `[wecom-http] request method=${method} target=${target} proxy=${proxyUrl || "none"} timeoutMs=${String(opts?.timeoutMs ?? "none")}`, ); const response = await undiciFetch(input, nextInit as Parameters[1]) as unknown as Response; console.log( `[wecom-http] response method=${method} target=${target} status=${response.status} durationMs=${Date.now() - startedAt}`, ); return response; } catch (err: unknown) { if (err instanceof Error && err.name === "TimeoutError") { console.error( `[wecom-http] timeout method=${method} target=${target} durationMs=${Date.now() - startedAt} proxy=${proxyUrl || "none"}`, ); } else if (err instanceof Error && err.name === "TypeError" && err.message === "fetch failed") { const cause = (err as any).cause; console.error( `[wecom-http] fetch failed method=${method} target=${target} durationMs=${Date.now() - startedAt} proxy=${proxyUrl || "none"}${cause ? ` cause=${String(cause)}` : ""}`, ); } else if (err instanceof Error && err.name === "AbortError") { console.error( `[wecom-http] aborted method=${method} target=${target} durationMs=${Date.now() - startedAt} proxy=${proxyUrl || "none"}`, ); } 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))); }