Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
219 lines
6.8 KiB
TypeScript
219 lines
6.8 KiB
TypeScript
import type {
|
|
AdapterContext,
|
|
BrowserActRequest,
|
|
BrowserActResult,
|
|
BrowserNavigateRequest,
|
|
BrowserNavigateResult,
|
|
BrowserProvider,
|
|
BrowserSnapshotNode,
|
|
BrowserSnapshotRequest,
|
|
BrowserSnapshotResult,
|
|
ComputerRef,
|
|
PageBrowserCommand,
|
|
PageBrowserResult,
|
|
SandboxProvider,
|
|
} from "@rakazo/adapter-kit";
|
|
import { FakeBrowserProvider, type FakeBrowserProviderOptions } from "./fake-browser.js";
|
|
|
|
const DETACHED_MESSAGE =
|
|
"Page browser is not attached to this computer's Chrome. Use computer_act on the desktop browser instead.";
|
|
|
|
type LivePayload = Partial<PageBrowserResult>;
|
|
|
|
export type LivePageBrowserDriver = (
|
|
computer: ComputerRef,
|
|
request: PageBrowserCommand,
|
|
context: AdapterContext,
|
|
) => Promise<LivePayload>;
|
|
|
|
/**
|
|
* Production page-browser adapter for the bot computer.
|
|
*
|
|
* Fake computers (and the fake sandbox) use the in-process DOM session.
|
|
* Real computers drive the live Chrome already on the graphical display via
|
|
* CDP (`rakazo-page-browser`). If that path cannot operate, return
|
|
* `fallback: "computer_act"` — never report success against a detached DOM.
|
|
*/
|
|
export class ComputerBrowserProvider implements BrowserProvider {
|
|
private readonly fake: FakeBrowserProvider;
|
|
private readonly sandbox?: SandboxProvider;
|
|
private readonly liveDriver?: LivePageBrowserDriver;
|
|
|
|
constructor(
|
|
options: FakeBrowserProviderOptions & {
|
|
sandbox?: SandboxProvider;
|
|
/** Test seam for the live CDP helper. */
|
|
liveDriver?: LivePageBrowserDriver;
|
|
} = {},
|
|
) {
|
|
const { sandbox, liveDriver, ...fakeOptions } = options;
|
|
this.sandbox = sandbox;
|
|
this.liveDriver = liveDriver;
|
|
this.fake = new FakeBrowserProvider(fakeOptions);
|
|
}
|
|
|
|
describe() {
|
|
return {
|
|
id: "computer",
|
|
contractVersion: "1",
|
|
adapterVersion: "0.1.0",
|
|
capabilities: {
|
|
page: Boolean(
|
|
this.liveDriver || this.sandbox?.pageBrowser || this.sandbox?.describe().id === "fake",
|
|
),
|
|
refs: true,
|
|
keyless: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
async navigate(
|
|
computer: ComputerRef,
|
|
request: BrowserNavigateRequest,
|
|
context: AdapterContext,
|
|
): Promise<BrowserNavigateResult> {
|
|
if (this.canUseInProcess(computer)) {
|
|
return this.fake.navigate(computer, request, context);
|
|
}
|
|
const live = await this.runLive(computer, { command: "navigate", url: request.url }, context);
|
|
if (live?.ok !== true || live.fallback === "computer_act") {
|
|
return {
|
|
url: request.url,
|
|
title: "",
|
|
fallback: "computer_act",
|
|
error: live?.error || DETACHED_MESSAGE,
|
|
};
|
|
}
|
|
return {
|
|
url: typeof live.url === "string" ? live.url : request.url,
|
|
title: typeof live.title === "string" ? live.title : "",
|
|
};
|
|
}
|
|
|
|
async snapshot(
|
|
computer: ComputerRef,
|
|
request: BrowserSnapshotRequest,
|
|
context: AdapterContext,
|
|
): Promise<BrowserSnapshotResult> {
|
|
if (this.canUseInProcess(computer)) {
|
|
return this.fake.snapshot(computer, request, context);
|
|
}
|
|
const live = await this.runLive(computer, { command: "snapshot" }, context);
|
|
if (live?.ok !== true || live.fallback === "computer_act") {
|
|
return {
|
|
url: "",
|
|
title: "",
|
|
tree: "",
|
|
elements: [],
|
|
fallback: "computer_act",
|
|
error: live?.error || DETACHED_MESSAGE,
|
|
};
|
|
}
|
|
const elements = Array.isArray(live.elements) ? live.elements : [];
|
|
return {
|
|
url: typeof live.url === "string" ? live.url : "",
|
|
title: typeof live.title === "string" ? live.title : "",
|
|
tree: typeof live.tree === "string" ? live.tree : formatTree(elements),
|
|
elements,
|
|
};
|
|
}
|
|
|
|
async act(
|
|
computer: ComputerRef,
|
|
request: BrowserActRequest,
|
|
context: AdapterContext,
|
|
): Promise<BrowserActResult> {
|
|
if (this.canUseInProcess(computer)) {
|
|
return this.fake.act(computer, request, context);
|
|
}
|
|
const live = await this.runLive(
|
|
computer,
|
|
{ command: "act", actions: request.actions },
|
|
context,
|
|
);
|
|
if (live?.ok !== true || live.fallback === "computer_act") {
|
|
return {
|
|
ok: false,
|
|
completed: typeof live?.completed === "number" ? live.completed : 0,
|
|
uncertain: live?.uncertain ?? true,
|
|
url: typeof live?.url === "string" ? live.url : "",
|
|
title: typeof live?.title === "string" ? live.title : "",
|
|
fallback: "computer_act",
|
|
error: live?.error || DETACHED_MESSAGE,
|
|
};
|
|
}
|
|
const elements = Array.isArray(live.elements) ? live.elements : undefined;
|
|
return {
|
|
ok: true,
|
|
completed: typeof live.completed === "number" ? live.completed : request.actions.length,
|
|
url: typeof live.url === "string" ? live.url : "",
|
|
title: typeof live.title === "string" ? live.title : "",
|
|
tree: typeof live.tree === "string" ? live.tree : elements ? formatTree(elements) : undefined,
|
|
elements,
|
|
};
|
|
}
|
|
|
|
private canUseInProcess(computer: ComputerRef): boolean {
|
|
return computer.kind === "fake";
|
|
}
|
|
|
|
private async runLive(
|
|
computer: ComputerRef,
|
|
request: PageBrowserCommand,
|
|
context: AdapterContext,
|
|
): Promise<LivePayload> {
|
|
context.signal.throwIfAborted();
|
|
try {
|
|
const result = this.liveDriver
|
|
? await this.liveDriver(computer, request, context)
|
|
: this.sandbox?.pageBrowser
|
|
? await this.sandbox.pageBrowser(computer, request, context)
|
|
: {
|
|
ok: false,
|
|
error: DETACHED_MESSAGE,
|
|
fallback: "computer_act" as const,
|
|
uncertain: false,
|
|
};
|
|
if (result?.ok === true) {
|
|
if (typeof result.url !== "string" || typeof result.title !== "string") {
|
|
throw new Error("Incomplete page browser response");
|
|
}
|
|
if (
|
|
request.command === "snapshot" &&
|
|
(typeof result.tree !== "string" || !Array.isArray(result.elements))
|
|
) {
|
|
throw new Error("Incomplete page snapshot");
|
|
}
|
|
if (
|
|
request.command === "act" &&
|
|
(!Number.isInteger(result.completed) ||
|
|
result.completed !== request.actions.length ||
|
|
result.uncertain)
|
|
) {
|
|
throw new Error("Unconfirmed page actions");
|
|
}
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
context.signal.throwIfAborted();
|
|
return {
|
|
uncertain: request.command === "act",
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
fallback: "computer_act",
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatTree(elements: BrowserSnapshotNode[]): string {
|
|
if (elements.length === 0) return "(no interactive elements)";
|
|
return elements
|
|
.map((el) => {
|
|
const value =
|
|
el.value !== undefined && el.value !== "" ? ` value=${JSON.stringify(el.value)}` : "";
|
|
return `- ${el.role} "${el.name}" [${el.ref}]${value}`;
|
|
})
|
|
.join("\n");
|
|
}
|