puppeteer-browser-agent
Version:
Puppeteer browser agent for use in AI agent systems
208 lines (177 loc) • 4.25 kB
text/typescript
import puppeteer from "puppeteer";
import type {
Viewport,
Page,
HTTPRequest,
HTTPResponse,
Frame,
Browser,
} from "puppeteer";
import type { IAgentDriver } from "./driver";
export type PageHeaders = Record<string, string>;
export type ViewportOpts = {
width?: number;
height?: number;
scaleFactor?: number;
};
export type BrowserOpts = {
debug?: boolean;
headless?: boolean;
viewport?: ViewportOpts;
};
export interface IAgentBrowser {
launch(): Promise<void>;
start(): Promise<Page>;
close(): void;
}
export class AgentBrowser implements IAgentBrowser {
page?: Page;
launched = false;
loaded = false;
debug = false;
headless = false;
downloadStarted = false;
responses = 0;
requests = 0;
view: ViewportOpts = {};
browser?: Browser;
driver: IAgentDriver;
constructor(driver: IAgentDriver, opts: BrowserOpts = {}) {
this.driver = driver;
this.debug = Boolean(opts.debug);
this.headless = Boolean(opts.headless);
this.view = opts.viewport || {};
}
public async launch() {
if (this.launched) {
return;
}
this.browser = await this.createPuppeteerBrowser();
this.page = await this.getNewPage();
await this.page?.setViewport(this.viewport);
this.launched = true;
}
async getNewPage() {
return await this.browser?.newPage();
}
async createPuppeteerBrowser() {
return await puppeteer.launch({
headless: this.headless ? "shell" : false,
});
}
public async start() {
await this.launch();
if (!this.page) {
throw new Error("Browser must be launched before it can be started");
}
const page = this.page;
page.on("request", (request: HTTPRequest) => {
this.onRequestBlock(request);
this.incRequestCount();
});
page.on("load", () => {
this.loaded = true;
});
page.on("framenavigated", async (frame: Frame) => {
if (frame === page.mainFrame()) {
if (frame.childFrames.length < 5) {
if (this.loaded) {
this.print("Loading page...");
}
this.loaded = false;
} else {
await this.sleep(500);
}
}
});
page.on("response", async (response: HTTPResponse) => {
this.incResponseCount();
if (this.isLargeResponse(response)) {
setTimeout(() => {
if (this.responses === 1) {
this.print("DOWNLOAD: A file download has been detected");
this.startDownload();
}
}, 2000);
}
});
return page;
}
public close() {
if (!this.browser) {
return;
}
this.browser.close();
}
protected get defaultViewport() {
return {
width: 1200,
height: 1200,
deviceScaleFactor: 1,
};
}
get viewport(): Viewport {
return {
...this.defaultViewport,
...this.view,
};
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
protected print(...msgs: any[]) {
// biome-ignore lint/style/useBlockStatements: <explanation>
if (!this.debug) return;
console.info(...msgs);
}
protected isBlock(_request: HTTPRequest) {
return false;
}
protected onRequestBlock(request: HTTPRequest) {
if (!this.isBlock(request)) {
return;
}
if (request.isNavigationRequest()) {
request.respond({
status: 200,
contentType: "application/octet-stream",
body: "Dummy file to block navigation",
});
} else {
request.continue();
}
}
protected incRequestCount() {
this.requests++;
}
protected startDownload() {
this.downloadStarted = true;
}
protected isLargeResponse(response: HTTPResponse): boolean {
const headers = response.headers();
return (
this.hasAttachment(headers) ||
this.hasLargeContent(headers) ||
this.isBinary(headers)
);
}
protected isBinary(headers: PageHeaders): boolean {
return headers["content-type"] === "application/octet-stream";
}
protected hasAttachment(headers: PageHeaders): boolean {
return Boolean(headers["content-disposition"]?.includes("attachment"));
}
protected hasLargeContent(headers: PageHeaders): boolean {
return Number(headers["content-length"]) > 1024 * this.maxMbs;
}
protected get maxMbs() {
return 200;
}
protected incResponseCount() {
this.responses++;
}
protected async sleep(ms: number, debug = true) {
if (this.debug && debug) {
this.print(`Sleeping ${ms} ms`);
}
return new Promise((resolve) => setTimeout(resolve, ms));
}
}