You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

165 lines
4.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type { BrowserContext, Locator, Page } from "playwright";
import { RpaError } from "@/modules/rpa/errors";
/** selectors 契约(与 SelectorGraph 对齐) */
export interface IRPASelectors {
resolve(key: string): string;
specs(envKey: string): string[];
widget(page: Page): Locator;
}
/** exec/runtime 契约(与 ExecutionLayer 对齐) */
export interface IRPAExecution {
evaluateOnLocator<T>(
locator: Locator,
script: string,
arg?: unknown,
): Promise<T>;
waitForBrowserFunction(
script: string,
arg: unknown,
timeoutMs: number,
): Promise<void>;
}
/** state 最小契约address-side 等使用) */
export interface IRPAState {
markAddressSide(side: "pickup" | "delivery"): void;
}
/** Kernel 上下文契约(打破 StateEngine ↔ RPAContext 循环依赖) */
export interface IRPAContext {
readonly page: Page;
readonly browserContext?: BrowserContext;
readonly selectors: IRPASelectors;
readonly exec: IRPAExecution;
readonly runtime: IRPAExecution;
readonly state: IRPAState;
getWidgetScrollAt(): number;
setWidgetScrollAt(ms: number): void;
}
/** 数组字段统一兜底,禁止 undefined 流入业务 */
export function ensureArray<T>(value: T[] | null | undefined): T[] {
return Array.isArray(value) ? value : [];
}
function isPlaywrightPage(value: unknown): value is Page {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value as Page;
return (
typeof candidate.locator === "function" &&
typeof candidate.waitForTimeout === "function" &&
typeof candidate.url === "function"
);
}
function hasContextContract(value: object): value is IRPAContext {
const ctx = value as IRPAContext;
return (
"selectors" in ctx &&
"exec" in ctx &&
"runtime" in ctx &&
"state" in ctx &&
"page" in ctx &&
typeof ctx.getWidgetScrollAt === "function" &&
typeof ctx.setWidgetScrollAt === "function"
);
}
/** duck-typing 识别 RPAContext避免跨 bundle instanceof 失效) */
export function isRPAContext(value: unknown): value is IRPAContext {
if (typeof value !== "object" || value === null) {
return false;
}
if (!hasContextContract(value)) {
return false;
}
return isPlaywrightPage((value as IRPAContext).page);
}
/** fail-fast校验 context contract 完整 */
export function assertRPAContext(
ctx: unknown,
source = "RPAContext",
options?: { allowMissingState?: boolean },
): asserts ctx is IRPAContext {
if (typeof ctx !== "object" || ctx === null) {
throw new RpaError(
"STRUCT_CHANGE",
`${source} 非法:非对象`,
{ retryable: false },
);
}
const typed = ctx as IRPAContext;
const hasCore =
typed.selectors instanceof Object &&
typed.exec instanceof Object &&
typed.runtime instanceof Object &&
isPlaywrightPage(typed.page) &&
typeof typed.getWidgetScrollAt === "function" &&
typeof typed.setWidgetScrollAt === "function";
if (!hasCore) {
throw new RpaError(
"STRUCT_CHANGE",
`${source} contract 断裂:缺少 selectors/runtime/state/page`,
{ retryable: false },
);
}
if (!options?.allowMissingState && !(typed.state instanceof Object)) {
throw new RpaError(
"STRUCT_CHANGE",
`${source}.state 未初始化`,
{ retryable: false },
);
}
}
export function assertPlaywrightPage(
page: unknown,
source = "page",
): asserts page is Page {
if (!isPlaywrightPage(page)) {
throw new RpaError(
"STRUCT_CHANGE",
`${source} 不是有效 Playwright Page`,
{ retryable: false },
);
}
}
export type RPAContextContractSnapshot = {
hasPage: boolean;
hasSelectors: boolean;
hasExec: boolean;
hasRuntime: boolean;
hasState: boolean;
};
export function snapshotContextContract(
ctx: unknown,
): RPAContextContractSnapshot {
if (typeof ctx !== "object" || ctx === null) {
return {
hasPage: false,
hasSelectors: false,
hasExec: false,
hasRuntime: false,
hasState: false,
};
}
const typed = ctx as Partial<IRPAContext>;
return {
hasPage: isPlaywrightPage(typed.page),
hasSelectors: typed.selectors instanceof Object,
hasExec: typed.exec instanceof Object,
hasRuntime: typed.runtime instanceof Object,
hasState: typed.state instanceof Object,
};
}