Paul Jialiang Wu agentic-portfolio English 中文 Español 한국어✉️ Free list
← ポートフォリオに戻る

AI-NATIVEシリーズ · AGENT ENGINEERING

デスクトップアプリ制御──4つの入口、どの扉を開くかはアプリ次第

Paul Jialiang Wu · 2026年9月23日 · 読了13分

Desktop app control: four doors into a desktop app — scriptable, accessible, vision-only, unreachable — with the measured rung for Finder, Feishu, WeChat and Slack.
macOSアプリが外部に公開しうる4つの制御面と、それぞれにかかるコストについて解説する。

この記事の趣旨 デスクトップ自動化に着手する前に、まず「このアプリはどの入口を開いているのか?」という問いに答えるための手引きである。アプリが実際に開いている扉はどれか。 入口は4つあるが、どれを使えるかはアプリが決めている。そしてその選択が、あなたの自動化が次のアップデートで生き残るかどうかを左右する。

なぜそれが重要なのか 多くのチームは「この扉が開いているはずだ」と思い込んでエージェントを作り、数か月後に現実を知って書き直す羽目になる。計測自体は数秒で済むが、世間で「スクリプト可能」と思われている4つのアプリも、実際に測るとそうではなかった。

この記事で得られることMac上のどんなアプリでも調べて、その「段階」を読み取れる。始める前に、10行で済むのか1万行必要なのか、そして自分が使っている最中でも自動化が動くのかどうかを事前に知ることができる。

デスクトップアプリ制御――本稿の射程と想定読者

これからエージェントをデスクトップアプリ――たとえばチャットクライアント、デザインツール、あるいはAPIが用意される見込みのない社内向けアプリ――に向けようとする人のために書いた。ここで扱うのは、アプリがどんな制御面を外部に見せているかの調べ方、それぞれの制御面がもたらすコスト、そして私自身がプローブを走らせて初めて知った二つの発見だ。クラウドAPIは範囲外とする。もし狙いのアプリにちゃんとしたAPIがあるなら、そちらを使えばいい。以下は、APIが存在しない場合だけに意味がある話である。

全体を貫く一枚の図がある。これは単なるイメージではなく、実用的な道具として紹介したい。

レストランと仕入れ先――一枚の図で考える

レストランの厨房が取引する仕入れ先は、どれも同じように簡単とは限らない。

最も優秀な仕入れ先は伝票を添えて積み荷口まで納品してくれる。厨房から一歩も出る必要がない。注文は名前で伝えればいい――「火曜用の小麦粉を40キロ」といった具合で、その名前は来年も同じ意味で通じる。

次に良い仕入れ先は電話注文に対応している。欲しいものを名前と位置で伝える――「三段目の棚の、arborioとラベルされたやつ」など。arborio。手間はかかるし棚の配置が時々変わるが、まだ言葉でやりとりできている。座標指定ではない。

三番目はそもそも電話がない。自分で市場まで出向き、目で屋台を探して指さす。これでも取引はできるが、本人が現地に昼間立っていなければならず、屋台が移動すれば新しい目印を探す羽目になる。

そして四番目の仕入れ先は、そもそも営業していない。

これらはすべて、macOSアプリが何を外部に公開し、何を拒むかに一対一で対応している。

仕入れ先アプリが公開するもの対象の指定方法アップデートで壊れないか
積み荷口+伝票R3 AppleScript dictionary (sdef)アプリ独自の語彙で名前を指定するたいてい維持される——公開された契約であるためだ
電話での注文R2 アクセシビリティツリー(AX)役割やタイトルで指定する多くの場合は維持される——UIが再構築されるまでは
市場まで足を運ぶR1 ピクセル+合成クリック座標や見た目で指定するほとんど維持されない——UI刷新は書き直しに等しい
そもそも開いていないR0 ウィンドウ面すらなし——
Desktop app control ladder: R3 scriptable (Finder, Safari, Terminal), R2 accessible (Feishu), R1 vision plus menu (WeChat), R0 unreachable (Slack, Telegram), with what each rung costs.
同じラダーに、各段ごとの実際のコストと、1台のMacで測定した各アプリの段を重ねている。

このラダーが本当に役立つのは、各段が相互に置き換えできず、どの段になるかはこちらで選べないからだ。どの段を開けるかは、あなたが現れるよりずっと前にアプリ側が決めている。 唯一こちらに残された決断は、「今知るか、オートメーションを書き終えた後で知るか」だけだ。

だから、仮定せずに実際に測定した。

小さなプローブを書き、アプリごとに4つの独立したチェックを用意して自分のマシンで実行した。チェックをあえて分離したのは、アプリが1つには応答し別の1つは拒否することがあり、その組み合わせこそが全体像だからだ。

$ appctl survey WeChat Feishu Slack Telegram Finder Safari Terminal

  ⬜ Slack       R0_UNREACHABLE
  ⬜ Telegram    R0_UNREACHABLE
  🟨 WeChat      R1_VISION          +MENU(63)
  🟦 Feishu      R2_ACCESSIBILITY   +MENU(61)
  🟩 Finder      R3_SCRIPTABLE      +MENU(152)
  🟩 Safari      R3_SCRIPTABLE
  🟩 Terminal    R3_SCRIPTABLE      +MENU(132)

最初の結果こそ、じっくり考える価値がある。

4つのチャットアプリ、どれもAppleScript辞書を持っていない。 WeChat、Lark/Feishu、Slack、Telegram——sdef すべて不合格。Finder、Safari、Terminalはすべて合格。実際に作業するアプリは下位段、Apple純正アプリは最上段にいる。

この事実ひとつで、よくある計画が根底から崩れる。みんなが真似するリファレンス実装——たとえばSafari harness in CLI-Anything、 これは素晴らしい(which is excellent)。SafariがAppleScript辞書を持っているからこそ、まずAppleScriptから始める設計になっている。このやり方をチャットアプリにそのまま流用しても、まったく通用しない。「うまくいかない」ではなく、根本的に移植できない。

想定外の発見:コマンド63個、ウィンドウはゼロ

WeChatは実に興味深い存在だ。なぜなら、2つの問いに対して矛盾する答えを返してくるからである。

アクセシビリティAPIに「WeChatのウィンドウはいくつあるか」と尋ねると、ゼロと返される。一方で、より下層でピクセルを実際に合成しているウィンドウサーバーに聞くと、9つと返ってくる。最大サイズは1418×879。つまりウィンドウ自体は存在しているが、WeChatはアクセシビリティAPIにはそれらを一切見せていない。

ここで「使えない」と判断してしまいがちだ。私自身、危うくそうしかけた。しかしメニューバーは別の制御面であり、WeChatの場合は7つのメニューにまたがる63個のコマンドが完全に公開されている。Show Next Unread Chat, Chats, Contactsなども含まれる。Favorites.

「クリックできない」からといって「コマンドできない」わけではない。アプリはウィンドウツリー全体を拒否しても、座標指定不要の完全なコマンドサーフェスを提供できる。「AXツリーが空=制御不能」と決めつけると、唯一開いていたドアを自ら閉じることになる。

レストランに例えるなら、その屋台には電話もなく倉庫も見せてくれないが、正面に紙のメニューが貼ってあり、番号で注文できる。ローディングドックよりは劣るが、「何もない」よりは遥かにマシで、しかも安定している。なぜならメニューコマンドは「位置」ではなく「名前」で指定されるからだ。

2つ目の発見:隠しウィンドウは何も写らない

WeChatはR1の段に位置しているため、読むにはピクセルキャプチャが必要になる。メインウィンドウはアプリがトレイで閉じていても1418×879で列挙できたので、私は楽観的だった。ウィンドウIDでキャプチャし、読み取ればユーザーを一切邪魔しないはずだと考えた。

キャプチャ自体は成功したが、出てきたのは94KBの真っ白な画像.

オフスクリーンのウィンドウは列挙できても撮影――描画済みのピクセル自体が存在しない。これは単なるバグではなく、スケジューリング全体に波及する制約そのものである。

ビジョン層(Vision-rung)での制御は、ユーザーの目を盗んで裏で動かすことはできない。 アプリは必ず画面上に表示されていなければならず、つまりエージェントはユーザーとディスプレイの主導権を奪い合うことになる。たとえば、あなたが仕事中の午後3時にWeChatをこっそり読み取るような自動化は、この方式では絶対に実現できない。

だからこのプローブは、単に層(rung)を報告するだけでは終わらない。R1が決して無人で動作しないことも明示し、その理由まで伝える。こここそが、私が本当に貢献したと自負する部分である。

層(rung)は「主張」ではなく「上限」で決まる

こうした分類器でありがちなのは、「WeChat: controllable.」(訳:WeChat:制御可能)といった能力をそのまま出力したくなる誘惑だ。しかし、その文の価値は、それを支える最も弱い根拠の分しかない。私は以前capping a guarantee at its weakest assumption(保証は最も弱い前提で上限を設けるべきだ)について書いたが、その規律が防ぐ失敗がまさにこれだ。保証が、証拠が裏付けるレベルではなく、誰かの希望的観測のレベルで主張されてしまう。

だからこのプローブも、その規律を借用している。層(rung)を断言することはなく、DERIVATION(導出)を4つの計測から行い、計測が強いるすべての上限(cap)を適用する:

🟨 WeChat  R1_VISION
   +MENU — 63 commands, no coordinates needed
   unattended: NO — needs the screen
   ⚠ capped: every window is offscreen: capture returns a blank image, so the
     app must be brought on screen before it can be read
   ⚠ capped: vision control competes with the human for the display — it cannot
     run unattended while the machine is in use

単に閉じているだけのアプリはR0と報告し、「このプローブでは結論が出せない」という上限を明示する――なぜなら「起動していない」と「制御不能」は全く別の事実であり、それらを混同していたら、上記の証拠でSlackやTelegramを誤って除外していたはずだ。こうした上限はREADMEの文章ではなくテストによって強制される。締切に耐えるルールはそれしかない。

実際に動かして壊れたもの――Macで一番簡単なアプリですら例外ではなかった

最初の探索的プローブから手作業でフィクスチャを記録し、それをもとに分類器を書き、19個のテストがすべて緑になって満足していた。だが、同じアプリにライブのバックエンドを向けてみると――

2つ問題があり、そのうち1つは正直かなり恥ずかしいものだった。

Finderの判定はR1_VISION.だった。Finder――このマシンで最もスクリプト可能なアプリであり、1993年以来AppleScriptの代表的ターゲットだ。私のバンドル探索は/Applicationsをスキャンしていたが、Finderは /System/Library/CoreServices。見つからなかったため、辞書の読み込みもできず、最上位の段が一気に最下位まで落ちた。簡単なケースを誤分類する分類器は、分類器がないよりもタチが悪い。なぜなら、信用してしまうからだ。

Feishuはアプリケーションバンドルそのものが見つからなかった。 プロセス名は Feishu、 アプリケーションバンドルは Lark.app。どんな名前からパスへの推測も、何も見つけられない。

どちらも根本原因は同じで、解決策も同じだ。実行中のプロセス自身に、どこにあるかを尋ねればいい。名前から推測する必要はない。 プロセスは自分の居場所を知っている。私は、システムが素直に教えてくれることを、わざわざ推測していたにすぎなかった。

そして、どちらも発見できたのは、フィクスチャが実測値で記録されていたからだ。もし作り話のデータだったら、私の間違った思い込みに永遠に同意していただろう。手作業で記録した数値も細部で間違っていた。WeChatのメニューコマンドを19個と書いていたが、実際は63個。ウィンドウも23個と書いていたが、フィルタ後は9個だった。今のフィクスチャには、実際の出力がそのまま貼り付けてある。--json.

自分で作るなら20分もあれば十分だ

全体は4つのプローブと純粋関数だけでできている。標準ライブラリと、OSがもともと持っているもの以外には一切依存しない。

  1. スクリプティング。 を実行する。sdef /Applications/Foo.app。辞書があれば段3。Info.plistのNSAppleScriptEnabled フラグは無視する。アプリごとに設定がバラバラなので、記録はしても判定には使わない。
  2. アクセシビリティ。 System Eventsにcount of windows を問い合わせる。アクセシビリティ権限が必要。権限がなければエラーになるが、そのエラーは「ウィンドウがゼロ」と訳さず、そのまま報告しなければならない——この2つは全く逆の意味だからだ。
  3. メニューバー。 アイテムを列挙し、子要素をカウントする。1回の往復で。Electronアプリの場合、メニューごとに1回の呼び出しで約3秒かかり、全体のプローブは21秒だった。
  4. ウィンドウサーバ AX APIが拒否したウィンドウを確認する唯一の手段だ。macOS 15以降では、CGWindowListCreateImage が廃止され、標準ライブラリからウィンドウリストにアクセスする道が絶たれたため、ちょっとしたSwiftヘルパーが必要になる。

分類の手順はこうだ。最も高いレベルで応答したサーフェスを採用し、メニューは独立して記録する。満たされなかった前提条件は脚注ではなく「上限」として扱う。分類器は純粋関数で設計し、I/Oを排除することで、Macが手元になくてもロジックをテストできるようにしている。

この分類器が見抜けないもの

過大な主張をするゲートは悪用の温床になる。ここでは、この分類器が本当に見えないものを明確にしておく。

測っているのは「到達できるかどうか」であって、「使いやすさ」ではない。 Feishuは R2 でライブなツリーを持ち、トップレベルには AXGroup とボタンが3つ並ぶ。Electronアプリは、支援技術クライアントからリクエストが来るまで本物のツリーを隠している。到達できることは、作業のスタート地点にすぎず、ゴールではない。

許可されているかどうかは一切わからない。 アプリの利用規約、雇用主のポリシー、会話相手の意向——これらはプローブでは測定できない。自分のマシンで自分のクライアントを操作するのは、普通のアクセシビリティ利用であり、すべてのスクリーンリーダーがやっていることだ。一方で、アカウントを大規模に自動化するのはまったく別の話で、答えも異なる。プローブは、たとえ進むべきでない場合でも、ドアが開いていることだけは淡々と教えてくる。

これはあくまでスナップショットだ。 レベル(rung)は動く。アプリはマイナーリリースでアクセシビリティ対応を追加したり、逆に撤回したりする。測定が安価なのは、何度でも再実行できるようにしているからで、参照用の数字ではない。

そして正直な限界:この分類器が教えてくれるのは「コスト」であって、「価値」ではない。 WeChatがビジョン経由でしか操作できず、63個のメニューコマンドがあると分かっても、それを自動化する価値があるかどうかは分からない。分かるのは、やると決めた場合にかかるコストだけであり、それこそ多くの人が最後に気づく数字だ。

一般化できる話

今回の具体的な発見はmacOS上のチャットアプリについてだが、この構造自体は今後も変わらない。

自分でコントロールできないものを自動化しようとするたび、そこには複数のインターフェースの梯子があり、自分でその段を選ぶことはできない。最も安上がりな手は、作り始める前に自分がどの段にいるかを測っておくことだ。この失敗パターンは普遍的で、いつも同じだ。誰かが「荷受け口がある」と思い込んで何ヶ月もコードを書き、統合段階になって「電話もない」ことに気づく。

最初に20分だけプローブを走らせていれば、それで済んだ話だ。


関連記事

執筆:Paul Jialiang Wu · agentic-portfolio · 2026年9月23日。ここに記載したすべての数値は、 appctl macOS 26を搭載した1台のMac上でPaul Jialiang Wuが実測したものであり、記録済みのfixturesを使えばオフラインでも再現できる。appctl --fake probe <app>。アプリは自分のものだが、ドアは誰にでも開かれている。