diff --git a/extensions/browser/src/browser-proxy-upload.ts b/extensions/browser/src/browser-proxy-upload.ts index 590ae3499c4f..7fa7c1221c4d 100644 --- a/extensions/browser/src/browser-proxy-upload.ts +++ b/extensions/browser/src/browser-proxy-upload.ts @@ -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 | null { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : null; -} - function readUploadPaths(body: Record): string[] | null { if (!Array.isArray(body.paths) || body.paths.length === 0) { return null; diff --git a/extensions/browser/src/browser/chrome.graphics.ts b/extensions/browser/src/browser/chrome.graphics.ts index 680665fa4298..5f24e38c8753 100644 --- a/extensions/browser/src/browser/chrome.graphics.ts +++ b/extensions/browser/src/browser/chrome.graphics.ts @@ -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; - 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[] { + 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( diff --git a/extensions/browser/src/browser/chrome.profile-decoration.ts b/extensions/browser/src/browser/chrome.profile-decoration.ts index c1da5846feb1..47a78f7583f1 100644 --- a/extensions/browser/src/browser/chrome.profile-decoration.ts +++ b/extensions/browser/src/browser/chrome.profile-decoration.ts @@ -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 | null { - const parsed = loadJsonFile(filePath); - return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) - ? (parsed as Record) - : null; + return asRecord(loadJsonFile(filePath)); } function safeWriteJson(filePath: string, data: Record) { saveJsonFile(filePath, data); } -function asRecord(value: unknown): Record | null { - return typeof value === "object" && value !== null && !Array.isArray(value) - ? (value as Record) - : null; -} - function readNestedRecord(root: unknown, key: string): Record | null { return asRecord(asRecord(root)?.[key]); } diff --git a/extensions/browser/src/cli/browser-cli-actions-input/register.navigation.ts b/extensions/browser/src/cli/browser-cli-actions-input/register.navigation.ts index 1af2360107ef..0fe65f91b842 100644 --- a/extensions/browser/src/cli/browser-cli-actions-input/register.navigation.ts +++ b/extensions/browser/src/cli/browser-cli-actions-input/register.navigation.ts @@ -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("", "Viewport height") .option("--target-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; } diff --git a/extensions/browser/src/cli/browser-cli-actions-observe.ts b/extensions/browser/src/cli/browser-cli-actions-observe.ts index 654786ba06a0..5a7ab1890690 100644 --- a/extensions/browser/src/cli/browser-cli-actions-observe.ts +++ b/extensions/browser/src/cli/browser-cli-actions-observe.ts @@ -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) { - 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); diff --git a/extensions/browser/src/cli/browser-cli-debug.ts b/extensions/browser/src/cli/browser-cli-debug.ts index f824d3841c89..3d5ad0228bed 100644 --- a/extensions/browser/src/cli/browser-cli-debug.ts +++ b/extensions/browser/src/cli/browser-cli-debug.ts @@ -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) { - 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, ) { 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( parent: BrowserParentOpts, params: BrowserRequestParams, @@ -56,10 +43,6 @@ async function callDebugRequest( return callBrowserRequest(parent, params, { timeoutMs: BROWSER_DEBUG_TIMEOUT_MS }); } -function resolveProfileQuery(profile?: string) { - return profile ? { profile } : undefined; -} - function resolveDebugQuery(params: { targetId?: unknown; clear?: unknown; diff --git a/extensions/browser/src/cli/browser-cli-manage.ts b/extensions/browser/src/cli/browser-cli-manage.ts index a383349cf546..99fcc1db4d58 100644 --- a/extensions/browser/src/cli/browser-cli-manage.ts +++ b/extensions/browser/src/cli/browser-cli-manage.ts @@ -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, -) { - const query: Record = {}; - 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) { - return runCommandWithRuntime(defaultRuntime, action, (err) => { - defaultRuntime.error(danger(String(err))); - defaultRuntime.exit(1); - }); -} - function parseTabIndex(value: string): number { return parseBrowserPositiveIntegerValue(value) ?? Number.NaN; } diff --git a/extensions/browser/src/cli/browser-cli-resize.ts b/extensions/browser/src/cli/browser-cli-resize.ts index 20cd6f160de8..2fdc0531dc80 100644 --- a/extensions/browser/src/cli/browser-cli-resize.ts +++ b/extensions/browser/src/cli/browser-cli-resize.ts @@ -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; diff --git a/extensions/browser/src/cli/browser-cli-shared.ts b/extensions/browser/src/cli/browser-cli-shared.ts index a819a868539b..9b9b66412e49 100644 --- a/extensions/browser/src/cli/browser-cli-shared.ts +++ b/extensions/browser/src/cli/browser-cli-shared.ts @@ -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) { + 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 | undefined { if (!query) { return undefined; @@ -78,17 +105,13 @@ export async function callBrowserRequest( params: BrowserRequestParams, extra?: { timeoutMs?: number; progress?: boolean }, ): Promise { - 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 }, diff --git a/extensions/browser/src/cli/browser-cli-state.ts b/extensions/browser/src/cli/browser-cli-state.ts index 301293eb45b6..6f670183523f 100644 --- a/extensions/browser/src/cli/browser-cli-state.ts +++ b/extensions/browser/src/cli/browser-cli-state.ts @@ -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) { - 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("", "Viewport height") .option("--target-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");