mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 19:34:00 +00:00
Summary: - The replacement PR extends Browser plugin labeled screenshots to honor Playwright full-page/ref/element scope, returns annotation bounding boxes, and updates docs, tests, and skill guidance. - PR surface: Source +415, Tests +550, Docs +24. Total +989 across 12 files. - Reproducibility: yes. Current main source shows the labeled Playwright helper ignores fullPage/ref/element and omits annotations, and the source PR supplies live before/after commands for the Browser plugin path. Automerge notes: - PR branch already contained follow-up commit before automerge: docs(browser): correct raw-CDP labels caveat in automation skill - PR branch already contained follow-up commit before automerge: fix(browser): preserve labelsSkipped semantics for off-viewport refs - PR branch already contained follow-up commit before automerge: docs(browser): scope labels docs by driver - PR branch already contained follow-up commit before automerge: docs(browser): fix labels annotation indent and document scope fix - PR branch already contained follow-up commit before automerge: docs(browser): indent annotations box schema under --labels bullet - PR branch already contained follow-up commit before automerge: docs(browser): indent labels annotation schema Validation: - ClawSweeper review passed for head70aca6c506. - Required merge gates passed before the squash merge. Prepared head SHA:70aca6c506Review: https://github.com/openclaw/openclaw/pull/92834#issuecomment-4700431344 Co-authored-by: FMLS <kfliuyang@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
209 lines
7.2 KiB
TypeScript
209 lines
7.2 KiB
TypeScript
/**
|
|
* Browser CLI inspection commands for screenshots and snapshots.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import type { Command } from "commander";
|
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import {
|
|
BROWSER_TAB_REFERENCE_HELP,
|
|
callBrowserRequest,
|
|
parseBrowserNonNegativeIntegerValue,
|
|
parseBrowserPositiveIntegerValue,
|
|
type BrowserParentOpts,
|
|
} from "./browser-cli-shared.js";
|
|
import {
|
|
danger,
|
|
defaultRuntime,
|
|
getRuntimeConfig,
|
|
shortenHomePath,
|
|
type SnapshotResult,
|
|
} from "./core-api.js";
|
|
|
|
function parseOptionalIntegerOption(
|
|
value: string | undefined,
|
|
label: string,
|
|
opts: { min: number },
|
|
): number | undefined {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const parsed =
|
|
opts.min === 0
|
|
? parseBrowserNonNegativeIntegerValue(value)
|
|
: parseBrowserPositiveIntegerValue(value);
|
|
if (parsed === undefined || parsed < opts.min) {
|
|
defaultRuntime.error(danger(`Invalid ${label}: must be an integer >= ${opts.min}`));
|
|
defaultRuntime.exit(1);
|
|
return undefined;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
/** Registers Browser screenshot and snapshot commands. */
|
|
export function registerBrowserInspectCommands(
|
|
browser: Command,
|
|
parentOpts: (cmd: Command) => BrowserParentOpts,
|
|
) {
|
|
browser
|
|
.command("screenshot")
|
|
.description("Capture a screenshot (prints the saved path)")
|
|
.argument("[targetId]", BROWSER_TAB_REFERENCE_HELP)
|
|
.option("--full-page", "Capture full scrollable page", false)
|
|
.option("--ref <ref>", "ARIA ref from ai snapshot")
|
|
.option("--element <selector>", "CSS selector for element screenshot")
|
|
.option(
|
|
"--labels",
|
|
"Overlay role refs on the screenshot (works with --full-page, --ref, and --element)",
|
|
false,
|
|
)
|
|
.option("--type <png|jpeg>", "Output type (default: png)", "png")
|
|
.action(async (targetId: string | undefined, opts, cmd) => {
|
|
const parent = parentOpts(cmd);
|
|
const profile = parent?.browserProfile;
|
|
try {
|
|
const result = await callBrowserRequest<{ path: string }>(
|
|
parent,
|
|
{
|
|
method: "POST",
|
|
path: "/screenshot",
|
|
query: profile ? { profile } : undefined,
|
|
body: {
|
|
targetId: normalizeOptionalString(targetId),
|
|
fullPage: Boolean(opts.fullPage),
|
|
ref: normalizeOptionalString(opts.ref),
|
|
element: normalizeOptionalString(opts.element),
|
|
labels: Boolean(opts.labels),
|
|
type: opts.type === "jpeg" ? "jpeg" : "png",
|
|
},
|
|
},
|
|
{ timeoutMs: 20000 },
|
|
);
|
|
if (parent?.json) {
|
|
defaultRuntime.writeJson(result);
|
|
return;
|
|
}
|
|
defaultRuntime.log(shortenHomePath(result.path));
|
|
} catch (err) {
|
|
defaultRuntime.error(danger(String(err)));
|
|
defaultRuntime.exit(1);
|
|
}
|
|
});
|
|
|
|
browser
|
|
.command("snapshot")
|
|
.description("Capture a snapshot (default: ai; aria is the accessibility tree)")
|
|
.option("--format <aria|ai>", "Snapshot format (default: ai)", "ai")
|
|
.option("--target-id <id>", BROWSER_TAB_REFERENCE_HELP)
|
|
.option("--limit <n>", "Max nodes (default: 500/800)")
|
|
.option("--mode <efficient>", "Snapshot preset (efficient)")
|
|
.option("--efficient", "Use the efficient snapshot preset", false)
|
|
.option("--interactive", "Role snapshot: interactive elements only", false)
|
|
.option("--compact", "Role snapshot: compact output", false)
|
|
.option("--depth <n>", "Role snapshot: max depth")
|
|
.option("--selector <sel>", "Role snapshot: scope to CSS selector")
|
|
.option("--frame <sel>", "Role snapshot: scope to an iframe selector")
|
|
.option("--labels", "Include label overlay screenshot with annotations", false)
|
|
.option("--urls", "Append discovered link URLs to AI snapshots", false)
|
|
.option("--out <path>", "Write snapshot to a file")
|
|
.action(async (opts, cmd) => {
|
|
const parent = parentOpts(cmd);
|
|
const profile = parent?.browserProfile;
|
|
const format = opts.format === "aria" ? "aria" : "ai";
|
|
const formatWasExplicit =
|
|
typeof cmd.getOptionValueSource === "function" &&
|
|
cmd.getOptionValueSource("format") === "cli";
|
|
const configMode =
|
|
!formatWasExplicit &&
|
|
format === "ai" &&
|
|
getRuntimeConfig().browser?.snapshotDefaults?.mode === "efficient"
|
|
? "efficient"
|
|
: undefined;
|
|
const mode = opts.efficient === true || opts.mode === "efficient" ? "efficient" : configMode;
|
|
const limit = parseOptionalIntegerOption(opts.limit, "--limit", { min: 1 });
|
|
const depth = parseOptionalIntegerOption(opts.depth, "--depth", { min: 0 });
|
|
if (
|
|
(opts.limit !== undefined && limit === undefined) ||
|
|
(opts.depth !== undefined && depth === undefined)
|
|
) {
|
|
return;
|
|
}
|
|
try {
|
|
const query: Record<string, string | number | boolean | undefined> = {
|
|
format,
|
|
targetId: normalizeOptionalString(opts.targetId),
|
|
limit,
|
|
interactive: opts.interactive ? true : undefined,
|
|
compact: opts.compact ? true : undefined,
|
|
depth,
|
|
selector: normalizeOptionalString(opts.selector),
|
|
frame: normalizeOptionalString(opts.frame),
|
|
labels: opts.labels ? true : undefined,
|
|
urls: opts.urls ? true : undefined,
|
|
mode,
|
|
profile,
|
|
};
|
|
const result = await callBrowserRequest<SnapshotResult>(
|
|
parent,
|
|
{
|
|
method: "GET",
|
|
path: "/snapshot",
|
|
query,
|
|
},
|
|
{ timeoutMs: 20000 },
|
|
);
|
|
|
|
if (opts.out) {
|
|
if (result.format === "ai") {
|
|
await fs.writeFile(opts.out, result.snapshot, "utf8");
|
|
} else {
|
|
const payload = JSON.stringify(result, null, 2);
|
|
await fs.writeFile(opts.out, payload, "utf8");
|
|
}
|
|
if (parent?.json) {
|
|
defaultRuntime.writeJson({
|
|
ok: true,
|
|
out: opts.out,
|
|
...(result.format === "ai" && result.imagePath
|
|
? { imagePath: result.imagePath }
|
|
: {}),
|
|
});
|
|
} else {
|
|
defaultRuntime.log(shortenHomePath(opts.out));
|
|
if (result.format === "ai" && result.imagePath) {
|
|
defaultRuntime.log(shortenHomePath(result.imagePath));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (parent?.json) {
|
|
defaultRuntime.writeJson(result);
|
|
return;
|
|
}
|
|
|
|
if (result.format === "ai") {
|
|
defaultRuntime.log(result.snapshot);
|
|
if (result.imagePath) {
|
|
defaultRuntime.log(shortenHomePath(result.imagePath));
|
|
}
|
|
return;
|
|
}
|
|
|
|
const nodes = "nodes" in result ? result.nodes : [];
|
|
defaultRuntime.log(
|
|
nodes
|
|
.map((n) => {
|
|
const indent = " ".repeat(Math.min(20, n.depth));
|
|
const name = n.name ? ` "${n.name}"` : "";
|
|
const value = n.value ? ` = "${n.value}"` : "";
|
|
return `${indent}- ${n.role}${name}${value}`;
|
|
})
|
|
.join("\n"),
|
|
);
|
|
} catch (err) {
|
|
defaultRuntime.error(danger(String(err)));
|
|
defaultRuntime.exit(1);
|
|
}
|
|
});
|
|
}
|