ブラウザ自動化のワークフローでは、人手による介入が必要な場合があります。ログインページで多要素認証が必要だったり、自動化スクリプトに渡したくない機密情報の入力が必要だったり、完全に自動化するには複雑すぎる作業があったりします。Human in the Loop を使うと、人間が Live View 経由でライブのブラウザセッションに入り、自動化できない部分を処理したあと、操作をスクリプトに戻せます。
- 認証フロー: プログラムから回避できない MFA、SSO、CAPTCHA があるログインページ
- 機密データの入力: 自動化スクリプトに渡したくない認証情報や個人情報を求めるフォーム
- 複雑な操作: ダッシュボードの設定やワークフローの承認など、完全に自動化するには難しい、または見合わない単発作業
- 確認ステップ: 注文の確認、生成コンテンツのレビュー、スクリプト続行前の承認
Human in the Loop は、任意の Browser Session で使えます。人間の介入には、次の 2 つの方法があります。
スクリプトは Cloudflare の CDP コマンドを使い、人間の介入を正式に要求し、完了を待ちます。
- 自動化スクリプトが、人間の入力が必要な場面に遭遇します。
- スクリプトは
Cloudflare.handoffCompleteイベントを購読します。 - スクリプトは mode:
tabでCloudflare.getLiveViewを送信し、返された URL を人間のオペレーターと共有します。 - スクリプトは、人間のオペレーター向けの指示付きで
Cloudflare.handoffCDP コマンドを送信し、Cloudflare.handoffCompleteを待ちます。 - オペレーターは Live View(/browser-run/features/live-view/)の URL を開き、必要な操作を完了して「Done」または「Failed」を選びます。
- 人間が引き継ぎを完了としてマークするか、引き継ぎがタイムアウトすると、
Cloudflare.handoffCompleteが発火します。 - 自動化は、介入が成功したかどうかを把握したうえで再開します。
完全なコードサンプルは、例: 構造化された引き継ぎ を参照してください。
より単純な用途では、引き継ぎを手動で管理できます。
- 自動化スクリプトが、人間の入力が必要なページに移動します。
- スクリプトはセッションのターゲット一覧から Live View の URL を取得し、人間のオペレーターと共有します。
- 人間のオペレーターは Live View の URL を開き、必要な操作を完了します。
- 自動化スクリプトは、ページ要素のポーリングやナビゲーションイベントの待機で完了を検出します。
完全なコードサンプルは、例: 手動検出 を参照してください。
Browser Run は、標準の Chrome DevTools Protocol(CDP) ↗ を、Cloudflare.* 名前空間の Cloudflare 固有コマンドで拡張します。これらのコマンドは Browser Run セッションに接続しているときだけ利用でき、人間の介入の要求、Live View URL の生成、引き継ぎ状態の追跡など、標準 CDP 仕様にはない機能を提供します。
これらのコマンドは、標準の CDP コマンドと同じ方法で CDP セッションから送信します。パラメータと戻り値の型の詳細は、プロトコルリファレンス を参照してください。
現在のページに対して人間の介入を要求します。ターゲットは CDP セッションから自動で解決されます。
const cdp = await page.createCDPSession();
const { handoffId } = await cdp.send("Cloudflare.handoff", {
// targetId will automatically be resolved from the CDP session
instructions: "Please log in",
timeout: 1800000, // optional, max 30 minutes, if undefined handoff will have no timeout
});ブラウザに複数のページがある場合に、特定のターゲットへ引き継ぎを要求するには:
// Get all targets
const { targetInfos } = await cdp.send("Target.getTargets");
// Find a specific target (for example, a page with a specific URL)
const target = targetInfos.find(
(t) => t.type === "page" && t.url.includes("example.com"),
);
if (!target) {
throw new Error("Target not found");
}
// Request handoff for the selected target
const { handoffId } = await cdp.send("Cloudflare.handoff", {
targetId: target.targetId,
instructions: "Please complete the CAPTCHA on this page",
});人間の介入が完了またはタイムアウトしたときに発行されます。人間の作業が終わったあと、自動化を再開するためにこのイベントを待ちます。
cdp.once("Cloudflare.handoffComplete", (result) => {
if (result.success) {
console.log("Handoff completed successfully");
} else {
console.log(`Handoff failed: ${result.reason}`);
}
});現在のページで引き継ぎが進行中かどうかを確認します。
const state = await cdp.send("Cloudflare.getHandoffState", {
targetId, // optional, defaults to the current page
});
if (state.active) {
console.log(
`Handoff ${state.handoffId} has been active for ${state.durationMs}ms`,
);
}ブラウザセッション向けの Live View URL を生成します。人間のオペレーターにライブブラウザへのアクセスを渡すには、Cloudflare.handoff と一緒に使います。
const { devtoolsFrontendUrl } = await cdp.send("Cloudflare.getLiveView", {
targetId, // optional, defaults to the current page
mode: "tab", // optional, one of "tab", "full", or "devtools" (default)
expiresInMs: 300000, // optional, default 5 minutes, max 1 hour
});
console.log(`Live View URL: ${devtoolsFrontendUrl}`);この例は、構造化された引き継ぎの流れを示します。スクリプトは Cloudflare の CDP コマンドで人間の介入を要求し、完了イベントを待ってから再開します。
import puppeteer, { type HandoffCompleteResponse } from "@cloudflare/puppeteer";
const ACCOUNT_ID = "<your-account-id>";
const API_TOKEN = "<your-api-token>";
// Connect to Browser Run
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-run/devtools/browser?keep_alive=600000`,
headers: { Authorization: `Bearer ${API_TOKEN}` },
});
// If using a Cloudflare Worker, you can use: await puppeteer.launch(env.MYBROWSER)
const page = await browser.newPage();
await page.goto("https://github.com/login");
// Create a CDP session to send Browser Run commands
const cdp = await page.createCDPSession();
// Get the Live View URL for the human operator
const { devtoolsFrontendUrl } = await cdp.send("Cloudflare.getLiveView", {
mode: "tab", // required, only "tab" supports handoff
expiresInMs: 300000, // optional, default is 5 minutes
});
// Share the Live View URL with the human operator (for example, send it via Slack, email, or display it in a UI)
console.log(`Human input needed. Open this URL: ${devtoolsFrontendUrl}`);
// Set up completion listener before initiating handoff
const handoffCompletePromise = new Promise<HandoffCompleteResponse>((resolve) => {
cdp.once("Cloudflare.handoffComplete", (event) => resolve(event as HandoffCompleteResponse));
});
// Request human intervention with specific instructions
const { handoffId } = await cdp.send("Cloudflare.handoff", {
instructions: "Please log in with your GitHub credentials",
timeout: 600_000, // 10 minute timeout
});
console.log(`Human intervention requested (ID: ${handoffId})`);
// Wait for human to complete the task
const result = await handoffCompletePromise;
if (result.success) {
console.log("Login successful, continuing automation...");
// Continue with your automation...
await page.goto("https://github.com/settings/profile");
} else {
console.error(`Human intervention failed: ${result.reason}`);
}
await browser.close();import type { HandoffCompleteResponse } from "@cloudflare/playwright";
import { chromium } from "playwright-core";
// If using a Cloudflare Worker, import { launch } from "@cloudflare/playwright" and use launch(env.MYBROWSER) instead.
const ACCOUNT_ID = "<your-account-id>";
const API_TOKEN = "<your-api-token>";
// Connect to Browser Run
const browser = await chromium.connectOverCDP(
`wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-run/devtools/browser?keep_alive=600000`,
{ headers: { Authorization: `Bearer ${API_TOKEN}` } },
);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = await context.newPage();
await page.goto("https://github.com/login");
// Create a CDP session to send Browser Run commands
const cdp = await context.newCDPSession(page);
// Get the Live View URL for the human operator
const { devtoolsFrontendUrl } = await cdp.send("Cloudflare.getLiveView", {
mode: "tab", // required, only "tab" supports handoff
expiresInMs: 300000, // optional, default is 5 minutes
});
// Share the Live View URL with the human operator (for example, send it via Slack, email, or display it in a UI)
console.log(`Human input needed. Open this URL: ${devtoolsFrontendUrl}`);
// Set up completion listener before initiating handoff.
// `cdp.once` for Cloudflare.* events needs a call-site cast because
// Playwright's `CDPSession.on/once/off` are declared as arrow-property
// signatures that TypeScript can't merge overloads into.
const handoffCompletePromise = new Promise<HandoffCompleteResponse>((resolve) => {
(cdp.once as (event: string, listener: (p: HandoffCompleteResponse) => void) => void)(
"Cloudflare.handoffComplete",
resolve,
);
});
// Request human intervention with specific instructions
const { handoffId } = await cdp.send("Cloudflare.handoff", {
instructions: "Please log in with your GitHub credentials",
timeout: 600000, // 10 minute timeout
});
console.log(`Human intervention requested (ID: ${handoffId})`);
// Wait for human to complete the task
const result = await handoffCompletePromise;
if (result.success) {
console.log("Login successful, continuing automation...");
// Continue with your automation...
await page.goto("https://github.com/settings/profile");
} else {
console.error(`Human intervention failed: ${result.reason}`);
}
await browser.close();この例は、Live View URL を共有し、完了をポーリングする手動の方法です。
import puppeteer from "@cloudflare/puppeteer";
const ACCOUNT_ID = "<your-account-id>";
const API_TOKEN = "<your-api-token>";
// Connect to Browser Run
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-run/devtools/browser?keep_alive=600000`,
headers: { Authorization: `Bearer ${API_TOKEN}` },
});
const page = await browser.newPage();
await page.goto("https://github.com/login");
// Create CDP session and get Live View URL
const cdp = await page.createCDPSession();
const { devtoolsFrontendUrl } = await cdp.send("Cloudflare.getLiveView", {
expiresInMs: 300000, // 5 minutes
});
// Share the Live View URL with the human operator (for example, send it via Slack, email, or display it in a UI)
console.log(`Human input needed. Open this URL: ${devtoolsFrontendUrl}`);
// Wait until GitHub reports a non-empty `<meta name="user-login">` in the
// page head. This meta tag is always present, but its content is empty
// for logged-out viewers and set to the username after login. Waiting for
// content to be populated is a reliable "human finished logging in" signal.
await page.waitForFunction(
() =>
document
.querySelector('meta[name="user-login"]')
?.getAttribute("content") !== "",
{ timeout: 300000 },
);
// Login complete, continue automation
console.log("Login complete. Continuing automation...");
// Verify we're logged in and read the username
const username = await page.$eval('meta[name="user-login"]', (el) =>
el.getAttribute("content"),
);
console.log(`Logged in as: ${username}`);
await page.goto("https://github.com/");
await browser.close();import type { GetLiveViewResponse } from "@cloudflare/playwright";
import { chromium } from "playwright-core";
// If using a Cloudflare Worker, import { launch } from "@cloudflare/playwright" and use launch(env.MYBROWSER) instead.
const ACCOUNT_ID = "<your-account-id>";
const API_TOKEN = "<your-api-token>";
// Connect to Browser Run
const browser = await chromium.connectOverCDP(
`wss://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-run/devtools/browser?keep_alive=600000`,
{ headers: { Authorization: `Bearer ${API_TOKEN}` } },
);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = await context.newPage();
await page.goto("https://github.com/login");
// Create CDP session and get Live View URL
const cdp = await context.newCDPSession(page);
const { devtoolsFrontendUrl }: GetLiveViewResponse = await cdp.send(
"Cloudflare.getLiveView",
{ expiresInMs: 300000 }, // 5 minutes
);
// Share the Live View URL with the human operator (for example, send it via Slack, email, or display it in a UI)
console.log(`Human input needed. Open this URL: ${devtoolsFrontendUrl}`);
// Wait until GitHub reports a non-empty `<meta name="user-login">` in the
// page head. This meta tag is always present, but its content is empty
// for logged-out viewers and set to the username after login. Waiting for
// content to be populated is a reliable "human finished logging in" signal.
await page.waitForFunction(
() =>
document
.querySelector('meta[name="user-login"]')
?.getAttribute("content") !== "",
null,
{ timeout: 300000 },
);
// Login complete, continue automation
console.log("Login complete. Continuing automation...");
// Verify we're logged in and read the username
const username = await page.$eval('meta[name="user-login"]', (el) =>
el.getAttribute("content"),
);
console.log(`Logged in as: ${username}`);
await page.goto("https://github.com/");
await browser.close();人間のオペレーターには、具体的で実行可能な案内を渡します。
// Good: specific and actionable
await cdp.send("Cloudflare.handoff", {
instructions:
"Review the items in the cart and if approved for checkout, click 'Done'. Otherwise, click 'Failed' and provide a reason.",
});作業の複雑さに合わせてタイムアウト時間を決めます。
// Quick tasks — 2-3 minutes
await cdp.send("Cloudflare.handoff", {
instructions: "Click the 'I agree' checkbox and submit",
timeout: 120000,
});
// Complex tasks - 10-15 minutes
await cdp.send("Cloudflare.handoff", {
instructions: "Complete the multi-page application form with test data",
timeout: 900000,
});必要に応じて引き継ぎの状態を確認します。
// Check if a handoff is already active before requesting a new one
const currentState = await cdp.send("Cloudflare.getHandoffState");
if (currentState.active) {
console.log(`Handoff already active: ${currentState.handoffId}`);
// Wait for current handoff or handle appropriately
} else {
// Safe to start new handoff
await cdp.send("Cloudflare.handoff", {
/* ... */
});
}