refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
+26
-70
@@ -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>;
|
||||
Reference in new issue
Block a user