Frameworks y el Playground
Notas para conectar frameworks concretos a un navegador alojado, y el Playground del panel para probar un script junto a la vista en vivo.
Frameworks
Todo lo que se conecte a Chrome por CDP funciona con la connectUrl. Es de un solo uso, así que un framework que se reconecta por su cuenta necesita una sesión nueva para cada conexión. El navegador arranca cuando te conectas, normalmente en pocos segundos (mira la nota sobre el timeout más arriba); no hay ningún estado que consultar antes.
// 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 y exposeFunction
El motor nunca envía eventos de consola al cliente, porque reenviarlos es justo lo que miden las páginas que buscan un cliente de automatización. Dos llamadas del driver dependen de ellos:
page.setContent()de Playwright espera un evento de consola que nunca llega, así que se agota el tiempo de espera (el de Puppeteer funciona). En su lugar, abre el HTML como una URLdata:o escríbelo tú mismo en la página, como se muestra abajo.exposeFunction()yexposeBinding(), en Playwright y Puppeteer, funcionan en la página que está abierta cuando las llamas y desaparecen en cuanto esa página navega. Llámalas después de la última navegación o, en su lugar, devuelve los resultados desdeevaluate.
// 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)Un helper para empezar
Crear la sesión con los reintentos de arriba, conectarse y detenerla siempre, en una sola función:
// 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");
// });Subir archivos
Una página que pide un archivo —un <input type="file">, un botón “examinar”, una zona de arrastrar y soltar— lo recibe igual en un navegador alojado que en uno local. El archivo se lee en tu máquina y sus bytes viajan por la conexión, así que el navegador nunca ve una ruta en tu disco y puedes subir un archivo que solo existe donde se ejecuta tu script. El límite es de 50 MB por archivo.
Playwright es lo más sencillo. setInputFiles coloca un archivo directamente en el input, sin ningún clic, y funciona incluso cuando el input está oculto: muchos sitios ocultan el input real detrás de un botón con estilos, así que apunta al input en sí en lugar de hacer clic.
// 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")Cuando un botón abre el diálogo de archivos del sistema y no hay ningún input al que puedas apuntar, captura el selector de archivos:
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() y fileChooser.accept() no están disponibles en los navegadores alojados: indican una ruta para que la lea el servidor, que podría apuntar a cualquier parte del worker, así que el gateway los rechaza. Construye el archivo dentro de la página a partir de los bytes que envías y pásalo al input:
// 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" });El mismo DataTransfer hace funcionar una zona de arrastrar y soltar: despacha dragenter, dragover y drop sobre ella con ese objeto. Un arrastre real por CDP con una ruta local (Input.dispatchDragEvent con archivos) se rechaza por la misma razón que uploadFile().
El Playground
El Playground del panel ejecuta un script contra uno de estos navegadores con la vista en vivo, la consola y las capturas de pantalla al lado, y la sección “Use in your code”, justo debajo, muestra las mismas opciones de sesión que el código de arriba. Su page es un pequeño helper que habla directamente por CDP, no Playwright, así que un script del Playground es un boceto que hay que traducir, no un archivo para pegar:
| Helper | Qué hace |
|---|---|
page.goto(url, { timeout? }) | Navegar y esperar el evento load. |
page.click(sel) · page.type(sel, text) · page.press(key) | Entrada real de mouse y teclado, con desplazamiento previo hasta el elemento. |
page.evaluate(fn, ...args) | Ejecutar una función en la página y recibir su resultado en JSON. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Esperar un elemento o la siguiente carga de página. |
page.scroll(px) · page.screenshot({ fullPage? }) | Desplazamiento con la rueda del mouse; un JPEG que aparece en la pestaña “Screenshots”. |
page.title() · page.url() · page.content() | El título, la dirección y el HTML del documento. |
log(...values) · sleep(ms) | Imprimir en la consola (los objetos se muestran formateados); pausar. |
cdp(method, params) | Un comando CDP directo al navegador (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Un comando CDP directo a la página (Page.*, Runtime.*, DOM.*, Network.*). |
Los errores indican la línea del script de la que provienen. “Share” copia un enlace que lleva el script y sus opciones de sesión en la URL, así que no se guarda nada de nuestro lado; quien lo abra lo ejecuta con su propio saldo.