Pular para o conteúdo

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.

javascript
// 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];
python
# 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")
javascript
// 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 URL data: ou escreva-o você mesmo na página, como abaixo.
  • O exposeFunction() e o exposeBinding(), 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 pelo evaluate.
javascript
// 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);
python
# 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:

typescript
// 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.

javascript
// 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
# 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:

javascript
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:

javascript
// 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:

HelperO 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.