openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
293 lines (292 loc) • 14.9 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs";
import { F as resolveTimerTimeoutMs } from "./number-coercion-CkhMiRw8.mjs";
import { a as extractErrorCode, d as toErrorObject } from "./error-coercion-W9BtigRm.mjs";
import { o as racePromiseWithAbortSignal } from "./src-D1BHGNTj.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { i as readResponseWithLimit } from "./http-response-body-ljSrZsaY.mjs";
import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs";
import { i as fetchWithSsrFGuard } from "./fetch-guard-C19WJ54-.mjs";
import { s as isLoopbackHost } from "./net-CpjDDLK3.mjs";
import "./error-runtime-oJAVOxQN.mjs";
import "./string-coerce-runtime-BbIJhy7G.mjs";
import "./response-limit-runtime-Fw-ZKZq8.mjs";
import "./number-runtime-D3v2w3D4.mjs";
import "./time-runtime-SlsIp1up.mjs";
import "./setup-tools-w-mO5UXy.mjs";
import "./runtime-config-snapshot--IkldAgP.mjs";
import "./ssrf-runtime-CR-jTHeC.mjs";
import { t as parseBrowserHttpUrl } from "./browser-cdp-DZRuqpQ-.mjs";
import { i as resolveBrowserConfig, s as resolveProfile } from "./config-NGx7eSU6.mjs";
import { m as parseBrowserErrorPayload, t as BROWSER_ACT_ERROR_CODES } from "./errors-BqlAThZX.mjs";
import { h as resolveBrowserRateLimitMessage } from "./cdp.helpers-C41Lpa_p.mjs";
import { n as getBridgeAuthForPort } from "./bridge-auth-registry-BvDYD1O6.mjs";
import { n as resolveBrowserControlAuth } from "./control-auth-BI9lnwdd.mjs";
import { AsyncLocalStorage } from "node:async_hooks";
//#region extensions/browser/src/browser/request-scope.ts
const requestScope = new AsyncLocalStorage();
/** Carry a dashboard's authority through the existing local Browser client transport. */
function withBrowserRequestScope(scope, run) {
return requestScope.run(scope, run);
}
function getBrowserRequestScope() {
return requestScope.getStore();
}
//#endregion
//#region extensions/browser/src/browser/client-fetch.ts
/**
* Browser control client transport.
*
* Sends requests to either an absolute HTTP browser-control URL or the local
* in-process dispatcher, adding loopback auth and operator-facing diagnostics.
*/
var BrowserServiceError = class extends Error {
constructor(message, metadata, status) {
super(message);
this.name = "BrowserServiceError";
this.status = status;
this.code = metadata?.code;
this.unrecognizedCode = metadata?.unrecognizedCode;
this.reason = metadata && "reason" in metadata ? metadata.reason : void 0;
this.details = metadata && "details" in metadata ? metadata.details : void 0;
}
};
function browserServiceErrorFromPayload(value, fallback, status) {
const parsed = parseBrowserErrorPayload(value);
const message = parsed?.error ?? fallback;
const modelHint = parsed?.code === BROWSER_ACT_ERROR_CODES.operationFailed && status !== 401 ? void 0 : resolveBrowserServiceModelHint(message, status);
return new BrowserServiceError(modelHint ? appendBrowserToolModelHint(message, modelHint) : message, parsed ?? void 0, status);
}
function isAbsoluteHttp(url) {
return /^https?:\/\//i.test(url.trim());
}
function isLoopbackHttpUrl(url) {
return isLoopbackHost(URL.parse(url)?.hostname ?? "");
}
function withLoopbackBrowserAuth(url, init) {
const headers = new Headers(init?.headers ?? {});
if (headers.has("authorization") || headers.has("x-openclaw-password") || !isLoopbackHttpUrl(url)) return {
...init,
headers
};
for (const resolveAuth of [() => getBridgeAuthForPort(parseBrowserHttpUrl(url, "browser control URL").port), () => resolveBrowserControlAuth(getRuntimeConfig())]) try {
const auth = resolveAuth();
if (auth?.token) headers.set("Authorization", `Bearer ${auth.token}`);
else if (auth?.password) headers.set("x-openclaw-password", auth.password);
else continue;
return {
...init,
headers
};
} catch {}
return {
...init,
headers
};
}
const BROWSER_TOOL_PERSISTENT_MODEL_HINT = "Do NOT retry the browser tool — it will keep failing. Use an alternative approach or inform the user that the browser is currently unavailable.";
const BROWSER_TOOL_TRANSIENT_MODEL_HINT = "This may be a transient browser error. Retry the browser tool once. If the same error persists, use an alternative approach or inform the user that the browser is currently unavailable.";
const BROWSER_TRANSIENT_NETWORK_ERROR_RE = /\b(?:ECONNRESET|ECONNABORTED|ENETRESET|ETIMEDOUT|EPIPE|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|UND_ERR_(?:CONNECT_TIMEOUT|HEADERS_TIMEOUT|BODY_TIMEOUT|SOCKET))\b|fetch failed|network error|other side closed|socket (?:hang up|terminated)|connection (?:reset|aborted|timed out)/i;
const BROWSER_PERSISTENT_FAILURE_RE = /\bECONNREFUSED\b|connection refused|browser control (?:is )?(?:disabled|not enabled)|invalid (?:auth|authentication|credentials|password|token)|authentication (?:failed|required)|unauthorized/i;
const BROWSER_ERROR_BODY_LIMIT_BYTES = 16384;
const BROWSER_SUCCESS_BODY_LIMIT_BYTES = 33554432;
function decodeBrowserControlResponseUtf8(body, status) {
try {
return new TextDecoder("utf-8", { fatal: true }).decode(body);
} catch {
throw browserServiceErrorFromPayload(void 0, `Browser control response was not valid UTF-8 (HTTP ${status})`, status);
}
}
function resolveBrowserFetchOperatorHint(url) {
if (isAbsoluteHttp(url)) return "If this is a sandboxed session, ensure the sandbox browser is running.";
try {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg?.browser, cfg);
const requestedProfile = new URL(url, "http://localhost").searchParams.get("profile")?.trim();
const profile = resolveProfile(resolved, requestedProfile || resolved.defaultProfile);
if (profile && (profile.driver !== "openclaw" || !profile.cdpIsLoopback || profile.attachOnly)) return "The browser profile is external to OpenClaw; make sure its browser/CDP endpoint is running and reachable. Restarting the OpenClaw gateway will not launch it.";
} catch {}
return `Run \`${formatCliCommand("openclaw browser doctor")}\` and check the Gateway logs.`;
}
function normalizeErrorMessage(err) {
const message = err instanceof Error ? normalizeOptionalString(err.message) : void 0;
if (message) return message;
return String(err);
}
function appendBrowserToolModelHint(message, hint) {
return `${message.replaceAll(BROWSER_TOOL_PERSISTENT_MODEL_HINT, "").replaceAll(BROWSER_TOOL_TRANSIENT_MODEL_HINT, "").trim()} ${hint}`;
}
function classifyBrowserFetchFailure(err) {
const directCode = extractErrorCode(err);
const formatted = formatErrorMessage(err);
const detail = directCode ? `${formatted} | ${directCode}` : formatted;
const detailLower = normalizeLowercaseStringOrEmpty(detail);
const nameLower = err instanceof Error ? normalizeLowercaseStringOrEmpty(err.name) : "";
if (nameLower === "aborterror") return "aborted";
if (BROWSER_PERSISTENT_FAILURE_RE.test(detail)) return "persistent";
if (nameLower.includes("timeout") || detailLower.includes("timed out") || detailLower.includes("timeout")) return "timeout";
if (BROWSER_TRANSIENT_NETWORK_ERROR_RE.test(detail)) return "transient-network";
return detailLower.includes("abort") || detailLower.includes("cancelled") || detailLower.includes("canceled") ? "aborted" : "persistent";
}
function resolveBrowserServiceModelHint(message, status) {
if (message.includes(BROWSER_TOOL_PERSISTENT_MODEL_HINT)) return BROWSER_TOOL_PERSISTENT_MODEL_HINT;
if (message.includes(BROWSER_TOOL_TRANSIENT_MODEL_HINT)) return BROWSER_TOOL_TRANSIENT_MODEL_HINT;
if (status === 401 || BROWSER_PERSISTENT_FAILURE_RE.test(message)) return BROWSER_TOOL_PERSISTENT_MODEL_HINT;
if (status === 408 || status === 504) return BROWSER_TOOL_TRANSIENT_MODEL_HINT;
if (status === void 0 || status < 500 || status > 599) return;
const kind = classifyBrowserFetchFailure(new Error(message));
return kind === "persistent" ? void 0 : resolveBrowserToolModelHint(kind);
}
function resolveBrowserToolModelHint(kind) {
if (kind === "timeout" || kind === "transient-network") return BROWSER_TOOL_TRANSIENT_MODEL_HINT;
return kind === "persistent" ? BROWSER_TOOL_PERSISTENT_MODEL_HINT : void 0;
}
function discardResponseBody(res) {
res.body?.cancel().catch(() => void 0);
}
function enhanceDispatcherPathError(url, err) {
const msg = normalizeErrorMessage(err);
const kind = classifyBrowserFetchFailure(err);
const operatorHint = resolveBrowserFetchOperatorHint(url);
const modelHint = resolveBrowserToolModelHint(kind);
const suffix = modelHint ? `${operatorHint} ${modelHint}` : operatorHint;
const normalized = msg.endsWith(".") ? msg : `${msg}.`;
return new Error(`${normalized} ${suffix}`, err instanceof Error ? { cause: err } : void 0);
}
function enhanceBrowserFetchError(url, err, timeoutMs) {
const operatorHint = resolveBrowserFetchOperatorHint(url);
const msg = normalizeErrorMessage(err);
const kind = classifyBrowserFetchFailure(err);
let message;
if (kind === "timeout") message = `Can't reach the OpenClaw browser control service (timed out after ${timeoutMs}ms). ${operatorHint} ${BROWSER_TOOL_TRANSIENT_MODEL_HINT}`;
else if (kind === "aborted") message = `Browser control request was cancelled. ${operatorHint}`;
else if (kind === "transient-network") message = `Can't reach the OpenClaw browser control service. ${operatorHint} (${msg}) ${BROWSER_TOOL_TRANSIENT_MODEL_HINT}`;
else message = appendBrowserToolModelHint(`Can't reach the OpenClaw browser control service. ${operatorHint} (${msg})`, BROWSER_TOOL_PERSISTENT_MODEL_HINT);
return new Error(message, err instanceof Error ? { cause: err } : void 0);
}
function createBrowserRequestAbort(timeoutMs, upstreamSignal) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timed out")), timeoutMs);
return {
signal: upstreamSignal ? AbortSignal.any([upstreamSignal, controller.signal]) : controller.signal,
dispose: () => clearTimeout(timer)
};
}
async function fetchHttpJson(url, init) {
const { timeoutMs } = init;
const abort = createBrowserRequestAbort(timeoutMs, init.signal);
const { signal } = abort;
let release;
try {
const guarded = await fetchWithSsrFGuard({
url,
init,
timeoutMs,
signal,
policy: { allowPrivateNetwork: true },
auditContext: "browser-control-client"
});
release = guarded.release;
const res = guarded.response;
if (!res.ok) {
if (res.status === 429) {
discardResponseBody(res);
throw new BrowserServiceError(`${resolveBrowserRateLimitMessage(url)} ${BROWSER_TOOL_PERSISTENT_MODEL_HINT}`);
}
const body = await readResponseWithLimit(res, BROWSER_ERROR_BODY_LIMIT_BYTES).catch(() => void 0);
const text = body ? decodeBrowserControlResponseUtf8(body, res.status) : "";
let parsed;
if (text) try {
parsed = JSON.parse(text);
} catch {}
throw browserServiceErrorFromPayload(parsed, text || `HTTP ${res.status}`, res.status);
}
const body = await readResponseWithLimit(res, BROWSER_SUCCESS_BODY_LIMIT_BYTES, { onOverflow: ({ maxBytes }) => new BrowserServiceError(`Browser control response exceeded ${maxBytes} bytes`) });
return JSON.parse(decodeBrowserControlResponseUtf8(body, res.status));
} finally {
abort.dispose();
await release?.();
}
}
/** Fetch JSON from browser control over HTTP or local dispatcher transport. */
async function fetchBrowserJson(url, init) {
const timeoutMs = resolveTimerTimeoutMs(init?.timeoutMs, 5e3);
const scope = getBrowserRequestScope();
let isDispatcherPath = false;
try {
if (isAbsoluteHttp(url)) {
if (scope) throw new Error("Dashboard browser requests must stay on the local managed browser");
return await fetchHttpJson(url, {
...withLoopbackBrowserAuth(url, init),
timeoutMs
});
}
isDispatcherPath = true;
const { dispatchBrowserControlRequest } = await import("./local-dispatch.runtime.js");
const parsed = new URL(url, "http://localhost");
const query = {};
for (const [key, value] of parsed.searchParams.entries()) query[key] = value;
if (scope) query.managedOnly = true;
let body = init?.body;
if (typeof body === "string") try {
body = JSON.parse(body);
} catch {}
const abort = createBrowserRequestAbort(timeoutMs, init?.signal);
const { signal } = abort;
const dispatchPromise = dispatchBrowserControlRequest({
method: init?.method?.toUpperCase() === "DELETE" ? "DELETE" : init?.method?.toUpperCase() === "POST" ? "POST" : "GET",
path: parsed.pathname,
query,
body,
signal,
...scope ? { assertCurrent: scope.assertCurrent } : {}
});
const result = await racePromiseWithAbortSignal(dispatchPromise, signal, ({ reason }) => toErrorObject(reason ?? /* @__PURE__ */ new Error("aborted"), "Non-Error rejection")).finally(abort.dispose);
if (result.status >= 400) {
if (result.status === 429) throw new BrowserServiceError(`${resolveBrowserRateLimitMessage(url)} ${BROWSER_TOOL_PERSISTENT_MODEL_HINT}`);
throw browserServiceErrorFromPayload(result.body, `HTTP ${result.status}`, result.status);
}
return result.body;
} catch (err) {
if (err instanceof BrowserServiceError) throw err;
if (isDispatcherPath) throw enhanceDispatcherPathError(url, err);
throw enhanceBrowserFetchError(url, err, timeoutMs);
}
}
//#endregion
//#region extensions/browser/src/browser/client-request.ts
function browserClientTimeout(target, requested, localDefault) {
return typeof target === "function" ? requested : resolveTimerTimeoutMs(requested, localDefault);
}
/** Send the same explicitly projected request through the selected transport. */
async function requestBrowserJson(target, path, opts = {}) {
if (typeof target === "function") return await target({
...opts,
method: opts.method ?? "GET",
path
});
const query = new URLSearchParams();
for (const [key, value] of Object.entries(opts.query ?? {})) if (value !== void 0) query.set(key, String(value));
const suffix = query.size ? `${path.includes("?") ? "&" : "?"}${query}` : "";
const profile = opts.profile ? `${suffix || path.includes("?") ? "&" : "?"}profile=${encodeURIComponent(opts.profile)}` : "";
return await fetchBrowserJson(`${target?.trim().replace(/\/$/, "") ?? ""}${path}${suffix}${profile}`, {
method: opts.method,
...opts.body === void 0 ? {} : {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(opts.body)
},
timeoutMs: opts.timeoutMs,
signal: opts.signal
});
}
function postBrowserJson(target, path, body, timeoutMs, opts) {
return requestBrowserJson(target, path, {
method: "POST",
body,
timeoutMs,
profile: opts?.profile,
signal: opts?.signal
});
}
//#endregion
export { fetchBrowserJson as a, BrowserServiceError as i, postBrowserJson as n, withBrowserRequestScope as o, requestBrowserJson as r, browserClientTimeout as t };