Рефакторинг Browser Evaluate с использованием CDP
Контекст
act:evaluate выполняет предоставленный пользователем JavaScript на странице. В настоящее время он работает через Playwright (page.evaluate или locator.evaluate). Playwright сериализует команды CDP для каждой страницы, поэтому зависший или долго выполняющийся evaluate может заблокировать очередь команд страницы и сделать так, что каждое последующее действие на этой вкладке будет выглядеть как «зависшее». PR #13498 добавляет прагматичную страховочную сеть (ограниченный evaluate, распространение прерывания и восстановление по возможности). В этом документе описывается более масштабный рефакторинг, который делает act:evaluate по своей сути изолированным от Playwright, так что зависший evaluate не может заблокировать обычные операции Playwright.
Цели
act:evaluateне может навсегда блокировать последующие действия браузера на той же вкладке.- Таймауты являются единым источником истины от начала до конца, чтобы вызывающая сторона могла полагаться на бюджет.
- Прерывание (abort) и таймаут обрабатываются одинаково как при HTTP, так и при внутрипроцессной диспетчеризации.
- Поддерживается привязка к элементу для evaluate без перевода всего остального с Playwright.
- Сохранение обратной совместимости для существующих вызывающих сторон и форматов данных.
Не цели
- Замена всех действий браузера (click, type, wait и т.д.) на реализации через CDP.
- Удаление существующей страховочной сети, введенной в PR #13498 (она остается полезным запасным вариантом).
- Введение новых небезопасных возможностей сверх существующего ограничения
browser.evaluateEnabled. - Добавление изоляции процессов (рабочий процесс/поток) для evaluate. Если после этого рефакторинга мы все еще увидим трудно восстанавливаемые зависшие состояния, это будет идеей для дальнейшей работы.
Текущая архитектура (Почему возникает зависание)
На высоком уровне:
- Вызывающие стороны отправляют
act:evaluateв сервис управления браузером. - Обработчик маршрута вызывает Playwright для выполнения JavaScript.
- Playwright сериализует команды страницы, поэтому evaluate, который никогда не завершается, блокирует очередь.
- Заблокированная очередь означает, что последующие операции click/type/wait на вкладке могут казаться зависшими.
Предлагаемая архитектура
1. Распространение дедлайна
Ввести единую концепцию бюджета и выводить все из нее:
- Вызывающая сторона устанавливает
timeoutMs(или дедлайн в будущем). - Внешний таймаут запроса, логика обработчика маршрута и бюджет выполнения внутри страницы используют один и тот же бюджет, с небольшим запасом там, где это необходимо для накладных расходов на сериализацию.
- Прерывание распространяется как
AbortSignalповсюду, чтобы отмена была согласованной.
Направление реализации:
- Добавить небольшой помощник (например,
createBudget({ timeoutMs, signal })), который возвращает:signal: связанный AbortSignaldeadlineAtMs: абсолютный дедлайнremainingMs(): оставшийся бюджет для дочерних операций
- Использовать этого помощника в:
src/browser/client-fetch.ts(HTTP и внутрипроцессная диспетчеризация)src/node-host/runner.ts(путь прокси)- реализациях действий браузера (Playwright и CDP)
2. Отдельный движок Evaluate (Путь CDP)
Добавить реализацию evaluate на основе CDP, которая не использует общую очередь команд Playwright для каждой страницы. Ключевое свойство заключается в том, что транспорт для evaluate — это отдельное WebSocket-соединение и отдельная сессия CDP, подключенная к цели. Направление реализации:
- Новый модуль, например
src/browser/cdp-evaluate.ts, который:- Подключается к настроенной конечной точке CDP (сокет на уровне браузера).
- Использует
Target.attachToTarget({ targetId, flatten: true })для полученияsessionId. - Выполняет либо:
Runtime.evaluateдля evaluate на уровне страницы, либоDOM.resolveNodeплюсRuntime.callFunctionOnдля evaluate элемента.
- При таймауте или прерывании:
- Отправляет
Runtime.terminateExecutionпо возможности для сессии. - Закрывает WebSocket и возвращает понятную ошибку.
- Отправляет
Примечания:
- Это все еще выполняет JavaScript на странице, поэтому прерывание может иметь побочные эффекты. Выигрыш в том, что это не блокирует очередь Playwright, и его можно отменить на транспортном уровне, убив сессию CDP.
3. Ref Story (Привязка к элементу без полной переработки)
Сложная часть — это привязка к элементу. CDP нужен дескриптор DOM или backendDOMNodeId, в то время как сегодня большинство действий браузера используют локаторы Playwright на основе ссылок (refs) из снапшотов. Рекомендуемый подход: сохранить существующие ссылки, но добавить опциональный идентификатор, разрешимый через CDP.
3.1 Расширение хранимой информации о ссылке
Расширить хранимые метаданные ссылки на роль, чтобы опционально включать идентификатор CDP:
- Сегодня:
{ role, name, nth } - Предлагается:
{ role, name, nth, backendDOMNodeId?: number }
Это позволяет всем существующим действиям на основе Playwright продолжать работать и позволяет CDP evaluate принимать то же значение ref, когда backendDOMNodeId доступен.
3.2 Заполнение backendDOMNodeId во время создания снапшота
При создании снапшота роли:
- Генерировать существующую карту ссылок на роли как и сегодня (role, name, nth).
- Получить AX-дерево через CDP (
Accessibility.getFullAXTree) и вычислить параллельную карту(role, name, nth) -> backendDOMNodeId, используя те же правила обработки дубликатов. - Объединить идентификатор обратно в хранимую информацию о ссылке для текущей вкладки.
Если сопоставление для ссылки не удалось, оставить backendDOMNodeId неопределенным. Это делает функцию реализуемой по возможности и безопасной для внедрения.
3.3 Поведение Evaluate с Ref
В act:evaluate:
- Если
refприсутствует и имеетbackendDOMNodeId, выполнить evaluate элемента через CDP. - Если
refприсутствует, но не имеетbackendDOMNodeId, вернуться к пути через Playwright (со страховочной сетью).
Опциональный аварийный выход:
- Расширить формат запроса, чтобы принимать
backendDOMNodeIdнапрямую для продвинутых вызывающих сторон (и для отладки), сохраняяrefв качестве основного интерфейса.
4. Сохранить путь аварийного восстановления
Даже с CDP evaluate есть другие способы заблокировать вкладку или соединение. Сохранить существующие механизмы восстановления (прерывание выполнения + отключение Playwright) в качестве последнего средства для:
- устаревших вызывающих сторон
- сред, где подключение CDP заблокировано
- непредвиденных крайних случаев Playwright
План реализации (Одна итерация)
Результаты
- Движок evaluate на основе CDP, работающий вне очереди команд Playwright для каждой страницы.
- Единый сквозной бюджет таймаута/прерывания, последовательно используемый вызывающими сторонами и обработчиками.
- Метаданные ссылки, которые могут опционально содержать
backendDOMNodeIdдля evaluate элемента. act:evaluateпредпочитает движок CDP, когда это возможно, и возвращается к Playwright, когда нет.- Тесты, доказывающие, что зависший evaluate не блокирует последующие действия.
- Логи/метрики, которые делают сбои и откаты видимыми.
Контрольный список реализации
- Добавить общий помощник «budget» для связывания
timeoutMs+ вышестоящегоAbortSignalв:- единый
AbortSignal - абсолютный дедлайн
- помощник
remainingMs()для последующих операций
- единый
- Обновить все пути вызывающей стороны для использования этого помощника, чтобы
timeoutMsозначал одно и то же везде:src/browser/client-fetch.ts(HTTP и внутрипроцессная диспетчеризация)src/node-host/runner.ts(путь прокси node)- CLI-обертки, вызывающие
/act(добавить--timeout-msкbrowser evaluate)
- Реализовать
src/browser/cdp-evaluate.ts:- подключение к сокету CDP на уровне браузера
Target.attachToTargetдля полученияsessionId- выполнение
Runtime.evaluateдля evaluate страницы - выполнение
DOM.resolveNode+Runtime.callFunctionOnдля evaluate элемента - при таймауте/прерывании: по возможности
Runtime.terminateExecution, затем закрытие сокета
- Расширить хранимые метаданные ссылки на роль, чтобы опционально включать
backendDOMNodeId:- сохранить существующее поведение
{ role, name, nth }для действий Playwright - добавить
backendDOMNodeId?: numberдля привязки к элементу через CDP
- сохранить существующее поведение
- Заполнять
backendDOMNodeIdво время создания снапшота (по возможности):- получать AX-дерево через CDP (
Accessibility.getFullAXTree) - вычислять
(role, name, nth) -> backendDOMNodeIdи объединять с хранимой картой ссылок - если сопоставление неоднозначно или отсутствует, оставлять идентификатор неопределенным
- получать AX-дерево через CDP (
- Обновить маршрутизацию
act:evaluate:- если нет
ref: всегда использовать CDP evaluate - если
refразрешается вbackendDOMNodeId: использовать CDP evaluate элемента - в противном случае: вернуться к Playwright evaluate (все еще ограниченному и прерываемому)
- если нет
- Сохранить существующий путь «последнего средства» восстановления как запасной вариант, а не путь по умолчанию.
- Добавить тесты:
- зависший evaluate завершается по таймауту в рамках бюджета, и следующий click/type выполняется успешно
- прерывание отменяет evaluate (отключение клиента или таймаут) и разблокирует последующие действия
- сбои сопоставления корректно приводят к откату на Playwright
- Добавить наблюдаемость:
- счетчики длительности evaluate и таймаутов
- использование terminateExecution
- частота откатов (CDP -> Playwright) и причины
Критерии приемки
- Намеренно зависший
act:evaluateвозвращается в рамках бюджета вызывающей стороны и не блокирует вкладку для последующих действий. timeoutMsведет себя одинаково в CLI, инструменте агента, прокси node и внутрипроцессных вызовах.- Если
refможет быть сопоставлен сbackendDOMNodeId, evaluate элемента использует CDP; в противном случае запасной путь все еще ограничен и восстанавливаем.
План тестирования
- Модульные тесты:
- логика сопоставления
(role, name, nth)между ссылками на роли и узлами AX-дерева. - поведение помощника бюджета (запас, вычисление оставшегося времени).
- логика сопоставления
- Интеграционные тесты:
- таймаут CDP evaluate возвращается в рамках бюджета и не блокирует следующее действие.
- прерывание отменяет evaluate и запускает прерывание выполнения по возможности.
- Контрактные тесты:
- Убедиться, что
BrowserActRequestиBrowserActResponseостаются совместимыми.
- Убедиться, что
Риски и меры по их снижению
- Сопоставление неидеально:
- Мера снижения: сопоставление по возможности, откат на Playwright evaluate и добавление инструментов отладки.
Runtime.terminateExecutionимеет побочные эффекты:- Мера снижения: использовать только при таймауте/прерывании и документировать поведение в ошибках.
- Дополнительные накладные расходы:
- Мера снижения: получать AX-дерево только при запросе снапшотов, кэшировать на цель и делать сессии CDP короткоживущими.
- Ограничения ретранслятора расширений:
- Мера снижения: использовать API подключения на уровне браузера, когда сокеты для каждой страницы недоступны, и сохранять текущий путь Playwright как запасной вариант.
Открытые вопросы
- Должен ли новый движок быть настраиваемым как
playwright,cdpилиauto? - Хотим ли мы предоставить новый формат «nodeRef» для продвинутых пользователей или оставить только
ref? - Как снапшоты фреймов и снапшоты с областью действия селектора должны участвовать в AX-сопоставлении?
План рефакторинга Unified Runtime StreamingПлан шлюза OpenResponses