План рефакторинга Browser Evaluate CDP
Контекст
act:evaluate выполняет предоставленный пользователем JavaScript на странице. Сегодня он работает через Playwright
(page.evaluate или locator.evaluate). Playwright сериализует команды CDP для каждой страницы, поэтому
зависший или долго выполняющийся evaluate может заблокировать очередь команд страницы и сделать каждое последующее действие
на этой вкладке выглядящим "зависшим".
PR #13498 добавляет прагматичную защитную сеть (ограниченный evaluate, распространение прерывания и best-effort
восстановление). Этот документ описывает более крупный рефакторинг, который делает act:evaluate изначально
изолированным от Playwright, чтобы зависший evaluate не мог заклинить нормальные операции Playwright.
Цели
act:evaluateне может навсегда заблокировать последующие действия браузера на той же вкладке.- Таймауты - единственный источник истины end-to-end, чтобы вызывающая сторона могла полагаться на бюджет.
- Прерывание и таймаут обрабатываются одинаково для HTTP и in-process диспетчеризации.
- Таргетинг элементов для 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 и in-process диспетчеризация)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.terminateExecutionbest-effort для сессии. - Закрывает WebSocket и возвращает четкую ошибку.
- Отправляет
Примечания:
- Это все еще выполняет JavaScript на странице, поэтому завершение может иметь побочные эффекты. Выигрыш в том, что это не заклинивает очередь Playwright, и его можно отменить на уровне транспорта путем убийства CDP сессии.
3. История Ref (таргетинг элементов без полной переписки)
Сложная часть - таргетинг элементов. CDP нужен дескриптор DOM или backendDOMNodeId, в то время как
сегодня большинство действий браузера используют локаторы Playwright на основе ref из снимков.
Рекомендуемый подход: сохранить существующие ref, но прикрепить опциональный CDP разрешаемый id.
3.1 Расширить информацию сохраненного Ref
Расширить метаданные сохраненного role ref, чтобы опционально включить CDP id:
- Сегодня:
{ role, name, nth } - Предлагается:
{ role, name, nth, backendDOMNodeId?: number }
Это сохраняет все существующие действия на основе Playwright работающими и позволяет CDP evaluate принимать
то же значение ref, когда доступен backendDOMNodeId.
3.2 Заполнение backendDOMNodeId во время снимка
При создании снимка role:
- Сгенерировать существующую карту role ref как сегодня (role, name, nth).
- Получить дерево AX через CDP (
Accessibility.getFullAXTree) и вычислить параллельную карту(role, name, nth) -> backendDOMNodeId, используя те же правила обработки дубликатов. - Объединить id обратно в сохраненную информацию ref для текущей вкладки.
Если маппинг не удается для ref, оставить backendDOMNodeId неопределенным. Это делает функцию
best-effort и безопасной для развертывания.
3.3 Поведение Evaluate с Ref
В act:evaluate:
- Если
refприсутствует и имеетbackendDOMNodeId, выполнить evaluate элемента через CDP. - Если
refприсутствует, но нетbackendDOMNodeId, откатиться к пути Playwright (с защитной сетью).
Опциональный escape hatch:
- Расширить форму запроса для прямого приема
backendDOMNodeIdдля продвинутых вызывающих (и для отладки), сохраняяrefкак основной интерфейс.
4. Сохранить путь восстановления последней надежды
Даже с CDP evaluate есть другие способы заклинить вкладку или соединение. Сохранить существующие механизмы восстановления (завершить выполнение + отключить Playwright) как последнее средство для:
- устаревших вызывающих
- окружений, где прикрепление CDP заблокировано
- неожиданных граничных случаев Playwright
План реализации (одна итерация)
Результаты
- Движок evaluate на основе CDP, который работает вне очереди команд Playwright для каждой страницы.
- Единый end-to-end бюджет таймаута/прерывания, используемый последовательно вызывающими и обработчиками.
- Метаданные Ref, которые могут опционально нести
backendDOMNodeIdдля evaluate элемента. act:evaluateпредпочитает движок CDP, когда возможно, и откатывается к Playwright, когда нет.- Тесты, доказывающие, что зависший evaluate не заклинивает последующие действия.
- Логи/метрики, делающие сбои и откаты видимыми.
Контрольный список реализации
- Добавить общий помощник "budget" для связывания
timeoutMs+ upstreamAbortSignalв:- единый
AbortSignal - абсолютный дедлайн
- помощник
remainingMs()для downstream операций
- единый
- Обновить все пути вызывающих для использования этого помощника, чтобы
timeoutMsозначал одно и то же везде:src/browser/client-fetch.ts(HTTP и in-process диспетчеризация)src/node-host/runner.ts(путь прокси узла)- CLI обертки, вызывающие
/act(добавить--timeout-msкbrowser evaluate)
- Реализовать
src/browser/cdp-evaluate.ts:- подключиться к сокету CDP уровня браузера
Target.attachToTargetдля полученияsessionId- выполнить
Runtime.evaluateдля evaluate страницы - выполнить
DOM.resolveNode+Runtime.callFunctionOnдля evaluate элемента - при таймауте/прерывании: best-effort
Runtime.terminateExecution, затем закрыть сокет
- Расширить метаданные сохраненного role ref для опционального включения
backendDOMNodeId:- сохранить существующее поведение
{ role, name, nth }для действий Playwright - добавить
backendDOMNodeId?: numberдля таргетинга элементов CDP
- сохранить существующее поведение
- Заполнить
backendDOMNodeIdво время создания снимка (best-effort):- получить дерево AX через CDP (
Accessibility.getFullAXTree) - вычислить
(role, name, nth) -> backendDOMNodeIdи объединить в сохраненную карту ref - если маппинг неоднозначен или отсутствует, оставить id неопределенным
- получить дерево 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, инструменте агента, прокси узла и вызовах in-process.- Если
refможет быть отображен вbackendDOMNodeId, evaluate элемента использует CDP; иначе путь отката все еще ограничен и восстанавливаем.
План тестирования
- Юнит-тесты:
- Логика сопоставления
(role, name, nth)между role ref и узлами дерева AX. - Поведение помощника Budget (запас, математика оставшегося времени).
- Логика сопоставления
- Интеграционные тесты:
- Таймаут CDP evaluate возвращается в пределах бюджета и не блокирует следующее действие.
- Прерывание отменяет evaluate и запускает завершение best-effort.
- Контрактные тесты:
- Убедиться, что
BrowserActRequestиBrowserActResponseостаются совместимыми.
- Убедиться, что
Риски и митигации
- Маппинг несовершенен:
- Митигация: маппинг best-effort, откат к Playwright evaluate и добавить инструменты отладки.
Runtime.terminateExecutionимеет побочные эффекты:- Митигация: использовать только при таймауте/прерывании и документировать поведение в ошибках.
- Дополнительные накладные расходы:
- Митигация: получать дерево AX только когда запрашиваются снимки, кэшировать для каждой цели и держать CDP сессию короткоживущей.
- Ограничения relay расширения:
- Митигация: использовать API прикрепления уровня браузера, когда сокеты для каждой страницы недоступны, и сохранить текущий путь Playwright как откат.
Открытые вопросы
- Должен ли новый движок быть настраиваемым как
playwright,cdpилиauto? - Хотим ли мы предоставить новый формат "nodeRef" для продвинутых пользователей, или сохранить только
ref? - Как должны участвовать снимки фреймов и снимки с областью селектора в маппинге AX?