fix(observation): validate document consistency during capture

This commit is contained in:
Ljy-0827
2026-09-09 15:58:40 +08:00
committed by drakezhang
parent eefbe03258
commit df5ed1a495
8 changed files with 135 additions and 0 deletions
+1
View File
@@ -55,6 +55,7 @@ export interface CdpRunner {
sendToTarget?<T = unknown>(target: CdpTarget, method: string, params?: object): Promise<T>;
detach?(tabId: number): Promise<void>;
getFrameGraph?(tabId: number): Promise<CdpFrameGraph>;
getAttachmentId?(tabId: number): string | undefined;
ensureAttachedToUrl?(tabId: number, expectedUrl: string | undefined): Promise<void>;
trackSessionTab?(sessionId: string, tabId: number): void;
releaseSessionTab?(sessionId: string, tabId: number): Promise<void>;
@@ -0,0 +1,69 @@
import type { CdpFrame } from "@/browser-driver/frame-graph";
import type { CdpRunner } from "../shared";
import { sendToCdpTarget } from "../shared";
import {
isAbortError as isCaptureAbort,
throwIfAborted as throwCaptureAborted,
} from "./capture-abort";
import type { DocumentIdentity } from "./facts";
/** Fresh, frame-scoped identity read. An isolated world supplies the owning
* document without querying each node or installing a page observer. */
export async function readDocumentIdentity(
cdp: CdpRunner,
frame: CdpFrame,
signal?: AbortSignal,
): Promise<DocumentIdentity | undefined> {
const attachmentId = cdp.getAttachmentId?.(frame.target.tabId);
if (!attachmentId || !frame.loaderId) return undefined;
let objectId: string | undefined;
const send = <T>(method: string, params: object) => {
throwCaptureAborted(signal);
return sendToCdpTarget<T>(cdp, frame.target, method, params);
};
try {
const world = await send<{ executionContextId: number }>("Page.createIsolatedWorld", {
frameId: frame.frameId,
worldName: "bsk-document-identity",
});
const element = await send<{ result?: { objectId?: string } }>("Runtime.evaluate", {
expression: "document.documentElement",
contextId: world.executionContextId,
});
objectId = element.result?.objectId;
if (!objectId) return undefined;
const reply = await send<{ node?: { backendNodeId?: number } }>("DOM.describeNode", {
objectId,
depth: 0,
});
const backendNodeId = reply.node?.backendNodeId;
if (!backendNodeId || cdp.getAttachmentId?.(frame.target.tabId) !== attachmentId)
return undefined;
return {
attachmentId,
target: frame.target,
frameId: frame.frameId,
loaderId: frame.loaderId,
documentElementBackendNodeId: backendNodeId,
};
} catch (error) {
if (isCaptureAbort(error)) throw error;
return undefined;
} finally {
if (objectId)
await sendToCdpTarget(cdp, frame.target, "Runtime.releaseObject", { objectId }).catch(
() => {},
);
}
}
export function sameDocument(a: DocumentIdentity, b: DocumentIdentity): boolean {
return (
a.attachmentId === b.attachmentId &&
a.target.tabId === b.target.tabId &&
a.target.sessionId === b.target.sessionId &&
a.frameId === b.frameId &&
a.loaderId === b.loaderId &&
a.documentElementBackendNodeId === b.documentElementBackendNodeId
);
}
+9
View File
@@ -1,3 +1,4 @@
import type { CdpTarget } from "@/browser-driver/frame-graph";
import { isOverlayHostNode } from "@/lib/overlay-bridge";
import { createCaptureCheckpoint } from "./capture-abort";
import type { CapturedNode } from "./capture-types";
@@ -92,3 +93,11 @@ export async function buildDocumentIndex<T extends DecodedNode>(
}
return { nodes, excludedBackendNodeIds };
}
export interface DocumentIdentity {
attachmentId: string;
target: CdpTarget;
frameId: string;
loaderId: string;
documentElementBackendNodeId: number;
}
@@ -21,6 +21,7 @@ export interface FrameContext {
export interface NormalizedDocument {
nodes: NodeFacts[];
index: DocumentIndex;
documentElementBackendNodeId?: number;
}
/** Interpret one document using a supplied projection. No live reads or frame scheduling. */
@@ -74,5 +75,11 @@ export async function normalizeDocument(
(node) => !node.tag.startsWith("#") && !index.excludedBackendNodeIds.has(node.backendNodeId),
),
index,
documentElementBackendNodeId: nodes.find(
(node) =>
node.nodeType === 1 &&
node.parentBackendNodeId !== null &&
index.nodes.get(node.parentBackendNodeId)?.nodeType === 9,
)?.backendNodeId,
};
}