← Browser Run / browser-run / features
Human in the Loop
Некоторые сценарии автоматизации браузера требуют ручного вмешательства. Странице входа может понадобиться многофакторная аутентификация, форме могут потребоваться конфиденциальные учётные данные, которые вы не хотите передавать скрипту автоматизации, а задача порой оказывается слишком сложной для полной автоматизации. Human in the Loop позволяет человеку подключиться к активной сессии браузера через Live View чтобы обработать то, с чем не справляется автоматизация, а затем вернуть управление скрипту.
Сценарии использования
- Потоки аутентификации: страницы входа с MFA, SSO или CAPTCHA, которые невозможно обойти программно
- Ввод конфиденциальных данных: формы, требующие учетных данных или личной информации, которую вы не хотите передавать скрипту автоматизации
- Сложные взаимодействия: разовые задачи, которые слишком сложно или нецелесообразно полностью автоматизировать, например настройка панели управления или подтверждение рабочего процесса
- Шаги проверки: подтверждение заказа, проверка сгенерированного контента или одобрение действия перед тем, как скрипт продолжит работу
Как это работает
Human in the Loop работает с любым Browser Session и предлагает два подхода к вмешательству человека:
Структурированная передача управления (рекомендуется)
Скрипт использует команды Cloudflare CDP, чтобы официально запросить вмешательство человека и дождаться его завершения:
- В процессе работы скрипта автоматизации возникает ситуация, требующая участия человека.
- Скрипт подписывается на
Cloudflare.handoffCompleteсобытие. - Скрипт отправляет
Cloudflare.getLiveViewс mode:tabи передаёт возвращённый URL-адрес оператору-человеку. - Скрипт отправляет
Cloudflare.handoffкоманду CDP с инструкциями для оператора-человека и затем ожидаетCloudflare.handoffComplete. - Оператор открывает URL-адрес Live View (/browser-run/features/live-view/), выполняет необходимые действия и выбирает "Done" или "Failed".
Cloudflare.handoffCompleteсрабатывает, когда человек отмечает передачу управления как завершённую, либо когда истекает время ожидания передачи.- Автоматизация продолжается, уже зная, успешным ли было вмешательство.
См. Пример: структурированная передача управления для просмотра полного примера кода.
Ручное определение
Для более простых сценариев передачу управления можно настроить вручную:
- Скрипт автоматизации переходит на страницу, где требуется участие человека.
- Скрипт получает Live View URL-адрес из списка целей сеанса и передаёт его оператору-человеку.
- Человек-оператор открывает URL-адрес Live View и выполняет необходимое действие.
- Скрипт автоматизации определяет завершение, опрашивая элементы страницы или ожидая события навигации.
См. Пример: ручное определение для просмотра полного примера кода.
Команды Cloudflare CDP
Browser Run расширяет стандартный Chrome DevTools Protocol (CDP) ↗ со специфичными для Cloudflare командами в разделе Cloudflare.* пространство имён. Эти команды доступны только при подключении к сессии Browser Run и предоставляют возможности, которых нет в стандартной спецификации CDP, например запрос вмешательства человека, создание Live View URL-адресов и отслеживание состояния передачи управления.
Эти команды отправляются через сессию CDP так же, как и любые стандартные команды CDP. Полное описание параметров и возвращаемых типов см. в справочник по протоколу.
Cloudflare.handoff
Запрашивает вмешательство человека для текущей страницы. Целевой объект определяется автоматически на основе сеанса 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
});Чтобы запросить передачу управления (handoff) для конкретной цели, если в браузере открыто несколько страниц:
// 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",
});Cloudflare.handoffComplete событие
Возникает, когда вмешательство человека завершается или истекает время ожидания. Отслеживайте это событие, чтобы возобновить автоматизацию после завершения работы человека.
cdp.once("Cloudflare.handoffComplete", (result) => {
if (result.success) {
console.log("Handoff completed successfully");
} else {
console.log(`Handoff failed: ${result.reason}`);
}
});Cloudflare.getHandoffState
Проверяет, активен ли handoff для текущей страницы.
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`,
);
}Cloudflare.getLiveView
Создает 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}`);Пример: структурированная передача управления
Этот пример показывает структурированный процесс передачи управления (handoff). Скрипт запрашивает вмешательство человека с помощью команд 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();Пример: ручное определение
В этом примере используется ручной подход: передача URL-адреса Live View и опрос статуса завершения:
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,
});Мониторинг состояния передачи
Проверяйте статус handoff, когда это нужно:
// 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", {
/* ... */
});
}