import type { Dispatcher } from "undici";
import { ProxyAgent, fetch as undiciFetch } from "undici";
type ProxyDispatcher = Dispatcher;
const proxyDispatchers = new Map<string, ProxyDispatcher>();
/**
* **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<Response> {
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 nextInit: RequestInit & { dispatcher?: Dispatcher } = {
...(init ?? {}),
...(signal ? { signal } : {}),
...(dispatcher ? { dispatcher } : {}),
};
return undiciFetch(input, nextInit as Parameters<typeof undiciFetch>[1]) as unknown as Promise<Response>;
}
/**
* **readResponseBodyAsBuffer (读取响应 Body)**
*
* 将 Response Body 读取为 Buffer,支持最大字节限制以防止内存溢出。
* 适用于下载媒体文件等场景。
*/
export async function readResponseBodyAsBuffer(res: Response, maxBytes?: number): Promise<Buffer> {
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)));
}