openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
277 lines • 11.9 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
import { zod } from '../third_party/index.js';
import { ToolCategory } from './categories.js';
import { definePageTool } from './ToolDefinition.js';
async function getSourceBox(page, element, fullPage) {
if (element) {
const viewport = page.viewport();
const [box, devicePixelRatio] = await Promise.all([
element.boundingBox(),
viewport
? (viewport.deviceScaleFactor ?? 1)
: page.evaluate(() => window.devicePixelRatio),
]);
return box ? { ...box, devicePixelRatio } : undefined;
}
if (fullPage) {
const dims = await page.evaluate(() => ({
width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
devicePixelRatio: window.devicePixelRatio,
}));
if (dims.width <= 0 || dims.height <= 0) {
return undefined;
}
return {
x: 0,
y: 0,
width: dims.width,
height: dims.height,
devicePixelRatio: dims.devicePixelRatio,
};
}
const viewport = page.viewport();
if (viewport) {
return {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
devicePixelRatio: viewport.deviceScaleFactor ?? 1,
};
}
// The browser is launched and connected with `defaultViewport: null`, so
// `page.viewport()` stays null until something emulates one. Fall back to the
// window's own dimensions, which is the area a viewport screenshot captures.
const dims = await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio,
}));
if (dims.width <= 0 || dims.height <= 0) {
return undefined;
}
return {
x: 0,
y: 0,
width: dims.width,
height: dims.height,
devicePixelRatio: dims.devicePixelRatio,
};
}
function computeDownscaleClip(box, maxWidth, maxHeight) {
const widthScale = maxWidth !== undefined
? Math.min(1, maxWidth / (box.width * box.devicePixelRatio))
: 1;
const heightScale = maxHeight !== undefined
? Math.min(1, maxHeight / (box.height * box.devicePixelRatio))
: 1;
const scale = Math.min(widthScale, heightScale);
if (scale >= 1) {
return undefined;
}
// Skip degenerate sub-pixel results.
if (Math.round(box.width * box.devicePixelRatio * scale) < 1 ||
Math.round(box.height * box.devicePixelRatio * scale) < 1) {
return undefined;
}
return {
x: box.x,
y: box.y,
width: box.width,
height: box.height,
scale,
};
}
export const screenshot = definePageTool(args => {
const { screenshotFormat, screenshotQuality, screenshotMaxWidth, screenshotMaxHeight, } = args ?? {};
const defaultFormat = screenshotFormat ?? 'png';
return {
name: 'take_screenshot',
description: `Take a screenshot of the page or element.`,
annotations: {
category: ToolCategory.DEBUGGING,
// Not read-only due to filePath param.
readOnlyHint: false,
},
schema: {
format: zod
.enum(['png', 'jpeg', 'webp'])
.default(defaultFormat)
.describe(`Type of format to save the screenshot as. Default is "${defaultFormat}"`),
quality: zod
.number()
.min(0)
.max(100)
.optional()
.describe('Compression quality for JPEG and WebP formats (0-100). Higher values mean better quality but larger file sizes. Ignored for PNG format.'),
uid: zod
.string()
.optional()
.describe('The uid of an element on the page from the page content snapshot. If omitted, takes a page screenshot.'),
fullPage: zod
.boolean()
.optional()
.describe('If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.'),
filePath: zod
.string()
.optional()
.describe('The absolute path, or a path relative to the current working directory, to save the screenshot to instead of attaching it to the response.'),
},
blockedByDialog: true,
verifyFilesSchema: {
filePath: true,
},
handler: async (request, response, context) => {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
if (request.params.uid && request.params.fullPage) {
throw new Error('Providing both "uid" and "fullPage" is not allowed.');
}
const page = request.page.pptrPage;
const element = __addDisposableResource(env_1, request.params.uid
? await request.page.getElementByUid(request.params.uid)
: undefined, false);
const format = request.params.format;
const quality = format === 'png'
? undefined
: (request.params.quality ?? screenshotQuality);
const fullPage = request.params.fullPage ?? false;
// `getElementByUid` mints a fresh ElementHandle per call, so dispose it
// once the capture is done to avoid leaking a remote object for the life
// of the page's execution context.
let screenshot;
// Compute a downscale clip when --screenshot-max-width or
// --screenshot-max-height is set and the source exceeds either bound.
// The smaller scale factor wins so both bounds are respected while
// preserving aspect ratio.
let clip;
if (screenshotMaxWidth !== undefined ||
screenshotMaxHeight !== undefined) {
const box = await getSourceBox(page, element, fullPage);
if (box) {
clip = computeDownscaleClip(box, screenshotMaxWidth, screenshotMaxHeight);
}
}
if (clip) {
// page.screenshot with clip lets the CDP scale param downscale the
// capture for viewport, full-page and element shots alike. We rely on
// Puppeteer's default of captureBeyondViewport=true when a clip is
// present so element/full-page captures below the fold still work.
screenshot = await page.screenshot({
type: format,
quality,
optimizeForSpeed: true,
clip,
});
}
else if (element) {
screenshot = await element.screenshot({
type: format,
quality,
optimizeForSpeed: true,
});
}
else {
screenshot = await page.screenshot({
type: format,
fullPage,
quality,
optimizeForSpeed: true,
});
}
if (request.params.uid) {
response.appendResponseLine(`Took a screenshot of node with uid "${request.params.uid}".`);
}
else if (fullPage) {
response.appendResponseLine('Took a screenshot of the full current page.');
}
else {
response.appendResponseLine("Took a screenshot of the current page's viewport.");
}
// Narrow `format` at the point of use: in the factory form of
// definePageTool TS widens the Schema generic, which loses the literal
// union from zod.enum on request.params.format.
const extension = format === 'jpeg' ? '.jpeg' : format === 'webp' ? '.webp' : '.png';
if (request.params.filePath) {
const result = await context.saveFile(screenshot, request.params.filePath, extension);
response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
}
else if (screenshot.length >= 2_000_000) {
const { filepath } = await context.saveTemporaryFile(screenshot, `screenshot${extension}`);
response.appendResponseLine(`Saved screenshot to ${filepath}.`);
}
else {
response.attachImage({
mimeType: `image/${format}`,
data: Buffer.from(screenshot).toString('base64'),
});
}
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}
},
};
});
//# sourceMappingURL=screenshot.js.map