Files
Moltbot/src/browser/routes/actions-core.ts
2025-12-20 01:27:51 +00:00

336 lines
9.1 KiB
TypeScript

import type express from "express";
import {
clickViaPlaywright,
closePageViaPlaywright,
dragViaPlaywright,
evaluateViaPlaywright,
fileUploadViaPlaywright,
fillFormViaPlaywright,
handleDialogViaPlaywright,
hoverViaPlaywright,
navigateBackViaPlaywright,
navigateViaPlaywright,
pressKeyViaPlaywright,
resizeViewportViaPlaywright,
runCodeViaPlaywright,
selectOptionViaPlaywright,
typeViaPlaywright,
waitForViaPlaywright,
} from "../pw-ai.js";
import type { BrowserRouteContext } from "../server-context.js";
import {
jsonError,
toBoolean,
toNumber,
toStringArray,
toStringOrEmpty,
} from "./utils.js";
type MouseButton = "left" | "right" | "middle";
type KeyboardModifier = "Alt" | "Control" | "ControlOrMeta" | "Meta" | "Shift";
function normalizeMouseButton(value: unknown): MouseButton | undefined {
const raw = toStringOrEmpty(value);
if (raw === "left" || raw === "right" || raw === "middle") return raw;
return undefined;
}
function normalizeModifiers(value: unknown): KeyboardModifier[] | undefined {
const raw = toStringArray(value);
if (!raw?.length) return undefined;
const normalized = raw.filter(
(m): m is KeyboardModifier =>
m === "Alt" ||
m === "Control" ||
m === "ControlOrMeta" ||
m === "Meta" ||
m === "Shift",
);
return normalized.length ? normalized : undefined;
}
export type BrowserActionCore =
| "back"
| "click"
| "close"
| "dialog"
| "drag"
| "evaluate"
| "fill"
| "hover"
| "navigate"
| "press"
| "resize"
| "run"
| "select"
| "type"
| "upload"
| "wait";
type ActionCoreParams = {
action: BrowserActionCore;
args: Record<string, unknown>;
targetId: string;
cdpPort: number;
ctx: BrowserRouteContext;
res: express.Response;
};
export async function handleBrowserActionCore(
params: ActionCoreParams,
): Promise<boolean> {
const { action, args, targetId, cdpPort, ctx, res } = params;
const target = targetId || undefined;
switch (action) {
case "close": {
const tab = await ctx.ensureTabAvailable(target);
await closePageViaPlaywright({ cdpPort, targetId: tab.targetId });
res.json({ ok: true, targetId: tab.targetId, url: tab.url });
return true;
}
case "resize": {
const width = toNumber(args.width);
const height = toNumber(args.height);
if (!width || !height) {
jsonError(res, 400, "width and height are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await resizeViewportViaPlaywright({
cdpPort,
targetId: tab.targetId,
width,
height,
});
res.json({ ok: true, targetId: tab.targetId, url: tab.url });
return true;
}
case "dialog": {
const accept = toBoolean(args.accept);
if (accept === undefined) {
jsonError(res, 400, "accept is required");
return true;
}
const promptText = toStringOrEmpty(args.promptText) || undefined;
const tab = await ctx.ensureTabAvailable(target);
const result = await handleDialogViaPlaywright({
cdpPort,
targetId: tab.targetId,
accept,
promptText,
});
res.json({ ok: true, ...result });
return true;
}
case "evaluate": {
const fn = toStringOrEmpty(args.function);
if (!fn) {
jsonError(res, 400, "function is required");
return true;
}
const ref = toStringOrEmpty(args.ref) || undefined;
const tab = await ctx.ensureTabAvailable(target);
const result = await evaluateViaPlaywright({
cdpPort,
targetId: tab.targetId,
fn,
ref,
});
res.json({ ok: true, result });
return true;
}
case "upload": {
const paths = toStringArray(args.paths) ?? [];
const tab = await ctx.ensureTabAvailable(target);
await fileUploadViaPlaywright({
cdpPort,
targetId: tab.targetId,
paths: paths.length ? paths : undefined,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "fill": {
const fields = Array.isArray(args.fields)
? (args.fields as Array<Record<string, unknown>>)
: null;
if (!fields?.length) {
jsonError(res, 400, "fields are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await fillFormViaPlaywright({
cdpPort,
targetId: tab.targetId,
fields,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "press": {
const key = toStringOrEmpty(args.key);
if (!key) {
jsonError(res, 400, "key is required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await pressKeyViaPlaywright({
cdpPort,
targetId: tab.targetId,
key,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "type": {
const ref = toStringOrEmpty(args.ref);
const text = toStringOrEmpty(args.text);
if (!ref || !text) {
jsonError(res, 400, "ref and text are required");
return true;
}
const submit = toBoolean(args.submit) ?? false;
const slowly = toBoolean(args.slowly) ?? false;
const tab = await ctx.ensureTabAvailable(target);
await typeViaPlaywright({
cdpPort,
targetId: tab.targetId,
ref,
text,
submit,
slowly,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "navigate": {
const url = toStringOrEmpty(args.url);
if (!url) {
jsonError(res, 400, "url is required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
const result = await navigateViaPlaywright({
cdpPort,
targetId: tab.targetId,
url,
});
res.json({ ok: true, targetId: tab.targetId, ...result });
return true;
}
case "back": {
const tab = await ctx.ensureTabAvailable(target);
const result = await navigateBackViaPlaywright({
cdpPort,
targetId: tab.targetId,
});
res.json({ ok: true, targetId: tab.targetId, ...result });
return true;
}
case "run": {
const code = toStringOrEmpty(args.code);
if (!code) {
jsonError(res, 400, "code is required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
const result = await runCodeViaPlaywright({
cdpPort,
targetId: tab.targetId,
code,
});
res.json({ ok: true, result });
return true;
}
case "click": {
const ref = toStringOrEmpty(args.ref);
if (!ref) {
jsonError(res, 400, "ref is required");
return true;
}
const doubleClick = toBoolean(args.doubleClick) ?? false;
const button = normalizeMouseButton(args.button);
const modifiers = normalizeModifiers(args.modifiers);
const tab = await ctx.ensureTabAvailable(target);
await clickViaPlaywright({
cdpPort,
targetId: tab.targetId,
ref,
doubleClick,
button,
modifiers,
});
res.json({ ok: true, targetId: tab.targetId, url: tab.url });
return true;
}
case "drag": {
const startRef = toStringOrEmpty(args.startRef);
const endRef = toStringOrEmpty(args.endRef);
if (!startRef || !endRef) {
jsonError(res, 400, "startRef and endRef are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await dragViaPlaywright({
cdpPort,
targetId: tab.targetId,
startRef,
endRef,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "hover": {
const ref = toStringOrEmpty(args.ref);
if (!ref) {
jsonError(res, 400, "ref is required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await hoverViaPlaywright({
cdpPort,
targetId: tab.targetId,
ref,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "select": {
const ref = toStringOrEmpty(args.ref);
const values = toStringArray(args.values);
if (!ref || !values?.length) {
jsonError(res, 400, "ref and values are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await selectOptionViaPlaywright({
cdpPort,
targetId: tab.targetId,
ref,
values,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
case "wait": {
const time = toNumber(args.time);
const text = toStringOrEmpty(args.text) || undefined;
const textGone = toStringOrEmpty(args.textGone) || undefined;
const tab = await ctx.ensureTabAvailable(target);
await waitForViaPlaywright({
cdpPort,
targetId: tab.targetId,
time,
text,
textGone,
});
res.json({ ok: true, targetId: tab.targetId });
return true;
}
default:
return false;
}
}