104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { formatCliCommand } from "../cli/command-format.js";
|
|
import {
|
|
createBrowserControlContext,
|
|
startBrowserControlServiceFromConfig,
|
|
} from "./control-service.js";
|
|
import { createBrowserRouteDispatcher } from "./routes/dispatcher.js";
|
|
|
|
function isAbsoluteHttp(url: string): boolean {
|
|
return /^https?:\/\//i.test(url.trim());
|
|
}
|
|
|
|
function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error {
|
|
const hint = isAbsoluteHttp(url)
|
|
? "If this is a sandboxed session, ensure the sandbox browser is running and try again."
|
|
: `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`;
|
|
const msg = String(err);
|
|
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
|
|
return new Error(
|
|
`Can't reach the clawd browser control service (timed out after ${timeoutMs}ms). ${hint}`,
|
|
);
|
|
}
|
|
return new Error(`Can't reach the clawd browser control service. ${hint} (${msg})`);
|
|
}
|
|
|
|
async function fetchHttpJson<T>(
|
|
url: string,
|
|
init: RequestInit & { timeoutMs?: number },
|
|
): Promise<T> {
|
|
const timeoutMs = init.timeoutMs ?? 5000;
|
|
const ctrl = new AbortController();
|
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
try {
|
|
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
throw new Error(text || `HTTP ${res.status}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
} finally {
|
|
clearTimeout(t);
|
|
}
|
|
}
|
|
|
|
export async function fetchBrowserJson<T>(
|
|
url: string,
|
|
init?: RequestInit & { timeoutMs?: number },
|
|
): Promise<T> {
|
|
const timeoutMs = init?.timeoutMs ?? 5000;
|
|
try {
|
|
if (isAbsoluteHttp(url)) {
|
|
return await fetchHttpJson<T>(url, init ? { ...init, timeoutMs } : { timeoutMs });
|
|
}
|
|
const started = await startBrowserControlServiceFromConfig();
|
|
if (!started) {
|
|
throw new Error("browser control disabled");
|
|
}
|
|
const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
|
|
const parsed = new URL(url, "http://localhost");
|
|
const query: Record<string, unknown> = {};
|
|
for (const [key, value] of parsed.searchParams.entries()) {
|
|
query[key] = value;
|
|
}
|
|
let body = init?.body;
|
|
if (typeof body === "string") {
|
|
try {
|
|
body = JSON.parse(body);
|
|
} catch {
|
|
// keep as string
|
|
}
|
|
}
|
|
const dispatchPromise = dispatcher.dispatch({
|
|
method:
|
|
init?.method?.toUpperCase() === "DELETE"
|
|
? "DELETE"
|
|
: init?.method?.toUpperCase() === "POST"
|
|
? "POST"
|
|
: "GET",
|
|
path: parsed.pathname,
|
|
query,
|
|
body,
|
|
});
|
|
|
|
const result = await (timeoutMs
|
|
? Promise.race([
|
|
dispatchPromise,
|
|
new Promise<never>((_, reject) =>
|
|
setTimeout(() => reject(new Error("timed out")), timeoutMs),
|
|
),
|
|
])
|
|
: dispatchPromise);
|
|
|
|
if (result.status >= 400) {
|
|
const message =
|
|
result.body && typeof result.body === "object" && "error" in result.body
|
|
? String((result.body as { error?: unknown }).error)
|
|
: `HTTP ${result.status}`;
|
|
throw new Error(message);
|
|
}
|
|
return result.body as T;
|
|
} catch (err) {
|
|
throw enhanceBrowserFetchError(url, err, timeoutMs);
|
|
}
|
|
}
|