// 每次请求显式指定 Bun 的 proxy,直连使用proxy: false以禁用环境代理。 export interface HttpClient { proxy: false | { url: string; respectNoProxy: false }; close(): void; } const direct: HttpClient = { proxy: false, close() {} }; const proxied = new Map(); const ENV_SNAPSHOT = { ...process.env }; export function directHttpClient(): HttpClient { return direct; } export function proxyHttpClient(url: string): HttpClient { let client = proxied.get(url); if (!client) { client = { proxy: { url, respectNoProxy: false }, close() {} }; proxied.set(url, client); } return client; } export function fetchVia(client: HttpClient): (input: RequestInfo | URL, init?: RequestInit) => Promise { return ((input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, { ...init, proxy: client.proxy } as RequestInit)) as ( input: RequestInfo | URL, init?: RequestInit, ) => Promise; } /** 站点请求的出口:开代理 → 设置里的站点代理地址,没填则用进程的 HTTPS_PROXY / HTTP_PROXY;都没有 → direct */ export interface SiteEgress { mode: "direct" | "proxy"; proxyUrl: string | null; } export function siteEgress( proxy: boolean, proxyUrl: string | null | undefined, env: Record = ENV_SNAPSHOT, ): SiteEgress { const url = proxy ? proxyUrl || env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy : null; return url ? { mode: "proxy", proxyUrl: url } : { mode: "direct", proxyUrl: null }; } /** 出口的比较键(不含代理口令):direct / proxy:<主机:端口> */ export function egressKey(e: SiteEgress): string { if (e.mode === "direct" || !e.proxyUrl) return "direct"; try { const u = new URL(e.proxyUrl); return `proxy:${u.protocol}//${u.host}`; } catch { return "proxy:?"; } } export function siteFetchFor(e: SiteEgress): (input: RequestInfo | URL, init?: RequestInit) => Promise { return fetchVia(e.mode === "proxy" && e.proxyUrl ? proxyHttpClient(e.proxyUrl) : directHttpClient()); } // ---------- 外网请求(TMDB / Brave / 图片):显式按环境变量 ---------- /** NO_PROXY 匹配(与常见实现一致:逗号分隔;`.a.com` / `a.com` 匹配自身与子域;`*` 匹配全部;IP 精确匹配) */ export function noProxyMatches(host: string, noProxy: string | undefined): boolean { if (!noProxy) return false; const h = host.toLowerCase().replace(/^\[|\]$/g, ""); for (const raw of noProxy.split(",")) { const p = raw.trim().toLowerCase().replace(/:\d+$/, ""); if (!p) continue; if (p === "*") return true; const d = p.replace(/^\*?\./, ""); if (h === d || h.endsWith(`.${d}`)) return true; } return false; } /** 外网请求按环境变量应走的代理(进程启动时的快照);没有 / NO_PROXY 命中为 null */ export function envProxyFor(target: URL, env: Record = ENV_SNAPSHOT): string | null { const get = (k: string) => env[k] ?? env[k.toLowerCase()]; if (noProxyMatches(target.hostname, get("NO_PROXY"))) return null; const byScheme = target.protocol === "https:" ? get("HTTPS_PROXY") : get("HTTP_PROXY"); const p = byScheme || get("ALL_PROXY"); return p ? p : null; } /** 外网请求用的 fetch:按环境变量显式选代理 / 直连(与之前隐式继承的行为一致) */ export const externalFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise = (( input: RequestInfo | URL, init?: RequestInit, ) => { let target: URL | null = null; try { target = new URL(input instanceof Request ? input.url : String(input)); } catch { // 交给 fetch 报错 } const proxy = target ? envProxyFor(target) : null; const client = proxy ? proxyHttpClient(proxy) : directHttpClient(); return fetchVia(client)(input, init); }) as (input: RequestInfo | URL, init?: RequestInit) => Promise;