Эксперименты

Рефакторинг 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: связанный AbortSignal
    • deadlineAtMs: абсолютный дедлайн
    • 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 во время создания снапшота

При создании снапшота роли:

  1. Генерировать существующую карту ссылок на роли как и сегодня (role, name, nth).
  2. Получить AX-дерево через CDP (Accessibility.getFullAXTree) и вычислить параллельную карту (role, name, nth) -> backendDOMNodeId, используя те же правила обработки дубликатов.
  3. Объединить идентификатор обратно в хранимую информацию о ссылке для текущей вкладки.

Если сопоставление для ссылки не удалось, оставить 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 не блокирует последующие действия.
  • Логи/метрики, которые делают сбои и откаты видимыми.

Контрольный список реализации

  1. Добавить общий помощник «budget» для связывания timeoutMs + вышестоящего AbortSignal в:
    • единый AbortSignal
    • абсолютный дедлайн
    • помощник remainingMs() для последующих операций
  2. Обновить все пути вызывающей стороны для использования этого помощника, чтобы timeoutMs означал одно и то же везде:
    • src/browser/client-fetch.ts (HTTP и внутрипроцессная диспетчеризация)
    • src/node-host/runner.ts (путь прокси node)
    • CLI-обертки, вызывающие /act (добавить --timeout-ms к browser evaluate)
  3. Реализовать src/browser/cdp-evaluate.ts:
    • подключение к сокету CDP на уровне браузера
    • Target.attachToTarget для получения sessionId
    • выполнение Runtime.evaluate для evaluate страницы
    • выполнение DOM.resolveNode + Runtime.callFunctionOn для evaluate элемента
    • при таймауте/прерывании: по возможности Runtime.terminateExecution, затем закрытие сокета
  4. Расширить хранимые метаданные ссылки на роль, чтобы опционально включать backendDOMNodeId:
    • сохранить существующее поведение { role, name, nth } для действий Playwright
    • добавить backendDOMNodeId?: number для привязки к элементу через CDP
  5. Заполнять backendDOMNodeId во время создания снапшота (по возможности):
    • получать AX-дерево через CDP (Accessibility.getFullAXTree)
    • вычислять (role, name, nth) -> backendDOMNodeId и объединять с хранимой картой ссылок
    • если сопоставление неоднозначно или отсутствует, оставлять идентификатор неопределенным
  6. Обновить маршрутизацию act:evaluate:
    • если нет ref: всегда использовать CDP evaluate
    • если ref разрешается в backendDOMNodeId: использовать CDP evaluate элемента
    • в противном случае: вернуться к Playwright evaluate (все еще ограниченному и прерываемому)
  7. Сохранить существующий путь «последнего средства» восстановления как запасной вариант, а не путь по умолчанию.
  8. Добавить тесты:
    • зависший evaluate завершается по таймауту в рамках бюджета, и следующий click/type выполняется успешно
    • прерывание отменяет evaluate (отключение клиента или таймаут) и разблокирует последующие действия
    • сбои сопоставления корректно приводят к откату на Playwright
  9. Добавить наблюдаемость:
    • счетчики длительности 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