mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
import type { NavigationCause, StepV3 } from "@/transport/types";
|
|
import { shouldIncludeDraft } from "./draft-policy";
|
|
import { hasRedirectQualifier } from "./navigation-policy";
|
|
import { unmatchedTarget } from "./target-matcher";
|
|
import type { RecordingDraftStep } from "./types";
|
|
|
|
interface CollapsedDraft {
|
|
draft: RecordingDraftStep;
|
|
draftIds: number[];
|
|
}
|
|
|
|
const TRANSITION_CAUSES: Record<string, NavigationCause> = {
|
|
typed: "user_typed",
|
|
generated: "user_typed",
|
|
keyword: "user_typed",
|
|
keyword_generated: "user_typed",
|
|
link: "link",
|
|
form_submit: "form_submit",
|
|
reload: "reload",
|
|
auto_bookmark: "browser",
|
|
start_page: "browser",
|
|
};
|
|
|
|
function isRedirect(step: Extract<RecordingDraftStep, { op: "navigate" }>): boolean {
|
|
return hasRedirectQualifier(step.transitionQualifiers);
|
|
}
|
|
|
|
function collapseRedirects(steps: RecordingDraftStep[]): CollapsedDraft[] {
|
|
const output: CollapsedDraft[] = [];
|
|
steps.forEach((step, index) => {
|
|
const previous = output[output.length - 1];
|
|
if (step.op === "navigate" && previous?.draft.op === "navigate" && isRedirect(step)) {
|
|
previous.draft = {
|
|
...previous.draft,
|
|
url: step.url,
|
|
postStateId: step.postStateId ?? previous.draft.postStateId,
|
|
};
|
|
previous.draftIds.push(index + 1);
|
|
return;
|
|
}
|
|
output.push({ draft: { ...step }, draftIds: [index + 1] });
|
|
});
|
|
return output;
|
|
}
|
|
|
|
function navigationCause(step: Extract<RecordingDraftStep, { op: "navigate" }>): NavigationCause {
|
|
if (step.cause) return step.cause;
|
|
const qualifiers = step.transitionQualifiers ?? [];
|
|
if (qualifiers.includes("forward_back")) return "history";
|
|
if (qualifiers.includes("from_address_bar")) return "user_typed";
|
|
return TRANSITION_CAUSES[step.transitionType ?? ""] ?? "browser";
|
|
}
|
|
|
|
function selection(values: string[], labels?: string[]): Array<{ value: string; label?: string }> {
|
|
return values.map((value, index) => ({
|
|
value,
|
|
...(labels?.[index] ? { label: labels[index] } : {}),
|
|
}));
|
|
}
|
|
|
|
interface ReduceDraftOptions {
|
|
includeTabSwitches: boolean;
|
|
redactValues: boolean;
|
|
}
|
|
|
|
function reduceDraft(
|
|
draft: RecordingDraftStep,
|
|
id: number,
|
|
options: ReduceDraftOptions,
|
|
): StepV3 | null {
|
|
if (!shouldIncludeDraft(draft)) return null;
|
|
if (draft.op === "switch_tab") {
|
|
if (!options.includeTabSwitches || !draft.preStateId || !draft.postStateId) return null;
|
|
return {
|
|
op: "switch_tab",
|
|
id,
|
|
state: draft.preStateId,
|
|
result: { state: draft.postStateId },
|
|
};
|
|
}
|
|
const state = draft.preStateId ?? draft.postStateId;
|
|
const resultState = draft.postStateId ?? draft.preStateId;
|
|
if (!state || !resultState) return null;
|
|
const common = { id, state, result: { state: resultState } };
|
|
|
|
switch (draft.op) {
|
|
case "navigate":
|
|
return { op: "navigate", ...common, to: draft.url, cause: navigationCause(draft) };
|
|
case "click":
|
|
return {
|
|
op: "click",
|
|
...common,
|
|
target: draft.matchedTarget ?? unmatchedTarget(draft.captureTarget),
|
|
};
|
|
case "hover":
|
|
return {
|
|
op: "hover",
|
|
...common,
|
|
target: draft.matchedTarget ?? unmatchedTarget(draft.captureTarget),
|
|
};
|
|
case "fill":
|
|
const fillIsRedacted = options.redactValues || draft.redacted === true;
|
|
return {
|
|
op: "fill",
|
|
...common,
|
|
target: draft.matchedTarget ?? unmatchedTarget(draft.captureTarget),
|
|
value: fillIsRedacted ? "***" : draft.value,
|
|
commit: draft.commit ?? "blur",
|
|
...(fillIsRedacted ? { redacted: true } : {}),
|
|
};
|
|
case "press":
|
|
return {
|
|
op: "press",
|
|
...common,
|
|
key: draft.key,
|
|
...(draft.captureTarget || draft.matchedTarget
|
|
? { target: draft.matchedTarget ?? unmatchedTarget(draft.captureTarget) }
|
|
: {}),
|
|
...(draft.modifiers?.length ? { modifiers: draft.modifiers } : {}),
|
|
};
|
|
case "select":
|
|
return {
|
|
op: "select",
|
|
...common,
|
|
target: draft.matchedTarget ?? unmatchedTarget(draft.captureTarget),
|
|
...(!options.redactValues ? { selection: selection(draft.values, draft.labels) } : {}),
|
|
};
|
|
case "scroll":
|
|
return { op: "scroll", ...common };
|
|
}
|
|
}
|
|
|
|
export interface ReducedTraceV3 {
|
|
steps: StepV3[];
|
|
stepIdByDraftId: Map<number, number>;
|
|
}
|
|
|
|
export function reduceTraceStepsV3(
|
|
steps: RecordingDraftStep[],
|
|
options: { includeTabSwitches?: boolean; redactValues?: boolean } = {},
|
|
): ReducedTraceV3 {
|
|
const output: StepV3[] = [];
|
|
const stepIdByDraftId = new Map<number, number>();
|
|
for (const { draft, draftIds } of collapseRedirects(steps)) {
|
|
const step = reduceDraft(draft, output.length + 1, {
|
|
includeTabSwitches: options.includeTabSwitches === true,
|
|
redactValues: options.redactValues === true,
|
|
});
|
|
if (!step) continue;
|
|
output.push(step);
|
|
for (const draftId of draftIds) stepIdByDraftId.set(draftId, step.id);
|
|
}
|
|
return { steps: output, stepIdByDraftId };
|
|
}
|