Chuyển đến nội dung

Framework và Playground

Ghi chú để kết nối từng framework với trình duyệt hosted, và Playground trong dashboard để thử một script ngay cạnh phần xem trực tiếp.

Framework

Bất cứ thứ gì gắn vào Chrome qua CDP đều hoạt động với connectUrl. URL này chỉ dùng một lần, nên framework nào tự kết nối lại sẽ cần một phiên mới cho mỗi kết nối. Trình duyệt khởi động khi bạn kết nối, thường trong vòng vài giây (xem ghi chú về timeout ở trên); không có trạng thái nào cần poll trước.

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 và exposeFunction

Engine không bao giờ gửi sự kiện console tới client, vì chính việc chuyển tiếp chúng là thứ mà các trang dò tìm client tự động hóa đo lường. Có hai lệnh gọi của driver phụ thuộc vào các sự kiện này:

  • page.setContent() của Playwright chờ một sự kiện console không bao giờ tới, nên sẽ bị timeout (bản của Puppeteer thì vẫn chạy). Thay vào đó, hãy mở HTML dưới dạng URL data:, hoặc tự ghi nó vào trang, như bên dưới.
  • exposeFunction() và exposeBinding(), trong cả Playwright lẫn Puppeteer, hoạt động trong trang đang mở lúc bạn gọi chúng, và sẽ mất khi trang đó điều hướng sang trang khác. Hãy gọi chúng sau lần điều hướng cuối cùng, hoặc thay vào đó trả kết quả về từ 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)

Một helper để bắt đầu

Tạo phiên với cơ chế thử lại ở trên, kết nối, và luôn dừng phiên, tất cả trong một hàm:

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");
// });

Upload file

Một trang yêu cầu một file — một <input type="file">, một nút “browse”, một vùng kéo-thả — nhận file theo cùng một cách trong một trình duyệt hosted như trong một trình duyệt local. File được đọc trên máy của bạn và các byte của nó truyền qua kết nối, nên trình duyệt không bao giờ thấy đường dẫn trên ổ đĩa của bạn, và bạn có thể upload một file chỉ tồn tại ở nơi script của bạn chạy. Giới hạn là 50 MB mỗi file.

Playwright là cách đơn giản nhất. setInputFiles đưa thẳng một file vào input, không cần click, và vẫn chạy ngay cả khi input bị ẩn — nhiều site giấu input thật đằng sau một nút được tạo kiểu riêng, nên hãy nhắm vào chính input đó thay vì đi click.

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")

Khi một nút mở hộp thoại chọn file của hệ thống và không có input nào để bạn nhắm tới, hãy bắt lấy chooser thay vì vậy:

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. elementHandle.uploadFile() và fileChooser.accept() không dùng được trên trình duyệt hosted: chúng chỉ định một đường dẫn để máy chủ đọc, mà đường dẫn đó có thể trỏ tới bất cứ đâu trên worker, nên gateway từ chối chúng. Hãy dựng file ngay bên trong trang từ các byte bạn gửi vào, rồi giao nó cho 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" });

Chính DataTransfer đó điều khiển một vùng kéo-thả: hãy dispatch dragenter, dragover và drop lên nó kèm theo object đó. Một thao tác kéo CDP thật với đường dẫn local (Input.dispatchDragEvent kèm file) bị từ chối vì cùng lý do như uploadFile().

Playground

Playground trong dashboard chạy một script trên một trong các trình duyệt này, với live view, console và ảnh chụp màn hình ngay bên cạnh, còn panel “Use in your code” bên dưới hiển thị cùng các tùy chọn phiên như code ở trên. page của nó là một helper nhỏ giao tiếp trực tiếp qua CDP, không phải Playwright, nên script Playground là bản phác thảo cần chuyển đổi, không phải file để dán nguyên vào:

HelperChức năng
page.goto(url, { timeout? })Điều hướng và chờ sự kiện load.
page.click(sel) · page.type(sel, text) · page.press(key)Thao tác chuột và bàn phím thật, cuộn phần tử vào tầm nhìn trước.
page.evaluate(fn, ...args)Chạy một hàm trong trang và nhận lại kết quả JSON.
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation()Chờ một phần tử, hoặc chờ lần tải trang tiếp theo.
page.scroll(px) · page.screenshot({ fullPage? })Cuộn bằng con lăn chuột; một ảnh JPEG xuất hiện trong tab “Screenshots”.
page.title() · page.url() · page.content()Tiêu đề, địa chỉ và HTML của tài liệu.
log(...values) · sleep(ms)In ra console (object được định dạng cho dễ đọc); tạm dừng.
cdp(method, params)Một lệnh CDP thô gửi tới trình duyệt (Target.*, Browser.*, Storage.*).
page.cdp(method, params)Một lệnh CDP thô gửi tới trang (Page.*, Runtime.*, DOM.*, Network.*).

Lỗi cho biết dòng script gây ra nó. “Share” sao chép một liên kết chứa script và các tùy chọn phiên ngay trong URL, nên không có gì được lưu ở phía chúng tôi; ai mở liên kết sẽ chạy script bằng số dư của chính họ.