mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* chore(deps): refresh dependencies with a seven-day cutoff * fix(deps): preserve runtime and tooling contracts after upgrades * fix(deps): align fixture lifetimes and preserve Unicode contracts * fix(ui): publish goal rejections and align integration fixtures Publish rejected goal actions through the existing renderer lifecycle. Start the Session Share service in integration fixtures, preserve same-job workflow authority, and distinguish pnpm launcher escalation from detached cleanup ownership. * test: follow Corepack ownership and await navigation handoff
464 lines
22 KiB
Diff
464 lines
22 KiB
Diff
diff --git a/build/src/McpPage.js b/build/src/McpPage.js
|
|
index 13eb306aea9d516c1e06f4d40a6a61f83bfada11..0c02d5fc9b164dad0f65b74ad926e5cbb6437fcf 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
|
|
@@ -516,7 +517,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}`;
|
|
diff --git a/build/src/OPENCLAW_PATCH_NOTICE.md b/build/src/OPENCLAW_PATCH_NOTICE.md
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7543bd478e184a709e45b54db944df3dd8231336
|
|
--- /dev/null
|
|
+++ b/build/src/OPENCLAW_PATCH_NOTICE.md
|
|
@@ -0,0 +1,20 @@
|
|
+# Temporary OpenClaw snapshot identity patch
|
|
+
|
|
+This package remains `chrome-devtools-mcp@1.9.0`. OpenClaw modifies three
|
|
+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`.
|
|
+
|
|
+- `TextSnapshot.js` carries that reviewed snapshot-owner implementation.
|
|
+- `McpPage.js` supplies the known element frame for DOM-result reverse lookup.
|
|
+- `third_party/index.js` exports its already-bundled `CdpFrame` class, required
|
|
+ by the backport. No bundled dependency implementation is replaced.
|
|
+
|
|
+The original package is the published npm 1.9.0 tarball with integrity
|
|
+`sha512-RnzXoJiUQ44hpOihWk90uOhLD/CnwDkDy0ldHMZONJ2nYQ+dWN1fq1luHHqyd+7FuYnyIlCY6uTThbN5ut9kSQ==`.
|
|
+The original `LICENSE` and `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) {
|
|
diff --git a/build/src/third_party/index.js b/build/src/third_party/index.js
|
|
index 5bce2352d7dd5950547fbb83e1c7d495605d9de5..6a86c23c58dc18ca620977357f1c06220e602b6c 100644
|
|
--- a/build/src/third_party/index.js
|
|
+++ b/build/src/third_party/index.js
|
|
@@ -1,3 +1,4 @@
|
|
+// Modified by OpenClaw: expose existing CdpFrame for the snapshot backport; see ../OPENCLAW_PATCH_NOTICE.md.
|
|
import childProcess, { execSync, execFile, spawnSync, spawn as spawn$1 } from 'node:child_process';
|
|
import fs, { readdirSync as readdirSync$1, readFileSync as readFileSync$1, accessSync, existsSync, constants as constants$2 } from 'node:fs';
|
|
import os, { tmpdir } from 'node:os';
|
|
@@ -181481,4 +181482,4 @@ const snapshot = snapshot$1;
|
|
const navigation = navigation$1;
|
|
const generateReport = generateReport$1;
|
|
|
|
-export { AsyncDisposableStack, Browser as BrowserEnum, CDPSessionEvent, CdpPage, Client, mcp as DevTools, DisposableStack, KnownDevices, ListRootsRequestSchema, ListRootsResultSchema, Locator, McpServer, Mutex$2 as Mutex, PipeTransport, PredefinedNetworkConditions, RootsListChangedNotificationSchema, ScreenRecorder, SetLevelRequestSchema, StdioClientTransport, StdioServerTransport, SuppressedError$1 as SuppressedError, ajv, detectBrowserPlatform, generateReport, getGcfEncode, getToonEncode, hideBin, navigation, puppeteer, resolveDefaultUserDataDir, index as semver, snapshot, Yargs as yargs, z$2 as zod };
|
|
+export { AsyncDisposableStack, Browser as BrowserEnum, CDPSessionEvent, CdpFrame, CdpPage, Client, mcp as DevTools, DisposableStack, KnownDevices, ListRootsRequestSchema, ListRootsResultSchema, Locator, McpServer, Mutex$2 as Mutex, PipeTransport, PredefinedNetworkConditions, RootsListChangedNotificationSchema, ScreenRecorder, SetLevelRequestSchema, StdioClientTransport, StdioServerTransport, SuppressedError$1 as SuppressedError, ajv, detectBrowserPlatform, generateReport, getGcfEncode, getToonEncode, hideBin, navigation, puppeteer, resolveDefaultUserDataDir, index as semver, snapshot, Yargs as yargs, z$2 as zod };
|