refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

+26 -70
View File
@@ -1,75 +1,28 @@
// CF-2 出口统一:每条对外 HTTP 请求走哪条出口由代码显式决定,不再隐式继承容器的 HTTP(S)_PROXY。
//
// Deno 2.9 的语义(2026-09-30 本机实测,见 docs/runbook/sites.md「出口」):
// - `fetch` 缺省客户端与 `Deno.createHttpClient({})` 都会读 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY(大小写都认)与 NO_PROXY;
// 读取发生在**客户端创建时**(缺省客户端在进程第一次 fetch 时创建);
// - `createHttpClient` 没有"不走代理"的选项,`NO_PROXY=*` 也不认;显式 `proxy` 会排在环境变量代理前面,
// 但 NO_PROXY 命中的主机连显式代理也绕过;
// - 所以直连客户端的做法是:创建那一刻把这几个环境变量临时拿掉(同步调用,同一个 isolate 内不会被别的代码插进来),建完立即还原。
// 代理客户端同样在拿掉环境变量后创建,保证只走给定的代理。
//
// 出口表:
// - 站点请求(适配器、RSS、登录、用户数据):站点开"使用代理"且设了代理地址 → 该代理;否则直连(siteClient);
// - TMDB、Brave、图片缓存等外网请求:照旧用环境变量里的代理(行为不变),但改成显式读 env 建客户端(externalFetch)。
const PROXY_ENV = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"] as const;
const NO_PROXY_ENV = ["NO_PROXY", "no_proxy"] as const;
/** 进程启动时的代理环境变量快照(外网请求显式使用;之后的临时改动不影响) */
const ENV_SNAPSHOT: Record<string, string | undefined> = (() => {
const out: Record<string, string | undefined> = {};
try {
for (const k of [...PROXY_ENV, ...NO_PROXY_ENV]) out[k] = Deno.env.get(k);
} catch {
// 没有 env 权限:当作没有代理
}
return out;
})();
/** 在"没有代理环境变量"的状态下同步执行 fn(建 HttpClient 用),结束立即还原 */
function withoutProxyEnv<T>(fn: () => T): T {
const saved = new Map<string, string>();
try {
for (const k of [...PROXY_ENV, ...NO_PROXY_ENV]) {
const v = Deno.env.get(k);
if (v !== undefined) {
saved.set(k, v);
Deno.env.delete(k);
}
}
} catch {
// 没有 env 权限时也没有可读的代理变量,直接建
}
try {
return fn();
} finally {
for (const [k, v] of saved) Deno.env.set(k, v);
}
// 每次请求显式指定 Bun 的 proxy,直连使用proxy: false以禁用环境代理。
export interface HttpClient {
proxy: false | { url: string; respectNoProxy: false };
close(): void;
}
let direct: Deno.HttpClient | null = null;
const proxied = new Map<string, Deno.HttpClient>();
/** 明确直连的客户端(不读任何代理环境变量) */
export function directHttpClient(): Deno.HttpClient {
direct ??= withoutProxyEnv(() => Deno.createHttpClient({}));
const direct: HttpClient = { proxy: false, close() {} };
const proxied = new Map<string, HttpClient>();
const ENV_SNAPSHOT = { ...process.env };
export function directHttpClient(): HttpClient {
return direct;
}
/** 只走给定代理的客户端(同一地址复用) */
export function proxyHttpClient(url: string): Deno.HttpClient {
let c = proxied.get(url);
if (!c) {
c = withoutProxyEnv(() => Deno.createHttpClient({ proxy: { url } }));
proxied.set(url, c);
export function proxyHttpClient(url: string): HttpClient {
let client = proxied.get(url);
if (!client) {
client = { proxy: { url, respectNoProxy: false }, close() {} };
proxied.set(url, client);
}
return c;
return client;
}
/** 用指定客户端发请求的 fetch */
export function fetchVia(client: Deno.HttpClient): typeof globalThis.fetch {
export function fetchVia(client: HttpClient): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
return ((input: RequestInfo | URL, init?: RequestInit) =>
globalThis.fetch(input, { ...init, client } as RequestInit)) as typeof globalThis.fetch;
globalThis.fetch(input, { ...init, proxy: client.proxy } as RequestInit)) as (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
}
/** 站点请求的出口:开代理 → 设置里的站点代理地址,没填则用进程的 HTTPS_PROXY / HTTP_PROXY;都没有 → direct */
@@ -98,7 +51,7 @@ export function egressKey(e: SiteEgress): string {
}
}
export function siteFetchFor(e: SiteEgress): typeof globalThis.fetch {
export function siteFetchFor(e: SiteEgress): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
return fetchVia(e.mode === "proxy" && e.proxyUrl ? proxyHttpClient(e.proxyUrl) : directHttpClient());
}
@@ -128,7 +81,10 @@ export function envProxyFor(target: URL, env: Record<string, string | undefined>
}
/** 外网请求用的 fetch:按环境变量显式选代理 / 直连(与之前隐式继承的行为一致) */
export const externalFetch: typeof globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
export const externalFetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = ((
input: RequestInfo | URL,
init?: RequestInit,
) => {
let target: URL | null = null;
try {
target = new URL(input instanceof Request ? input.url : String(input));
@@ -137,5 +93,5 @@ export const externalFetch: typeof globalThis.fetch = ((input: RequestInfo | URL
}
const proxy = target ? envProxyFor(target) : null;
const client = proxy ? proxyHttpClient(proxy) : directHttpClient();
return globalThis.fetch(input, { ...init, client } as RequestInit);
}) as typeof globalThis.fetch;
return fetchVia(client)(input, init);
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;