mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 06:20:22 +00:00
194 lines
5.2 KiB
TypeScript
194 lines
5.2 KiB
TypeScript
import type { SnapshotAriaNode } from "./client.js";
|
|
import {
|
|
getRoleSnapshotStats,
|
|
type RoleRefMap,
|
|
type RoleSnapshotOptions,
|
|
} from "./pw-role-snapshot.js";
|
|
import { CONTENT_ROLES, INTERACTIVE_ROLES, STRUCTURAL_ROLES } from "./snapshot-roles.js";
|
|
|
|
export type ChromeMcpSnapshotNode = {
|
|
id?: string;
|
|
role?: string;
|
|
name?: string;
|
|
value?: string | number | boolean;
|
|
description?: string;
|
|
children?: ChromeMcpSnapshotNode[];
|
|
};
|
|
|
|
function normalizeRole(node: ChromeMcpSnapshotNode): string {
|
|
const role = typeof node.role === "string" ? node.role.trim().toLowerCase() : "";
|
|
return role || "generic";
|
|
}
|
|
|
|
function normalizeString(value: unknown): string | undefined {
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
return trimmed || undefined;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function escapeQuoted(value: string): string {
|
|
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
}
|
|
|
|
function shouldIncludeNode(params: {
|
|
role: string;
|
|
name?: string;
|
|
options?: RoleSnapshotOptions;
|
|
}): boolean {
|
|
if (params.options?.interactive && !INTERACTIVE_ROLES.has(params.role)) {
|
|
return false;
|
|
}
|
|
if (params.options?.compact && STRUCTURAL_ROLES.has(params.role) && !params.name) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function shouldCreateRef(role: string, name?: string): boolean {
|
|
return INTERACTIVE_ROLES.has(role) || (CONTENT_ROLES.has(role) && Boolean(name));
|
|
}
|
|
|
|
type DuplicateTracker = {
|
|
counts: Map<string, number>;
|
|
keysByRef: Map<string, string>;
|
|
duplicates: Set<string>;
|
|
};
|
|
|
|
function createDuplicateTracker(): DuplicateTracker {
|
|
return {
|
|
counts: new Map(),
|
|
keysByRef: new Map(),
|
|
duplicates: new Set(),
|
|
};
|
|
}
|
|
|
|
function registerRef(
|
|
tracker: DuplicateTracker,
|
|
ref: string,
|
|
role: string,
|
|
name?: string,
|
|
): number | undefined {
|
|
const key = `${role}:${name ?? ""}`;
|
|
const count = tracker.counts.get(key) ?? 0;
|
|
tracker.counts.set(key, count + 1);
|
|
tracker.keysByRef.set(ref, key);
|
|
if (count > 0) {
|
|
tracker.duplicates.add(key);
|
|
return count;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function flattenChromeMcpSnapshotToAriaNodes(
|
|
root: ChromeMcpSnapshotNode,
|
|
limit = 500,
|
|
): SnapshotAriaNode[] {
|
|
const boundedLimit = Math.max(1, Math.min(2000, Math.floor(limit)));
|
|
const out: SnapshotAriaNode[] = [];
|
|
|
|
const visit = (node: ChromeMcpSnapshotNode, depth: number) => {
|
|
if (out.length >= boundedLimit) {
|
|
return;
|
|
}
|
|
const ref = normalizeString(node.id);
|
|
if (ref) {
|
|
out.push({
|
|
ref,
|
|
role: normalizeRole(node),
|
|
name: normalizeString(node.name) ?? "",
|
|
value: normalizeString(node.value),
|
|
description: normalizeString(node.description),
|
|
depth,
|
|
});
|
|
}
|
|
for (const child of node.children ?? []) {
|
|
visit(child, depth + 1);
|
|
if (out.length >= boundedLimit) {
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
visit(root, 0);
|
|
return out;
|
|
}
|
|
|
|
export function buildAiSnapshotFromChromeMcpSnapshot(params: {
|
|
root: ChromeMcpSnapshotNode;
|
|
options?: RoleSnapshotOptions;
|
|
maxChars?: number;
|
|
}): {
|
|
snapshot: string;
|
|
truncated?: boolean;
|
|
refs: RoleRefMap;
|
|
stats: { lines: number; chars: number; refs: number; interactive: number };
|
|
} {
|
|
const refs: RoleRefMap = {};
|
|
const tracker = createDuplicateTracker();
|
|
const lines: string[] = [];
|
|
|
|
const visit = (node: ChromeMcpSnapshotNode, depth: number) => {
|
|
const role = normalizeRole(node);
|
|
const name = normalizeString(node.name);
|
|
const value = normalizeString(node.value);
|
|
const description = normalizeString(node.description);
|
|
const maxDepth = params.options?.maxDepth;
|
|
if (maxDepth !== undefined && depth > maxDepth) {
|
|
return;
|
|
}
|
|
|
|
const includeNode = shouldIncludeNode({ role, name, options: params.options });
|
|
if (includeNode) {
|
|
let line = `${" ".repeat(depth)}- ${role}`;
|
|
if (name) {
|
|
line += ` "${escapeQuoted(name)}"`;
|
|
}
|
|
const ref = normalizeString(node.id);
|
|
if (ref && shouldCreateRef(role, name)) {
|
|
const nth = registerRef(tracker, ref, role, name);
|
|
refs[ref] = nth === undefined ? { role, name } : { role, name, nth };
|
|
line += ` [ref=${ref}]`;
|
|
}
|
|
if (value) {
|
|
line += ` value="${escapeQuoted(value)}"`;
|
|
}
|
|
if (description) {
|
|
line += ` description="${escapeQuoted(description)}"`;
|
|
}
|
|
lines.push(line);
|
|
}
|
|
|
|
for (const child of node.children ?? []) {
|
|
visit(child, depth + 1);
|
|
}
|
|
};
|
|
|
|
visit(params.root, 0);
|
|
|
|
for (const [ref, data] of Object.entries(refs)) {
|
|
const key = tracker.keysByRef.get(ref);
|
|
if (key && !tracker.duplicates.has(key)) {
|
|
delete data.nth;
|
|
}
|
|
}
|
|
|
|
let snapshot = lines.join("\n");
|
|
let truncated = false;
|
|
const maxChars =
|
|
typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0
|
|
? Math.floor(params.maxChars)
|
|
: undefined;
|
|
if (maxChars && snapshot.length > maxChars) {
|
|
snapshot = `${snapshot.slice(0, maxChars)}\n\n[...TRUNCATED - page too large]`;
|
|
truncated = true;
|
|
}
|
|
|
|
const stats = getRoleSnapshotStats(snapshot, refs);
|
|
return truncated ? { snapshot, truncated, refs, stats } : { snapshot, refs, stats };
|
|
}
|