mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
refactor(observation): organize capture around document facts
This commit is contained in:
@@ -4,7 +4,8 @@ import {
|
||||
type CdpTarget,
|
||||
cdpTargetKey,
|
||||
} from "@/browser-driver/frame-graph";
|
||||
import type { CapturedNode, CapturedViewModel } from "./capture";
|
||||
import type { CapturedNode } from "./facts";
|
||||
import { captureCheckpoint } from "./facts";
|
||||
|
||||
export interface FrameOwnedAxNode {
|
||||
nodeId: string;
|
||||
@@ -23,6 +24,16 @@ export interface FrameDocument<T extends FrameOwnedAxNode> extends CdpFrame {
|
||||
contextScopeId: string;
|
||||
axNodes: T[];
|
||||
domNodes: CapturedNode[];
|
||||
excludedBackendNodeIds?: ReadonlySet<number>;
|
||||
}
|
||||
|
||||
/** Explicit snapshot document data used to join DOM and AX ownership. */
|
||||
export interface FrameDomInput {
|
||||
nodes: CapturedNode[];
|
||||
rootFrameId?: string;
|
||||
frameNodes?: ReadonlyMap<string, CapturedNode[]>;
|
||||
frameOwnerBackendNodeIds?: ReadonlyMap<string, number>;
|
||||
frameParentIds?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
interface Ownership {
|
||||
@@ -47,17 +58,13 @@ function targetBackendKey(target: CdpTarget, backendNodeId: number): string {
|
||||
function frameList<T extends FrameOwnedAxNode>(
|
||||
graph: CdpFrameGraph | null,
|
||||
batches: FrameAxBatch<T>[],
|
||||
captured: CapturedViewModel,
|
||||
captured: FrameDomInput,
|
||||
): CdpFrame[] {
|
||||
const frames = new Map<string, CdpFrame>();
|
||||
for (const frame of graph?.frames ?? []) frames.set(frame.frameId, frame);
|
||||
for (const batch of batches) {
|
||||
if (!frames.has(batch.frame.frameId)) frames.set(batch.frame.frameId, batch.frame);
|
||||
}
|
||||
for (const frameId of captured.frameNodes?.keys() ?? []) {
|
||||
if (!frames.has(frameId))
|
||||
frames.set(frameId, { frameId, target: { tabId: batches[0]?.frame.target.tabId ?? 0 } });
|
||||
}
|
||||
return [...frames.values()].map((frame) => {
|
||||
const ownerBackendNodeId =
|
||||
frame.ownerBackendNodeId ?? captured.frameOwnerBackendNodeIds?.get(frame.frameId);
|
||||
@@ -70,11 +77,14 @@ function frameList<T extends FrameOwnedAxNode>(
|
||||
});
|
||||
}
|
||||
|
||||
export function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
||||
export async function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
||||
graph: CdpFrameGraph | null,
|
||||
batches: FrameAxBatch<T>[],
|
||||
captured: CapturedViewModel,
|
||||
): FrameDocument<T>[] {
|
||||
captured: FrameDomInput,
|
||||
signal?: AbortSignal,
|
||||
unresolved?: (frame: CdpFrame) => void,
|
||||
): Promise<FrameDocument<T>[]> {
|
||||
let work = 0;
|
||||
const frames = frameList(graph, batches, captured);
|
||||
const frameById = new Map(frames.map((frame) => [frame.frameId, frame]));
|
||||
const rootFrameId = graph?.rootFrameId ?? captured.rootFrameId ?? frames[0]?.frameId;
|
||||
@@ -83,54 +93,74 @@ export function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
||||
const backendOwner = new Map<string, string>();
|
||||
for (const frame of frames) {
|
||||
for (const node of domNodesForFrame(frame.frameId)) {
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
backendOwner.set(targetBackendKey(frame.target, node.backendNodeId), frame.frameId);
|
||||
}
|
||||
}
|
||||
|
||||
const candidates = new Map<string, OwnedCandidate<T>>();
|
||||
for (const batch of batches) {
|
||||
const nodeById = new Map(batch.nodes.map((node) => [node.nodeId, node]));
|
||||
const nodeById = new Map<string, T>();
|
||||
for (const node of batch.nodes) {
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
nodeById.set(node.nodeId, node);
|
||||
}
|
||||
let reported = false;
|
||||
const ownershipByNodeId = new Map<string, Ownership>();
|
||||
const resolving = new Set<string>();
|
||||
const resolveOwnership = (node: T): Ownership => {
|
||||
const cached = ownershipByNodeId.get(node.nodeId);
|
||||
if (cached) return cached;
|
||||
if (node.frameId && frameById.has(node.frameId)) {
|
||||
const ownership = { frameId: node.frameId, strength: 4 };
|
||||
ownershipByNodeId.set(node.nodeId, ownership);
|
||||
return ownership;
|
||||
}
|
||||
if (typeof node.backendDOMNodeId === "number") {
|
||||
const frameId = backendOwner.get(
|
||||
targetBackendKey(batch.frame.target, node.backendDOMNodeId),
|
||||
);
|
||||
const resolveOwnership = async (node: T): Promise<Ownership> => {
|
||||
const path: T[] = [];
|
||||
const visiting = new Set<string>();
|
||||
let current: T | undefined = node;
|
||||
let ownership: Ownership = { frameId: batch.frame.frameId, strength: 1 };
|
||||
while (current) {
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
const cached = ownershipByNodeId.get(current.nodeId);
|
||||
if (cached) {
|
||||
ownership = cached;
|
||||
break;
|
||||
}
|
||||
if (visiting.has(current.nodeId)) break;
|
||||
visiting.add(current.nodeId);
|
||||
if (current.frameId) {
|
||||
const frame = frameById.get(current.frameId);
|
||||
// An explicit contradiction must not fall through to a guessed owner.
|
||||
if (!frame || cdpTargetKey(frame.target) !== cdpTargetKey(batch.frame.target)) {
|
||||
if (!reported) {
|
||||
unresolved?.(batch.frame);
|
||||
reported = true;
|
||||
}
|
||||
ownership = { frameId: "", strength: 0 };
|
||||
break;
|
||||
}
|
||||
ownership = { frameId: frame.frameId, strength: 4 };
|
||||
ownershipByNodeId.set(current.nodeId, ownership);
|
||||
break;
|
||||
}
|
||||
const frameId =
|
||||
typeof current.backendDOMNodeId === "number"
|
||||
? backendOwner.get(targetBackendKey(batch.frame.target, current.backendDOMNodeId))
|
||||
: undefined;
|
||||
if (frameId) {
|
||||
const ownership = { frameId, strength: 3 };
|
||||
ownershipByNodeId.set(node.nodeId, ownership);
|
||||
return ownership;
|
||||
ownership = { frameId, strength: 3 };
|
||||
ownershipByNodeId.set(current.nodeId, ownership);
|
||||
break;
|
||||
}
|
||||
path.push(current);
|
||||
current = current.parentId ? nodeById.get(current.parentId) : undefined;
|
||||
}
|
||||
if (node.parentId && !resolving.has(node.nodeId)) {
|
||||
const parent = nodeById.get(node.parentId);
|
||||
if (parent) {
|
||||
resolving.add(node.nodeId);
|
||||
const inherited = resolveOwnership(parent);
|
||||
resolving.delete(node.nodeId);
|
||||
const ownership = {
|
||||
frameId: inherited.frameId,
|
||||
strength: Math.min(2, inherited.strength),
|
||||
};
|
||||
ownershipByNodeId.set(node.nodeId, ownership);
|
||||
return ownership;
|
||||
}
|
||||
for (let i = path.length - 1; i >= 0; i--) {
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
ownership = { frameId: ownership.frameId, strength: Math.min(2, ownership.strength) };
|
||||
ownershipByNodeId.set(path[i].nodeId, ownership);
|
||||
}
|
||||
const ownership = { frameId: batch.frame.frameId, strength: 1 };
|
||||
ownershipByNodeId.set(node.nodeId, ownership);
|
||||
if (!ownershipByNodeId.has(node.nodeId)) ownershipByNodeId.set(node.nodeId, ownership);
|
||||
return ownership;
|
||||
};
|
||||
|
||||
for (const node of batch.nodes) {
|
||||
const ownership = resolveOwnership(node);
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
const ownership = await resolveOwnership(node);
|
||||
if (!ownership.frameId) continue;
|
||||
const key = targetNodeKey(batch.frame.target, node.nodeId);
|
||||
const existing = candidates.get(key);
|
||||
if (!existing || ownership.strength > existing.ownership.strength) {
|
||||
@@ -144,6 +174,7 @@ export function buildFrameDocuments<T extends FrameOwnedAxNode>(
|
||||
);
|
||||
const axNodesByFrame = new Map<string, T[]>();
|
||||
for (const candidate of candidates.values()) {
|
||||
if (work++ % 256 === 0) await captureCheckpoint(signal);
|
||||
const { node, target, ownership } = candidate;
|
||||
const parentFrameId = node.parentId
|
||||
? ownershipByTargetNode.get(targetNodeKey(target, node.parentId))
|
||||
|
||||
Reference in New Issue
Block a user