338 lines
8.2 KiB
TypeScript
338 lines
8.2 KiB
TypeScript
import { fetchBrowserJson } from "./client-fetch.js";
|
|
|
|
export type BrowserStatus = {
|
|
enabled: boolean;
|
|
profile?: string;
|
|
running: boolean;
|
|
cdpReady?: boolean;
|
|
cdpHttp?: boolean;
|
|
pid: number | null;
|
|
cdpPort: number;
|
|
cdpUrl?: string;
|
|
chosenBrowser: string | null;
|
|
detectedBrowser?: string | null;
|
|
detectedExecutablePath?: string | null;
|
|
detectError?: string | null;
|
|
userDataDir: string | null;
|
|
color: string;
|
|
headless: boolean;
|
|
noSandbox?: boolean;
|
|
executablePath?: string | null;
|
|
attachOnly: boolean;
|
|
};
|
|
|
|
export type ProfileStatus = {
|
|
name: string;
|
|
cdpPort: number;
|
|
cdpUrl: string;
|
|
color: string;
|
|
running: boolean;
|
|
tabCount: number;
|
|
isDefault: boolean;
|
|
isRemote: boolean;
|
|
};
|
|
|
|
export type BrowserResetProfileResult = {
|
|
ok: true;
|
|
moved: boolean;
|
|
from: string;
|
|
to?: string;
|
|
};
|
|
|
|
export type BrowserTab = {
|
|
targetId: string;
|
|
title: string;
|
|
url: string;
|
|
wsUrl?: string;
|
|
type?: 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: "ai";
|
|
targetId: string;
|
|
url: string;
|
|
snapshot: string;
|
|
truncated?: boolean;
|
|
refs?: Record<string, { role: string; name?: string; nth?: number }>;
|
|
stats?: {
|
|
lines: number;
|
|
chars: number;
|
|
refs: number;
|
|
interactive: number;
|
|
};
|
|
labels?: boolean;
|
|
labelsCount?: number;
|
|
labelsSkipped?: number;
|
|
imagePath?: string;
|
|
imageType?: "png" | "jpeg";
|
|
};
|
|
|
|
function buildProfileQuery(profile?: string): string {
|
|
return profile ? `?profile=${encodeURIComponent(profile)}` : "";
|
|
}
|
|
|
|
function withBaseUrl(baseUrl: string | undefined, path: string): string {
|
|
const trimmed = baseUrl?.trim();
|
|
if (!trimmed) {
|
|
return path;
|
|
}
|
|
return `${trimmed.replace(/\/$/, "")}${path}`;
|
|
}
|
|
|
|
export async function browserStatus(
|
|
baseUrl?: string,
|
|
opts?: { profile?: string },
|
|
): Promise<BrowserStatus> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
return await fetchBrowserJson<BrowserStatus>(withBaseUrl(baseUrl, `/${q}`), {
|
|
timeoutMs: 1500,
|
|
});
|
|
}
|
|
|
|
export async function browserProfiles(baseUrl?: string): Promise<ProfileStatus[]> {
|
|
const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>(
|
|
withBaseUrl(baseUrl, `/profiles`),
|
|
{
|
|
timeoutMs: 3000,
|
|
},
|
|
);
|
|
return res.profiles ?? [];
|
|
}
|
|
|
|
export async function browserStart(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
await fetchBrowserJson(withBaseUrl(baseUrl, `/start${q}`), {
|
|
method: "POST",
|
|
timeoutMs: 15000,
|
|
});
|
|
}
|
|
|
|
export async function browserStop(baseUrl?: string, opts?: { profile?: string }): Promise<void> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
await fetchBrowserJson(withBaseUrl(baseUrl, `/stop${q}`), {
|
|
method: "POST",
|
|
timeoutMs: 15000,
|
|
});
|
|
}
|
|
|
|
export async function browserResetProfile(
|
|
baseUrl?: string,
|
|
opts?: { profile?: string },
|
|
): Promise<BrowserResetProfileResult> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
return await fetchBrowserJson<BrowserResetProfileResult>(
|
|
withBaseUrl(baseUrl, `/reset-profile${q}`),
|
|
{
|
|
method: "POST",
|
|
timeoutMs: 20000,
|
|
},
|
|
);
|
|
}
|
|
|
|
export type BrowserCreateProfileResult = {
|
|
ok: true;
|
|
profile: string;
|
|
cdpPort: number;
|
|
cdpUrl: string;
|
|
color: string;
|
|
isRemote: boolean;
|
|
};
|
|
|
|
export async function browserCreateProfile(
|
|
baseUrl: string | undefined,
|
|
opts: {
|
|
name: string;
|
|
color?: string;
|
|
cdpUrl?: string;
|
|
driver?: "openclaw" | "extension";
|
|
},
|
|
): Promise<BrowserCreateProfileResult> {
|
|
return await fetchBrowserJson<BrowserCreateProfileResult>(
|
|
withBaseUrl(baseUrl, `/profiles/create`),
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name: opts.name,
|
|
color: opts.color,
|
|
cdpUrl: opts.cdpUrl,
|
|
driver: opts.driver,
|
|
}),
|
|
timeoutMs: 10000,
|
|
},
|
|
);
|
|
}
|
|
|
|
export type BrowserDeleteProfileResult = {
|
|
ok: true;
|
|
profile: string;
|
|
deleted: boolean;
|
|
};
|
|
|
|
export async function browserDeleteProfile(
|
|
baseUrl: string | undefined,
|
|
profile: string,
|
|
): Promise<BrowserDeleteProfileResult> {
|
|
return await fetchBrowserJson<BrowserDeleteProfileResult>(
|
|
withBaseUrl(baseUrl, `/profiles/${encodeURIComponent(profile)}`),
|
|
{
|
|
method: "DELETE",
|
|
timeoutMs: 20000,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function browserTabs(
|
|
baseUrl?: string,
|
|
opts?: { profile?: string },
|
|
): Promise<BrowserTab[]> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>(
|
|
withBaseUrl(baseUrl, `/tabs${q}`),
|
|
{ timeoutMs: 3000 },
|
|
);
|
|
return res.tabs ?? [];
|
|
}
|
|
|
|
export async function browserOpenTab(
|
|
baseUrl: string | undefined,
|
|
url: string,
|
|
opts?: { profile?: string },
|
|
): Promise<BrowserTab> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
return await fetchBrowserJson<BrowserTab>(withBaseUrl(baseUrl, `/tabs/open${q}`), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url }),
|
|
timeoutMs: 15000,
|
|
});
|
|
}
|
|
|
|
export async function browserFocusTab(
|
|
baseUrl: string | undefined,
|
|
targetId: string,
|
|
opts?: { profile?: string },
|
|
): Promise<void> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/focus${q}`), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ targetId }),
|
|
timeoutMs: 5000,
|
|
});
|
|
}
|
|
|
|
export async function browserCloseTab(
|
|
baseUrl: string | undefined,
|
|
targetId: string,
|
|
opts?: { profile?: string },
|
|
): Promise<void> {
|
|
const q = buildProfileQuery(opts?.profile);
|
|
await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), {
|
|
method: "DELETE",
|
|
timeoutMs: 5000,
|
|
});
|
|
}
|
|
|
|
export async function browserTabAction(
|
|
baseUrl: string | undefined,
|
|
opts: {
|
|
action: "list" | "new" | "close" | "select";
|
|
index?: number;
|
|
profile?: string;
|
|
},
|
|
): Promise<unknown> {
|
|
const q = buildProfileQuery(opts.profile);
|
|
return await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/action${q}`), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: opts.action,
|
|
index: opts.index,
|
|
}),
|
|
timeoutMs: 10_000,
|
|
});
|
|
}
|
|
|
|
export async function browserSnapshot(
|
|
baseUrl: string | undefined,
|
|
opts: {
|
|
format: "aria" | "ai";
|
|
targetId?: string;
|
|
limit?: number;
|
|
maxChars?: number;
|
|
refs?: "role" | "aria";
|
|
interactive?: boolean;
|
|
compact?: boolean;
|
|
depth?: number;
|
|
selector?: string;
|
|
frame?: string;
|
|
labels?: boolean;
|
|
mode?: "efficient";
|
|
profile?: string;
|
|
},
|
|
): 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));
|
|
}
|
|
if (typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)) {
|
|
q.set("maxChars", String(opts.maxChars));
|
|
}
|
|
if (opts.refs === "aria" || opts.refs === "role") {
|
|
q.set("refs", opts.refs);
|
|
}
|
|
if (typeof opts.interactive === "boolean") {
|
|
q.set("interactive", String(opts.interactive));
|
|
}
|
|
if (typeof opts.compact === "boolean") {
|
|
q.set("compact", String(opts.compact));
|
|
}
|
|
if (typeof opts.depth === "number" && Number.isFinite(opts.depth)) {
|
|
q.set("depth", String(opts.depth));
|
|
}
|
|
if (opts.selector?.trim()) {
|
|
q.set("selector", opts.selector.trim());
|
|
}
|
|
if (opts.frame?.trim()) {
|
|
q.set("frame", opts.frame.trim());
|
|
}
|
|
if (opts.labels === true) {
|
|
q.set("labels", "1");
|
|
}
|
|
if (opts.mode) {
|
|
q.set("mode", opts.mode);
|
|
}
|
|
if (opts.profile) {
|
|
q.set("profile", opts.profile);
|
|
}
|
|
return await fetchBrowserJson<SnapshotResult>(withBaseUrl(baseUrl, `/snapshot?${q.toString()}`), {
|
|
timeoutMs: 20000,
|
|
});
|
|
}
|
|
|
|
// Actions beyond the basic read-only commands live in client-actions.ts.
|