mcpField
A
ai-browser

ai-browser

ローカル
github.com/Glitteringブラウザ自動化5今日更新

ai-browser について

ai-browserは、スクリーンショットではなくセマンティックなアクセシビリティツリーを通じてエージェントが操作する、画面表示型のElectronブラウザーです。MCPツールとWebSocket JSON-RPC APIを備え、人も同じウィンドウを操作できます。

主な機能

  1. 01スクリーンショットに代わるセマンティックツリー
  2. 02ナビゲーション、タブ、入力、スクロール
  3. 03ネットワークとダウンロードの確認
  4. 04操作結果に効果状態を表示
  5. 05MCPとWebSocket JSON-RPCのインターフェース

活用シーン

  1. 01エージェントによるページの読み取りと操作
  2. 02ブラウザー操作後の変化の検証
  3. 03画面上のブラウザーを人が引き継ぐ
  4. 04リクエストとダウンロード済みファイルの確認

MCP 対応

MCPサーバーは、ナビゲーション、ページ読み取り、操作、タブ、ネットワーク確認などのツールをMCPクライアントに提供します。Electronを起動するほか、実行中のブラウザーを再利用できます。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"glittering-ai-browser": {
"command": "npx",
"args": [
"-y",
"@glittering0717/ai-browser"
]
}
}
}
起動コマンドnpx -y @glittering0717/ai-browser

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npm@glittering0717/ai-browser2.3.1

ツール

17

掲載時のツール一覧 · 今日に更新

パラメーター型必須説明
tabintegerいいえタブ ID(デフォルト:アクティブなタブ)。
urlstringはい移動先の URL。

詳細

AI Browser

中国語でお読みの方はこちら:README-CN.md(3 分で読める概要 + 中国語のインストールガイド)

エージェントだけが操作する、本物のブラウザーです。
スクリーンショットではなくセマンティックツリーとしてページを読み取り、信頼できるブラウザー入力を使って操作します。通常の表示可能な Electron ウィンドウなので、人間はいつでもマウスを使えます。

CI MIT Electron Platforms Tests MCP

実際のノイズの多いフィード(アクセシビリティノード 300 個以上)が型付き構造になり、1 回の ui.act でフィードが切り替わるとツリーも変化します。すべての呼び出しは、1 つの WebSocket を介した生の JSON-RPC メッセージです。SDK も座標も必要ありません。
AI Browser のデモ:大量の情報が並ぶフィードページに移動し、ページ全体を 1 つのセマンティックツリーとして取得して、型付きアイテムを抽出し、要素 ID で操作します。すべて生の WebSocket JSON-RPC を介して行います
再現方法:npm start を実行し、続けて node examples/feed-tree.mjs を実行します

人間とエージェントが同じブラウザー、同じソケットを使います。エージェントが操作した後、そのまま停止して待機します。人間が実際のウィンドウで記事をクリックすると、エージェントは次の読み取り時に新しいタブに気づき、人間が選んだページで作業を続けます。ロックも引き継ぎ API もありません。
人間による操作への切り替えデモ:エージェントがフィードを一覧表示して一時停止し、人間が実際のブラウザーウィンドウで記事をクリックします。エージェントは新しいタブを検出し、選択されたページでスクロールを再開します
実際に試すには、npm start を実行し、続けて node examples/human-takeover.mjs を実行します。YOUR TURN と表示されたら、好きな場所をクリックしてください。

概要。 タブを 2 つの同等なインターフェースで公開する Electron アプリです。MCP サーバー(20 個の browse_* ツール)と、生の WebSocket JSON-RPC API(ws://localhost:9223、メソッドは ui.*)を提供します。エージェントはページを移動し、構造化されたセマンティックツリー(role / label / value / states / actions)を読み取り、data-ai-id でクリックや入力を行います。スクリーンショットを撮る段階も OCR もありません。src/ にキャプチャ呼び出しは一切ありません。

概要ではないもの。 ヘッドレスではありません。ウィンドウは実際に開いて描画され、操作可能な状態を保ちます。人間が操作に加わるをご覧ください。また、人間と「共同操作」するものでもありません。引き継ぎはエージェントとユーザーの間の会話であり、ブラウザー API ではありません。

セキュリティに関する注意。 WebSocket サーバーには認証がありません。localhost で待ち受け、接続した相手を信頼します。ポート 9223 をループバック以外に公開しないでください。また、信頼できないネットワークの使用は避けてください。


インストール

特別な理由がない限り、ビルド済みパッケージをインストールしてください。 オプション A はダブルクリックするだけの単一ファイルです。Node.js も依存関係ツリーもビルド手順も必要ありません。ソースからのインストール(オプション C)では、約 276 MB の node_modules に加え、約 100 MB の Electron エンジンをダウンロードします。お使いのプラットフォームに合うパッケージがない場合、パッケージが正常に動作しない場合、またはソースから実行する必要がある場合にのみ使用してください。

オプション A — macOS インストールパッケージ(推奨)

GitHub ReleasesからAI-Browser-<version>-arm64.dmgをダウンロードします(ダウンロード容量は約110 MB、ディスク上の容量は約245 MBです。大部分を占めるのはバンドルされたChromiumエンジンで、これこそが製品です)。dmgを開き、AI Browserを/Applicationsにドラッグして起動します。これで完了です。

ダウンロードする前に、次の2点をご確認ください。

  • 公証されていません。 macOS Gatekeeperにより、初回起動時に開くことが拒否されます。アプリを右クリックして開くを選び、1回だけ確認すれば、その後は通常どおり実行できます。(この手順を省くには有料のApple Developerアカウントが必要です。それまでは、これが正直なところです。)
  • ビルドされているのはmacOS arm64のみです。 WindowsとLinux向けのパッケージはまだありません。どちらの環境もこのプロジェクトでは検証されていないためです。互換性をご覧ください。

エージェントは引き続きMCPサーバー経由で接続します(下記のオプションB)。2つの接続経路は連携します。パッケージ版アプリがすでに起動している場合、MCPサーバーはポート9223でアプリを検出し、Electronの2つ目のインスタンスを起動せずに、既存のものを再利用します。起動方法にかかわらず、ブラウザーは1つ、ログインセッションも1つです。そのため、「エンジンにはパッケージ版、MCPブリッジにはnpx」という構成も、一般的なエージェントのセットアップです。

オプション B — npx(エージェント向けの接続経路)

パッケージは@glittering0717/ai-browserとして公開されているため、リポジトリをクローンする必要はありません。

npx -y @glittering0717/ai-browser --help

表示されるブラウザーウィンドウを直接開くには:

npx -y @glittering0717/ai-browser start

Node ≥ 18とデスクトップセッション(GUIアプリのため)が必要です。起動時に専用のエンジンが必要な場合、初回実行時に準備されます(約100 MB、ダウンロードは1回のみ)。npm ≥ 11では、依存関係のインストールスクリプトがデフォルトでブロックされます。ランチャーがこれを検出し、Electron独自のダウンローダーを実行するため、追加の操作は不要です。GitHubリリースからのダウンロードが遅い、またはブロックされる場合(中国本土のネットワークではよくあります)は、ミラーを指定してください。

ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npx -y @glittering0717/ai-browser start
オプション C — ソースから実行(代替手段)

お使いのプラットフォーム向けのパッケージがない場合、パッケージで問題が発生する場合、またはコードを変更する必要がある場合に使用します。3つの方法の中で最も容量が大きく、npm installで依存関係ツリー全体(約276 MB)がダウンロードされ、その後Electronエンジン(約100 MB)もダウンロードされます。

git clone https://github.com/Glittering/ai-browser.git
cd ai-browser
npm install

Electronバイナリのダウンロードが遅い、または完全に失敗する場合は、ミラーを使用してください。

ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install

または、プロジェクトのルートに.npmrcを作成して、ミラーを常に使うように設定できます。

electron_mirror=https://npmmirror.com/mirrors/electron/

ほかの原因を疑う前に、バイナリが実際に配置されたことを確認してください。バイナリがなくても、「インストールは成功しました」と表示されることがあります。

ls node_modules/electron/dist

インストールを最初から最後まで確認するには:

npm start

1280×800のウィンドウが開き、ターミナルにAI Browser WS server listening on ws://localhost:9223と表示されます。(最初のタブはデフォルトのスタートページで開きます。無視して別のページに移動してください。)

パッケージを自分でビルドするには:

npm install
node scripts/build_dmg.cjs              # current arch
node scripts/build_dmg.cjs --arch arm64 # or x64

出力先はdist/です。このスクリプトは@electron/packagerとmacOSに標準搭載されているhdiutilを使用するため、外部バイナリのダウンロードはありません。背景にある理由(このプロジェクトのビルドをelectron-builderが拒否する理由を含む)は、docs/DEPENDENCY_AND_DISTRIBUTION_AUDIT.mdに記載されています。


エージェントを接続する

これを MCP クライアントの設定ファイル(Claude Desktop の設定、~/.cursor/mcp.json、または プロジェクトレベルの .mcp.json)に追加します。

{
  "mcpServers": {
    "ai-browser": {
      "command": "npx",
      "args": ["-y", "@glittering0717/ai-browser", "mcp"]
    }
  }
}

すぐに使える設定ファイルが examples/mcp-config.json にあります。ソースから利用する場合は、 代わりに command: "node" の参照先を /absolute/path/to/ai-browser/src/main/mcp_server.js に指定できます。

ブラウザーを自分で起動する必要はありません。 最初の browse_* 呼び出し時に、MCP サーバーは ポート 9223 を確認します。何も待ち受けていない場合は Electron をデタッチモードで起動し、WebSocket サーバーが 起動するまで最大 30 秒待機します(最初の呼び出しでは、エンジンの準備にさらに時間がかかることがあります)。すべてのクライアントで共有する長時間起動のウィンドウを 使いたい場合は、オプション A でインストールした AI Browser アプリを起動するか、 npx -y @glittering0717/ai-browser start を実行するか、ソースから npm start を実行してください。2 回目以降の起動では、既存のウィンドウにフォーカスが移ります。

examples/AGENT_INSTRUCTIONS.md には、CLAUDE.md や Cursor Rule、または任意のシステムプロンプトにコピーして貼り付けられるプロンプトがあります。ツールのループ、トークンを節約するパラメーター、人に確認を求めるタイミングを説明しています。 これを自分でインストールするエージェント(Cline など)は、この README の代わりに、機械向けの簡潔な llms-install.md を参照できます。


MCP ツール

情報の正本は src/main/mcp_tools.js です。ツールは 17 個あります。各ツールのスキーマが毎ターン エージェントのコンテキストに挿入されるため、意図的に数を絞っています。

ツール概要
browse_navigateアクティブなタブ(または指定したタブ)で URL を読み込みます。
browse_get_treeページをセマンティックツリーとページコンテキスト(モーダル、メッセージ、統計情報)として読み取ります。
browse_actdata-ai-id に対して click / type(カーソル位置に追記)/ setContent(すべて置換)/ clear / focus / hover / scroll_to / upload / get_value(読み取り専用)/ drag / press / wheel を実行します。
browse_diffsnapshot で基準状態を取得し、その後 diff で追加・削除・変更されたパスと変更前後の値を取得します。アクションが実際に何かを実行したことを証明する方法です。
browse_capabilitiesページで何ができるか、またどの操作経路を使うべきかを調べます(下記参照)。
browse_repaintウィンドウに古いページが表示されている場合に、タブのコンポジターに再描画を強制します。repaint_count を返します。
browse_downloadsタブのダウンロード一覧と、ディスク上のパスを取得します。ダウンロードは非同期です。エクスポートボタンをクリックした後にこれをポーリングしてください。
browse_canvasキャプチャした描画呼び出しを通じて <canvas> を読み取ります。list / read / configure / capture を使用します。試験的な機能です。下記のキャンバスのセクションを参照してください。
browse_evaluateページコンテキストで JS の式を実行します(上限 5000 文字、Node の識別子は拒否されます)。
browse_scrollページをスクロールするか、要素を表示範囲内にスクロールします。
browse_waitbutton_enabled / modal_appeared / text_contains / url_contains の条件が満たされるまでポーリングします。
browse_tabsタブの list / new / close / switch を実行します。switch は以降の呼び出しのデフォルト対象を設定します。
browse_networkChrome DevTools 形式のネットワーク調査を行います。list(メソッド・URL・ステータス・タイプでフィルター)/ get(リクエストとレスポンスのヘッダーおよび本文)/ clear / configure を使用します。
browse_quitブラウザー全体を終了し、プロセスを解放します。
すべての操作結果がすでに示していること:effect_state

browse_act を使えば、入力がページに届いたかどうかを推測する必要はもうありません。すべての結果に、明確な判定が含まれます。

effect_state意味retry
committedページに送信され、ページから確認応答があったno
none何も起きなかったことが確実(該当するターゲットがない、パラメーターが不正、CDP が停止中など)safe
unknown何も起きなかったとは証明できない(タイムアウト、貼り付けは送信されたが消費されなかった、キーシーケンスが途中まで実行されたなど)verify_first

デフォルトは none ではなく unknown です。ブラウザーが判断できないときは、その旨を伝えます。unknown を考えずに再試行すると、フォームが二重送信されることがあります。まずページを読み取ってください。unknown の結果には、その内容を正確に伝える1文の effect_hint も含まれます。

target が見つからない場合、結果には stale_ref / ref_epoch / tree_epoch / ref_diagnosis が追加されます。「該当するターゲットがない」には、実際にはまったく異なる3つの状況が隠れているためです。

  • ref_diagnosis: "stale" — その ref を取得した後にページが再読み込みされています(ref_epoch < tree_epoch)。新しい ref を取得してください。同じ ref で再試行しても失敗し続けます。
  • ref_diagnosis: "never_seen" — これまでに読み取ったツリーのどこにも、その ref は存在しません。再読み込みしても解決しません。値が間違っている(data-ai-id ではなく表示テキストを指定している)、入力ミスがある、または別のタブの ref を使っている可能性があります。
  • ref_diagnosis: "present_in_tree" — 現在のツリーには存在しますが、DOM では見つかりません。iframe、シャドウルート、または切り離されたノードの可能性があります。

3つとも effect_state: "none"(何も起きていない)と retry: "verify_first" を返します。副作用はありませんが、まったく同じ呼び出しを繰り返しても意味がありません。

実行されたことを証明する:browse_diff

ツールが success を報告しても、何かが変わった証拠にはなりません。ブラウザーは、クリックが無視された要素に届いた場合でも、アプリによってノードがまったく別の場所に移動された場合でも、入力後にフィールドが自動的に空になった場合でも、問題なく成功を報告します。書き込み操作はすべて、スナップショットと差分のペアで囲んでください。

const base = await browse_diff({ operation: "snapshot", js: "app.graph.serialize()" });
await browse_act({ action: "click", target: "e:button-25" });
const d = await browse_diff({ operation: "diff", snapshot: base.snapshot });
// d.verdict === "changed"
// d.changed_paths → [{ path: "nodes[7].widgets.steps", from: 20, to: 21, reason: "value" }]
  • verdict は "changed" / "unchanged" を明示するため、誤って作成した空配列から「何も起きなかった」と推測する必要はありません。
  • すべての項目には path と変更前後の両方の値が含まれるため、変更された理由を確認できます。
  • 配列は集合としてではなく、インデックスごとに要素単位で比較されます。ノードが移動した場合は変更された項目として扱われ、「削除1件+追加1件」とはなりません。これでは、並べ替えが単一のイベントではなく2つのイベントに見えてしまいます。
  • 浮動小数点の誤差は許容されます(201354.48743718592 と ...1859 は変更ではありません)。一方、座標が実際に22単位移動した場合は変更と判定されます。
  • js を省略すると、サイトに依存しない DOM スナップショットが取得されます。各要素の role / label / value / disabled / size に加え、ページ上の表示テキストも含まれます。サイトごとの JS は不要で、単純な <div> のステータステキストの変更も検出されます。
  • js を省略すると、ページが window にアプリ独自のデータモデルを公開している場合、そのフィンガープリントも取得されます。キャンバス描画のアプリでは、DOM は静止画にすぎません。ノードをドラッグしたりウィジェットを編集したりすると <canvas> が再描画されますが、要素の走査では何も検出できないため、DOM のみの差分では、実際に行われた書き込みが unchanged と判定されます。ComfyUI で文書化されている書き込み経路(node.widgets[].value)では、DOM はバイト単位でまったく変化しませんでした。フィンガープリントを使えば、同じ書き込みが model.fp に現れます(ノード数が変化した場合は model.count にも現れます)。この処理は読み取り専用です(loadGraphData はローダーであり、呼び出されることはありません)。最も有望に見える候補ではなく、実際に読み戻せる候補を選びます。また、名前のスコアは高くても返せるモデルがないライブラリ名前空間は除外されます。
  • rearm: true を指定すると、次の差分に向けてベースラインが更新されるため、複数のステップにわたって「何も変わっていないことを確認する」処理をループできます。forget: true を指定すると、ベースラインが破棄されます。
  • 式の実行時エラー、存在しない ID、タブをまたぐ比較はいずれも、明示的なエラーを返します。空のスナップショットに変換されることはありません。空のスナップショットに変換されると、その後の差分がすべて「unchanged」となり、検証結果がすべて誤って成功扱いされるためです。
このページではどのルートを使うべきですか? browse_capabilities

見慣れないサイトで試行錯誤する前に、まず確認しましょう。1 回の呼び出しで、セマンティックツリーのサイズとロール/アクションのヒストグラム、キャンバスの数、同一オリジンの iframe とその内部サイズ、アプリが window にぶら下げているオブジェクト(形状から自動検出:has .graph / ._nodes / getState / serialize)、すでに呼び出された JSON エンドポイント、そして推奨ルートとその根拠が報告されます。

  • L1_structured_data — アプリが独自のモデルを公開している場合。グラフ全体を JSON として読み書きします。
  • L2_semantic_tree — ロール/ラベルで対象を見つけ、data-ai-id で操作するのに十分な要素がある場合。
  • L3_pointer_gestures — 構造化された手段がない操作に限ります(ノードのドラッグ、接続の描画、ホイールでのパン/ズーム)。必ずその後に browse_diff を実行します。

落とし穴も明示されます。たとえば、「本文は同一オリジンの iframe 内にあります」や、「キャンバスは 7 個ありますが、セマンティック要素は 17 個だけです。キャンバスに描画された項目を座標で操作しようとするのはやめて、代わりにデータモデルを探してください」といった内容です。

往復を減らすためのメモ:

  • browse_get_tree は subset: "interactive" を指定するとレイアウトのみの分岐を除外でき、mode: "diff" を指定すると前回の読み取り以降に出現/消失したノードのみを返し、ax: true を指定するとアクセシビリティレイヤー経由で読み取れます(その場合、リンクには url が含まれます)。
  • type は追記し、setContent は置き換えます。 以前はどちらも同じ動作(内容全体の置き換え)で、段階的な編集ができませんでした。Weibo 形式のトピックタグを挿入すると、次の type でそれが消えてしまっていました。現在の type はキャレット位置に書き込みます(params.at:end(デフォルト)/start/cursor)。そのため、「トピックを挿入してから本文を書き続ける」操作が可能です。フィールドの内容を入れ直すには、setContent(または最初に clear)を使います。
  • browse_act は action: "get_value" も受け付けます。これは要素の全文を取得する読み取り専用の方法です。browse_get_tree は長い値を 200 文字で切り詰めるため、重要な機能です(下記参照)。
  • ポインタージェスチャーは第一級の機能です。drag、press、wheel が使えます。キャンバス/ボード/並べ替え可能な UI はこれらを基盤としており、どれだけ evaluate を使っても代用できません。Pointer Capture を使うフレームワークでは合成 PointerEvent で操作できません(React Flow のキャンバスで測定したところ、ノードの位置は正確に 0,0 しか変化しませんでした)。そのため、これらの操作は CDP の信頼された入力を経由します。
    • drag:dx/dy、to_x/to_y、または to_target(別の要素の上にドロップ)を指定します。from_anchor/to_anchor(left/right/top/bottom/center)を使うと辺をつかめます。辺を指定した場合、その要素の接続ハンドルが優先されますが、そのハンドルが実際にヒットテスト可能な場合に限ります(下記の注を参照)。from_x/from_y を指定すると任意の点から開始できます(空のキャンバスのパン操作など)。この場合、target は不要です。
    • press:key(Delete、Escape、ArrowRight、Enter、…または "Meta+a" のようなキーの組み合わせ)を指定します。キー入力のシーケンスには keys を使います。ノードの削除、元に戻す、選択解除などは、すべてキーボードのみで操作できます。
    • wheel:delta_x/delta_y と、キャンバスをズームするための hold: ["Control"] を指定します。browse_scroll では実行できません。browse_scroll は window.scrollBy を呼び出しますが、無限キャンバスはこれを完全に無視します。
    • ドラッグの結果には drag_from/drag_to と、それぞれの地点に実際にある要素(from_at_id)が報告されます。また、押下が別の要素に着地した場合は、warning と start_hit_target: false も返されます。これがないと、見た目上は「ドラッグしても何も起きない」状態と区別がつきません。
  • actions は role ではなく、振る舞いから推定されます。 算出された cursor がドラッグ用カーソルの要素や、draggable="true"/cursor: pointer/onclick を持つ要素は、ロールが group であっても、ツリー上で drag/click を通知するようになりました。これがないと、role: group のノードばかりのキャンバスでは actions: [] と報告され、エージェントはそれらが操作可能だとまったく判断できませんでした。

  • browse_network はタブを開いた時点からリクエストを取得します。POST リクエストの本文も含まれます。サブスクリプションは不要です。リクエストヘッダーはデフォルトで秘匿されます (authorization / cookie → [REDACTED])。表示するには include_sensitive_headers: true を渡してください。
  • ほとんどのツールでは、複数タブで作業するための tab を省略できます。省略した場合はアクティブなタブが対象になります。

人間が途中で操作する

フォーカスを奪いません

ウィンドウがバックグラウンドにある状態でも、エージェントは問題なく動作します。CDP の信頼された入力 (Input.dispatchKeyEvent / Input.dispatchMouseEvent)では、ウィンドウが最前面にある必要はありません。また、 アプリはすでに disable-backgrounding-occluded-windows、disable-renderer-backgrounding、 disable-background-timer-throttling を指定して実行されるため、バックグラウンドウィンドウでもスロットリングされたり、 イベントが失われたりすることはありません。

これは以前から act / get_tree / evaluate / peek / scroll では当てはまっていましたが、かつては 2 つの処理で呼び出しのたびにウィンドウが最前面に移動していました。new_tab と set_active_tab です(これらは webContents.focus() を呼び出し、ウィンドウ全体をアクティブにしていました)。さらに、エージェントがブラウザーを起動した場合でも、 起動時に show() でウィンドウが表示されていました。この両方を修正しました。

  • new_tab / set_active_tab はウィンドウのフォーカスを奪わなくなりました。Emulation.setFocusEmulationEnabled により document.hasFocus() が true を返し、フォーカスに依存する遅延読み込みが通常どおり動作するため、ページは引き続きフォーカスされていると 認識します。一方、キーボードのフォーカスは、最後に設定した場所にとどまります。
  • エージェントがブラウザーを起動した場合(MCP 経由)、ウィンドウは showInactive() で表示されます。 表示されますが、フォーカスは奪いません。あなたが npm start を実行した場合は、通常どおり最前面に表示されます。

つまり、ウィンドウは表示されたままです。これこそが目的ですが、エージェントが操作している間に 最前面に移動することはありません。作業を続けられます。

AI_BROWSER_FOCUS=always npm start   # pre-1.2 behaviour: show + moveTop + focus on every action
AI_BROWSER_FOCUS=never npm start    # never change focus (the window is still shown)

デフォルトは auto です。ウィンドウを表示したままにし(最小化または非表示の場合は元に戻し)、フォーカスは決して奪いません。 値にかかわらず、ウィンドウは常に表示されます。これは製品上の不変条件です。

npm run focus は、これらすべてを外部から検証します(macOS の lsappinfo を使用するため、アクセシビリティの 許可は不要です)。また、フォーカスを奪うように設定した意図的な対照群も含まれています。そのため、測定が機能しなくなった場合に テストスイートが形だけ通過するのではなく、失敗するようになっています。

さらに、より強い主張、つまりウィンドウはフォアグラウンドである必要さえないことも検証します。 別のアプリがフォアグラウンドにある状態で、API の代表的な処理を実行し、それぞれの実際の結果を確認します。入力した文字が反映されること、 クリックが正確に 1 回発生すること、get_tree がツリーを返すこと、キャンバスの getImageData が実際のピクセルを返すこと、 network_list がリクエストを検出すること、navigate が完了することを確認します。さらに、バックグラウンドに 10 秒間置いた後も、 一度も最前面に移動せずに、これらすべてが引き続き動作することを確認します。

document.hasFocus() に関する注意:意図的に Emulation.setFocusEmulationEnabled を有効にしているため、ウィンドウが バックグラウンドにある場合でも true を返します。これにより、フォーカスに依存する遅延読み込みやアニメーションの停止を防いでいます。 したがって、これは「フォアグラウンドにいるか」を判定する有効な方法ではありません。テストでは代わりに外部の監視手段を使います。


ここは特に誤解されやすい部分なので、はっきり説明します。エージェントがブラウザーを操作し、人間はいつでもウィンドウの操作を引き継げます。

実際の Electron ウィンドウなので、Chrome と同じように、マウスとキーボードを使って、許可を申請することなく、 いつでもクリック、入力、スクロール、ログインができます。これにより、エージェントでは本当に対応できない操作も行えます。

  • ログイン/SSOや、エージェントが知るべきでないパスワードが必要な操作
  • QR コードのスキャン(アプリからウェブへの認証)
  • スライダー CAPTCHA や画像 CAPTCHA、SMS またはメールのコード
  • 支払いの確認、リスク管理の中間画面、OTP 画面
  • 続行するには認証情報の捏造が必要になる操作 引き継ぎの仕組み:ブラウザーでは、ありません。 AI Browser 側で引き継ぎを調整するものはなく、その必要もありません。エージェントとユーザーの間で交わされる会話です。
  1. エージェントが自分ではできないことに遭遇します。
  2. チャットで率直に伝えます。「ブラウザーウィンドウで QR コードをスキャンしてから、『done』と返信してください。」
  3. そこで処理を止めて返信を待ちます。うまくいかないクリックを繰り返すことはありません。
  4. 「done」の後、browse_get_tree で状態を再度読み取り、新しい状態を確認してから続行します。

スクリプトで同じことを行う例については、examples/with-human.mjs をご覧ください。この例ではプロンプトを表示し、readline で Enter が押されるのを待ちます。ブラウザーは、その間に何が起きたかを認識しません。

エージェントが実際に注意すべき点は、次の 2 つです。

  • 人間がウィンドウで作業している間は、browse_quit を絶対に呼び出さないでください。 ウィンドウが閉じられ、プロセスが終了するため、途中まで解決した内容が失われます。終了するのはタスクが完了してからにしてください。
  • ログイン状態は保持されます。 タブは persist:ai-browser セッションパーティションに保存されるため、Cookie とセッションは再起動後も維持されます。通常、人間が対応する必要があるのは、マシンごとにサイトあたり 1 回だけです。

Raw WebSocket API

MCP は軽量な stdio アダプターです。MCP でできることはすべて、任意の言語から ws://localhost:9223 に接続し、JSON-RPC 2.0 を直接使って実行できます。信頼できる情報源は、 src/main/ws_server.js のメソッド切り替え処理です。

この操作のためにクライアントを作成する必要はありません。CLI も同じプロトコルを使用します。

ai-browser status                                   # running? pid / port / tabs
ai-browser call ui.list_tabs
ai-browser call ui.get_tree '{"subset":"interactive"}'
ai-browser call ui.navigate '{"url":"https://example.com"}'
echo '{"url":"https://example.com"}' | ai-browser call ui.navigate -

call は未加工の result を1行の JSON として出力します(jq にパイプできます)。成功時は 0、呼び出しに失敗した場合は 1、コマンドラインが不正な場合は 2 で終了します。--port、--timeout、--pretty を使用できます。

つまり、1つの実装にアクセスする方法は3つあります。MCP stdio(ai-browser mcp)、WebSocket 経由の JSON-RPC、この CLI です。いずれも別々のコードパスではありません。

メソッド用途対応先
ui.get_treeセマンティックツリー(+コンテキスト)。パラメーター:focusedOnly、ax、subset、mode、tabbrowse_get_tree
ui.actparams.action + params.target。アクションごとの引数は params.params の下にネストしますbrowse_act
ui.navigateURL を読み込みます(params.url、任意の tab)。{ok, error?} を返します。{ok:false} には必ず理由が含まれます。browse_navigate
ui.snapshot / ui.diffベースラインを取得し(params.js、任意の label)、それと比較します(params.snapshot、rearm、forget)browse_diff
ui.capabilitiesページを調査します:ツリーヒストグラム、キャンバス、iframe、アプリのグローバル変数、エンドポイント、推奨ルートbrowse_capabilities
ui.repaintタブのコンポジターに再描画を強制します。repaint_count を返すため、再描画が行われたことを確認できますbrowse_repaint
ui.evaluateMCP と同じガードを適用し、ページコンテキストで JS を実行しますbrowse_evaluate
ui.scrollページまたは要素をスクロールします(direction、amount、target)。ドキュメントのスクロール専用です。キャンバスアプリでは ui.act{action:'wheel'} を使用してください。browse_scroll
ui.wait条件が成立するかタイムアウトするまでポーリングしますbrowse_wait
ui.new_tab / ui.close_tab / ui.list_tabs / ui.set_active_tabタブ管理(MCP 側では、これら4つを browse_tabs{op} に統合しています)browse_tabs
ui.subscribe / ui.unsubscribeイベントの購読。params.events を指定し、すべてのイベントを対象にする場合は "*" を指定します—(WS のみ。以下を参照)
ui.downloadsこのタブのダウンロード。ディスク上のパスを含みます(state、received_bytes、dir)browse_downloads
ui.get_focusedフォーカスされている要素のサブツリー—
ui.peek折りたたまれた、またはホバー時のみ表示される機能にホバーし、確定操作を行わずに表示・非表示になったもの(revealed / hidden)を返します。ID は実際の data-ai-id なので、そのまま ui.act に渡せます。revertMs: 0 にするとマウスがその位置に留まり、見つかったものをクリックできます—
ui.network_bodyURL の部分文字列(url_pattern)でレスポンス本文を取得します—(browse_network に置き換えられました)
ui.quit正常にシャットダウンしますbrowse_quit

MCP の表記とは2点異なります。どちらも上記の切り替え処理で確認できます。focused_only ではなく focusedOnly(キャメルケース)を使用します。また、ui.act のアクションごとの引数は params.params の下にネストします。 ui.subscribe / ui.unsubscribe は、id のない通知としても有効です。

ui.subscribe は意図的に MCP ツールとして公開されていません。MCP の stdio 経路ではサーバーからクライアントへの通知が破棄されるため、MCP の呼び出し側は購読してもイベントを一度も受信できません。リアルタイムイベントは Raw WebSocket でのみ利用できます。プル型の処理には、代わりに browse_network を使用してください。

import WebSocket from 'ws';

const ws = new WebSocket('ws://localhost:9223');
let id = 0;
const call = (method, params) => new Promise((resolve, reject) => {
  const myId = ++id;
  const onMsg = (raw) => {
    const m = JSON.parse(raw.toString());
    if (m.id === myId) { ws.off('message', onMsg); m.error ? reject(new Error(m.error.message)) : resolve(m.result); }
  };
  ws.on('message', onMsg);
  ws.send(JSON.stringify({ jsonrpc: '2.0', id: myId, method, params }));
});

ws.on('open', async () => {
  await call('ui.navigate', { url: 'https://example.com' });
  const { tree } = await call('ui.get_tree', { subset: 'interactive' });
  console.log(tree.role, tree.children?.length);
  ws.close();
});

エラーコードは JSON-RPC に準拠します:-32700 は解析エラー、-32601 は不明なメソッド、-32602 は無効なパラメーター(ui.evaluate のガードはこのコードを返します)、-32603 は内部エラーです。


仕組み

   agent ── MCP stdio (17 browse_*) ──┐
   agent ── WS JSON-RPC :9223 (ui.*) ─┤
                                      ▼
 ┌──────────────────────────────────────────────────┐
 │ Electron main                                    │
 │   index.js        window · tab strip · watchdog  │
 │   ws_server.js    JSON-RPC routing, event stream │
 │   page_manager.js tabs · id-matched IPC · CDP    │
 └───────────────┬──────────────────────────────────┘
                 │ IPC (request-id matched)
 ┌───────────────▼──────────────────────────────────┐
 │ preload (contextIsolation:true, nodeIntegration:false)
 │   bridge.cjs · extractor.cjs · actions.cjs ·     │
 │   watcher.cjs  (captcha / message / state scan)  │
 └───────────────┬──────────────────────────────────┘
                 ▼
              live DOM → semantic tree

読み取り。 返されるツリーの各ノードには data-ai-id が付与されます。これは要素自身の DOM id がある場合はその値、ない場合は生成された e:tag-N です。ID は読み取り間で安定しています。すでに ID が付いている要素はそのまま保持し、生成される ID には単調増加するカウンターを使うため、他の要素より上に要素を挿入しても番号は振り直されません。(以前はそうではなく、古い ID が気付かないうちに別の要素を指すようになり、操作は成功したと報告される一方で、誤った要素に対して実行されていました。)ページ遷移時にリセットされます。ID とともに、role、label、value、states、actions、そして開いているモーダル、一時的なメッセージ、必須フィールドのヒント、概要統計を含む context ブロックも返されます。読み取り方法は 2 つあります。デフォルトの preload 抽出器と、ax: true の背後にあるアクセシビリティレイヤーです。後者ではリンクに url も付与されます。

ページを初めて読み取ると、要素に data-ai-id が付与されます。これが読み取りによって行われる唯一の書き込みであり、後続の操作を解決可能にします。繰り返し読み取っても、何も書き込みません。 属性値が実際に変化するときに限り、属性を設定します。値がすでに設定済みでも属性を設定すると、実際の Chromium は MutationObserver レコードを生成することが測定で確認されたためです。このガードを導入する前は、4000 要素のページで読み取りごとに 4001 件のレコードが発生していました。ページ自身のオブザーバーに大量の通知を送る読み取りは、測定対象に影響を与える読み取りです。

書き込み。 アクションは合成 JS ではなく、信頼されたブラウザー入力です。メインプロセスは Chrome DevTools Protocol を介してページを操作します。クリックには Input.dispatchMouseEvent を、文字入力には文字ごとの Input.dispatchKeyEvent(rawKeyDown → char → keyUp)を使います。その後、elementFromPoint でクリックが実際に対象に当たったかを検証します(シャドウルートをたどります。通常の呼び出しでは、Web コンポーネント内の要素に対してホストが返されるため、こうしないと、そのようなクリックはすべて合成イベントにひそかに格下げされます)。イベントは実際のキーボードおよびマウス入力として届くため、フレームワークごとの分岐を設けることなく、Draft.js、ProseMirror、CKEditor、Quill、通常の <textarea> をすべて同じ方法で操作できます。

制約事項。 extractor.cjs はクリーンルーム仕様ではなく、DOM に対するヒューリスティックです。一般的なページは適切に処理しますが、特殊なページでは誤ったラベルが付くことがあります。ブラウザーウィンドウはハードウェアアクセラレーションを無効にし、さらに Chromium の 3 つのスイッチ(--disable-backgrounding-occluded-windows、--disable-renderer-backgrounding、--disable-background-timer-throttling)を指定して起動します。これにより、ブラウザーの上に IDE のウィンドウがあっても、注入された入力がひそかに破棄されるのを防ぎます。


使用例

実行可能なスクリプトは examples/ にあり、ws://localhost:9223 に直接接続します。

ファイル内容
mcp-config.json最小限の MCP クライアント設定です。プレースホルダーのパスを置き換えてください。
AGENT_INSTRUCTIONS.mdエージェント向けのコピー&ペースト用指示です。人間への引き継ぎ手順も含みます。
raw-ws.mjs最小構成の生 WebSocket クライアントです。
read-page.mjs1 つのページを読み込み、操作可能な要素を表示します。
feed-tree.mjs情報が密集したフィードを型付き構造に変換して操作します。すべて生の JSON-RPC で行います(デモその 1)。
with-human.mjs人間の介入が必要な場合の一連の手順です。
human-takeover.mjsエージェントが一時停止し、あなたがクリックすると、自分のページ上でエージェントが再開します(デモその 2)。
node examples/read-page.mjs

トラブルシューティング

ページは更新されましたが、ウィンドウには古い内容が表示されたままです。 以前は、タブを何度も切り替える必要がありました。コンポジターに BrowserView を再描画させる唯一の方法が、setActiveTab による removeBrowserView + addBrowserView + setBounds で、ナビゲーションでは再描画されなかったためです。現在は、すべてのナビゲーションシグナルが再描画をトリガーします。did-navigate、did-navigate-in-page(SPA のルート変更ではロードイベントが一切発生しません)、did-frame-finish-load(iframe 内のキャンバスエディターは、トップレベルのドキュメントの完了後、かなり時間が経ってから起動します)に加え、ウィンドウの show / restore / focus も対象です。これは、このウィンドウがフォーカスを奪わないよう設計されているため、何分もの間エディターの背後に置かれることがある点で重要です。それでも表示が古いままなら、ui.repaint を呼び出すと必要に応じて強制的に再描画し、repaint_count を返すので、実際に再描画されたことを確認できます。

アクションは成功と報告されましたが、何も変わりませんでした。 知っておくべき原因が 2 つあります。

  • 生の WS では、ui.act はアクションごとの引数を params.params の下にネストします — {action:"type", target:"<id>", params:{text:"…"}}。これを {text:"…"} のようにフラット化すると、空文字列が渡されます。以前は冪等性のショートサーキットに該当し、何も入力されていないのに success が報告されていました。現在は明示的なエラーを返します。
  • アクションが実際に何もしていない場合もあります(オーバーレイがクリックを妨げた、アプリが変更を元に戻したなど)。その場合は browse_diff を使い、戻り値を信用するのではなく、スナップショットと差分のペアで書き込みを囲みます。

アクションがいつまでも戻りません。 ページにアクセスするすべての経路には期限が設けられているため、メインスレッドがブロックされたページ(重い再計算、大きな JSON.parse、ループするサードパーティースクリプトなど)がツール呼び出しを停止させることはありません。メインスレッドを 120s 占有するページでの測定結果:

呼び出し動作
ui.get_tree / ui.get_focused5s でエラーになります(getTree timeout)
ui.evaluate10s でエラーになります(CDP が 5s、その後 preload のフォールバックが 5s)
ui.act15s で success:false, timed_out:true を返します
ui.capabilities通常どおり返ります(約 15s)
ui.list_tabs / ui.network_list即時に返ります — ページにはアクセスしません

ui.act の制限時間は要求された処理に応じて調整されるため、正当な長時間処理が中断されることはありません。type / setContent の制限時間は text.length × interval_ms、drag は duration_ms + steps、press は repeat × interval_ms で、clear には 20s が設定されます。params.params.timeout_ms(下限 1s、上限 300s)またはグローバル設定 AI_BROWSER_ACTION_TIMEOUT_MS で上書きできます。タイムアウト時は、アクションが一部だけ適用された可能性があることが明示的に警告されます — ポインターがすでに押されている、文字が半分だけ入力されているなどの可能性があるため、同じ書き込みを二重に実行するのではなく、再試行の前に読み取り直してください。

アプリが自動的に再起動します。 Electron の NetworkService 全体が応答しなくなることがあります。ポートは引き続きリッスンしているのに、すべてのナビゲーションが ERR_FAILED で失敗し、curl は問題なく動作します。ウォッチドッグはメインプロセスから 30s ごとに確認を行い、連続 5 回の確認に失敗すると network_wedged をブロードキャストして再起動します(MCP クライアントは自動的に再接続します)。リクエストごとの期限が 5s のため、実際に応答しなくなった場合、おおむね 2 分で再起動します。

  • 確認対象を変更するには、AI_BROWSER_WATCHDOG_URLS=https://www.baidu.com/,https://example.com/ を設定します(カンマ区切り)。デフォルトは example.com と www.baidu.com で、この複数指定は意図的なものです。どれか 1 つの対象が応答すればネットワークは正常とみなされるため、不安定なホストや地域制限のあるホストが 1 つあっても、再起動ループは発生しません。ネットワークから到達できない対象を 1 つだけ設定しないでください。
  • 完全に無効にするには、AI_BROWSER_WATCHDOG=0 を設定します(off / false / no も使用できます)。

ポート 9223 はすでに使用中です。 別のインスタンス(以前のセッションから残っているものがよくあります)または別の何かがポートを使用しています。そのインスタンスを終了するか、AI Browser に別のポートを割り当ててください。

AI_BROWSER_PORT=9224 npm start

すべてのクライアントでポートを統一してください。アプリと MCP サーバーは、どちらも src/shared/config.js から AI_BROWSER_PORT を読み込みます。 Electron が見つからない、または起動直後に失敗します。 約 100 MB のエンジンがダウンロードされていません。npm パッケージを使用している場合は、もう一度起動するだけでランチャーが自動的にダウンロードします(ネットワークが遅い場合は ELECTRON_MIRROR を設定してください。[インストール](#install)を参照)。ソースから実行している場合は、ls node_modules/electron/dist を確認し、ミラーを指定して npm install を再実行してください。AI_BROWSER_PORT を設定しても解決しません。これはバイナリが見つからない問題であり、ポートの競合ではありません。

入力は受け付けられますが、何も起こりません。 通常はウィンドウが隠れています。これを防ぐために上記の 3 つの disable-*-backgrounding スイッチがありますが、それでもクリックが成功したと報告されるのに何も動かない場合は、AI Browser のウィンドウを前面に表示して、もう一度お試しください。

ログイン状態が何度も失われます。 セッションは ~/.ai-browser に保存されます。保存先を変更する場合、別々の AI_BROWSER_USER_DATA ディレクトリは別々のセッションストアになる点に注意してください。一方で「ログイン済み」でも、もう一方では「ログイン済み」ではありません。

AI_BROWSER_USER_DATA=~/.ai-browser-work npm start

ウィンドウの下部に赤いバーが表示されました。 これはメインプロセスの例外です。操作対象のページではなく、AI Browser 自体のバグです。意図的にモーダルダイアログにはしていません。ネイティブの dialog.showMessageBox は macOS では NSAlert のモーダルループとなり、メインプロセス全体をブロックします(計測では、ダイアログがない場合は WebSocket 呼び出しが 4ms だったのに対し、ダイアログがある場合は8 秒間応答がありませんでした)。その結果、自己回復する JS 例外が「すべての RPC がタイムアウトする」状態に変わってしまいます。バーにはメッセージとログのパスが表示され、プロセスは動作し続けます。× をクリックすると閉じられます。

すべての例外は ~/.ai-browser/logs/error.jsonl に同期的に追記され(2MB × 4 のローテーション)、最後の 20 件の WebSocket 呼び出しも添付されるため、問題発生時に何が行われていたかを確認できます。API の ui.errors / browse_errors で読み取るか、ファイルに対して tail を実行してください。shutdown と対になる記録がない boot の記録は、前回プロセスが強制終了したことを示します。

ウィンドウが一度も作成されていない段階で例外が発生した場合、バーを表示する場所がないため、代わりにネイティブのボックスを一度だけ表示します(error_dialog_fallback として記録されます)。これにより、アプリが何も通知せず停止することを防ぎます。どちらの表示方法も切り替えられます。AI_BROWSER_ERROR_BANNER=0 でバーを無効にし、AI_BROWSER_ERROR_DIALOG=1 でモーダルボックスを強制します。後者はメインプロセスをブロックするため、起動時のクラッシュを意図的に確認するときだけ使用してください。

要素はページ上にはっきり表示されているのに、ツリーにない

サイト固有のスクリプトを書く前に、次のリストを順に確認してください。「querySelector のチェーンで固定しよう」という反射的な対応が、このプロジェクトのこれまでの開発で142個のサイト固有スクリプトと731個のハードコードされたセレクターを生みましたが、そのほとんどは必要ありませんでした。

#確認事項方法原因だった場合
1読み取りレイヤーの不足ですか?node tools/coverage_probe.cjs https://the.site/クリック可能な要素がツリーにありません → 報告してください。修正箇所はスクリプトではなく src/preload/extractor.cjs です
2操作レイヤーの不足ですか?ノードはツリーにあるが、ui.act が失敗するか何もしない要素は読み取れますが操作できません(iframe/OOPIF、canvas、信頼された入力、未マウントの入力など)→ これもブラウザー側で修正する問題です
3タイミングの問題ですか?2~3秒後にツリーを再読み込みするか、wait_until を使うツリーを読み取った時点では、SPA のレンダリングがまだ終わっていませんでした
4ホバーしている間だけ存在しますか?ui.peek { target, revertMs: 0 }ホバー時に表示される操作要素です。peek はホバーして表示された要素を返します。revertMs: 0 を渡すと開いたままになるので、返された ID を使って ui.act click してください
5名前がありませんか?ui.peek を使うか、クリックして表示内容を確認するサイトが aria-label/title/alt を指定していないアイコンのみのボタンに、何もないところから名前を付けることはできません。ただし、開いたパネルには通常名前があります。どのアイコンか推測する代わりに、パネルを開いてそちらを読み取ってください
6ここまで確認してから:自分で固定する自分のスクリプト内のセレクター自分のスクリプト内にとどめてください。「このサイトの仕組み」として組み込まないでください。次のサイトには当てはまりません

coverage_probe は、人がクリック対象と呼ぶもの、つまりブラウザー自体が手のカーソル(cursor:pointer)で描画する要素(カーソルを継承するすべての子孫ではなく、ポインターが始まる要素)を数え、それらのうち data-ai-id を持つものの数を報告します。

$ node tools/coverage_probe.cjs https://www.csdn.net/
可点目标(手型边界): 257
  ├ 自己带 data-ai-id(直接可操作): 257 → 100%
  ├ 被单个交互后代盖满且它在树里(壳子,点内层等价): 0
  ├ 祖先歧义(同一祖先下多个手型后代,点祖先指不准): 0
  └ 整条祖先链无 id(真看不见): 0
可用率(in_tree + by_desc + anc_only): 100 % | 真缺口: 0

80%未満の場合はゼロ以外の終了コードを返すため、CI に組み込めます。2026-10-17 に測定(Baidu/Bilibili/CSDN):クリック境界の修正前は 82%/97%/88%、修正後は 100%/100%/100% でした。ui.get_tree あたり約40ミリ秒です。

この3つの数値は、分母の取り方が間違っていました。 2026-10-19 まで、このツールはトップドキュメントしか数えていませんでした。ページの本体がプロセス間 iframe 内にある場合、「クリック対象4件、利用可能率100%、不足0件」と報告する一方で、コースカタログ全体が見えない状態でした。現在は到達可能なすべての frame-N を測定し、到達できなかったフレームの数も表示します。分母に未測定の領域が含まれるのは、測定を誤るよりも悪いためです。2026-10-19 の再測定結果:Baidu 100%、CSDN 99%(プロセス間フレーム8件を新たに計上し、残る不足は2件)、超星のコースページは82%で、残る2件の不足を明示しました。こうした間違い、つまり重要な部分をひそかに除外する指標についてのレポートは、docs/POSTMORTEM_FRAME_VISIBILITY.md にあります。

数値では対象外となるものが2つあります。追いかける必要はありません。ページが読み取り可能な名前を付けていない要素(alt のない画像のみのリンク。Bilibili のクリック可能なノードの53%)には、名前を作り出さない限りラベルを付けられません。また、canvas には読み取り可能な DOM 自体がありません。これらは本当の限界であり、バグではありません。継承された cursor によって分母が膨らみ、最初の測定が3倍もずれていた理由を含む詳細なレポートは、docs/AGENT_OPERABILITY_FEEDBACK.md §6 にあります。


テストと開発

npm test             # vitest — 411 tests across 27 files
npm run smoke        # Electron contract layer, offline fixture      — 31 checks
npm run capabilities # text fields, canvas, page source, network, DOM editing — 54 checks
npm run richtext     # rich-text editors, nested menus, forms, errors, tags, upload — 117 checks
npm run network      # request log, POST bodies, headers, pagination, redaction — 41 checks
npm run value        # value truncation contract + read-only get_value — 15 checks
npm run canvas       # canvas draw-call capture — 13 checks
npm run modeldiff    # canvas app data-model diff (the "unchanged" false green) — 12 checks
npm run audit        # every advertised action must exist; wait/downloads/permissions/quit — 35 checks
npm run nav          # address bar + load-failure error page (the "white screen" report) — 19 checks
npm run focus        # no focus stealing + full capability while backgrounded — 32 checks
npm run interact     # trusted drag / key press / wheel (pointer gestures) — 31 checks
npm run view         # viewport repaint + snapshot/diff verification — 28 checks
npm run e2e:all      # 一次跑完上面全部套件(每个独立端口)+ 可信汇总 — 428 checks
npm run realsites    # navigate/read/act against a matrix of real sites

すべての E2E スイートは、オフラインのフィクスチャを使用して実際の Electron を起動し、失敗するとゼロ以外の終了コードを返します。 これらを合わせることで、jsdom では構造上テストできない項目を網羅します。プリロードの整合性、厳格な CSP 下での ui.evaluate、CDP のクリックヒットテスト、マルチタブのライフサイクル、イベントのファンアウト、そして evaluate のガードレールです。

各スイートは、起動したばかりのブラウザーと通信していることを検証します。 アサーションを実行する前に ui.server_info を呼び出し、返された pid を起動した子プロセスのものと比較します。一致しない場合は、 他のインスタンスについての結果を黙って生成するのではなく、終了コード 4 で中断します。 この問題は推測ではなく実測されています。偽の WebSocket サーバーがポートを占有していると、スイートは ui.new_tab/ui.evaluate/ui.get_tree を問題なく送信し、何も気づきませんでした。再現方法: REPRO_PORT=9799 node e2e/_repro_identity.cjs。

npm run e2e:all は、各スイートに個別のポートも割り当てます。プロファイルとして /tmp/ai-browser-e2e-<port> を共有し、Electron のシングルインスタンスロックも共有するためです。

すべてのスイートで WS ポートが空いている必要があります。実行中の npm start を先に停止するか、別のポートを指定してください。

AI_BROWSER_PORT=9333 npm run smoke

スイートは独自の Electron を分離されたプロファイル(デフォルトでは /tmp/ai-browser-e2e-<port>)で起動するため、 ログイン済みセッションが保存されている実際の ~/.ai-browser プロファイルには触れません。また、 終了時には Ctrl-C/タイムアウト/クラッシュの場合も含め、Electron をクリーンアップします。孤立したプロセスが ポートとシングルインスタンスロックを占有し続けると、npm start の起動時に "another AI Browser instance is already running" と表示されて起動できなくなるためです。

各機能スイートで検証した内容

npm run view — 誤った安心感をひそかにもたらす 2 つの問題を対象にしたスイートです。

ビューポート。 BrowserView は完全に読み込み済みでも古いピクセルを表示し続けることがあります。再描画を強制できる唯一の操作はタブの切り替えでした。だからこそ、「タブを何度かクリックする」と直っていたのです。現在は、ナビゲーションのシグナルごと、およびウィンドウの show/restore/focus 時に再描画を実行します。このスイートではスクリーンショットではなく、累積再描画カウンターによってそれを検証します。スクリーンショットの判定には人の目が必要ですが、カウンターは誤りを明らかにできます。また、デバウンスが自ら再実行されないこと(再描画ループが発生すると、いつまでもちらつきます)と、再描画によってページデータが変化しないことも検証します。

検証。 ui.diff が役立つのは、両方向で正しい場合に限られるため、各アサーションはペアで実行されます。実際に変更があれば必ず報告され、変更がないページは報告されてはいけません。重要なのは否定的な対照です。変更を捏造する diff や、エラー時にスナップショットがひそかに空になり(その結果、以降の diff がすべて「変更なし」と報告される)ようなことがあれば、検証全体が誤った成功判定になります。またこのスイートでは、パラメーター層の入力ミスにより、何も入力していないのに type が成功を報告する不具合も再現できるようにしています。

npm run richtext — 最も詳細なスイートです。リッチテキストエディターと書式設定、2 階層および 3 階層のネストメニュー、必須フィールドと任意フィールド(ネイティブの required、aria-required、赤いアスタリスク)、エラーと警告の表示(フィールド単位、フォーム単位、トースト、モーダルダイアログ)、ラジオボタン/チェックボックス/セレクト/コンボボックス、タグ入力と Weibo 形式のトピック挿入、画像アップロードを検証します。操作に関するすべてのアサーションを、セマンティックツリーだけでなく実際の DOMと照合します。

このスイートによって、製品には 2 つの変更が加えられました。

  • 自身を閉じるボタンをクリックすると、以前は二重に実行されていました(ディスパッチ後にヒットテストが行われ、対象がなくなっているのを検出して、2回目のクリックにフォールバックしていました)。その結果、送信やメンションが重複していました。現在はディスパッチ前にヒットテストを行い、フォールバックしません。
  • フィールドレベルのエラーは、以前は見えませんでした。ARIA ロールのない素の<span>がエラーを保持しており、レイアウトとして除去されていました。現在はcontext.errors / context.field_errorsに表示され、ダイアログコンテナ内だけでなく、ドキュメント全体を対象にスキャンされます。
  • 以前は、3つの読み取り処理がコンテナのクラス名に依存していました。赤いアスタリスクは、クラス名にmodal/dialog/popupなどが含まれるコンテナ内だけでスキャンされていたため、通常のページにあるフォームの必須フィールドは見えませんでした。modals[].fields[]のエントリーにはidがなく、あるフィールドのエラーをそのフィールドに対応付けられませんでした。また、素の<span>/<div>にある補助テキスト(文字数カウンターや形式のヒント)は、セマンティックツリーから除去され、別の取得経路もありませんでした。現在は3つすべてがcontextのトップレベルにあります。required_hintsはドキュメント全体を対象にスキャンされ、fields[]のエントリーはmodals[]とforms[]で同じ形式(idを含み、実際のDOMで検証済み)になり、フォームに隣接する素のspan内のテキストはhintsに格納されます。セマンティックツリー自体の除去処理は維持されるため、トークンコストは変わりません。

npm run capabilities — プレーンな入力欄、password、number、textarea、contenteditableのほか、readonly/disabled/maxlengthを含む、あらゆる種類のテキストフィールドへの入力(CJK入力と1000文字のテキストを含む)、ページソースの読み取り、レスポンスボディの取得、ui.evaluateによるページ編集を検証します。

npm run network — POSTリクエストを完全なリクエストボディ付きで取得できること、ヘッダーがデフォルトで秘匿されること、ボディが暗黙に切り詰められずページネーションされることを検証します。また、削除ルールも固定します。保持するバイト数の上限は全タブで共通のため、以前はバックグラウンドタブが上限をひそかに使い切ると、エージェントがアクティブタブで読もうとしていたボディが削除されていました。リクエスト自体は実際に行われ、ボディも取得されていましたが、別のタブのノイズによって追い出されていたのです。現在は、バックグラウンドで保存中のボディも含め、まず非アクティブタブのデータを削除します。また、ui.network_configureはactive_tab/body_bytesを報告するため、将来evictedが発生した際に推測ではなく原因を特定できます。

npm run modeldiff — ui.diffが以前、誤った結果を返していた唯一のケースを検証するテストスイートです。キャンバスで描画するアプリのDOMは静止画のようなものです。ノードをドラッグしたりウィジェットを編集したりすると<canvas>が再描画されますが、要素の走査で検出できるものは何も動きません。そのため、DOMのみを使うデフォルトのスナップショットは、実際に行われた書き込みに対してunchangedを返していました。ComfyUIで文書化されている書き込み経路であるnode.widgets[].valueも同様で、DOMはバイト単位でまったく変化しません。このスイートは、自前で作成したフィクスチャでモデルのフィンガープリントを検証します(スナップ処理や量子化によって「元に戻す」が正しくなくなるため、誰かの稼働中のプロジェクトでは決して検証しません)。

  • ノードを追加するとverdict: changedとなり、差分パスは**model.count/model.fpのみ**です。nodes.*もtext*もゼロであり、これによって変更の検出がモデルによるもので、DOMがたまたま動いたことによるものではないとテストで証明します。
  • ComfyUIのケースでは、ノード数を変えずに1つのウィジェット値を変更してもchangedとなり、具体的にはmodel.countではなくmodel.fpが報告されます。
  • 双方向の検証です。変更されていないページはunchangedのままである必要があり、アプリオブジェクトのない通常のページにmodelフィールドが追加されてはいけません(候補をでっち上げる仕組みによる誤検出を防ぎます)。
  • モデルのシグナルがDOMの変更を覆い隠してはいけません。両方が変化した場合は、両方とも報告されます。

このスイートが固定する設計上のポイントは2つあります。どちらも、苦い経験から学んだものです。

  • 選定の基準は、名前が有望そうかどうかではなく、実際に読み戻せるかどうかです。ライブラリの名前空間は名前に基づく評価は高くても、返せるモデルがないため、null を返すことが除外の理由になります。
  • ランクはオブジェクトが備えているアクセサーのうち最も優れたものに基づき、読み取りでは serialize() があればそれを経由します(JSON セーフなデータを返します。ライブコレクションは通常、循環参照になっています)。この2つをまとめると、すべての候補が serialize と報告され、同率になり、単なるシリアライザーが実際のモデルより上位になる可能性があります。このバグはレビューではなく、同率時の優先順位を検証するテストで検出されました。
Canvas:ピクセルではなく描画呼び出しを読み取る

セマンティックツリーでは、<canvas> は role: canvas として報告されるだけで、それ以上の情報はありません。テキストもピクセルもありません。 browse_canvas は、スクリーンショットを撮らずにこの問題を解決します。レコーダーをページのメインワールドに注入し、Canvas API をラップして、描画呼び出しそのものを記録します。

ポイントは、canvas はコードによって描画されるため、コードは何を描画したかをすでに把握しているということです。fillText からは文字列そのものを取得できます。canvas 内のテキストに OCR は不要です。

// browse_canvas(operation: "read", canvas_id: "canvas:nav-…:frame-main:1")
{
  "kind": "2d", "readability": "semantic",
  "texts": [
    { "seq": 2, "method": "fillText", "text": "确认支付",
      "bounds_canvas": { "x": 20, "y": 40, "width": 120, "height": 30 },
      "style": { "font": "20px sans-serif", "fill_style": "#000000" } }
  ],
  "regions": [ { "seq": 3, "kind": "rect", "method": "fillRect", "bounds_canvas": {…} } ]
}

レコーダーは乗っ取りではなく、ティーとして機能します。まずネイティブ API を呼び出し(Reflect.apply(original, …))、その後に独自の try/catch 内で記録します。記録に失敗しても、戻り値、例外、this、描画順序が変わることはありません。

知っておくべき制限が2つあります。

  • フックはページの初回読み込み後にインストールされるため、初回読み込み中に描画された内容は復元できません。browse_canvas(operation: "configure", reload: true) を実行すると、フックを設定した状態で再読み込みされ、履歴全体を取得できます。(根本原因:Electron で新規作成されたタブでは、CDP コマンドを await すると処理が停止するため、did-finish-load までインストールを待つ必要があります。)
  • **WebGL は不透明です。**頂点、テクスチャ、シェーダーからビジネス上の意味を逆算することはできず、テキストは通常、グリフアトラスです。そのような canvas は推測せず、opaque としてマークされます。この場合に限り、operation: "capture" は canvas 領域の PNG を返します。デフォルトでは無効で、明示的に呼び出す必要があり、エージェントのビジョントークンを消費します。

AI_BROWSER_CANVAS_HOOK=0 で完全に無効化できます。

利用する前に知っておきたい制約
  • node.value は200文字で切り詰められます。 これはCDPの制限ではなく、このプロジェクトの制限です。切り詰められた場合、ノードには value_truncated: true、value_full_length、すぐに呼び出せる value_fetch ヒントが付与されます。読み取り専用でページネーションに対応した browse_act(action: "get_value") を使用してください。
  • パスワードフィールドの値は一切公開されず、value_sensitive: true のみが示されます。セマンティックツリーはそのままエージェントのコンテキストに渡されるため、秘密情報を含めないようにしてください。
  • js_error はすべてのタブにブロードキャストされます(各イベントに tabId が含まれます)。そのため、順序やインデックスは保証されません。tabId でフィルタリングし、「最初のエラーが目的のエラー」と決めつけないでください。
  • 「接続ハンドル」が使われるのは、実際のポインターがそこに当たる場合だけです。 多くのサイトでは、ハンドルを pointer-events: none と opacity: 0 の装飾として描画しています(rhtvキャンバスで確認済み。すべてのハンドルが pe:none / op:0 です)。そのような要素をアンカーにすると、代わりにノード本体が押されてしまいます。そのため、リゾルバーは elementFromPoint で自己チェックし、要素の境界の中点にフォールバックして via: "edge" を報告します。ハンドルを想定していた場所で via: "edge" が表示された場合、それが正確な結果です。その場所ではドラッグによる接続はできず、サイト側で別の方法を使ってノードを接続する必要があります。
  • ui.scroll はドキュメントをスクロールします。 window.scrollBy を実行するため、キャンバスベースのエディター、マップ、チャートでは何も起こりません。その場合は ui.act{action:'wheel'} を使用してください。

npm run realsites は e2e/realsites/sites.cjs を基にしたテーブル駆動方式で、現在8つのカテゴリ(検索、金融、メディア、開発、eコマース、SPA、エディター、マーケットプレイス)にわたる17サイトを対象としています。実行結果には2つの数値が表示され、意図的に1つにまとめられることはありません。

  • 動作正解率 / behaviour rate — ブラウザーは想定どおりに動作したか。ログインが必要なサイトや、ボット対策の壁を表示するサイトは、その壁を正しく検出できた場合、合格として数えられます。これは検出が機能していることを意味するためです。
  • コンテンツ成功率 / content rate — 実際にコンテンツを読み取れたか。サイトの minNodes を超えるノード数がツリーに返されたサイトのみが対象です。壁はコンテンツではありません。

単一の「成功率」を報告すると、正しく検出されたログイン画面によってコンテンツの数値が水増しされてしまいます。また、実行結果にはコーパスのサイズ、サイトごとの実行回数、総実行回数、タイムスタンプも表示されるため、レポート内のパーセンテージを鵜呑みにせず再計算できます。

構成:

src/main/      Electron main — ws_server · page_manager (tabs, CDP) · mcp_server · mcp_tools · axExtractor
src/preload/   bridge · extractor (semantic tree, data-ai-id) · actions · watcher
src/renderer/  tab bar UI
src/shared/    config (AI_BROWSER_* env) · guards (evaluate safety) · protocol · watchdog
e2e/           smoke_ws.cjs · realsites/
tests/         vitest unit suites
skills/        optional higher-level recipes built on the tools

ポート、ユーザーデータディレクトリ、UA のオーバーライドの唯一の定義元は src/shared/config.js です。guards.js は evaluate の制限の唯一の定義元であり、両方のトランスポートで同一に適用されます。


互換性

項目対応状況
エージェント任意のMCP stdioクライアントに対応します。代わりに、任意の言語から生のWSを使用することもできます
Node≥ 18
OSmacOS と Linux — これらの環境で開発・テストされています。Windows:実験的で、未テストです。 ElectronとCDPのみを使用しているためビルドできるはずですが、実行した人はいません。npm install とウィンドウレイヤーのデバッグが必要になることを想定してください。いずれの場合も結果を報告してください。
ページのフレームワークReact / Vue / Angular / vanilla DOM — 要素の特定はフレームワークベースではなく、DOMベースです
エディターフレームワークごとのコードパスではなく、信頼された入力を通じて操作するリッチエディター
ページの種類静的ページとSPA。動的な更新はツリー、dom_change / state_changed イベントで確認できます

貢献

Issue や PR を歓迎します。特に、セマンティックツリーでページを正しく取得できない再現ケースは、このプロジェクトにとって最も役立つバグ報告です。

質問、「ツリーがこのページを正しく取得できない」という報告、エージェントの実行ログは、GitHub Discussions への投稿がおすすめです。このプロジェクトにとって最も有益なフィードバックが集まる場所です。

私たちが守ろうとしていること:

  • コアは汎用的に保つこと。 サイト固有の知識は src/main と src/preload の外に置いてください。
  • IPC はリクエスト ID に対応させ、 CDP セッションは参照カウント方式を維持してください。そうしないとタブがリークします。
  • スクリーンショットは使いません。 動作にピクセルが必要な変更であれば、設計上の課題はその手前にあります。
  • evaluate は常に保護します(長さ制限、Node 識別子の禁止)。両方のトランスポートで同時に適用してください。
  • 動作が変わるときは、エージェント向けの指示も変更します。 MCP ツールの一覧、ws_server.js のメソッド、examples/AGENT_INSTRUCTIONS.md の内容は一致している必要があります。mcp_tools.js と食い違うドキュメントはバグです。

ライセンス

MIT © Glittering

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。