UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

376 lines (375 loc) 12.4 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { _ as INTERACTIVE_ROLES, g as CONTENT_ROLES, v as STRUCTURAL_ROLES } from "./chrome-BWowRFBm.js"; import { Script } from "node:vm"; //#region extensions/browser/src/browser/evaluate-source.ts const FUNCTION_SOURCE_PATTERN = /^(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/; const EXPRESSION_RESULT_NAME = "__openclawEvaluateExpressionResult"; function canParseAsExpression(source) { try { new Script(`"use strict";\n(${source});`); return true; } catch { return false; } } function normalizeBrowserEvaluateFunctionSource(source, params = {}) { const trimmed = source.trim(); if (!trimmed) return ""; if (FUNCTION_SOURCE_PATTERN.test(trimmed) && canParseAsExpression(trimmed)) return trimmed; const argumentName = params.argumentName; const args = argumentName ? `(${argumentName})` : "()"; if (canParseAsExpression(trimmed)) { const invokeArgs = argumentName ? argumentName : ""; return [ `${args} => {`, `const ${EXPRESSION_RESULT_NAME} = (${trimmed});`, `return typeof ${EXPRESSION_RESULT_NAME} === "function" ? ${EXPRESSION_RESULT_NAME}(${invokeArgs}) : ${EXPRESSION_RESULT_NAME};`, "}" ].join("\n"); } return `async ${args} => {\n${trimmed}\n}`; } //#endregion //#region extensions/browser/src/browser/url-pattern.ts /** * URL pattern matching for Browser response and wait tools. */ function wildcardPatternToRegExp(pattern) { let source = "^"; for (let index = 0; index < pattern.length; index += 1) { const char = pattern[index] ?? ""; if (char === "*") { if (pattern[index + 1] === "*") { source += ".*"; index += 1; } else source += "[^/]*"; continue; } source += char.replace(/[\\^$+?.()|[\]{}]/gu, "\\$&"); } source += "$"; return new RegExp(source, "u"); } /** Matches exact, wildcard, or substring URL patterns against a browser URL. */ function matchBrowserUrlPattern(pattern, url) { const trimmedPattern = pattern.trim(); if (!trimmedPattern) return false; if (trimmedPattern === url) return true; if (trimmedPattern === "*") return true; if (trimmedPattern.includes("*")) return wildcardPatternToRegExp(trimmedPattern).test(url); return url.includes(trimmedPattern); } //#endregion //#region extensions/browser/src/browser/form-fields.ts /** * Browser form field normalization. * * Converts model/client fill field payloads into the compact field shape used * by Playwright and Chrome MCP fill actions. */ /** Default field type for fill actions when no type is provided. */ const DEFAULT_FILL_FIELD_TYPE = "text"; function normalizeBrowserFormFieldRef(value) { return normalizeOptionalString(value) ?? ""; } function normalizeBrowserFormFieldType(value) { return (normalizeOptionalString(value) ?? "") || "text"; } /** Normalize a form field value to the types accepted by fill actions. */ function normalizeBrowserFormFieldValue(value) { return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : void 0; } /** Normalize one form field descriptor from untrusted route/tool input. */ function normalizeBrowserFormField(record) { const ref = normalizeBrowserFormFieldRef(record.ref); if (!ref) return null; const type = normalizeBrowserFormFieldType(record.type); const value = normalizeBrowserFormFieldValue(record.value); return value === void 0 ? { ref, type } : { ref, type, value }; } //#endregion //#region extensions/browser/src/browser/pw-role-snapshot.ts /** * Playwright role snapshot helpers. * * Converts ARIA or AI snapshots into compact role/name text with stable refs * and duplicate disambiguation for agent actions. */ /** Compute snapshot line/char/ref statistics. */ function getRoleSnapshotStats(snapshot, refs) { const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length; return { lines: snapshot.split("\n").length, chars: snapshot.length, refs: Object.keys(refs).length, interactive }; } function getIndentLevel(line) { const match = line.match(/^(\s*)/); return match ? Math.floor(match[1].length / 2) : 0; } function matchInteractiveSnapshotLine(line, options) { const depth = getIndentLevel(line); if (options.maxDepth !== void 0 && depth > options.maxDepth) return null; const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/); if (!match) return null; const roleRaw = match[2]; const name = match[3]; const suffix = match[4]; if (roleRaw.startsWith("/")) return null; return { roleRaw, role: normalizeLowercaseStringOrEmpty(roleRaw), ...name ? { name } : {}, suffix }; } function createRoleNameTracker() { const counts = /* @__PURE__ */ new Map(); const refsByKey = /* @__PURE__ */ new Map(); return { counts, refsByKey, getKey(role, name) { return `${role}:${name ?? ""}`; }, getNextIndex(role, name) { const key = this.getKey(role, name); const current = counts.get(key) ?? 0; counts.set(key, current + 1); return current; }, trackRef(role, name, ref) { const key = this.getKey(role, name); const list = refsByKey.get(key) ?? []; list.push(ref); refsByKey.set(key, list); }, getDuplicateKeys() { const out = /* @__PURE__ */ new Set(); for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key); return out; } }; } function removeNthFromNonDuplicates(refs, tracker) { const duplicates = tracker.getDuplicateKeys(); for (const [ref, data] of Object.entries(refs)) { const key = tracker.getKey(data.role, data.name); if (!duplicates.has(key)) delete refs[ref]?.nth; } } function compactTree(tree) { const lines = tree.split("\n"); const result = []; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; if (line.includes("[ref=")) { result.push(line); continue; } if (line.includes(":") && !line.trimEnd().endsWith(":")) { result.push(line); continue; } const currentIndent = getIndentLevel(line); let hasRelevantChildren = false; for (let j = i + 1; j < lines.length; j += 1) { if (getIndentLevel(lines[j]) <= currentIndent) break; if (lines[j]?.includes("[ref=")) { hasRelevantChildren = true; break; } } if (hasRelevantChildren) result.push(line); } return result.join("\n"); } function processLine(line, refs, options, tracker, nextRef) { const depth = getIndentLevel(line); if (options.maxDepth !== void 0 && depth > options.maxDepth) return null; const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/); if (!match) return options.interactive ? null : line; const [, prefix, roleRaw, name, suffix] = match; if (roleRaw.startsWith("/")) return options.interactive ? null : line; const role = normalizeLowercaseStringOrEmpty(roleRaw); const isInteractive = INTERACTIVE_ROLES.has(role); const isContent = CONTENT_ROLES.has(role); const isStructural = STRUCTURAL_ROLES.has(role); if (options.interactive && !isInteractive) return null; if (options.compact && isStructural && !name) return null; if (!(isInteractive || isContent && name)) return line; const ref = nextRef(); const nth = tracker.getNextIndex(role, name); tracker.trackRef(role, name, ref); refs[ref] = { role, name, nth }; let enhanced = `${prefix}${roleRaw}`; if (name) enhanced += ` "${name}"`; enhanced += ` [ref=${ref}]`; if (nth > 0) enhanced += ` [nth=${nth}]`; if (suffix) enhanced += suffix; return enhanced; } function buildInteractiveSnapshotLines(params) { const out = []; for (const line of params.lines) { const parsed = matchInteractiveSnapshotLine(line, params.options); if (!parsed) continue; if (!INTERACTIVE_ROLES.has(parsed.role)) continue; const resolved = params.resolveRef(parsed); if (!resolved?.ref) continue; params.recordRef(parsed, resolved.ref, resolved.nth); let enhanced = `- ${parsed.roleRaw}`; if (parsed.name) enhanced += ` "${parsed.name}"`; enhanced += ` [ref=${resolved.ref}]`; if ((resolved.nth ?? 0) > 0) enhanced += ` [nth=${resolved.nth}]`; if (params.includeSuffix(parsed.suffix)) enhanced += parsed.suffix; out.push(enhanced); } return out; } /** Normalize a role snapshot ref accepted by browser actions. */ function parseRoleRef(raw) { const trimmed = raw.trim(); if (!trimmed) return null; const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed; if (/^e\d+$/i.test(normalized)) return normalized; if (/^\d{1,9}$/.test(normalized)) return normalized; return null; } /** Build a role snapshot and refs from Playwright ARIA snapshot text. */ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) { const lines = ariaSnapshot.split("\n"); const refs = {}; const tracker = createRoleNameTracker(); let counter = 0; const nextRef = () => { counter += 1; return `e${counter}`; }; if (options.interactive) { const result = buildInteractiveSnapshotLines({ lines, options, resolveRef: ({ role, name }) => { const ref = nextRef(); const nth = tracker.getNextIndex(role, name); tracker.trackRef(role, name, ref); return { ref, nth }; }, recordRef: ({ role, name }, ref, nth) => { refs[ref] = { role, name, nth }; }, includeSuffix: (suffix) => suffix.includes("[") }); removeNthFromNonDuplicates(refs, tracker); return { snapshot: result.join("\n") || "(no interactive elements)", refs }; } const result = []; for (const line of lines) { const processed = processLine(line, refs, options, tracker, nextRef); if (processed !== null) result.push(processed); } removeNthFromNonDuplicates(refs, tracker); const tree = result.join("\n") || "(empty)"; return { snapshot: options.compact ? compactTree(tree) : tree, refs }; } function parseAiSnapshotRef(suffix) { const eMatch = suffix.match(/\[ref=(e\d+)\]/i); if (eMatch) return eMatch[1]; const numMatch = suffix.match(/\[ref=(\d{1,9})\]/); return numMatch ? numMatch[1] : null; } /** * Build a role snapshot from Playwright's AI snapshot output while preserving Playwright's own * aria-ref ids (e.g. ref=e13). This makes the refs self-resolving across calls. */ /** Build a role snapshot and refs from Playwright AI snapshot text. */ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) { const lines = aiSnapshot.split("\n"); const refs = {}; if (options.interactive) return { snapshot: buildInteractiveSnapshotLines({ lines, options, resolveRef: ({ suffix }) => { const ref = parseAiSnapshotRef(suffix); return ref ? { ref } : null; }, recordRef: ({ role, name }, ref) => { refs[ref] = { role, ...name ? { name } : {} }; }, includeSuffix: () => true }).join("\n") || "(no interactive elements)", refs }; const out = []; for (const line of lines) { const depth = getIndentLevel(line); if (options.maxDepth !== void 0 && depth > options.maxDepth) continue; const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/); if (!match) { out.push(line); continue; } const roleRaw = match[2]; const name = match[3]; const suffix = match[4]; if (roleRaw.startsWith("/")) { out.push(line); continue; } const role = normalizeLowercaseStringOrEmpty(roleRaw); const isStructural = STRUCTURAL_ROLES.has(role); if (options.compact && isStructural && !name) continue; const ref = parseAiSnapshotRef(suffix); if (ref) refs[ref] = { role, ...name ? { name } : {} }; out.push(line); } const tree = out.join("\n") || "(empty)"; return { snapshot: options.compact ? compactTree(tree) : tree, refs }; } //#endregion //#region extensions/browser/src/browser/snapshot-urls.ts /** Appends a compact numbered link list to a snapshot string. */ function appendSnapshotUrls(snapshot, urls) { if (urls.length === 0) return snapshot; return `${snapshot}\n\nLinks:\n${urls.map((entry, index) => `${index + 1}. ${entry.text} -> ${entry.url}`).join("\n")}`; } //#endregion export { parseRoleRef as a, normalizeBrowserFormFieldValue as c, getRoleSnapshotStats as i, matchBrowserUrlPattern as l, buildRoleSnapshotFromAiSnapshot as n, DEFAULT_FILL_FIELD_TYPE as o, buildRoleSnapshotFromAriaSnapshot as r, normalizeBrowserFormField as s, appendSnapshotUrls as t, normalizeBrowserEvaluateFunctionSource as u };