103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
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)));
|
|
}
|
|
|