Frameworks e o Playground
Notas para conectar frameworks específicos a um navegador hospedado e o Playground do painel, para testar um script ao lado da visualização ao vivo.
Frameworks
Qualquer coisa que se conecte ao Chrome via CDP funciona com a connectUrl. Ela é de uso único, então um framework que se reconecta sozinho precisa de uma nova sessão para cada conexão. O navegador inicia quando você se conecta, geralmente em poucos segundos (veja a observação sobre o timeout acima); não há nenhum status para 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 e exposeFunction
O motor nunca envia eventos de console ao cliente, porque repassá-los é exatamente o que medem as páginas que procuram um cliente de automação. Duas chamadas do driver dependem deles:
- O
page.setContent()do Playwright espera por um evento de console que nunca chega, então dá timeout (o do Puppeteer funciona). Em vez disso, abra o HTML como uma URLdata:ou escreva-o você mesmo na página, como abaixo. - O
exposeFunction()e oexposeBinding(), no Playwright e no Puppeteer, funcionam na página que está aberta quando você os chama e somem assim que essa página navega. Chame-os depois da última navegação ou, em vez disso, devolva os resultados peloevaluate.
// 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)Um helper para começar
Criar com as novas tentativas descritas acima, conectar e sempre encerrar, tudo numa só função:
// 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");
// });Enviando arquivos
Uma página que pede um arquivo — um <input type="file">, um botão de “procurar”, uma área de arrastar e soltar — recebe esse arquivo da mesma forma num navegador hospedado e num local. O arquivo é lido na sua máquina e seus bytes trafegam pela conexão, então o navegador nunca vê um caminho no seu disco e você pode enviar um arquivo que só existe onde o seu script roda. O limite é de 50 MB por arquivo.
Playwright é o mais simples. O setInputFiles coloca um arquivo direto no input, sem nenhum clique, e funciona mesmo quando o input está oculto — muitos sites escondem o input de verdade atrás de um botão estilizado, então mire no próprio input em vez de clicar.
// 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")Quando um botão abre a janela de arquivos do sistema e não há nenhum input que você possa mirar, capture o seletor de arquivos em vez disso:
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. O elementHandle.uploadFile() e o fileChooser.accept() não estão disponíveis em navegadores hospedados: eles indicam um caminho para o servidor ler, que poderia apontar para qualquer lugar do worker, então o gateway os recusa. Construa o arquivo dentro da página a partir dos bytes que você envia e entregue-o ao 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" });O mesmo DataTransfer comanda uma área de arrastar e soltar: dispare dragenter, dragover e drop nela carregando esse objeto. Um arraste de CDP de verdade com um caminho local (Input.dispatchDragEvent com arquivos) é recusado pela mesma razão que o uploadFile().
O Playground
O Playground do painel roda um script contra um desses navegadores com a visualização ao vivo, o console e os screenshots ao lado, e a área “Use in your code”, logo abaixo, mostra as mesmas opções de sessão do código acima. O page dele é um pequeno helper que fala diretamente via CDP, e não o Playwright, então um script do Playground é um rascunho para adaptar, não um arquivo para colar:
| Helper | O que faz |
|---|---|
page.goto(url, { timeout? }) | Navega e espera o evento load. |
page.click(sel) · page.type(sel, text) · page.press(key) | Input real de mouse e teclado, rolando o elemento para a área visível antes. |
page.evaluate(fn, ...args) | Executa uma função na página e devolve o resultado em JSON. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Espera um elemento ou o próximo carregamento de página. |
page.scroll(px) · page.screenshot({ fullPage? }) | Rolagem com a roda do mouse; um JPEG que vai para a aba “Screenshots”. |
page.title() · page.url() · page.content() | O título, o endereço e o HTML do documento. |
log(...values) · sleep(ms) | Imprime no console (objetos saem formatados); pausa. |
cdp(method, params) | Um comando CDP bruto para o navegador (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Um comando CDP bruto para a página (Page.*, Runtime.*, DOM.*, Network.*). |
Os erros informam a linha do script de onde vieram. Share copia um link que leva o script e as opções de sessão na própria URL, então nada fica armazenado do nosso lado; quem abrir o link roda o script com o próprio saldo.