Frameworks und der Playground
Hinweise, wie Sie bestimmte Frameworks mit einem gehosteten Browser verbinden, und der Playground im Dashboard, in dem Sie ein Skript neben der Live-Ansicht ausprobieren.
Frameworks
Alles, was sich per CDP an Chrome anhängt, funktioniert mit der connectUrl. Sie gilt nur einmal; ein Framework, das sich selbstständig neu verbindet, braucht daher für jede Verbindung eine neue Session. Der Browser startet, wenn Sie sich verbinden, meist innerhalb weniger Sekunden (siehe den Hinweis zum Timeout oben); vorher muss kein Status abgefragt werden.
// Patchright (optional; a Playwright fork): npm i patchright
import { chromium } from "patchright";
const browser = await chromium.connectOverCDP(connectUrl, { timeout: 120_000 });
const page = browser.contexts()[0].pages()[0];# Browser Use
from browser_use import Agent, Browser
agent = Agent(task="Find the cheapest flight to Lisbon next Friday", llm=llm, browser=Browser(cdp_url=connect_url))
await agent.run()
# Crawl4AI
from crawl4ai import AsyncWebCrawler, BrowserConfig
config = BrowserConfig(browser_mode="custom", cdp_url=connect_url, use_managed_browser=True)
async with AsyncWebCrawler(config=config) as crawler:
result = await crawler.arun("https://example.com")// Stagehand v4
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
const stagehand = await Stagehand.create({ browser: await localBrowser.connect({ cdpUrl: connectUrl }) });setContent und exposeFunction
Die Engine sendet nie Console-Events an den Client, denn Seiten, die nach einem Automatisierungs-Client suchen, messen genau diese Weiterleitung. Zwei Treiberaufrufe hängen davon ab:
page.setContent()von Playwright wartet auf ein Console-Event, das nie eintrifft, und läuft deshalb in einen Timeout (in Puppeteer funktioniert der Aufruf). Öffnen Sie das HTML stattdessen alsdata:-URL, oder schreiben Sie es selbst in die Seite, wie unten gezeigt.exposeFunction()undexposeBinding()funktionieren in Playwright und Puppeteer in der Seite, die beim Aufruf geöffnet ist, und verschwinden, sobald diese Seite navigiert. Rufen Sie sie nach der letzten Navigation auf, oder geben Sie Ergebnisse stattdessen überevaluatezurück.
// Instead of await page.setContent(html):
// Short HTML: a data: URL (the page gets no origin of its own, so no cookies or storage)
await page.goto("data:text/html," + encodeURIComponent(html));
// Any HTML: write it into a page you have navigated to; it keeps that page's origin
await page.evaluate((html) => {
document.open();
document.write(html);
document.close();
}, html);# Instead of page.set_content(html):
from urllib.parse import quote
page.goto("data:text/html," + quote(html))
# or
page.evaluate("html => { document.open(); document.write(html); document.close(); }", html)Ein Helper als Ausgangspunkt
Anlegen mit den Retries von oben, verbinden und immer stoppen – alles in einer Funktion:
// clearcote-hosted.ts
import { chromium, type Browser } from "playwright"; // or "patchright"
const API = "https://www.clearcotelabs.com/api/v1/browsers";
const AUTH = { authorization: "Bearer " + process.env.CLEARCOTE_API_KEY };
const RETRY_CODES = new Set(["NO_CAPACITY", "NO_WORKER"]);
export async function createSession(options: Record<string, unknown> = {}, attempts = 5) {
for (let i = 0; ; i++) {
const res = await fetch(API, {
method: "POST",
headers: { ...AUTH, "content-type": "application/json" },
body: JSON.stringify(options),
});
const body = await res.json().catch(() => ({}));
if (res.ok) return body as { id: string; connectUrl: string; worker: string };
const retry = RETRY_CODES.has(body.code) || (res.status === 429 && !body.code) || [500, 502, 504].includes(res.status);
if (!retry || i + 1 >= attempts) throw new Error([res.status, body.code, body.error].filter(Boolean).join(" "));
await new Promise((r) => setTimeout(r, Math.min(15_000, 1000 * 2 ** i) * (0.5 + Math.random())));
}
}
export async function withBrowser<T>(options: Record<string, unknown>, work: (browser: Browser) => Promise<T>) {
const session = await createSession(options);
try {
const browser = await chromium.connectOverCDP(session.connectUrl, { timeout: 120_000 });
try {
return await work(browser);
} finally {
await browser.close().catch(() => {});
}
} finally {
// Ends the session if closing the browser did not (a no-op otherwise).
await fetch(API + "/" + session.id, { method: "DELETE", headers: AUTH }).catch(() => {});
}
}
// await withBrowser({ profile: { name: "shop", persist: true }, country: "de" }, async (browser) => {
// const page = browser.contexts()[0].pages()[0];
// await page.goto("https://example.com");
// });Dateien hochladen
Eine Seite, die nach einer Datei fragt — ein <input type="file">, ein „Durchsuchen“-Button, ein Drag-and-Drop-Bereich — nimmt sie in einem gehosteten Browser genauso entgegen wie in einem lokalen. Die Datei wird auf Ihrem Rechner gelesen, und ihre Bytes laufen über die Verbindung, sodass der Browser nie einen Pfad auf Ihrer Festplatte sieht und Sie eine Datei hochladen können, die nur dort existiert, wo Ihr Skript läuft. Das Limit liegt bei 50 MB pro Datei.
Playwright ist am einfachsten. setInputFiles legt eine Datei direkt in das Input-Feld, ohne Klick, und funktioniert auch, wenn das Input-Feld verborgen ist — viele Sites verbergen das echte Input-Feld hinter einem gestylten Button, zielen Sie also auf das Input-Feld selbst, statt zu klicken.
// Node.js + Playwright
await page.setInputFiles("input[type=file]", "C:/Users/me/invoice.pdf");
// or build the file in your script, with nothing on disk:
await page.setInputFiles("input[type=file]", {
name: "invoice.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("%PDF-1.4 …"),
});# Python + Playwright
page.set_input_files("input[type=file]", "/home/me/invoice.pdf")Öffnet ein Button den System-Dateidialog und gibt es kein Input-Feld, auf das Sie zielen können, fangen Sie stattdessen den Dateidialog ab:
const [chooser] = await Promise.all([
page.waitForEvent("filechooser"),
page.click("text=Browse"), // the page's own button
]);
await chooser.setFiles("C:/Users/me/invoice.pdf");Puppeteer. elementHandle.uploadFile() und fileChooser.accept()stehen in gehosteten Browsern nicht zur Verfügung: Sie benennen einen Pfad, den der Server lesen soll und der irgendwohin auf dem Worker zeigen könnte, weshalb das Gateway sie ablehnt. Bauen Sie die Datei in der Seite selbst aus Bytes, die Sie hineinsenden, und übergeben Sie sie dem Input-Feld:
// Puppeteer: construct the file in the page
const b64 = fs.readFileSync("invoice.pdf").toString("base64");
await page.evaluate(({ b64, name, type }) => {
const data = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const dt = new DataTransfer();
dt.items.add(new File([data], name, { type }));
const input = document.querySelector("input[type=file]");
input.files = dt.files;
input.dispatchEvent(new Event("change", { bubbles: true }));
}, { b64, name: "invoice.pdf", type: "application/pdf" });Dasselbe DataTransfer-Objekt treibt eine Drag-and-Drop-Zone an: Lösen Sie darauf dragenter, dragover und drop aus, die dieses Objekt tragen. Ein echtes CDP-Drag mit einem lokalen Pfad (Input.dispatchDragEvent mit Dateien) wird aus demselben Grund abgelehnt wie uploadFile().
Der Playground
Der Playground im Dashboard führt ein Skript gegen einen dieser Browser aus, mit Live-Ansicht, Konsole und Screenshots daneben, und das Panel „Use in your code“ darunter zeigt dieselben Session-Optionen wie der Code oben. Sein page ist ein kleiner Helper, der direkt CDP spricht, nicht Playwright – ein Playground-Skript ist also eine Skizze, die Sie übertragen müssen, keine Datei zum Einfügen:
| Helper | Wirkung |
|---|---|
page.goto(url, { timeout? }) | Navigieren und auf das load-Event warten. |
page.click(sel) · page.type(sel, text) · page.press(key) | Echte Maus- und Tastatureingaben; das Element wird vorher in den sichtbaren Bereich gescrollt. |
page.evaluate(fn, ...args) | Eine Funktion in der Seite ausführen und ihr JSON-Ergebnis zurückbekommen. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Auf ein Element warten oder darauf, dass die nächste Seite geladen ist. |
page.scroll(px) · page.screenshot({ fullPage? }) | Mit dem Mausrad scrollen; ein JPEG, das im Tab Screenshots landet. |
page.title() · page.url() · page.content() | Titel, Adresse und HTML des Dokuments. |
log(...values) · sleep(ms) | In die Konsole schreiben (Objekte werden formatiert ausgegeben); pausieren. |
cdp(method, params) | Ein roher CDP-Befehl an den Browser (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Ein roher CDP-Befehl an die Seite (Page.*, Runtime.*, DOM.*, Network.*). |
Fehler nennen die Skriptzeile, aus der sie stammen. Share kopiert einen Link, der das Skript und seine Session-Optionen in der URL trägt, bei uns wird also nichts gespeichert; wer den Link öffnet, führt das Skript auf eigenes Guthaben aus.