refactor: dedupe browser trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-07 23:10:01 +01:00
parent 7a28572caa
commit aaa88398bf
12 changed files with 65 additions and 47 deletions

View File

@@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type { ResolvedBrowserConfig } from "./config.js";
export type BrowserExecutable = {
@@ -115,7 +118,7 @@ function execText(
encoding: "utf8",
maxBuffer,
});
return String(output ?? "").trim() || null;
return normalizeOptionalString(output) ?? null;
} catch {
return null;
}
@@ -192,7 +195,7 @@ function detectDefaultChromiumExecutableMac(): BrowserExecutable | null {
if (!appPathRaw) {
return null;
}
const appPath = appPathRaw.trim().replace(/\/$/, "");
const appPath = appPathRaw.replace(/\/$/, "");
const exeName = execText("/usr/bin/defaults", [
"read",
path.join(appPath, "Contents", "Info"),
@@ -201,7 +204,7 @@ function detectDefaultChromiumExecutableMac(): BrowserExecutable | null {
if (!exeName) {
return null;
}
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
const exePath = path.join(appPath, "Contents", "MacOS", exeName);
if (!exists(exePath)) {
return null;
}
@@ -430,12 +433,12 @@ function readWindowsCommandForProgId(progId: string): string | null {
return null;
}
const match = output.match(/REG_\w+\s+(.+)$/im);
return match?.[1]?.trim() || null;
return normalizeOptionalString(match?.[1]) ?? null;
}
function expandWindowsEnvVars(value: string): string {
return value.replace(/%([^%]+)%/g, (_match, name) => {
const key = String(name ?? "").trim();
const key = normalizeOptionalString(name) ?? "";
return key ? (process.env[key] ?? `%${key}%`) : _match;
});
}

View File

@@ -2,6 +2,7 @@ import { type ChildProcess, type ChildProcessWithoutNullStreams, spawn } from "n
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { ensurePortAvailable } from "../infra/ports.js";
import { rawDataToString } from "../infra/ws.js";
@@ -196,7 +197,7 @@ export async function getChromeWebSocketUrl(
return cdpUrl;
}
const version = await fetchChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
const wsUrl = normalizeOptionalString(version?.webSocketDebuggerUrl) ?? "";
if (!wsUrl) {
return null;
}
@@ -409,7 +410,8 @@ export async function launchOpenClawChrome(
}
if (!(await isChromeReachable(profile.cdpUrl))) {
const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
const stderrOutput =
normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "";
const stderrHint = stderrOutput
? `\nChrome stderr:\n${stderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}`
: "";

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { BrowserFormField } from "./client-actions-core.js";
export const DEFAULT_FILL_FIELD_TYPE = "text";
@@ -5,11 +6,11 @@ export const DEFAULT_FILL_FIELD_TYPE = "text";
type BrowserFormFieldValue = NonNullable<BrowserFormField["value"]>;
export function normalizeBrowserFormFieldRef(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
export function normalizeBrowserFormFieldType(value: unknown): string {
const type = typeof value === "string" ? value.trim() : "";
const type = normalizeOptionalString(value) ?? "";
return type || DEFAULT_FILL_FIELD_TYPE;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { hasProxyEnvConfigured } from "../infra/net/proxy-env.js";
import {
isPrivateNetworkAllowedByPolicy,
@@ -46,7 +47,7 @@ export async function assertBrowserNavigationAllowed(
lookupFn?: LookupFn;
} & BrowserNavigationPolicyOptions,
): Promise<void> {
const rawUrl = String(opts.url ?? "").trim();
const rawUrl = normalizeOptionalString(opts.url) ?? "";
if (!rawUrl) {
throw new InvalidBrowserNavigationUrlError("url is required");
}
@@ -94,7 +95,7 @@ export async function assertBrowserNavigationResultAllowed(
lookupFn?: LookupFn;
} & BrowserNavigationPolicyOptions,
): Promise<void> {
const rawUrl = String(opts.url ?? "").trim();
const rawUrl = normalizeOptionalString(opts.url) ?? "";
if (!rawUrl) {
return;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type {
Browser,
BrowserContext,
@@ -146,7 +147,7 @@ function roleRefsKey(cdpUrl: string, targetId: string) {
}
function isBlockedTarget(cdpUrl: string, targetId?: string): boolean {
const normalizedTargetId = targetId?.trim() || "";
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
if (!normalizedTargetId) {
return false;
}
@@ -154,7 +155,7 @@ function isBlockedTarget(cdpUrl: string, targetId?: string): boolean {
}
function markTargetBlocked(cdpUrl: string, targetId?: string): void {
const normalizedTargetId = targetId?.trim() || "";
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
if (!normalizedTargetId) {
return;
}
@@ -162,7 +163,7 @@ function markTargetBlocked(cdpUrl: string, targetId?: string): void {
}
function clearBlockedTarget(cdpUrl: string, targetId?: string): void {
const normalizedTargetId = targetId?.trim() || "";
const normalizedTargetId = normalizeOptionalString(targetId) ?? "";
if (!normalizedTargetId) {
return;
}
@@ -237,7 +238,7 @@ export function rememberRoleRefsForTarget(opts: {
frameSelector?: string;
mode?: NonNullable<PageState["roleRefsMode"]>;
}): void {
const targetId = opts.targetId.trim();
const targetId = normalizeOptionalString(opts.targetId) ?? "";
if (!targetId) {
return;
}
@@ -267,12 +268,13 @@ export function storeRoleRefsForTarget(opts: {
state.roleRefs = opts.refs;
state.roleRefsFrameSelector = opts.frameSelector;
state.roleRefsMode = opts.mode;
if (!opts.targetId?.trim()) {
const targetId = normalizeOptionalString(opts.targetId);
if (!targetId) {
return;
}
rememberRoleRefsForTarget({
cdpUrl: opts.cdpUrl,
targetId: opts.targetId,
targetId,
refs: opts.refs,
frameSelector: opts.frameSelector,
mode: opts.mode,
@@ -284,7 +286,7 @@ export function restoreRoleRefsForTarget(opts: {
targetId?: string;
page: Page;
}): void {
const targetId = opts.targetId?.trim() || "";
const targetId = normalizeOptionalString(opts.targetId) ?? "";
if (!targetId) {
return;
}
@@ -523,7 +525,7 @@ async function pageTargetId(page: Page): Promise<string | null> {
const session = await page.context().newCDPSession(page);
try {
const info = (await session.send("Target.getTargetInfo")) as TargetInfoResponse;
const targetId = String(info?.targetInfo?.targetId ?? "").trim();
const targetId = normalizeOptionalString(info?.targetInfo?.targetId) ?? "";
return targetId || null;
} finally {
await session.detach().catch(() => {});
@@ -703,7 +705,7 @@ async function closeBlockedNavigationTarget(opts: {
// Quarantine the concrete page first; then persist by target id when available.
markPageRefBlocked(opts.cdpUrl, opts.page);
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
const fallbackTargetId = opts.targetId?.trim() || "";
const fallbackTargetId = normalizeOptionalString(opts.targetId) ?? "";
const targetIdToBlock = resolvedTargetId || fallbackTargetId;
if (targetIdToBlock) {
markTargetBlocked(opts.cdpUrl, targetIdToBlock);
@@ -899,8 +901,9 @@ async function tryTerminateExecutionViaCdp(opts: {
return;
}
const target = pages.find((p) => String(p.id ?? "").trim() === opts.targetId);
const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
const targetId = normalizeOptionalString(opts.targetId) ?? "";
const target = pages.find((p) => normalizeOptionalString(p.id) === targetId);
const wsUrlRaw = normalizeOptionalString(target?.webSocketDebuggerUrl) ?? "";
if (!wsUrlRaw) {
return;
}
@@ -931,8 +934,9 @@ async function tryTerminateExecutionViaCdp(opts: {
send("Target.attachToTarget", { targetId: opts.targetId, flatten: true }),
1500,
)) as { sessionId?: unknown };
if (typeof attached?.sessionId === "string" && attached.sessionId.trim()) {
sessionId = attached.sessionId;
const attachedSessionId = normalizeOptionalString(attached?.sessionId);
if (attachedSessionId) {
sessionId = attachedSessionId;
}
}
await runWithTimeout(send("Runtime.terminateExecution", undefined, sessionId), 1500);
@@ -990,7 +994,7 @@ export async function forceDisconnectPlaywrightForTarget(opts: {
// Best-effort: kill any stuck JS to unblock the target's execution context before we
// disconnect Playwright's CDP connection.
const targetId = opts.targetId?.trim() || "";
const targetId = normalizeOptionalString(opts.targetId) ?? "";
if (targetId) {
await tryTerminateExecutionViaCdp({ cdpUrl: normalized, targetId }).catch(() => {});
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { formatErrorMessage } from "../infra/errors.js";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import type { BrowserActRequest, BrowserFormField } from "./client-actions-core.js";
@@ -231,7 +232,7 @@ export async function pressKeyViaPlaywright(opts: {
delayMs?: number;
ssrfPolicy?: SsrFPolicy;
}): Promise<void> {
const key = String(opts.key ?? "").trim();
const key = normalizeOptionalString(opts.key) ?? "";
if (!key) {
throw new Error("key is required");
}
@@ -336,7 +337,7 @@ export async function evaluateViaPlaywright(opts: {
timeoutMs?: number;
signal?: AbortSignal;
}): Promise<unknown> {
const fnText = String(opts.fn ?? "").trim();
const fnText = normalizeOptionalString(opts.fn) ?? "";
if (!fnText) {
throw new Error("function is required");
}
@@ -512,13 +513,13 @@ export async function waitForViaPlaywright(opts: {
});
}
if (opts.selector) {
const selector = String(opts.selector).trim();
const selector = normalizeOptionalString(opts.selector) ?? "";
if (selector) {
await page.locator(selector).first().waitFor({ state: "visible", timeout });
}
}
if (opts.url) {
const url = String(opts.url).trim();
const url = normalizeOptionalString(opts.url) ?? "";
if (url) {
await page.waitForURL(url, { timeout });
}
@@ -527,7 +528,7 @@ export async function waitForViaPlaywright(opts: {
await page.waitForLoadState(opts.loadState, { timeout });
}
if (opts.fn) {
const fn = String(opts.fn).trim();
const fn = normalizeOptionalString(opts.fn) ?? "";
if (fn) {
await page.waitForFunction(fn, { timeout });
}
@@ -709,8 +710,8 @@ export async function setInputFilesViaPlaywright(opts: {
if (!opts.paths.length) {
throw new Error("paths are required");
}
const inputRef = typeof opts.inputRef === "string" ? opts.inputRef.trim() : "";
const element = typeof opts.element === "string" ? opts.element.trim() : "";
const inputRef = normalizeOptionalString(opts.inputRef) ?? "";
const element = normalizeOptionalString(opts.element) ?? "";
if (inputRef && element) {
throw new Error("inputRef and element are mutually exclusive");
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { formatCliCommand } from "../cli/command-format.js";
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
import { normalizeTimeoutMs } from "./pw-tools-core.shared.js";
@@ -16,7 +17,7 @@ export async function responseBodyViaPlaywright(opts: {
body: string;
truncated?: boolean;
}> {
const pattern = String(opts.url ?? "").trim();
const pattern = normalizeOptionalString(opts.url) ?? "";
if (!pattern) {
throw new Error("url is required");
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { formatErrorMessage } from "../infra/errors.js";
import { parseRoleRef } from "./pw-role-snapshot.js";
@@ -21,7 +22,7 @@ export function bumpDownloadArmId(): number {
}
export function requireRef(value: unknown): string {
const raw = typeof value === "string" ? value.trim() : "";
const raw = normalizeOptionalString(value) ?? "";
const roleRef = raw ? parseRoleRef(raw) : null;
const ref = roleRef ?? (raw.startsWith("@") ? raw.slice(1) : raw);
if (!ref) {
@@ -34,8 +35,8 @@ export function requireRefOrSelector(
ref: string | undefined,
selector: string | undefined,
): { ref?: string; selector?: string } {
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
const trimmedRef = normalizeOptionalString(ref) ?? "";
const trimmedSelector = normalizeOptionalString(selector) ?? "";
if (!trimmedRef && !trimmedSelector) {
throw new Error("ref or selector is required");
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
import { type AriaSnapshotNode, formatAriaSnapshot, type RawAXNode } from "./cdp.js";
import { assertBrowserNavigationAllowed, withBrowserNavigationPolicy } from "./navigation-guard.js";
@@ -140,7 +141,7 @@ export async function snapshotRoleViaPlaywright(opts: {
}
if (opts.refsMode === "aria") {
if (opts.selector?.trim() || opts.frameSelector?.trim()) {
if (normalizeOptionalString(opts.selector) || normalizeOptionalString(opts.frameSelector)) {
throw new Error("refs=aria does not support selector/frame snapshots yet.");
}
const maybe = page as unknown as WithSnapshotForAI;
@@ -166,8 +167,8 @@ export async function snapshotRoleViaPlaywright(opts: {
};
}
const frameSelector = opts.frameSelector?.trim() || "";
const selector = opts.selector?.trim() || "";
const frameSelector = normalizeOptionalString(opts.frameSelector) ?? "";
const selector = normalizeOptionalString(opts.selector) ?? "";
const locator = frameSelector
? selector
? page.frameLocator(frameSelector).locator(selector)
@@ -213,7 +214,7 @@ export async function navigateViaPlaywright(opts: {
);
};
const url = String(opts.url ?? "").trim();
const url = normalizeOptionalString(opts.url) ?? "";
if (!url) {
throw new Error("url is required");
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { devices as playwrightDevices } from "playwright-core";
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
import { withPageScopedCdpClient } from "./pw-session.page-cdp.js";
@@ -69,7 +70,7 @@ export async function setGeolocationViaPlaywright(opts: {
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : undefined,
});
const origin =
opts.origin?.trim() ||
normalizeOptionalString(opts.origin) ||
(() => {
try {
return new URL(page.url()).origin;
@@ -99,7 +100,7 @@ export async function setLocaleViaPlaywright(opts: {
}): Promise<void> {
const page = await getPageForTargetId(opts);
ensurePageState(page);
const locale = String(opts.locale ?? "").trim();
const locale = normalizeOptionalString(opts.locale) ?? "";
if (!locale) {
throw new Error("locale is required");
}
@@ -127,7 +128,7 @@ export async function setTimezoneViaPlaywright(opts: {
}): Promise<void> {
const page = await getPageForTargetId(opts);
ensurePageState(page);
const timezoneId = String(opts.timezoneId ?? "").trim();
const timezoneId = normalizeOptionalString(opts.timezoneId) ?? "";
if (!timezoneId) {
throw new Error("timezoneId is required");
}
@@ -159,7 +160,7 @@ export async function setDeviceViaPlaywright(opts: {
}): Promise<void> {
const page = await getPageForTargetId(opts);
ensurePageState(page);
const name = String(opts.name ?? "").trim();
const name = normalizeOptionalString(opts.name) ?? "";
if (!name) {
throw new Error("device name is required");
}

View File

@@ -1,7 +1,8 @@
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
export function sanitizeUntrustedFileName(fileName: string, fallbackName: string): string {
const trimmed = String(fileName ?? "").trim();
const trimmed = normalizeOptionalString(fileName) ?? "";
if (!trimmed) {
return fallbackName;
}

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { fetchOk, normalizeCdpHttpBaseForJsonEndpoints } from "./cdp.helpers.js";
import { appendCdpPath } from "./cdp.js";
import { closeChromeMcpTab, focusChromeMcpTab } from "./chrome-mcp.js";
@@ -56,7 +57,7 @@ export function createProfileSelectionOps({
};
const pickDefault = () => {
const last = profileState.lastTargetId?.trim() || "";
const last = normalizeOptionalString(profileState.lastTargetId) ?? "";
const lastResolved = last ? resolveById(last) : null;
if (lastResolved && lastResolved !== "AMBIGUOUS") {
return lastResolved;