実験
ブラウザ Evaluate CDP リファクタリング
背景
act:evaluate は、ユーザーが提供した JavaScript をページ内で実行します。現在は Playwright (page.evaluate または locator.evaluate) を介して実行されています。Playwright はページごとに CDP コマンドを直列化するため、固まった、または長時間実行される evaluate はページコマンドキューをブロックし、そのタブでの後続のすべてのアクションが「固まった」ように見える原因となります。PR #13498 は実用的な安全策(制限付き evaluate、中止伝播、ベストエフォートでの回復)を追加しました。このドキュメントは、act:evaluate を Playwright から本質的に分離し、固まった evaluate が通常の Playwright 操作を妨げないようにする、より大規模なリファクタリングについて説明します。
目標
act:evaluateが同じタブ での後続のブラウザアクションを恒久的にブロックできないようにする。- タイムアウトがエンドツーエンドで単一の信頼できる情報源となり、呼び出し元が予算を頼りにできるようにする。
- HTTP とプロセス内ディスパッチの両方で、中止とタイムアウトが同じ方法で扱われるようにする。
- Playwright からすべてを切り替えることなく、evaluate のための要素ターゲティングをサポートする。
- 既存の呼び出し元とペイロードに対する後方互換性を維持する。
非目標
- すべてのブラウザアクション(クリック、タイプ、待機など)を CDP 実装に置き換えること。
- PR #13498 で導入された既存の安全策を削除すること(有用なフォールバックとして残す)。
- 既存の
browser.evaluateEnabledゲートを超える新しい安全でない機能を導入すること。 - evaluate のためのプロセス分離(ワーカープロセス/スレッド)を追加すること。このリファクタリング後も回復困難な固まった状態が見られる場合、それはフォローアップのアイデアとする。
現在のアーキテクチャ(固まる理由)
大まかに説明すると:
- 呼び出し元は
act:evaluateをブラウザ制御サービスに送信する。 - ルートハンドラは JavaScript を実行するために Playwright を呼び出す。
- Playwright はページコマンドを直列化するため、終了しない evaluate はキューをブロックする。
- 固まったキューは、そのタブでの後続のクリック/タイプ/待機操作がハングしているように見えることを意味する。
提案アーキテクチャ
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 パス)
Playwright のページごとのコマンドキューを共有しない、CDP ベースの evaluate 実装を追加する。重要な特性は、evaluate トランスポートが別個の WebSocket 接続であり、ターゲットにアタッチされた別個の CDP セッションであること。実装方針:
- 新しいモジュール、例:
src/browser/cdp-evaluate.tsを追加し、以下を行う:- 設定された CDP エンドポイント(ブラウザレベルのソケット)に接続する。
Target.attachToTarget({ targetId, flatten: true })を使用してsessionIdを取得する。- 以下を実行する:
- ページレベルの evaluate の場合は
Runtime.evaluate、または - 要素 evaluate の場合は
DOM.resolveNodeとRuntime.callFunctionOn。
- ページレベルの evaluate の場合は
- タイムアウトまたは中止時:
- セッションに対してベストエフォートで
Runtime.terminateExecutionを送信する。 - WebSocket を閉じ、明確なエラーを返す。
- セッションに対してベストエフォートで
注意点:
- これでも JavaScript はページ内で実行されるため、終了には副作用がある可能性がある。利点は、Playwright キューを妨げず、CDP セッションを終了することでトランスポート層でキャンセル可能であること。
3. 参照ストーリー(完全な書き換えなしでの要素ターゲティング)
難しい部分は要素ターゲティングです。CDP は DOM ハンドルまたは backendDOMNodeId を必要としますが、現在ほとんどのブラウザアクションはスナップショットからの参照に基づく Playwright ロケーターを使用しています。推奨されるアプローチ:既存の参照を保持するが、オプションで CDP で解決可能な ID を付与する。
3.1 保存された参照情報の拡張
保存されたロール 参照メタデータを拡張し、オプションで CDP ID を含める:
- 現在:
{ role, name, nth } - 提案:
{ role, name, nth, backendDOMNodeId?: number }
これにより、既存の Playwright ベースのアクションがすべて機能し続け、backendDOMNodeId が利用可能な場合に CDP evaluate が同じ ref 値を受け入れられるようになります。
3.2 スナップショット作成時に backendDOMNodeId を設定
ロールスナップショットを生成する際:
- 現在と同じように既存のロール参照マップ(role, name, nth)を生成する。
- CDP を介して AX ツリーを取得し(
Accessibility.getFullAXTree)、同じ重複処理ルールを使用して(role, name, nth) -> backendDOMNodeIdの並列マップを計算する。 - 現在のタブの保存された参照情報に ID をマージし戻す。
参照のマッピングが失敗した場合は、backendDOMNodeId を未定義のままにする。これにより、この機能はベストエフォートで安全にロールアウトできる。