Files
clawdbot/src/browser/client.ts
2025-12-20 00:53:56 +00:00

306 lines
6.8 KiB
TypeScript

import { loadConfig } from "../config/config.js";
import { fetchBrowserJson } from "./client-fetch.js";
import { resolveBrowserConfig } from "./config.js";
export type BrowserStatus = {
enabled: boolean;
controlUrl: string;
running: boolean;
pid: number | null;
cdpPort: number;
chosenBrowser: string | null;
userDataDir: string | null;
color: string;
headless: boolean;
attachOnly: boolean;
};
export type BrowserTab = {
targetId: string;
title: string;
url: string;
type?: string;
};
export type ScreenshotResult = {
ok: true;
path: string;
targetId: string;
url: string;
};
export type EvalResult = {
ok: true;
targetId: string;
url: string;
result: {
type: string;
subtype?: string;
value?: unknown;
description?: string;
unserializableValue?: string;
preview?: unknown;
};
};
export type QueryResult = {
ok: true;
targetId: string;
url: string;
matches: Array<{
index: number;
tag: string;
id?: string;
className?: string;
text?: string;
value?: string;
href?: string;
outerHTML?: string;
}>;
};
export type DomResult = {
ok: true;
targetId: string;
url: string;
format: "html" | "text";
text: string;
};
export type SnapshotAriaNode = {
ref: string;
role: string;
name: string;
value?: string;
description?: string;
backendDOMNodeId?: number;
depth: number;
};
export type SnapshotResult =
| {
ok: true;
format: "aria";
targetId: string;
url: string;
nodes: SnapshotAriaNode[];
}
| {
ok: true;
format: "domSnapshot";
targetId: string;
url: string;
nodes: Array<{
ref: string;
parentRef: string | null;
depth: number;
tag: string;
id?: string;
className?: string;
role?: string;
name?: string;
text?: string;
href?: string;
type?: string;
value?: string;
}>;
}
| {
ok: true;
format: "ai";
targetId: string;
url: string;
snapshot: string;
};
export function resolveBrowserControlUrl(overrideUrl?: string) {
const cfg = loadConfig();
const resolved = resolveBrowserConfig(cfg.browser);
const url = overrideUrl?.trim() ? overrideUrl.trim() : resolved.controlUrl;
return url.replace(/\/$/, "");
}
export async function browserStatus(baseUrl: string): Promise<BrowserStatus> {
return await fetchBrowserJson<BrowserStatus>(`${baseUrl}/`, {
timeoutMs: 1500,
});
}
export async function browserStart(baseUrl: string): Promise<void> {
await fetchBrowserJson(`${baseUrl}/start`, {
method: "POST",
timeoutMs: 15000,
});
}
export async function browserStop(baseUrl: string): Promise<void> {
await fetchBrowserJson(`${baseUrl}/stop`, {
method: "POST",
timeoutMs: 15000,
});
}
export async function browserTabs(baseUrl: string): Promise<BrowserTab[]> {
const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>(
`${baseUrl}/tabs`,
{ timeoutMs: 3000 },
);
return res.tabs ?? [];
}
export async function browserOpenTab(
baseUrl: string,
url: string,
): Promise<BrowserTab> {
return await fetchBrowserJson<BrowserTab>(`${baseUrl}/tabs/open`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url }),
timeoutMs: 15000,
});
}
export async function browserFocusTab(
baseUrl: string,
targetId: string,
): Promise<void> {
await fetchBrowserJson(`${baseUrl}/tabs/focus`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetId }),
timeoutMs: 5000,
});
}
export async function browserCloseTab(
baseUrl: string,
targetId: string,
): Promise<void> {
await fetchBrowserJson(`${baseUrl}/tabs/${encodeURIComponent(targetId)}`, {
method: "DELETE",
timeoutMs: 5000,
});
}
export async function browserScreenshot(
baseUrl: string,
opts: {
targetId?: string;
fullPage?: boolean;
},
): Promise<ScreenshotResult> {
const q = new URLSearchParams();
if (opts.targetId) q.set("targetId", opts.targetId);
if (opts.fullPage) q.set("fullPage", "true");
const suffix = q.toString() ? `?${q.toString()}` : "";
return await fetchBrowserJson<ScreenshotResult>(
`${baseUrl}/screenshot${suffix}`,
{
timeoutMs: 20000,
},
);
}
export async function browserEval(
baseUrl: string,
opts: {
js: string;
targetId?: string;
awaitPromise?: boolean;
},
): Promise<EvalResult> {
return await fetchBrowserJson<EvalResult>(`${baseUrl}/eval`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
js: opts.js,
targetId: opts.targetId,
await: Boolean(opts.awaitPromise),
}),
timeoutMs: 15000,
});
}
export async function browserQuery(
baseUrl: string,
opts: {
selector: string;
targetId?: string;
limit?: number;
},
): Promise<QueryResult> {
const q = new URLSearchParams();
q.set("selector", opts.selector);
if (opts.targetId) q.set("targetId", opts.targetId);
if (typeof opts.limit === "number") q.set("limit", String(opts.limit));
return await fetchBrowserJson<QueryResult>(
`${baseUrl}/query?${q.toString()}`,
{
timeoutMs: 15000,
},
);
}
export async function browserDom(
baseUrl: string,
opts: {
format: "html" | "text";
targetId?: string;
maxChars?: number;
selector?: string;
},
): Promise<DomResult> {
const q = new URLSearchParams();
q.set("format", opts.format);
if (opts.targetId) q.set("targetId", opts.targetId);
if (typeof opts.maxChars === "number")
q.set("maxChars", String(opts.maxChars));
if (opts.selector) q.set("selector", opts.selector);
return await fetchBrowserJson<DomResult>(`${baseUrl}/dom?${q.toString()}`, {
timeoutMs: 20000,
});
}
export async function browserSnapshot(
baseUrl: string,
opts: {
format: "aria" | "domSnapshot" | "ai";
targetId?: string;
limit?: number;
},
): Promise<SnapshotResult> {
const q = new URLSearchParams();
q.set("format", opts.format);
if (opts.targetId) q.set("targetId", opts.targetId);
if (typeof opts.limit === "number") q.set("limit", String(opts.limit));
return await fetchBrowserJson<SnapshotResult>(
`${baseUrl}/snapshot?${q.toString()}`,
{
timeoutMs: 20000,
},
);
}
export async function browserClickRef(
baseUrl: string,
opts: {
ref: string;
targetId?: string;
},
): Promise<{ ok: true; targetId: string; url: string }> {
return await fetchBrowserJson<{ ok: true; targetId: string; url: string }>(
`${baseUrl}/click`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ref: opts.ref,
targetId: opts.targetId,
}),
timeoutMs: 20000,
},
);
}
// Actions beyond the basic read-only commands live in client-actions.ts.