mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
import type { RecordStepPayload } from "../record-bridge";
|
|
import { hasRedirectQualifier } from "./navigation-policy";
|
|
import type { RecordingDraftStep, TargetMatchHint } from "./types";
|
|
|
|
export interface RecordingStepBuffer {
|
|
steps: RecordingDraftStep[];
|
|
currentUrl?: string;
|
|
pendingNavigation: boolean;
|
|
pendingNavigationDeadline?: number;
|
|
}
|
|
|
|
const NAVIGATION_TRIGGER_WINDOW_MS = 3_000;
|
|
|
|
function toDraftStep(
|
|
payload: RecordStepPayload,
|
|
targetHint?: TargetMatchHint,
|
|
): RecordingDraftStep | null {
|
|
const pageUrl = payload.page_url;
|
|
const common = {
|
|
...(pageUrl ? { pageUrl } : {}),
|
|
...(targetHint ? { targetHint } : {}),
|
|
};
|
|
switch (payload.op) {
|
|
case "click":
|
|
return payload.target ? { op: "click", captureTarget: payload.target, ...common } : null;
|
|
case "hover":
|
|
return payload.target ? { op: "hover", captureTarget: payload.target, ...common } : null;
|
|
case "fill":
|
|
return payload.target
|
|
? {
|
|
op: "fill",
|
|
captureTarget: payload.target,
|
|
value: payload.value ?? "",
|
|
...(payload.commit ? { commit: payload.commit } : {}),
|
|
...(payload.redacted ? { redacted: true } : {}),
|
|
...common,
|
|
}
|
|
: null;
|
|
case "press":
|
|
return payload.key
|
|
? {
|
|
op: "press",
|
|
key: payload.key,
|
|
...(payload.target ? { captureTarget: payload.target } : {}),
|
|
...(payload.modifiers?.length ? { modifiers: payload.modifiers } : {}),
|
|
...common,
|
|
}
|
|
: null;
|
|
case "select":
|
|
return payload.target && payload.values
|
|
? {
|
|
op: "select",
|
|
captureTarget: payload.target,
|
|
values: payload.values,
|
|
...(payload.labels?.length ? { labels: payload.labels } : {}),
|
|
...common,
|
|
}
|
|
: null;
|
|
case "navigate":
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function annotateLastStepNavigation(buffer: RecordingStepBuffer, url: string): number {
|
|
for (let index = buffer.steps.length - 1; index >= 0; index -= 1) {
|
|
const step = buffer.steps[index];
|
|
if (!step) continue;
|
|
if (step.op === "click" || step.op === "press" || step.op === "select" || step.op === "fill") {
|
|
step.navigatedTo = url;
|
|
return index;
|
|
}
|
|
break;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
export type NavigationObserveResult =
|
|
| { kind: "noop" }
|
|
| { kind: "annotated"; index: number }
|
|
| { kind: "appended"; index: number }
|
|
| { kind: "coalesce_redirect"; url: string };
|
|
|
|
export function observeRecordedNavigation(
|
|
buffer: RecordingStepBuffer,
|
|
url: string,
|
|
causedByAction?: boolean,
|
|
transitionType?: string,
|
|
transitionQualifiers?: string[],
|
|
): NavigationObserveResult {
|
|
if (!url || url === buffer.currentUrl) return { kind: "noop" };
|
|
buffer.currentUrl = url;
|
|
|
|
const pendingIsCurrent =
|
|
buffer.pendingNavigation &&
|
|
(buffer.pendingNavigationDeadline === undefined ||
|
|
buffer.pendingNavigationDeadline >= Date.now());
|
|
|
|
if (causedByAction === true || (causedByAction === undefined && pendingIsCurrent)) {
|
|
buffer.pendingNavigation = false;
|
|
buffer.pendingNavigationDeadline = undefined;
|
|
const annotatedIndex = annotateLastStepNavigation(buffer, url);
|
|
if (annotatedIndex >= 0) return { kind: "annotated", index: annotatedIndex };
|
|
} else {
|
|
buffer.pendingNavigation = false;
|
|
buffer.pendingNavigationDeadline = undefined;
|
|
}
|
|
|
|
if (hasRedirectQualifier(transitionQualifiers)) {
|
|
return { kind: "coalesce_redirect", url };
|
|
}
|
|
buffer.steps.push({
|
|
op: "navigate",
|
|
url,
|
|
pageUrl: url,
|
|
transitionType,
|
|
transitionQualifiers,
|
|
});
|
|
return { kind: "appended", index: buffer.steps.length - 1 };
|
|
}
|
|
|
|
export function appendRecordedPayload(
|
|
buffer: RecordingStepBuffer,
|
|
payload: RecordStepPayload,
|
|
targetHint?: TargetMatchHint,
|
|
): number | null {
|
|
if (payload.op === "navigate") {
|
|
if (!payload.url) return null;
|
|
const result = observeRecordedNavigation(
|
|
buffer,
|
|
payload.url,
|
|
payload.navigation_caused_by_action,
|
|
payload.transitionType,
|
|
payload.transitionQualifiers,
|
|
);
|
|
return result.kind === "appended" ? result.index : null;
|
|
}
|
|
const step = toDraftStep(
|
|
{ ...payload, page_url: payload.page_url ?? buffer.currentUrl },
|
|
targetHint,
|
|
);
|
|
if (!step) return null;
|
|
buffer.steps.push(step);
|
|
if (step.op === "click" || step.op === "press" || step.op === "select" || step.op === "fill") {
|
|
buffer.pendingNavigation = payload.expects_navigation === true;
|
|
buffer.pendingNavigationDeadline = buffer.pendingNavigation
|
|
? Date.now() + NAVIGATION_TRIGGER_WINDOW_MS
|
|
: undefined;
|
|
}
|
|
return buffer.steps.length - 1;
|
|
}
|