Как решать капчу в Browser Use с помощью собственного инструмента

browser use captcha - How to Solve CAPTCHAs in Browser Use With a Custom Tool

Чтобы решать капчу в Browser Use в собственном браузере, зарегистрируйте свой инструмент, который считывает sitekey виджета со страницы, запрашивает у CapSkip токен и записывает его в форму, а затем сообщите агенту в его системном сообщении, что такой инструмент есть. Этого хватает для решения капчи в Browser Use на локальном Chromium, если не считать двух лимитов: оба по умолчанию равны 180 секундам, и их нужно поднять, потому что решение reCAPTCHA может длиться дольше любого из них. Часть с системным сообщением обязательна. Системный промпт Browser Use по умолчанию говорит модели, что капчи решаются автоматически. Это верно для облачных браузеров Browser Use и неверно для локального Chromium, поэтому без такого сообщения агент ждёт решения, которое так и не придёт. В этом руководстве инструмент для капчи в Browser Use строится шаг за шагом и проверен на Browser Use 0.13.10.

Что понадобится

  • CapSkip, запущенный на машине с Windows. Инструмент ниже справляется с reCAPTCHA v2, включая невидимые виджеты и виджеты Enterprise, а также с виджетом Cloudflare Turnstile.
  • Python 3.11 или новее (этого требует Browser Use) с установленными browser-use и пакетом capskip версии 1.3.0 или новее. Browser Use управляет браузером Chrome или Chromium, который находит на машине.
  • API-ключ для той чат-модели, которую вы даёте агенту; для Claude задайте его как ANTHROPIC_API_KEY. В примерах используется Claude Opus 5.5 через ChatAnthropic с включённым адаптивным мышлением (adaptive thinking), и этот аргумент важен: без него Browser Use 0.13.10 принудительно задаёт модели выбор инструмента, а Claude Opus 5.5 и Sonnet 5.5 такое отклоняют, так что каждый шаг проваливается.
  • Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства; в режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы агент на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# Quoted, so cmd.exe does not read >= as a redirect
pip install browser-use "capskip>=1.3.0"

Почему Browser Use игнорирует капчу в локальном браузере?

Потому что ему так велели. Правила для браузера в системном промпте, который Browser Use использует с настройками по умолчанию, содержат строку "CAPTCHAs are automatically solved by the browser" (в переводе: капчи решаются браузером автоматически), за которой следует указание не пытаться решать их вручную. Эта строка описывает облачные браузеры Browser Use, которые решают капчи в собственном прокси и сообщают об этом библиотеке. В библиотеке есть соответствующий сторожевой компонент (watchdog), который включается настройкой captcha_solver в BrowserProfile (по умолчанию она включена) и ставит агента на паузу, пока облачный браузер решает капчу. Он слушает только события, которые отправляют эти облачные браузеры.

На Chromium на вашей собственной машине такое событие не приходит никогда. Агент видит виджет, верит правилам и ждёт, прокручивает страницу или пробует другой путь, пока не исчерпает шаги. Когда прогон заканчивается так, Browser Use предлагает перейти на свои облачные браузеры. Если же вы хотите держать браузер на своём железе, альтернатива в том, чтобы дать агенту инструмент, который действительно решает виджет, и исправить правило, которое ему дали.

Шаг 1: регистрируем инструмент solve_captcha

Собственные инструменты представляют собой асинхронные функции, зарегистрированные декоратором tools.action на экземпляре Tools. Browser Use подставляет специальные параметры по имени: browser_session даёт вам живую сессию, а page_url даёт адрес текущей страницы. Инструменту нужны два небольших фрагмента JavaScript: один находит виджет, другой вписывает токен.

FIND = """() => {
  const w = document.querySelector(
    '.g-recaptcha[data-sitekey], .cf-turnstile[data-sitekey]');
  if (!w) return null;
  return {
    kind: w.classList.contains('cf-turnstile') ? 'turnstile' : 'recaptcha',
    sitekey: w.dataset.sitekey,
    invisible: w.dataset.size === 'invisible' || w.tagName !== 'DIV',
    enterprise: !!document.querySelector(
      'script[src*="recaptcha/enterprise.js"]'),
    callback: w.dataset.callback || null,
  };
}"""

FILL = """(kind, token, callback) => {
  const name = kind === 'turnstile' ? 'cf-turnstile-response'
                                    : 'g-recaptcha-response';
  const fields = document.querySelectorAll(`[name="${name}"]`);
  fields.forEach(f => { f.value = token; });
  if (callback && typeof window[callback] === 'function') {
    window[callback](token);
  }
  return fields.length;
}"""

Оба написаны как стрелочные функции, потому что метод evaluate объекта страницы ничего другого не принимает. Дополнительные аргументы он передаёт как JSON и всегда возвращает строку: объект приходит закодированным в JSON, число приходит своими цифрами, а null приходит пустой строкой. FILL записывает токен в поле ответа, созданное виджетом, и вызывает функцию, указанную в data-callback, если виджет её объявляет, поскольку некоторые формы ждут этот callback вместо чтения поля.

FIND также считывает две вещи, от которых зависит решение. Виджет, привязанный к кнопке, невидим даже без атрибута data-size, поэтому любой элемент g-recaptcha, который не является div, считается невидимым. А страница, которая загружает скрипт enterprise.js от Google, решается как reCAPTCHA Enterprise, поскольку виджеты Enterprise используют ту же разметку, что и стандартные.

@tools.action(
    "Solve the reCAPTCHA v2 or Cloudflare Turnstile widget on the current "
    "page. Call it after the other fields are filled, then submit the form "
    "unless the page has already moved on."
)
async def solve_captcha(browser_session: BrowserSession, page_url: str):
    page = await browser_session.must_get_current_page()
    raw = await page.evaluate(FIND)
    widget = json.loads(raw) if raw else None
    if not widget:
        return ActionResult(error="No reCAPTCHA or Turnstile widget found.")

    try:
        if widget["kind"] == "turnstile":
            result = await solver.turnstile(widget["sitekey"], page_url)
        else:
            extra = {"invisible": 1} if widget["invisible"] else {}
            result = await solver.recaptcha(
                widget["sitekey"], page_url,
                enterprise=int(widget["enterprise"]), **extra)
    except CapSkipError as exc:
        return ActionResult(error=f"CapSkip did not solve it: {exc!r}")

    filled = await page.evaluate(
        FILL, widget["kind"], result["code"], widget["callback"])
    if filled == "0":
        return ActionResult(error="Solved, but no response field to fill.")
    return ActionResult(
        extracted_content=f"Solved the {widget['kind']} CAPTCHA on {page_url} "
                          "and filled in the token. Submit the form now, "
                          "unless the page has already moved on.",
    )

Строку описания модель читает, когда решает, какое действие выполнить, поэтому в ней сказано не только что делает инструмент, но и когда его вызывать. Сообщение об успехе идёт только в extracted_content. Если ActionResult задаёт ещё и long_term_memory, Browser Use показывает модели память и отбрасывает содержимое, так что инструкция вроде "Submit the form now" в содержимом до модели так и не дойдёт. Каждый сбой возвращается не исключением, а как ActionResult с ошибкой, которую модель может прочитать и на которую может отреагировать.

Используйте здесь AsyncCapSkip, а не синхронный клиент CapSkip. Browser Use держит соединение с браузером в том же событийном цикле, что и ваш инструмент, и блокирующее решение заморозило бы его целиком на всё время решения. В Python AsyncCapSkip представляет собой настоящий клиент на asyncio, а не псевдоним.

Шаг 2: сообщаем агенту об инструменте

Передайте инструменты агенту и добавьте один абзац в его системное сообщение. extend_system_message добавляется после промпта по умолчанию, поэтому оказывается после правила, которое должно исправить.

EXTRA = (
    "This browser does not solve CAPTCHAs on its own, whatever the rules "
    "above say. When a page shows a reCAPTCHA or Turnstile widget, fill in "
    "the other fields, call solve_captcha, then submit the form unless the "
    "page has already moved on. Never click the CAPTCHA checkbox or "
    "challenge yourself."
)

agent = Agent(
    task="Sign up at https://example.com/signup with YOUR_EMAIL.",
    llm=ChatAnthropic(model="claude-opus-5-5", thinking={"type": "adaptive"}),
    tools=tools,
    extend_system_message=EXTRA,
    step_timeout=420,
)

Порядок в этом абзаце важен. В руководстве по сроку действия токена reCAPTCHA объясняется, что такой токен действителен всего около двух минут. При этом каждый шаг модели занимает секунды. Если сначала решить капчу, а потом заполнять длинную форму, токен может истечь до отправки, поэтому агенту велено решать капчу в последнюю очередь. Модель может сделать все три действия за один шаг: ввести текст, решить капчу и нажать кнопку отправки, а Browser Use выполняет действия шага по порядку. Оговорка о том, что страница уже ушла дальше, нужна для невидимых виджетов. Их callback обычно сам отправляет форму, так что к тому моменту, когда FILL его вызвал, страница уже перешла дальше, и второй клик только провалился бы.

Аргумент thinking у ChatAnthropic нужен не для красоты. Browser Use запрашивает у Claude следующее действие через вызов инструмента, и в версии 0.13.10 без thinking он принудительно задаёт этот выбор инструмента. Claude Opus 5.5 и Sonnet 5.5 отклоняют принудительный выбор инструмента с ошибкой 400, так что агент проваливал бы каждый шаг. С включённым адаптивным мышлением Browser Use позволяет модели самой выбрать инструмент, и запрос проходит.

Шаг 3: поднимаем оба лимита в 180 секунд

Решение капчи в Browser Use должно уложиться в два отдельных отсчёта времени, и оба по умолчанию равны 180 секундам.

  • Лимит на одно действие. Каждое действие обёрнуто в таймаут, который читается из переменной окружения BROWSER_USE_ACTION_TIMEOUT_S, и агент никогда не передаёт собственное значение. Переменная читается один раз, когда Browser Use впервые загружает свой модуль инструментов, а это делает любой импорт Agent или Tools, поэтому задавать её после этого бесполезно.
  • Таймаут шага. Параметр step_timeout у Agent охватывает весь шаг: подготовку состояния страницы, вызов модели, на который сам Browser Use отводит до 90 секунд в зависимости от модели, и каждое действие в шаге.

С другой стороны, SDK опрашивает reCAPTCHA до 300 секунд (recaptchaTimeout), а собственные настройки reCAPTCHA в CapSkip дают задаче до 250 секунд на ожидание потока и ещё 250 на решение. Большинство решений завершается гораздо раньше, но очередь или медленный прокси могут растянуть решение дольше 180 секунд. Когда оба лимита стоят по умолчанию, первым срабатывает таймаут шага, потому что шаг начался раньше действия, и агент сообщает, что шаг превысил таймаут в 180 секунд. Поднимите только step_timeout, и в дело вступит лимит действия: он прерывает решение посреди опроса и сообщает, что браузер, возможно, не отвечает, ссылаясь на мёртвый CDP WebSocket. Здесь это сообщение вводит в заблуждение. С браузером всё в порядке; решение просто пережило лимит. Поставьте оба лимита выше лимита SDK.

import os

# Before anything imports Agent or Tools from browser_use.
os.environ.setdefault("BROWSER_USE_ACTION_TIMEOUT_S", "330")

from browser_use import Agent  # noqa: E402

# Up to 300 s of SDK polling (a few more for the last poll), one
# model call (90 s for Claude) and the page state, with room to spare.
agent = Agent(task="...", llm=llm, tools=tools,
              extend_system_message=EXTRA, step_timeout=420)

Шаг, завершившийся по таймауту, к тому же засчитывается агенту как один из последовательных сбоев, а пять сбоев подряд останавливают агента, так что слишком тесный лимит обходится дороже одного повтора.

Запуск CapSkip на сервере

Инструмент работает в вашем процессе Python, рядом с агентом. Важно, где работает этот процесс, а не где находится браузер: даже если cdp_url сессии указывает на Chrome на другой машине, запрос на решение всё равно идёт от вашего скрипта к CapSkip. Пока скрипт и CapSkip делят один ПК с Windows, 127.0.0.1 подходит. Когда агент переезжает на VPS, в раннер CI или в задание по расписанию на другой машине, локальная петля указывает не на ту машину, и инструмент возвращает в качестве ошибки NetworkException. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что агент сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт правилом Windows Firewall. Решатель остаётся на вашей собственной машине с Windows, и решения по-прежнему не тарифицируются, сколько бы капч ни встретил агент.

SDK сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в AsyncCapSkip, как это делает полный пример.

Полный рабочий пример

# pip install browser-use "capskip>=1.3.0"
import asyncio
import json
import os

# Browser Use reads this once, when its tools load, so set it first.
os.environ.setdefault("BROWSER_USE_ACTION_TIMEOUT_S", "330")

from browser_use import ActionResult, Agent, BrowserSession, ChatAnthropic, Tools
from capskip import AsyncCapSkip, CapSkipError

solver = AsyncCapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
tools = Tools()

FIND = """() => {
  const w = document.querySelector(
    '.g-recaptcha[data-sitekey], .cf-turnstile[data-sitekey]');
  if (!w) return null;
  return {
    kind: w.classList.contains('cf-turnstile') ? 'turnstile' : 'recaptcha',
    sitekey: w.dataset.sitekey,
    invisible: w.dataset.size === 'invisible' || w.tagName !== 'DIV',
    enterprise: !!document.querySelector(
      'script[src*="recaptcha/enterprise.js"]'),
    callback: w.dataset.callback || null,
  };
}"""

FILL = """(kind, token, callback) => {
  const name = kind === 'turnstile' ? 'cf-turnstile-response'
                                    : 'g-recaptcha-response';
  const fields = document.querySelectorAll(`[name="${name}"]`);
  fields.forEach(f => { f.value = token; });
  if (callback && typeof window[callback] === 'function') {
    window[callback](token);
  }
  return fields.length;
}"""


@tools.action(
    "Solve the reCAPTCHA v2 or Cloudflare Turnstile widget on the current "
    "page. Call it after the other fields are filled, then submit the form "
    "unless the page has already moved on."
)
async def solve_captcha(browser_session: BrowserSession, page_url: str):
    page = await browser_session.must_get_current_page()
    raw = await page.evaluate(FIND)
    widget = json.loads(raw) if raw else None
    if not widget:
        return ActionResult(error="No reCAPTCHA or Turnstile widget found.")
    try:
        if widget["kind"] == "turnstile":
            result = await solver.turnstile(widget["sitekey"], page_url)
        else:
            extra = {"invisible": 1} if widget["invisible"] else {}
            result = await solver.recaptcha(
                widget["sitekey"], page_url,
                enterprise=int(widget["enterprise"]), **extra)
    except CapSkipError as exc:
        return ActionResult(error=f"CapSkip did not solve it: {exc!r}")
    filled = await page.evaluate(
        FILL, widget["kind"], result["code"], widget["callback"])
    if filled == "0":
        return ActionResult(error="Solved, but no response field to fill.")
    return ActionResult(
        extracted_content=f"Solved the {widget['kind']} CAPTCHA on {page_url} "
                          "and filled in the token. Submit the form now, "
                          "unless the page has already moved on.",
    )


EXTRA = (
    "This browser does not solve CAPTCHAs on its own, whatever the rules "
    "above say. When a page shows a reCAPTCHA or Turnstile widget, fill in "
    "the other fields, call solve_captcha, then submit the form unless the "
    "page has already moved on. Never click the CAPTCHA checkbox or "
    "challenge yourself."
)


async def main():
    agent = Agent(
        task="Sign up at https://example.com/signup with YOUR_EMAIL, "
             "then report what the page says.",
        # Adaptive thinking lets Browser Use leave the tool choice to Claude.
        llm=ChatAnthropic(model="claude-opus-5-5", thinking={"type": "adaptive"}),
        tools=tools,
        extend_system_message=EXTRA,
        step_timeout=420,
    )
    history = await agent.run(max_steps=25)
    print(history.final_result())


asyncio.run(main())

Мы прогнали этот цикл от начала до конца на тестовых страницах с виджетом-чекбоксом, невидимым виджетом, привязанным к кнопке, страницей Enterprise и виджетом Turnstile, заменив модель скриптом, чтобы каждый шаг был предсказуемым. Промпт приходит с дополнительным абзацем после правил по умолчанию, solve_captcha появляется среди действий, которые может выбрать модель, токен попадает в форму, функция из data-callback срабатывает, инструкция инструмента доходит до модели, и отправка принимается. Мы также перехватили запрос, который отправляет ChatAnthropic, чтобы убедиться, что адаптивное мышление оставляет выбор инструмента модели. С настоящей моделью разница лишь в том, что модель сама решает, когда вызвать инструмент, и именно для этого нужны описание и дополнительный абзац. Сырые эндпоинты, которые стоят за обоими методами, описывает справочник API.

Частые ошибки и что они означают

Что вы видитеПричинаИсправить
Агент ждёт, прокручивает страницу или сдаётся на странице с капчей и так и не вызывает инструментСистемный промпт по умолчанию говорит, что капчи решаются автоматически, и это никто не исправилПередайте extend_system_message с абзацем из шага 2
Каждый шаг падает с ошибкой 400: тип tool_choice не поддерживается для этой моделиChatAnthropic создан без thinking, поэтому Browser Use принудительно задал выбор инструментаПередайте thinking={"type": "adaptive"} в ChatAnthropic
Шаг завершился по таймауту через 180 секундМедленное решение вместе с вызовом модели не уложились в step_timeoutПередайте step_timeout=420 в Agent и поднимите также лимит на действие
Ошибка: Action solve_captcha timed out after 180s. The browser may be unresponsive (dead CDP WebSocket).При поднятом step_timeout решение пережило лимит на одно действие; с браузером всё в порядкеЗадайте BROWSER_USE_ACTION_TIMEOUT_S больше 300 до загрузки Browser Use
Переменная задана, а лимит всё ещё равен 180 секундамЕё задали уже после импорта Agent или ToolsПеренесите присваивание в начало входного скрипта, выше всех импортов, которые подтягивают browser_use
Весь агент замирает, пока решается капчаСинхронный клиент CapSkip блокирует событийный цикл, в котором работает соединение с браузеромИспользуйте AsyncCapSkip и вызывайте его методы через await
Инструмент сообщает, что виджет не найденСтраница отрисовывает виджет скриптом без data-sitekey, помещает его в iframe или использует другую капчуСчитайте sitekey из собственного запроса виджета в DevTools и доработайте FIND и FILL для этой страницы; оба выполняются в документе верхнего уровня, поэтому для формы внутри iframe нужен отдельный поиск
Ошибка инструмента: Solved, but no response field to fillВиджет Turnstile переименовывает своё поле через data-response-field-name или отключает его через data-response-fieldСчитывайте этот атрибут в FIND и записывайте токен в поле с указанным именем
Токен вписан, а сайт всё равно отклоняет отправкуТокен истёк до отправки, или форма ждёт callback, который зарегистрировала в скриптеПусть агент решает капчу прямо перед отправкой; если data-callback пуст, вызывайте собственный callback страницы
Страница использует reCAPTCHA v3 на кнопкеFIND принимает её за v2Вызовите solver.recaptcha с version v3 и action, который использует страница
Инструмент возвращает ошибку NetworkExceptionCapSkip не запущен либо хост и порт не подходят для машины, где работает агентЗапустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server

FAQ

Решает ли Browser Use капчи сам?

Только на своих облачных браузерах, где капчу решает собственный прокси Browser Use, а библиотека ждёт результата. В библиотеке с открытым исходным кодом нет ничего, что решало бы капчу в браузере, который вы запускаете сами, будь то локальный Chromium или ваш собственный Chrome, подключённый по CDP URL. Именно для такой конфигурации и написано это руководство.

Можно ли вместо этого дать агенту MCP-сервер CapSkip?

Можно. Browser Use умеет загружать инструменты MCP-сервера в свой экземпляр Tools, а MCP-сервер CapSkip предлагает инструмент решения для каждого поддерживаемого типа. Однако эти инструменты возвращают токен, и модели потом приходится самой вписывать его в страницу собственным скриптом. Так модель принимает два решения вместо одного, причём второе должно соответствовать разметке каждой страницы. Собственный инструмент оставляет чтение и запись страницы в коде. Путь через MCP подходит агентам, в которые нельзя добавить код, а страница MCP-сервера для распознавания капчи показывает, как подключить такой агент.

А как быть с hCaptcha или полностраничной проверкой Cloudflare?

CapSkip не решает hCaptcha, поэтому FIND сопоставляет только классы reCAPTCHA и Turnstile и никогда не срабатывает на виджет h-captcha. Полностраничная проверка Cloudflare устроена иначе, чем виджет Turnstile: ей нужны значения cData и chlPageData самой проверки и user agent, возвращённый вместе с токеном, поэтому в нынешнем виде этот инструмент её не покрывает. Загляните на страницу решателя Cloudflare Turnstile за подробностями. Другие типы CapSkip, например GeeTest или Capy Puzzle, можно добавить в тот же инструмент как дополнительные ветки.

Может ли агент, работающий в облаке, достучаться до моего решателя?

Да. Переведите CapSkip в режим Server в настройках подключения, чтобы он слушал сетевой адрес, а не локальную петлю, прочитайте этот адрес из CAPSKIP_HOST там, где работает агент, и передайте его в AsyncCapSkip. VPS, хост контейнеров, раннер CI и задание по расписанию на облачной платформе подключаются через один и тот же HTTP API. Если маршрут идёт через интернет, используйте статический публичный IP и правило файрвола. Решатель остаётся на вашем собственном железе, поэтому долгий прогон агента, который встречает капчу на каждой странице, не стоит ничего сверх того.

Коротко

Решение капчи в Browser Use в собственном браузере сводится к четырём изменениям, а когда агент переезжает с машины решателя, к ним добавляется пятое. Зарегистрируйте инструмент solve_captcha, который считывает sitekey через page.evaluate, ждёт AsyncCapSkip через await и записывает токен обратно, вызывая callback виджета, если он есть. Исправьте правило по умолчанию через extend_system_message, чтобы агент вызывал инструмент прямо перед отправкой. Передайте адаптивное мышление в ChatAnthropic, чтобы Claude принял запрос. Поднимите BROWSER_USE_ACTION_TIMEOUT_S до загрузки Browser Use и step_timeout у Agent, оба выше 300 секунд SDK. И переключите CapSkip в режим Server, если агент работает где угодно, кроме машины решателя.

Агент, который часами ходит по сайтам, может встречать проверку на каждой второй странице, а обход капчи на вашей собственной машине с Windows справляется с каждой из них без платы за каждое решение.