mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 15:00:20 +00:00
309 lines
8.5 KiB
TypeScript
309 lines
8.5 KiB
TypeScript
import { createRequire } from "node:module";
|
|
import type { ConnectionOptions } from "node:tls";
|
|
import { pathToFileURL } from "node:url";
|
|
import type { Dispatcher } from "undici";
|
|
import { Agent as UndiciAgent, ProxyAgent } from "undici";
|
|
|
|
type ProxyRule = RegExp | URL | string;
|
|
type TlsCert = ConnectionOptions["cert"];
|
|
type TlsKey = ConnectionOptions["key"];
|
|
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
type GaxiosFetchRequestInit = RequestInit & {
|
|
agent?: unknown;
|
|
cert?: TlsCert;
|
|
dispatcher?: Dispatcher;
|
|
fetchImplementation?: FetchLike;
|
|
key?: TlsKey;
|
|
noProxy?: ProxyRule[];
|
|
proxy?: string | URL;
|
|
};
|
|
|
|
type ProxyAgentLike = {
|
|
connectOpts?: { cert?: TlsCert; key?: TlsKey };
|
|
proxy: URL;
|
|
};
|
|
|
|
type TlsAgentLike = {
|
|
options?: { cert?: TlsCert; key?: TlsKey };
|
|
};
|
|
|
|
type GaxiosPrototype = {
|
|
_defaultAdapter: (this: unknown, config: GaxiosFetchRequestInit) => Promise<unknown>;
|
|
};
|
|
|
|
type GaxiosConstructor = {
|
|
prototype: GaxiosPrototype;
|
|
};
|
|
|
|
const TEST_GAXIOS_CONSTRUCTOR_OVERRIDE = "__OPENCLAW_TEST_GAXIOS_CONSTRUCTOR__";
|
|
|
|
let installState: "not-installed" | "installing" | "shimmed" | "installed" = "not-installed";
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
function hasDispatcher(value: unknown): value is Dispatcher {
|
|
return isRecord(value) && typeof value.dispatch === "function";
|
|
}
|
|
|
|
function hasProxyAgentShape(value: unknown): value is ProxyAgentLike {
|
|
return isRecord(value) && value.proxy instanceof URL;
|
|
}
|
|
|
|
function hasTlsAgentShape(value: unknown): value is TlsAgentLike {
|
|
return isRecord(value) && isRecord(value.options);
|
|
}
|
|
|
|
function resolveTlsOptions(
|
|
init: GaxiosFetchRequestInit,
|
|
url: URL,
|
|
): { cert?: TlsCert; key?: TlsKey } {
|
|
const explicit = {
|
|
cert: init.cert,
|
|
key: init.key,
|
|
};
|
|
if (explicit.cert !== undefined || explicit.key !== undefined) {
|
|
return explicit;
|
|
}
|
|
|
|
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
|
if (hasProxyAgentShape(agent)) {
|
|
return {
|
|
cert: agent.connectOpts?.cert,
|
|
key: agent.connectOpts?.key,
|
|
};
|
|
}
|
|
if (hasTlsAgentShape(agent)) {
|
|
return {
|
|
cert: agent.options?.cert,
|
|
key: agent.options?.key,
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function urlMayUseProxy(url: URL, noProxy: ProxyRule[] = []): boolean {
|
|
const rules = [...noProxy];
|
|
const envRules = (process.env.NO_PROXY ?? process.env.no_proxy)?.split(",") ?? [];
|
|
for (const rule of envRules) {
|
|
const trimmed = rule.trim();
|
|
if (trimmed.length > 0) {
|
|
rules.push(trimmed);
|
|
}
|
|
}
|
|
|
|
for (const rule of rules) {
|
|
if (rule instanceof RegExp) {
|
|
if (rule.test(url.toString())) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
if (rule instanceof URL) {
|
|
if (rule.origin === url.origin) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
if (rule.startsWith("*.") || rule.startsWith(".")) {
|
|
const cleanedRule = rule.replace(/^\*\./, ".");
|
|
if (url.hostname.endsWith(cleanedRule)) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
if (rule === url.origin || rule === url.hostname || rule === url.href) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function resolveProxyUri(init: GaxiosFetchRequestInit, url: URL): string | undefined {
|
|
if (init.proxy) {
|
|
const proxyUri = String(init.proxy);
|
|
return urlMayUseProxy(url, init.noProxy) ? proxyUri : undefined;
|
|
}
|
|
|
|
const envProxy =
|
|
process.env.HTTPS_PROXY ??
|
|
process.env.https_proxy ??
|
|
process.env.HTTP_PROXY ??
|
|
process.env.http_proxy;
|
|
if (!envProxy) {
|
|
return undefined;
|
|
}
|
|
|
|
return urlMayUseProxy(url, init.noProxy) ? envProxy : undefined;
|
|
}
|
|
|
|
function buildDispatcher(init: GaxiosFetchRequestInit, url: URL): Dispatcher | undefined {
|
|
if (init.dispatcher) {
|
|
return init.dispatcher;
|
|
}
|
|
|
|
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
|
if (hasDispatcher(agent)) {
|
|
return agent;
|
|
}
|
|
|
|
const { cert, key } = resolveTlsOptions(init, url);
|
|
const proxyUri =
|
|
resolveProxyUri(init, url) ?? (hasProxyAgentShape(agent) ? String(agent.proxy) : undefined);
|
|
if (proxyUri) {
|
|
return new ProxyAgent({
|
|
requestTls: cert !== undefined || key !== undefined ? { cert, key } : undefined,
|
|
uri: proxyUri,
|
|
});
|
|
}
|
|
|
|
if (cert !== undefined || key !== undefined) {
|
|
return new UndiciAgent({
|
|
connect: { cert, key },
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function isModuleNotFoundError(err: unknown): err is NodeJS.ErrnoException {
|
|
return isRecord(err) && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND");
|
|
}
|
|
|
|
function hasGaxiosConstructorShape(value: unknown): value is GaxiosConstructor {
|
|
return (
|
|
typeof value === "function" &&
|
|
"prototype" in value &&
|
|
isRecord(value.prototype) &&
|
|
typeof value.prototype._defaultAdapter === "function"
|
|
);
|
|
}
|
|
|
|
function getTestGaxiosConstructorOverride(): GaxiosConstructor | null | undefined {
|
|
const testGlobal = globalThis as Record<string, unknown>;
|
|
if (!Object.prototype.hasOwnProperty.call(testGlobal, TEST_GAXIOS_CONSTRUCTOR_OVERRIDE)) {
|
|
return undefined;
|
|
}
|
|
const override = testGlobal[TEST_GAXIOS_CONSTRUCTOR_OVERRIDE];
|
|
if (override === null) {
|
|
return null;
|
|
}
|
|
if (hasGaxiosConstructorShape(override)) {
|
|
return override;
|
|
}
|
|
throw new Error("invalid gaxios test constructor override");
|
|
}
|
|
|
|
function isDirectGaxiosImportMiss(err: unknown): boolean {
|
|
if (!isModuleNotFoundError(err)) {
|
|
return false;
|
|
}
|
|
return (
|
|
typeof err.message === "string" &&
|
|
(err.message.includes("Cannot find package 'gaxios'") ||
|
|
err.message.includes("Cannot find module 'gaxios'"))
|
|
);
|
|
}
|
|
|
|
async function loadGaxiosConstructor(): Promise<GaxiosConstructor | null> {
|
|
const testOverride = getTestGaxiosConstructorOverride();
|
|
if (testOverride !== undefined) {
|
|
return testOverride;
|
|
}
|
|
|
|
try {
|
|
const require = createRequire(import.meta.url);
|
|
const resolvedPath = require.resolve("gaxios");
|
|
const mod = await import(pathToFileURL(resolvedPath).href);
|
|
const candidate = isRecord(mod) ? mod.Gaxios : undefined;
|
|
if (!hasGaxiosConstructorShape(candidate)) {
|
|
throw new Error("gaxios: missing Gaxios export");
|
|
}
|
|
return candidate;
|
|
} catch (err) {
|
|
if (isDirectGaxiosImportMiss(err)) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function installLegacyWindowFetchShim(): void {
|
|
if (
|
|
typeof globalThis.fetch !== "function" ||
|
|
typeof (globalThis as Record<string, unknown>).window !== "undefined"
|
|
) {
|
|
return;
|
|
}
|
|
(globalThis as Record<string, unknown>).window = { fetch: globalThis.fetch };
|
|
}
|
|
|
|
export function createGaxiosCompatFetch(
|
|
baseFetch: FetchLike = globalThis.fetch.bind(globalThis),
|
|
): FetchLike {
|
|
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
const gaxiosInit = (init ?? {}) as GaxiosFetchRequestInit;
|
|
const requestUrl =
|
|
input instanceof Request
|
|
? new URL(input.url)
|
|
: new URL(typeof input === "string" ? input : input.toString());
|
|
const dispatcher = buildDispatcher(gaxiosInit, requestUrl);
|
|
|
|
const nextInit: RequestInit = { ...gaxiosInit };
|
|
delete (nextInit as GaxiosFetchRequestInit).agent;
|
|
delete (nextInit as GaxiosFetchRequestInit).cert;
|
|
delete (nextInit as GaxiosFetchRequestInit).fetchImplementation;
|
|
delete (nextInit as GaxiosFetchRequestInit).key;
|
|
delete (nextInit as GaxiosFetchRequestInit).noProxy;
|
|
delete (nextInit as GaxiosFetchRequestInit).proxy;
|
|
|
|
if (dispatcher) {
|
|
(nextInit as RequestInit & { dispatcher: Dispatcher }).dispatcher = dispatcher;
|
|
}
|
|
|
|
return baseFetch(input, nextInit);
|
|
};
|
|
}
|
|
|
|
export async function installGaxiosFetchCompat(): Promise<void> {
|
|
if (installState !== "not-installed" || typeof globalThis.fetch !== "function") {
|
|
return;
|
|
}
|
|
|
|
installState = "installing";
|
|
|
|
try {
|
|
const Gaxios = await loadGaxiosConstructor();
|
|
if (!Gaxios) {
|
|
installLegacyWindowFetchShim();
|
|
installState = "shimmed";
|
|
return;
|
|
}
|
|
|
|
const prototype = Gaxios.prototype;
|
|
const originalDefaultAdapter = prototype._defaultAdapter;
|
|
const compatFetch = createGaxiosCompatFetch();
|
|
|
|
prototype._defaultAdapter = function patchedDefaultAdapter(
|
|
this: unknown,
|
|
config: GaxiosFetchRequestInit,
|
|
): Promise<unknown> {
|
|
if (config.fetchImplementation) {
|
|
return originalDefaultAdapter.call(this, config);
|
|
}
|
|
return originalDefaultAdapter.call(this, {
|
|
...config,
|
|
fetchImplementation: compatFetch,
|
|
});
|
|
};
|
|
|
|
installState = "installed";
|
|
} catch (err) {
|
|
installState = "not-installed";
|
|
throw err;
|
|
}
|
|
}
|