docs: document browser utility helpers

This commit is contained in:
Peter Steinberger
2026-06-04 07:50:05 -04:00
parent 383531da96
commit 29ddb9d926
16 changed files with 84 additions and 2 deletions

View File

@@ -1,3 +1,6 @@
/**
* Rate-limit message selection for Browser service providers.
*/
const BROWSER_SERVICE_RATE_LIMIT_MESSAGE =
"Browser service rate limit reached. " +
"Wait for the current session to complete, or retry later.";
@@ -22,6 +25,7 @@ function isBrowserbaseUrl(url: string): boolean {
}
}
/** Returns the provider-specific rate-limit message for a browser service URL. */
export function resolveBrowserRateLimitMessage(url: string): string {
return isBrowserbaseUrl(url)
? BROWSERBASE_RATE_LIMIT_MESSAGE

View File

@@ -1,3 +1,6 @@
/**
* Request policy helpers for profile-aware Browser control server routes.
*/
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type BrowserRequestProfileParams = {
@@ -6,6 +9,7 @@ type BrowserRequestProfileParams = {
profile?: string | null;
};
/** Normalizes route paths so mutation-policy checks compare stable slash forms. */
export function normalizeBrowserRequestPath(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
@@ -18,6 +22,7 @@ export function normalizeBrowserRequestPath(value: string): string {
return withLeadingSlash.replace(/\/+$/, "");
}
/** Returns true when a control request mutates persistent browser profile state. */
export function isPersistentBrowserProfileMutation(method: string, path: string): boolean {
const normalizedPath = normalizeBrowserRequestPath(path);
if (
@@ -29,6 +34,7 @@ export function isPersistentBrowserProfileMutation(method: string, path: string)
return method === "DELETE" && /^\/profiles\/[^/]+$/.test(normalizedPath);
}
/** Resolves the requested profile from query, body, or route defaults. */
export function resolveRequestedBrowserProfile(
params: BrowserRequestProfileParams,
): string | undefined {

View File

@@ -1,3 +1,7 @@
/**
* Runtime config refresh helpers for Browser profiles that can be hot-reloaded
* without restarting the whole Browser plugin server.
*/
import { loadBrowserConfigForRuntimeRefresh } from "./config-refresh-source.js";
import { resolveBrowserConfig, resolveProfile, type ResolvedBrowserProfile } from "./config.js";
import type { BrowserServerState } from "./server-context.types.js";
@@ -82,6 +86,7 @@ function applyResolvedConfig(
}
}
/** Refreshes the Browser runtime's resolved config from disk when hot reload is enabled. */
export function refreshResolvedBrowserConfigFromDisk(params: {
current: BrowserServerState;
refreshConfigFromDisk: boolean;
@@ -98,6 +103,7 @@ export function refreshResolvedBrowserConfigFromDisk(params: {
applyResolvedConfig(params.current, freshResolved);
}
/** Resolves a profile after an optional cached/fresh config reload. */
export function resolveBrowserProfileWithHotReload(params: {
current: BrowserServerState;
refreshConfigFromDisk: boolean;

View File

@@ -1,3 +1,7 @@
/**
* Browser plugin runtime lifecycle helpers for startup relay setup and shutdown
* cleanup.
*/
import type { Server } from "node:http";
import { getPwAiModule } from "./pw-ai-module.js";
import { isPwAiLoaded } from "./pw-ai-state.js";
@@ -6,6 +10,7 @@ import { ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } from "./ser
import { startTrackedBrowserTabCleanupTimer } from "./session-tab-cleanup.js";
import { registerBrowserUnhandledRejectionHandler } from "./unhandled-rejections.js";
/** Creates Browser server state and starts runtime-wide cleanup handlers. */
export async function createBrowserRuntimeState(params: {
resolved: BrowserServerState["resolved"];
port: number;
@@ -31,6 +36,7 @@ export async function createBrowserRuntimeState(params: {
return state;
}
/** Stops Browser profiles, the optional HTTP server, and loaded Playwright state. */
export async function stopBrowserRuntime(params: {
current: BrowserServerState | null;
getState: () => BrowserServerState | null;

View File

@@ -1 +1,4 @@
/**
* Re-export for Browser download filename sanitization.
*/
export { sanitizeUntrustedFileName } from "../sdk-security-runtime.js";

View File

@@ -1,3 +1,7 @@
/**
* Browser screenshot normalization helpers that bound screenshots for media
* transport and model input.
*/
import {
buildImageResizeSideGrid,
getImageMetadata,
@@ -9,6 +13,7 @@ import {
export const DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE = 2000;
export const DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
/** Downscales/re-encodes screenshots to fit Browser plugin byte and dimension caps. */
export async function normalizeBrowserScreenshot(
buffer: Buffer,
opts?: {

View File

@@ -1,8 +1,13 @@
/**
* Helpers for appending discovered page links to text snapshots.
*/
/** Link metadata appended to Browser page snapshots. */
export type SnapshotUrlEntry = {
text: string;
url: string;
};
/** Appends a compact numbered link list to a snapshot string. */
export function appendSnapshotUrls(snapshot: string, urls: readonly SnapshotUrlEntry[]): string {
if (urls.length === 0) {
return snapshot;

View File

@@ -1,6 +1,10 @@
/**
* SSRF policy helpers for Browser routes that need one-off hostname grants.
*/
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
/** Returns an SSRF policy with the hostname added to allowedHostnames. */
export function withAllowedHostname(
ssrfPolicy: SsrFPolicy | undefined,
hostname: string,

View File

@@ -1,9 +1,14 @@
/**
* Target id resolution helpers for Browser tab aliases and user-facing ids.
*/
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
/** Result for resolving a user-supplied tab id, label, or target prefix. */
type TargetIdResolution =
| { ok: true; targetId: string }
| { ok: false; reason: "not_found" | "ambiguous"; matches?: string[] };
/** Resolves exact tab ids/labels first, then unique target-id prefixes. */
export function resolveTargetIdFromTabs(
input: string,
tabs: Array<{ targetId: string; suggestedTargetId?: string; tabId?: string; label?: string }>,

View File

@@ -1,6 +1,10 @@
/**
* Test helper for reserving a loopback port for Browser control server tests.
*/
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
/** Returns an available 127.0.0.1 TCP port. */
export async function getFreePort(): Promise<number> {
while (true) {
const port = await new Promise<number>((resolve, reject) => {

View File

@@ -1,15 +1,21 @@
/**
* Test fetch resolver that bypasses mocked global fetch when Browser tests need
* a real HTTP client.
*/
import { createRequire } from "node:module";
type FetchLike = ((input: string | URL, init?: RequestInit) => Promise<Response>) & {
mock?: unknown;
};
/** Fetch shape used by Browser integration test helpers. */
export type BrowserTestFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
function isUsableFetch(value: unknown): value is FetchLike {
return typeof value === "function" && !("mock" in (value as FetchLike));
}
/** Returns undici fetch when usable, falling back to an unmocked global fetch. */
export function getBrowserTestFetch(): BrowserTestFetch {
const require = createRequire(import.meta.url);
const vitest = (globalThis as { vi?: { doUnmock?: (id: string) => void } }).vi;

View File

@@ -1,5 +1,10 @@
/**
* Timer delay normalization for Browser waits and cleanup loops.
*/
/** Largest timeout delay accepted reliably by Node timers. */
export const MAX_SAFE_TIMEOUT_DELAY_MS = 2_147_483_647;
/** Clamps timer delays to Node's safe range with an optional lower bound. */
export function normalizeBrowserTimerDelayMs(timeoutMs: number, opts?: { minMs?: number }): number {
const rawMinMs = opts?.minMs ?? 1;
const minMs = Math.min(

View File

@@ -1,7 +1,12 @@
/**
* Trash helpers for Browser-owned files constrained to user and OpenClaw temp
* roots.
*/
import os from "node:os";
import { movePathToTrash as movePathToTrashWithAllowedRoots } from "openclaw/plugin-sdk/browser-config";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
/** Moves a path to trash only when it lives under allowed Browser roots. */
export async function movePathToTrash(targetPath: string): Promise<string> {
return await movePathToTrashWithAllowedRoots(targetPath, {
allowedRoots: [os.homedir(), resolvePreferredOpenClawTmpDir()],

View File

@@ -1,3 +1,7 @@
/**
* Browser-specific unhandled rejection filter for benign Playwright dialog
* races.
*/
import { collectErrorGraphCandidates } from "openclaw/plugin-sdk/error-runtime";
import { registerUnhandledRejectionHandler } from "openclaw/plugin-sdk/runtime-env";
@@ -27,6 +31,7 @@ function readPlaywrightMethod(err: unknown): string | undefined {
return typeof method === "string" ? method : undefined;
}
/** Detects Playwright "no dialog is showing" races that can escape as rejections. */
export function isPlaywrightDialogRaceUnhandledRejection(reason: unknown): boolean {
for (const candidate of collectErrorGraphCandidates(reason, (current) => [
current.cause,
@@ -56,6 +61,7 @@ export function isPlaywrightDialogRaceUnhandledRejection(reason: unknown): boole
return false;
}
/** Installs the Browser unhandled-rejection filter and returns its disposer. */
export function registerBrowserUnhandledRejectionHandler(): () => void {
return registerUnhandledRejectionHandler(isPlaywrightDialogRaceUnhandledRejection);
}

View File

@@ -1,3 +1,6 @@
/**
* URL pattern matching for Browser response and wait tools.
*/
function wildcardPatternToRegExp(pattern: string): RegExp {
let source = "^";
for (let index = 0; index < pattern.length; index += 1) {
@@ -17,6 +20,7 @@ function wildcardPatternToRegExp(pattern: string): RegExp {
return new RegExp(source, "u");
}
/** Matches exact, wildcard, or substring URL patterns against a browser URL. */
export function matchBrowserUrlPattern(pattern: string, url: string): boolean {
const trimmedPattern = pattern.trim();
if (!trimmedPattern) {

View File

@@ -1,5 +1,7 @@
// Browser screenshot descriptions piggyback on the existing media image
// understanding contract. No browser-specific model registry lives here.
/**
* Browser screenshot description helpers built on the shared media image
* understanding contract. No browser-specific model registry lives here.
*/
import { readFile } from "node:fs/promises";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
@@ -7,9 +9,11 @@ import type { describeImageFile as DescribeImageFileFn } from "openclaw/plugin-s
import type { saveMediaBuffer as SaveMediaBufferFn } from "../sdk-setup-tools.js";
import type { normalizeBrowserScreenshot as NormalizeBrowserScreenshotFn } from "./screenshot.js";
/** Default prompt for turning browser screenshots into text-only page context. */
export const DEFAULT_BROWSER_SCREENSHOT_DESCRIPTION_PROMPT =
"Describe what is visible in this browser screenshot. Capture page layout, headings, primary content blocks, visible text, and notable interactive elements so a text-only assistant can reason about the page.";
/** Input context for browser screenshot image understanding. */
export type BrowserScreenshotDescriptionContext = {
cfg: OpenClawConfig;
filePath: string;
@@ -30,12 +34,14 @@ export type BrowserScreenshotDescriptionContext = {
};
};
/** Dependencies injected so Browser tests can avoid loading media runtimes. */
export type BrowserScreenshotDescriptionDeps = {
describeImageFile: typeof DescribeImageFileFn;
normalizeBrowserScreenshot: typeof NormalizeBrowserScreenshotFn;
saveMediaBuffer: typeof SaveMediaBufferFn;
};
/** Result returned from browser screenshot description. */
export type BrowserScreenshotDescriptionResult = {
text: string;
provider?: string;
@@ -78,6 +84,7 @@ async function resolveImageUnderstandingFilePath(
return saved.path;
}
/** Produces a text description for a browser screenshot, or null when no text was produced. */
export async function describeBrowserScreenshot(
ctx: BrowserScreenshotDescriptionContext,
deps: BrowserScreenshotDescriptionDeps,
@@ -104,6 +111,7 @@ export async function describeBrowserScreenshot(
};
}
/** Neutralizes model-generated MEDIA directives before feeding text back to tools. */
export function neutralizeMediaDirectives(text: string): string {
if (!text || !/media:/i.test(text)) {
return text;