openclaw/patches/chrome-devtools-mcp@1.10.1.patch

562 lines
26 KiB
Diff

diff --git a/build/src/McpPage.js b/build/src/McpPage.js
index 3be93a33ad4afb206223290c96fd8749cd49a131..f6887160eb88fb0d19792d20ca6466496ca48b9c 100644
--- a/build/src/McpPage.js
+++ b/build/src/McpPage.js
@@ -1,3 +1,4 @@
+// Modified by OpenClaw: temporary snapshot-identity backport; see OPENCLAW_PATCH_NOTICE.md.
/**
* @license
* Copyright 2025 Google LLC
@@ -105,7 +106,7 @@ import { DevToolsCommentBridge } from './devtools/DevToolsCommentBridge.js';
import { createTargetUniverse, } from './devtools/DevtoolsUtils.js';
import { ConsoleCollector, NetworkCollector, } from './collectors/PageCollector.js';
import { TextSnapshot } from './TextSnapshot.js';
-import { PredefinedNetworkConditions, DevTools, } from './third_party/index.js';
+import { PredefinedNetworkConditions, DevTools, CdpFrame, } from './third_party/index.js';
const DEFAULT_TIMEOUT = 5_000;
const NAVIGATION_TIMEOUT = 10_000;
import { logger } from './utils/logger.js';
@@ -549,7 +550,7 @@ export class McpPage {
logger?.(`No backendNodeId for stashed DOM element with index ${index}`);
return `stashed-${index}`;
}
- const cdpElementId = this.textSnapshot?.resolveCdpElementId(backendNodeId);
+ const cdpElementId = this.textSnapshot?.resolveCdpElementId(backendNodeId, elementHandle.frame);
if (!cdpElementId) {
logger?.(`Could not get cdpElementId for backend node ${backendNodeId}`);
return `stashed-${index}`;
@@ -639,44 +640,62 @@ export class McpPage {
}
}
async getMatchedStylesForUid(uid) {
- if (!this.textSnapshot) {
- throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use take_snapshot to capture one.`);
- }
- const node = this.textSnapshot.idToNode.get(uid);
- if (!node) {
- throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
- }
- const backendNodeId = node.backendNodeId;
- if (!isBackendNodeId(backendNodeId)) {
- throw new Error(`Failed to resolve backend node ID for element with uid "${uid}".`);
- }
- if (!this.#devtoolsUniverse) {
- throw new Error(`DevTools universe is not available for page ${this.id ?? '?'}.`);
- }
- const targetManager = this.#devtoolsUniverse.universe.context.get(DevTools.TargetManager);
- let domNode;
- let cssModel = null;
- for (const dom of targetManager.models(DevTools.DOMModel.DOMModel)) {
- const nodeMap = await dom.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
- const frontendNode = nodeMap?.get(backendNodeId);
- if (frontendNode) {
- domNode = frontendNode;
- cssModel = dom.target().model(DevTools.CSSModel.CSSModel);
- break;
+ const env_3 = { stack: [], error: void 0, hasError: false };
+ try {
+ if (!this.textSnapshot) {
+ throw new Error(`No snapshot found for page ${this.id ?? '?'}. Use take_snapshot to capture one.`);
}
+ const node = this.textSnapshot.idToNode.get(uid);
+ if (!node) {
+ throw new Error(`Element uid "${uid}" not found on page ${this.id}.`);
+ }
+ const snapshotBackendNodeId = node.backendNodeId;
+ if (!isBackendNodeId(snapshotBackendNodeId)) {
+ throw new Error(`Failed to resolve backend node ID for element with uid "${uid}".`);
+ }
+ const handle = __addDisposableResource(env_3, await this.#resolveElementHandle(node, uid), false);
+ const liveBackendNodeId = await handle.backendNodeId();
+ if (liveBackendNodeId !== snapshotBackendNodeId) {
+ throw new Error(`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with take_snapshot.`);
+ }
+ const backendNodeId = snapshotBackendNodeId;
+ if (!this.#devtoolsUniverse) {
+ throw new Error(`DevTools universe is not available for page ${this.id ?? '?'}.`);
+ }
+ const targetManager = this.#devtoolsUniverse.universe.context.get(DevTools.TargetManager);
+ const frame = handle.frame;
+ const mainFrame = this.pptrPage.mainFrame();
+ const frameTarget = frame instanceof CdpFrame ? targetManager.targetById(frame._id) : null;
+ const target = frameTarget ??
+ (frame instanceof CdpFrame &&
+ mainFrame instanceof CdpFrame &&
+ frame.client === mainFrame.client
+ ? targetManager.primaryPageTarget()
+ : null);
+ const dom = target?.model(DevTools.DOMModel.DOMModel);
+ const nodeMap = await dom?.pushNodesByBackendIdsToFrontend(new Set([backendNodeId]));
+ const domNode = nodeMap?.get(backendNodeId);
+ const cssModel = dom?.target().model(DevTools.CSSModel.CSSModel);
+ if (!domNode || !cssModel) {
+ throw new Error(`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with take_snapshot.`);
+ }
+ const targetElement = domNode.enclosingElementOrSelf();
+ if (!targetElement) {
+ throw new Error(`Element with uid "${uid}" is not an element node and has no parent element.`);
+ }
+ const matchedStyles = await cssModel.getMatchedStyles(targetElement.id);
+ if (!matchedStyles) {
+ throw new Error(`Could not retrieve matched styles for element with uid "${uid}".`);
+ }
+ return matchedStyles;
}
- if (!domNode || !cssModel) {
- throw new Error(`Element with uid "${uid}" was detached or no longer exists on the page. Please take a new snapshot with take_snapshot.`);
- }
- const targetElement = domNode.enclosingElementOrSelf();
- if (!targetElement) {
- throw new Error(`Element with uid "${uid}" is not an element node and has no parent element.`);
+ catch (e_3) {
+ env_3.error = e_3;
+ env_3.hasError = true;
}
- const matchedStyles = await cssModel.getMatchedStyles(targetElement.id);
- if (!matchedStyles) {
- throw new Error(`Could not retrieve matched styles for element with uid "${uid}".`);
+ finally {
+ __disposeResources(env_3);
}
- return matchedStyles;
}
async getDevToolsData() {
try {
diff --git a/build/src/OPENCLAW_PATCH_NOTICE.md b/build/src/OPENCLAW_PATCH_NOTICE.md
new file mode 100644
index 0000000000000000000000000000000000000000..abdbc42e09793bf885493afc41be15a28d0fe432
--- /dev/null
+++ b/build/src/OPENCLAW_PATCH_NOTICE.md
@@ -0,0 +1,27 @@
+# Temporary OpenClaw snapshot identity patch
+
+This package remains `chrome-devtools-mcp@1.10.1`. OpenClaw modifies two
+published runtime files to backport the snapshot identity repair proposed in
+https://github.com/ChromeDevTools/chrome-devtools-mcp/pull/2788.
+
+The reviewed upstream source is commit
+`06c8d4bc44f68bde8bd3fcd97dcc47375df81fe9`, forward-ported onto the
+`chrome-devtools-mcp-v1.10.1` source tag at
+`e52c6b59b476c5e04d8dd9fd4bd017ba3b3d65df`.
+
+- `TextSnapshot.js` carries the reviewed snapshot-owner implementation.
+- `McpPage.js` supplies the known element frame for DOM-result reverse lookup
+ and resolves CSS-style UIDs through the same guarded snapshot handle. CSS
+ lookup stays within the captured renderer and fails closed when the embedded
+ DevTools target cannot represent a cross-renderer frame.
+
+Version 1.10.1 already exports its bundled `CdpFrame` implementation, so this
+patch does not modify third-party bundle code.
+
+The original package is the published npm 1.10.1 tarball with integrity
+`sha512-Klw6HWDqHC/XS1JwZldd2r49aUhbUJN9m9Mvcx4SEueIPXtzuQX+QelxAViobv8YUkDZ7HWDrmViR6LeYK0wAw==`.
+The original `LICENSE` and `build/src/third_party/THIRD_PARTY_NOTICES` remain
+unchanged.
+
+Remove this patch and notice after adopting a published upstream release that
+contains the repair and passes the same snapshot/stdio regression proof.
diff --git a/build/src/TextSnapshot.js b/build/src/TextSnapshot.js
index fe2bae7bc287e43d8a7f41f5886496a4f82c0cd7..5b2671a4b416633710886c9089762e0b9018ff66 100644
--- a/build/src/TextSnapshot.js
+++ b/build/src/TextSnapshot.js
@@ -1,3 +1,4 @@
+// Modified by OpenClaw: temporary snapshot-identity backport; see OPENCLAW_PATCH_NOTICE.md.
/**
* @license
* Copyright 2025 Google LLC
@@ -55,7 +56,17 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
+import { CdpFrame } from './third_party/index.js';
import { logger } from './utils/logger.js';
+function stableNodeKey(node, sessionId) {
+ if (!sessionId ||
+ !node.loaderId ||
+ !node.backendNodeId ||
+ node.backendNodeId < 0) {
+ return;
+ }
+ return `${sessionId}_${node.loaderId}_${node.backendNodeId}`;
+}
export class TextSnapshot {
static nextSnapshotId = 1;
static resetCounter() {
@@ -67,6 +78,7 @@ export class TextSnapshot {
selectedElementUid;
hasSelectedElement;
verbose;
+ #nodesByFrame;
constructor(data) {
this.root = data.root;
this.idToNode = data.idToNode;
@@ -74,9 +86,31 @@ export class TextSnapshot {
this.selectedElementUid = data.selectedElementUid;
this.hasSelectedElement = data.hasSelectedElement;
this.verbose = data.verbose;
+ this.#nodesByFrame = data.nodesByFrame;
}
static async create(page, options = {}) {
const verbose = options.verbose ?? false;
+ const mainFrame = page.pptrPage.mainFrame();
+ const frameStates = new Map();
+ for (const frame of page.pptrPage.frames()) {
+ if (frame instanceof CdpFrame) {
+ frameStates.set(frame, {
+ client: frame.client,
+ loaderId: frame._loaderId,
+ });
+ }
+ }
+ const currentClient = (frame) => {
+ const state = frameStates.get(frame);
+ if (!state ||
+ !(frame instanceof CdpFrame) ||
+ frame.detached ||
+ frame.client !== state.client ||
+ frame._loaderId !== state.loaderId) {
+ throw new Error('Snapshot document changed. Take a new snapshot.');
+ }
+ return state.client;
+ };
const rootNode = await page.pptrPage.accessibility.snapshot({
includeIframes: true,
interestingOnly: !verbose,
@@ -91,32 +125,90 @@ export class TextSnapshot {
let idCounter = 0;
const idToNode = new Map();
const seenUniqueIds = new Set();
- const seenBackendNodeIds = new Set();
+ const nodeFrames = new Map();
+ const documentIds = new Map();
+ const nodesByFrame = new Map();
+ const keyCounts = new Map();
+ const pending = [{ node: rootNode, frame: mainFrame }];
+ for (const entry of pending) {
+ const node = entry.node;
+ let frame = entry.frame;
+ if (node !== rootNode && node.role === 'RootWebArea') {
+ const env_1 = { stack: [], error: void 0, hasError: false };
+ try {
+ const handle = __addDisposableResource(env_1, await node.elementHandle(), false);
+ if (!handle) {
+ throw new Error('Snapshot document disappeared. Take a new snapshot.');
+ }
+ frame = handle.frame;
+ }
+ catch (e_1) {
+ env_1.error = e_1;
+ env_1.hasError = true;
+ }
+ finally {
+ __disposeResources(env_1);
+ }
+ }
+ const key = stableNodeKey(node, currentClient(frame).id());
+ if (key !== undefined) {
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
+ }
+ if (node.role === 'RootWebArea') {
+ documentIds.set(frame, node.loaderId);
+ }
+ nodeFrames.set(node, frame);
+ for (const child of node.children ?? []) {
+ pending.push({ node: child, frame });
+ }
+ }
+ // A repeated backend identity cannot identify either occurrence across captures.
+ for (const [key, count] of keyCounts) {
+ if (count > 1) {
+ uniqueBackendNodeIdToMcpId.delete(key);
+ }
+ }
const assignIds = (node) => {
- let id = '';
- // @ts-expect-error untyped backendNodeId.
- const backendNodeId = node.backendNodeId;
- // @ts-expect-error untyped loaderId.
- const uniqueBackendId = `${node.loaderId}_${backendNodeId}`;
- const existingMcpId = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
- if (existingMcpId !== undefined) {
- // Re-use MCP exposed ID if the uniqueId is the same.
- id = existingMcpId;
+ const frame = nodeFrames.get(node);
+ if (!frame) {
+ throw new Error('Snapshot node has no frame');
}
- else {
- // Only generate a new ID if we have not seen the node before.
- id = `${snapshotId}_${idCounter++}`;
- uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
+ const key = stableNodeKey(node, currentClient(frame).id());
+ const stableKey = key !== undefined && keyCounts.get(key) === 1 ? key : undefined;
+ const id = (stableKey !== undefined
+ ? uniqueBackendNodeIdToMcpId.get(stableKey)
+ : undefined) ?? `${snapshotId}_${idCounter++}`;
+ if (stableKey !== undefined) {
+ uniqueBackendNodeIdToMcpId.set(stableKey, id);
+ seenUniqueIds.add(stableKey);
}
- seenUniqueIds.add(uniqueBackendId);
- seenBackendNodeIds.add(backendNodeId);
const nodeWithId = {
...node,
id,
children: node.children
? node.children.map(child => assignIds(child))
: [],
+ elementHandle: async () => {
+ currentClient(frame);
+ const handle = await node.elementHandle();
+ try {
+ currentClient(frame);
+ return handle;
+ }
+ catch (error) {
+ await handle?.dispose();
+ throw error;
+ }
+ },
};
+ if (nodeWithId.backendNodeId) {
+ let nodes = nodesByFrame.get(frame);
+ if (!nodes) {
+ nodes = new Map();
+ nodesByFrame.set(frame, nodes);
+ }
+ nodes.set(nodeWithId.backendNodeId, nodeWithId);
+ }
// The AXNode for an option doesn't contain its `value`.
// Therefore, set text content of the option as value.
if (node.role === 'option') {
@@ -129,13 +221,14 @@ export class TextSnapshot {
return nodeWithId;
};
const rootNodeWithId = assignIds(rootNode);
- await TextSnapshot.insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, rootNodeWithId, seenBackendNodeIds, options.extraHandles ?? []);
+ await TextSnapshot.insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, nodesByFrame, documentIds, currentClient, options.extraHandles ?? []);
const snapshot = new TextSnapshot({
root: rootNodeWithId,
snapshotId: String(snapshotId),
idToNode,
hasSelectedElement: false,
verbose,
+ nodesByFrame,
});
const data = options.devtoolsData ?? (await page.getDevToolsData());
if (data?.cdpBackendNodeId) {
@@ -150,65 +243,88 @@ export class TextSnapshot {
}
return snapshot;
}
- resolveCdpElementId(cdpBackendNodeId) {
+ resolveCdpElementId(cdpBackendNodeId, frame) {
if (!cdpBackendNodeId) {
logger?.('no cdpBackendNodeId');
return;
}
- // TODO: index by backendNodeId instead.
+ if (frame) {
+ return this.#nodesByFrame.get(frame)?.get(cdpBackendNodeId)?.id;
+ }
+ let match;
const queue = [this.root];
while (queue.length) {
const current = queue.pop();
if (current.backendNodeId === cdpBackendNodeId) {
- return current.id;
+ if (match !== undefined) {
+ return;
+ }
+ match = current.id;
}
for (const child of current.children) {
queue.push(child);
}
}
- return;
+ return match;
}
// ExtraHandles represent DOM nodes which might not be part of the accessibility tree, e.g. DOM nodes
// returned by third-party developer tools. We insert them into the tree by finding the closest ancestor
// in the tree and inserting the node as a child. The ancestor's child nodes are re-parented if necessary.
- static async insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, rootNodeWithId, seenBackendNodeIds, extraHandles) {
+ static async insertExtraNodes(page, idToNode, seenUniqueIds, snapshotId, idCounter, nodesByFrame, documentIds, currentClient, extraHandles) {
const { uniqueBackendNodeIdToMcpId } = page;
const createExtraNode = async (handle) => {
- const backendNodeId = await handle.backendNodeId();
- if (!backendNodeId || seenBackendNodeIds.has(backendNodeId)) {
- return null;
- }
- const uniqueBackendId = `custom_${backendNodeId}`;
- if (seenUniqueIds.has(uniqueBackendId)) {
- return null;
+ const env_2 = { stack: [], error: void 0, hasError: false };
+ try {
+ const frame = handle.frame;
+ const client = currentClient(frame);
+ const backendNodeId = await handle.backendNodeId();
+ let nodes = nodesByFrame.get(frame);
+ if (!backendNodeId || nodes?.has(backendNodeId)) {
+ return null;
+ }
+ const documentId = documentIds.get(frame);
+ const uniqueBackendId = documentId
+ ? `custom_${client.id()}_${documentId}_${backendNodeId}`
+ : undefined;
+ const id = (uniqueBackendId !== undefined
+ ? uniqueBackendNodeIdToMcpId.get(uniqueBackendId)
+ : undefined) ?? `${snapshotId}_${idCounter++}`;
+ if (uniqueBackendId !== undefined) {
+ uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
+ seenUniqueIds.add(uniqueBackendId);
+ }
+ const tagHandle = __addDisposableResource(env_2, await handle.getProperty('localName'), false);
+ const tagValue = await tagHandle.jsonValue();
+ const extraNode = {
+ role: tagValue,
+ id,
+ backendNodeId,
+ children: [],
+ elementHandle: async () => {
+ currentClient(frame);
+ return await handle.evaluateHandle(element => element);
+ },
+ };
+ if (!nodes) {
+ nodes = new Map();
+ nodesByFrame.set(frame, nodes);
+ }
+ nodes.set(backendNodeId, extraNode);
+ return extraNode;
}
- seenBackendNodeIds.add(backendNodeId);
- let id = '';
- const mcpId = uniqueBackendNodeIdToMcpId.get(uniqueBackendId);
- if (mcpId !== undefined) {
- id = mcpId;
+ catch (e_2) {
+ env_2.error = e_2;
+ env_2.hasError = true;
}
- else {
- id = `${snapshotId}_${idCounter++}`;
- uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id);
+ finally {
+ __disposeResources(env_2);
}
- seenUniqueIds.add(uniqueBackendId);
- const tagHandle = await handle.getProperty('localName');
- const tagValue = await tagHandle.jsonValue();
- const extraNode = {
- role: tagValue,
- id,
- backendNodeId,
- children: [],
- elementHandle: async () => handle,
- };
- return extraNode;
};
const findAncestorNode = async (handle) => {
- const env_1 = { stack: [], error: void 0, hasError: false };
+ const env_3 = { stack: [], error: void 0, hasError: false };
try {
let ancestorHandle = await handle.evaluateHandle(el => el.parentElement);
- const stack = __addDisposableResource(env_1, new DisposableStack(), false);
+ const stack = __addDisposableResource(env_3, new DisposableStack(), false);
while (ancestorHandle) {
stack.use(ancestorHandle);
const ancestorElement = ancestorHandle.asElement();
@@ -217,9 +333,9 @@ export class TextSnapshot {
}
const ancestorBackendId = await ancestorElement.backendNodeId();
if (ancestorBackendId) {
- const ancestorNode = idToNode
- .values()
- .find(node => node.backendNodeId === ancestorBackendId);
+ const ancestorNode = nodesByFrame
+ .get(handle.frame)
+ ?.get(ancestorBackendId);
if (ancestorNode) {
return ancestorNode;
}
@@ -229,40 +345,37 @@ export class TextSnapshot {
}
return null;
}
- catch (e_1) {
- env_1.error = e_1;
- env_1.hasError = true;
+ catch (e_3) {
+ env_3.error = e_3;
+ env_3.hasError = true;
}
finally {
- __disposeResources(env_1);
+ __disposeResources(env_3);
}
};
- const findDescendantNodes = async (backendNodeId) => {
+ const findDescendantNodes = async (frame, backendNodeId) => {
const descendantIds = new Set();
if (!backendNodeId) {
return descendantIds;
}
try {
- // @ts-expect-error internal API
- const client = page.pptrPage._client();
- if (client) {
- const { node } = await client.send('DOM.describeNode', {
- backendNodeId,
- depth: -1,
- pierce: true,
- });
- const collect = (node) => {
- if (node.backendNodeId && node.backendNodeId !== backendNodeId) {
- descendantIds.add(node.backendNodeId);
- }
- if (node.children) {
- for (const child of node.children) {
- collect(child);
- }
+ const client = currentClient(frame);
+ const { node } = await client.send('DOM.describeNode', {
+ backendNodeId,
+ depth: -1,
+ pierce: true,
+ });
+ const collect = (node) => {
+ if (node.backendNodeId && node.backendNodeId !== backendNodeId) {
+ descendantIds.add(node.backendNodeId);
+ }
+ if (node.children) {
+ for (const child of node.children) {
+ collect(child);
}
- };
- collect(node);
- }
+ }
+ };
+ collect(node);
}
catch (e) {
logger?.(`Failed to collect descendants for backend node ${backendNodeId}`, e);
@@ -302,8 +415,15 @@ export class TextSnapshot {
continue;
}
idToNode.set(extraNode.id, extraNode);
- const attachTarget = (await findAncestorNode(handle)) || rootNodeWithId;
- const descendantIds = await findDescendantNodes(extraNode.backendNodeId);
+ const attachTarget = (await findAncestorNode(handle)) ??
+ nodesByFrame
+ .get(handle.frame)
+ ?.values()
+ .find(node => node.role === 'RootWebArea');
+ if (!attachTarget) {
+ throw new Error('Extra node has no snapshot document. Take a new snapshot.');
+ }
+ const descendantIds = await findDescendantNodes(handle.frame, extraNode.backendNodeId);
reorgInfo.push({ extraNode, attachTarget, descendantIds });
}
for (const { extraNode, attachTarget, descendantIds } of reorgInfo) {