初めて chrome-use で Slack デスクトップ版につなぎ、snapshot で出てきた名前付きのボタンの山を見たとき、少し驚いた。これはWebページを操作するのとまったく変わらない。あとで納得した。Slack デスクトップ版は、そもそもWebページなのだ。
Electron アプリは、Chromium レンダリングエンジンに Node.js の外殻を足したものだ。Slack、VS Code、Discord、Figma、Notion を「デスクトップソフト」として使っているつもりでも、その画面はすべて Chromium がレンダリングした HTML で、外側にネイティブウィンドウの殻をかぶせているだけだ。この事実を受け入れると、もともとかなり難しかったことが、崩れるように簡単になる。デスクトップアプリの操作は、Webページの操作へと退化する。

デスクトップ自動化はもともとどれほど扱いにくかったか
まず、この道がなかったとき、プログラムにデスクトップアプリを操作させるのがどれほど難しかったかを話そう。
ひとつの方法は、スクリーンショットを撮り、ボタンのピクセル座標を探し、そこをクリックすることだ。解像度が変わり、テーマが変わり、ウィンドウが動いただけで、座標はすべてずれる。しかもプログラムは画面に何があるのかを「理解」できず、ただ1枚の画像を見ているだけだ。もうひとつの方法は、システムのアクセシビリティ API、つまり macOS の Accessibility や Windows の UIAutomation を使うことだ。これならコントロールツリーを取得できるが、プラットフォームごとに API が別で、しかも Electron アプリがこの API に公開する情報は、しばしば不完全だ。なぜなら、その「コントロール」は実際にはネイティブコントロールではなく div だからだ。
どちらの方法も手間がかかるし、どちらもひとつの事実を迂回している。このアプリの内部には、構造がはっきりしていて意味もある DOM があるのに、外側からピクセル越し、あるいは変換された層越しに、それへ手を伸ばしているのだ。
Chromium には最初から扉がある
レンダリングエンジンが Chromium なので、Electron アプリは自然に Chrome DevTools Protocol を話せる。起動時に --remote-debugging-port を付けると、CDP ポートが開き、内部の DOM がそのまま露出する。
open -a "Slack" --args --remote-debugging-port=9222
chrome-use connect 9222
chrome-use snapshot -i
(アプリごとに衝突しないポートを割り当てればよい。全プラットフォームの起動コマンドはドキュメントサイトにあるので、ここでは詳しく展開しない。)
snapshot が返すのは、その DOM のアクセシビリティ視点だ。すべてのボタンや入力欄は名前を持つ要素であり、@ref で特定できる。あなたは「送信ボタンはたぶん右下あたりのピクセルにある」と推測しているのではない。button "Send" そのものを取得している。ウィンドウが動いても、テーマが変わっても影響しない。結びついているのは座標ではなく要素だからだ。agent にとって、この違いは本質的だ。いまや agent は、このデスクトップアプリがどんな姿をしているかを読み取れる。スクリーンショットを相手に当てずっぽうでクリックしているのではない。
必ず知っておくべきトレードオフ
ここには、ブラウザの場面とは違う点がある。思い込みやすいので、別に話しておく価値がある。
普段使っている Chrome を操作するとき、chrome-use はブラウザ拡張と native messaging を使う。デバッグポートには触れないので、「リモートデバッグを許可するか」という確認ダイアログもなく、Runtime.enable のようなプロトコル層の痕跡もない。しかし Electron アプリには Chrome ウェブストアの拡張を入れられない。だから、この道では --remote-debugging-port を使うしかない。つまり、ブラウザの場面にあった「痕跡ゼロ」のような利点は、Electron では得られない。前提は、自分でデバッグポートを開いて、そこへ接続することだ。
Slack や VS Code のような「自分のアプリ」を自動化するなら、これはまったく問題ではない。誰かをだまそうとしているのではなく、すでにログインしている自分のツールを自動化したいだけだからだ。ただし、ブラウザでの「検出されない」という期待を持ったまま Electron を操作しに来るなら、まず期待値を調整する必要がある。これは「自分のアプリへ接続する」ことであって、「他人のサイトを隠れて操作する」ことではない。
おもしろい細部:アプリの中にさらにアプリを入れられる
Electron アプリの内部には、しばしば <webview> がさらに埋め込まれている。たとえば Slack で開いた第三者アプリのパネルは、本質的にはもうひとつの独立したWebページだ。これらは CDP では独立した target になる。接続後にまず chrome-use tab で一覧を出すと、メインウィンドウ、設定ウィンドウ、そしてそれらの webview が、それぞれ target になっているのが見える。個別に切り替えて操作できる。
だから、ひとつの Electron アプリは「ひとつのWebページ」ではない。「ネイティブの殻の中に重なったWebページの束」だ。これを理解すると、マルチウィンドウや入れ子パネルのような、もともと頭を悩ませる場面は、すべて「正しい target に切り替える」だけになる。
境界
この方法は、Chromium 内核のアプリにだけ成り立つ。Swift/AppKit で書かれた純粋なネイティブアプリには CDP ポートがなく、接続できない。その場合は、システムのアクセシビリティに戻るか、別のツールを使う必要がある。iPhone 上のネイティブ app を操作するとき、私は別の仕組みを使っている。判断は簡単だ。Slack、VS Code、Discord、Figma、Notion、Spotify のような「ひとつのコードでマルチプラットフォームに対応する」デスクトップアプリは、基本的に Electron だ。
「デスクトップアプリは実はWebページだ」と理解してから、私はデスクトップツールを見ると、まずそれが Electron かどうかを確認するようになった。Electron なら、それを操作することは、もはや私がずっと泥臭いと感じていた「デスクトップ自動化」ではない。すでによく知っている、Webページを開いて扱うためのあの方法になる。

微信
支付宝
コメント
コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。