| 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 OpenClaw gateway (OpenClaw.app menubar, or \`${formatCliCommand("openclaw gateway")}\`) and try again.`; |
| const msg = String(err); |
| const msgLower = msg.toLowerCase(); |
| const looksLikeTimeout = |
| msgLower.includes("timed out") || |
| msgLower.includes("timeout") || |
| msgLower.includes("aborted") || |
| msgLower.includes("abort") || |
| msgLower.includes("aborterror"); |
| if (looksLikeTimeout) { |
| return new Error( |
| `Can't reach the openclaw browser control service (timed out after ${timeoutMs}ms). ${hint}`, |
| ); |
| } |
| return new Error(`Can't reach the openclaw 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, 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 { |
| |
| } |
| } |
| 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); |
| } |
| } |
|
|