refactor(browser): consolidate browser CLI and normalization helpers (#119432)

This commit is contained in:
Peter Steinberger
2026-08-04 20:26:21 -07:00
committed by GitHub
parent bf1bdb429e
commit 767dc7019d
10 changed files with 107 additions and 209 deletions
@@ -16,6 +16,7 @@ import {
type BrowserProxyUploadV1,
} from "./browser-proxy-envelope.js";
import { DEFAULT_UPLOAD_DIR, resolveExistingUploadPaths } from "./browser/paths.js";
import { asRecord } from "./record-shared.js";
const logger = createSubsystemLogger("browser");
const BROWSER_PROXY_UPLOAD_ROOT_NAME = ".proxy-uploads";
@@ -76,12 +77,6 @@ export function isBrowserProxyUploadRequest(params: {
return Boolean(body && Array.isArray(body.paths) && body.paths.length > 0);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readUploadPaths(body: Record<string, unknown>): string[] | null {
if (!Array.isArray(body.paths) || body.paths.length === 0) {
return null;
@@ -5,6 +5,7 @@
* exact RunningChrome instance that owns the process.
*/
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { asRecord, isRecord } from "../record-shared.js";
import { redactCdpErrorText, withCdpSocket } from "./cdp.helpers.js";
import { getChromeWebSocketUrl, type RunningChrome } from "./chrome.js";
import type {
@@ -15,8 +16,6 @@ import type {
BrowserVideoEncodeCapability,
} from "./client.types.js";
type UnknownRecord = Record<string, unknown>;
type ChromeGraphicsProbeOptions = {
httpTimeoutMs?: number;
handshakeTimeoutMs?: number;
@@ -24,12 +23,6 @@ type ChromeGraphicsProbeOptions = {
ssrfPolicy?: SsrFPolicy;
};
function asRecord(value: unknown): UnknownRecord | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as UnknownRecord)
: null;
}
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
@@ -63,65 +56,36 @@ function readSize(value: unknown): { width: number; height: number } {
};
}
function readRecordArray(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter(isRecord) : [];
}
function readDevices(value: unknown): BrowserGraphicsDevice[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((item) => {
const device = asRecord(item);
if (!device) {
return [];
}
return [
{
vendorId: readNumber(device.vendorId),
deviceId: readNumber(device.deviceId),
vendor: readString(device.vendorString),
device: readString(device.deviceString),
driverVendor: readString(device.driverVendor),
driverVersion: readString(device.driverVersion),
},
];
});
return readRecordArray(value).map((device) => ({
vendorId: readNumber(device.vendorId),
deviceId: readNumber(device.deviceId),
vendor: readString(device.vendorString),
device: readString(device.deviceString),
driverVendor: readString(device.driverVendor),
driverVersion: readString(device.driverVersion),
}));
}
function readVideoDecoding(value: unknown): BrowserVideoDecodeCapability[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((item) => {
const capability = asRecord(item);
if (!capability) {
return [];
}
return [
{
profile: readString(capability.profile),
minResolution: readSize(capability.minResolution),
maxResolution: readSize(capability.maxResolution),
},
];
});
return readRecordArray(value).map((capability) => ({
profile: readString(capability.profile),
minResolution: readSize(capability.minResolution),
maxResolution: readSize(capability.maxResolution),
}));
}
function readVideoEncoding(value: unknown): BrowserVideoEncodeCapability[] {
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((item) => {
const capability = asRecord(item);
if (!capability) {
return [];
}
return [
{
profile: readString(capability.profile),
maxResolution: readSize(capability.maxResolution),
maxFramerateNumerator: readNumber(capability.maxFramerateNumerator),
maxFramerateDenominator: readNumber(capability.maxFramerateDenominator),
},
];
});
return readRecordArray(value).map((capability) => ({
profile: readString(capability.profile),
maxResolution: readSize(capability.maxResolution),
maxFramerateNumerator: readNumber(capability.maxFramerateNumerator),
maxFramerateDenominator: readNumber(capability.maxFramerateDenominator),
}));
}
function firstAttribute(
@@ -7,6 +7,7 @@
import fs from "node:fs";
import path from "node:path";
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
import { asRecord } from "../record-shared.js";
import {
DEFAULT_OPENCLAW_BROWSER_COLOR,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
@@ -19,22 +20,13 @@ function decoratedMarkerPath(userDataDir: string) {
}
function safeReadJson(filePath: string): Record<string, unknown> | null {
const parsed = loadJsonFile(filePath);
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: null;
return asRecord(loadJsonFile(filePath));
}
function safeWriteJson(filePath: string, data: Record<string, unknown>) {
saveJsonFile(filePath, data);
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readNestedRecord(root: unknown, key: string): Record<string, unknown> | null {
return asRecord(asRecord(root)?.[key]);
}
@@ -3,12 +3,13 @@
*/
import type { Command } from "commander";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ACT_MAX_VIEWPORT_DIMENSION } from "../../browser/act-policy.js";
import { runBrowserResizeWithOutput } from "../browser-cli-resize.js";
import {
parseBrowserViewportDimension,
runBrowserResizeWithOutput,
} from "../browser-cli-resize.js";
import {
BROWSER_TAB_REFERENCE_HELP,
callBrowserRequest,
parseBrowserPositiveIntegerValue,
type BrowserParentOpts,
} from "../browser-cli-shared.js";
import { danger, defaultRuntime } from "../core-api.js";
@@ -19,21 +20,6 @@ export function registerBrowserNavigationCommands(
browser: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
) {
const parsePositiveInteger = (value: unknown, label: string): number | undefined => {
const parsed = parseBrowserPositiveIntegerValue(value);
if (parsed === undefined) {
defaultRuntime.error(danger(`Invalid ${label}: must be a positive integer`));
defaultRuntime.exit(1);
return undefined;
}
if (parsed > ACT_MAX_VIEWPORT_DIMENSION) {
defaultRuntime.error(danger(`Invalid ${label}: maximum is ${ACT_MAX_VIEWPORT_DIMENSION}`));
defaultRuntime.exit(1);
return undefined;
}
return parsed;
};
browser
.command("navigate")
.description("Navigate the current tab to a URL")
@@ -73,8 +59,8 @@ export function registerBrowserNavigationCommands(
.argument("<height>", "Viewport height")
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (width: string, height: string, opts, cmd) => {
const normalizedWidth = parsePositiveInteger(width, "width");
const normalizedHeight = parsePositiveInteger(height, "height");
const normalizedWidth = parseBrowserViewportDimension(width, "width");
const normalizedHeight = parseBrowserViewportDimension(height, "height");
if (normalizedWidth === undefined || normalizedHeight === undefined) {
return;
}
@@ -9,7 +9,6 @@ import {
resolveBrowserExtractTimeoutMs,
validateBrowserExtractSchema,
} from "../browser-extract.js";
import { runCommandWithRuntime } from "../core-api.js";
import {
completeWithPreparedSimpleCompletionModel,
extractAssistantText,
@@ -23,9 +22,11 @@ import {
BROWSER_TAB_REFERENCE_HELP,
callBrowserRequest,
parseBrowserPositiveIntegerOption,
printBrowserJsonResult,
runBrowserCliCommand as runBrowserObserve,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
import { danger, defaultRuntime, getRuntimeConfig, shortenHomePath } from "./core-api.js";
import { defaultRuntime, getRuntimeConfig, shortenHomePath } from "./core-api.js";
const browserCliExtractDeps = {
completeWithPreparedSimpleCompletionModel,
@@ -58,13 +59,6 @@ function parseSchemaOption(value: string | undefined): JsonSchemaObject | undefi
return parsed as JsonSchemaObject;
}
function runBrowserObserve(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
});
}
/** Registers Browser commands that observe current page state without direct input. */
export function registerBrowserActionObserveCommands(
browser: Command,
@@ -140,8 +134,7 @@ export function registerBrowserActionObserveCommands(
const text = result.content.find((block) => block.type === "text")?.text;
throw new Error(text || "Browser extract failed");
}
if (parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(parent, result)) {
return;
}
const text = result.content.find((block) => block.type === "text")?.text;
@@ -173,8 +166,7 @@ export function registerBrowserActionObserveCommands(
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(parent, result)) {
return;
}
defaultRuntime.writeJson(result.messages);
@@ -199,8 +191,7 @@ export function registerBrowserActionObserveCommands(
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(parent, result)) {
return;
}
defaultRuntime.log(`PDF: ${shortenHomePath(result.path)}`);
@@ -241,8 +232,7 @@ export function registerBrowserActionObserveCommands(
},
{ timeoutMs: timeoutMs ?? 20000 },
);
if (parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(parent, result)) {
return;
}
defaultRuntime.log(result.response.body);
@@ -3,13 +3,15 @@
*/
import type { Command } from "commander";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { runCommandWithRuntime } from "../core-api.js";
import {
BROWSER_TAB_REFERENCE_HELP,
callBrowserRequest,
printBrowserJsonResult as printJsonResult,
resolveBrowserProfileQuery as resolveProfileQuery,
runBrowserCliCommand,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
import { danger, defaultRuntime, shortenHomePath } from "./core-api.js";
import { defaultRuntime, shortenHomePath } from "./core-api.js";
const BROWSER_DEBUG_TIMEOUT_MS = 20000;
@@ -20,20 +22,13 @@ type DebugContext = {
profile?: string;
};
function runBrowserDebug(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
});
}
async function withDebugContext(
cmd: Command,
parentOpts: (cmd: Command) => BrowserParentOpts,
action: (context: DebugContext) => Promise<void>,
) {
const parent = parentOpts(cmd);
await runBrowserDebug(() =>
await runBrowserCliCommand(() =>
action({
parent,
profile: parent.browserProfile,
@@ -41,14 +36,6 @@ async function withDebugContext(
);
}
function printJsonResult(parent: BrowserParentOpts, result: unknown): boolean {
if (!parent.json) {
return false;
}
defaultRuntime.writeJson(result);
return true;
}
async function callDebugRequest<T>(
parent: BrowserParentOpts,
params: BrowserRequestParams,
@@ -56,10 +43,6 @@ async function callDebugRequest<T>(
return callBrowserRequest<T>(parent, params, { timeoutMs: BROWSER_DEBUG_TIMEOUT_MS });
}
function resolveProfileQuery(profile?: string) {
return profile ? { profile } : undefined;
}
function resolveDebugQuery(params: {
targetId?: unknown;
clear?: unknown;
@@ -4,11 +4,13 @@
*/
import type { Command } from "commander";
import { formatBrowserGraphicsSummary } from "../browser/chrome.graphics.js";
import { runCommandWithRuntime } from "../core-api.js";
import {
BROWSER_TAB_REFERENCE_HELP,
callBrowserRequest,
parseBrowserPositiveIntegerValue,
printBrowserJsonResult as printJsonResult,
resolveBrowserProfileQuery as resolveProfileQuery,
runBrowserCliCommand as runBrowserCommand,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
import {
@@ -37,28 +39,6 @@ type BrowserDoctorCheck = {
warning?: boolean;
};
function resolveProfileQuery(
profile?: string,
extra?: Record<string, string | number | boolean | undefined>,
) {
const query: Record<string, string | number | boolean | undefined> = {};
if (profile) {
query.profile = profile;
}
if (extra) {
Object.assign(query, extra);
}
return Object.keys(query).length > 0 ? query : undefined;
}
function printJsonResult(parent: BrowserParentOpts, payload: unknown): boolean {
if (!parent?.json) {
return false;
}
defaultRuntime.writeJson(payload);
return true;
}
function sanitizeTableCell(value: string): string {
// Strip C0/C1 control characters (Unicode Cc) so profile names cannot inject
// terminal escapes into the printed table.
@@ -124,13 +104,6 @@ async function runBrowserToggle(
defaultRuntime.log(info(`🦞 browser [${name}] running: ${status.running}${headlessLabel}`));
}
function runBrowserCommand(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
});
}
function parseTabIndex(value: string): number {
return parseBrowserPositiveIntegerValue(value) ?? Number.NaN;
}
@@ -2,9 +2,28 @@
* Shared Browser CLI resize runner used by resize and set viewport commands.
*/
import { ACT_MAX_VIEWPORT_DIMENSION } from "../browser/act-policy.js";
import { callBrowserResize, type BrowserParentOpts } from "./browser-cli-shared.js";
import {
callBrowserResize,
parseBrowserPositiveIntegerValue,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
import { danger, defaultRuntime } from "./core-api.js";
/** Parses a bounded viewport dimension for both Browser resize commands. */
export function parseBrowserViewportDimension(value: unknown, label: string): number | undefined {
const parsed = parseBrowserPositiveIntegerValue(value);
if (parsed !== undefined && parsed <= ACT_MAX_VIEWPORT_DIMENSION) {
return parsed;
}
const reason =
parsed === undefined
? "must be a positive integer"
: `maximum is ${ACT_MAX_VIEWPORT_DIMENSION}`;
defaultRuntime.error(danger(`Invalid ${label}: ${reason}`));
defaultRuntime.exit(1);
return undefined;
}
/** Validates viewport dimensions, sends resize action, and writes CLI output. */
export async function runBrowserResizeWithOutput(params: {
parent: BrowserParentOpts;
@@ -11,6 +11,7 @@ import {
BROWSER_REQUEST_GATEWAY_SCOPES,
} from "../browser-gateway-contract.js";
import { normalizeBrowserTimerDelayMs } from "../browser/timer-delay.js";
import { danger, defaultRuntime, runCommandWithRuntime } from "../core-api.js";
import { callGatewayFromCli, type GatewayRpcOpts } from "./core-api.js";
/** Parent Browser CLI options inherited by subcommands. */
@@ -30,6 +31,32 @@ type BrowserRequestParams = {
body?: unknown;
};
/** Runs a Browser CLI command with the standard runtime error handling. */
export function runBrowserCliCommand(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (error) => {
defaultRuntime.error(danger(String(error)));
defaultRuntime.exit(1);
});
}
/** Writes a Browser command result when structured output was requested. */
export function printBrowserJsonResult(parent: BrowserParentOpts, payload: unknown): boolean {
if (!parent?.json) {
return false;
}
defaultRuntime.writeJson(payload);
return true;
}
/** Combines the selected Browser profile with optional request query fields. */
export function resolveBrowserProfileQuery(
profile?: string,
extra?: BrowserRequestParams["query"],
): BrowserRequestParams["query"] {
const query = { ...(profile ? { profile } : {}), ...extra };
return Object.keys(query).length > 0 ? query : undefined;
}
function normalizeQuery(query: BrowserRequestParams["query"]): Record<string, string> | undefined {
if (!query) {
return undefined;
@@ -78,17 +105,13 @@ export async function callBrowserRequest<T>(
params: BrowserRequestParams,
extra?: { timeoutMs?: number; progress?: boolean },
): Promise<T> {
const resolvedTimeoutMs =
const resolvedTimeout =
typeof extra?.timeoutMs === "number" && Number.isFinite(extra.timeoutMs)
? normalizeBrowserTimerDelayMs(extra.timeoutMs)
: typeof opts.timeout === "string"
? normalizeBrowserTimerDelayMs(parseBrowserPositiveIntegerOption(opts.timeout, "--timeout"))
: undefined;
const resolvedTimeout =
typeof resolvedTimeoutMs === "number" && Number.isFinite(resolvedTimeoutMs)
? resolvedTimeoutMs
: undefined;
const timeout = typeof resolvedTimeout === "number" ? String(resolvedTimeout) : opts.timeout;
const timeout = resolvedTimeout === undefined ? opts.timeout : String(resolvedTimeout);
const payload = await callGatewayFromCli(
BROWSER_REQUEST_GATEWAY_METHOD,
{ ...opts, timeout },
+10 -37
View File
@@ -3,17 +3,17 @@
* HTTP context settings.
*/
import type { Command } from "commander";
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { ACT_MAX_VIEWPORT_DIMENSION } from "../browser/act-policy.js";
import { runCommandWithRuntime } from "../core-api.js";
import { runBrowserResizeWithOutput } from "./browser-cli-resize.js";
import { parseBrowserViewportDimension, runBrowserResizeWithOutput } from "./browser-cli-resize.js";
import {
BROWSER_TAB_REFERENCE_HELP,
callBrowserRequest,
parseBrowserPositiveIntegerValue,
printBrowserJsonResult,
runBrowserCliCommand as runBrowserCommand,
type BrowserParentOpts,
} from "./browser-cli-shared.js";
import { registerBrowserCookiesAndStorageCommands } from "./browser-cli-state.cookies-storage.js";
@@ -24,30 +24,12 @@ function parseOnOff(raw: string): boolean | null {
return parsed === undefined ? null : parsed;
}
function parsePositiveInteger(value: unknown, label: string): number | undefined {
const parsed = parseBrowserPositiveIntegerValue(value);
if (parsed === undefined) {
defaultRuntime.error(danger(`Invalid ${label}: must be a positive integer`));
defaultRuntime.exit(1);
return undefined;
}
if (parsed > ACT_MAX_VIEWPORT_DIMENSION) {
defaultRuntime.error(danger(`Invalid ${label}: maximum is ${ACT_MAX_VIEWPORT_DIMENSION}`));
defaultRuntime.exit(1);
return undefined;
}
return parsed;
}
function parseFiniteNumberOption(value: string | undefined, label: string): number | undefined {
if (value === undefined) {
return undefined;
}
const raw = value.trim();
const parsed = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(raw)
? Number(raw)
: Number.NaN;
if (!Number.isFinite(parsed)) {
const parsed = parseStrictFiniteNumber(value);
if (parsed === undefined) {
defaultRuntime.error(danger(`Invalid ${label}: must be a finite number`));
defaultRuntime.exit(1);
return undefined;
@@ -55,13 +37,6 @@ function parseFiniteNumberOption(value: string | undefined, label: string): numb
return parsed;
}
function runBrowserCommand(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
});
}
async function runBrowserSetRequest(params: {
parent: BrowserParentOpts;
path: string;
@@ -80,8 +55,7 @@ async function runBrowserSetRequest(params: {
},
{ timeoutMs: 20000 },
);
if (params.parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(params.parent, result)) {
return;
}
defaultRuntime.log(params.successMessage);
@@ -104,8 +78,8 @@ export function registerBrowserStateCommands(
.argument("<height>", "Viewport height")
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
.action(async (widthRaw: string, heightRaw: string, opts, cmd) => {
const width = parsePositiveInteger(widthRaw, "width");
const height = parsePositiveInteger(heightRaw, "height");
const width = parseBrowserViewportDimension(widthRaw, "width");
const height = parseBrowserViewportDimension(heightRaw, "height");
if (width === undefined || height === undefined) {
return;
}
@@ -186,8 +160,7 @@ export function registerBrowserStateCommands(
},
{ timeoutMs: 20000 },
);
if (parent?.json) {
defaultRuntime.writeJson(result);
if (printBrowserJsonResult(parent, result)) {
return;
}
defaultRuntime.log("headers set");