DisPatch:ローカルAIエージェント用のセルフホスト・チャットアプリ
- カテゴリ
- AIとローカルLLM
- 公開日
- 2026年7月11日
- 更新日
- 2026年7月11日
- 著者
- Jacob Lloyd — プロジェクト完了後、AIの支援を受けて執筆
- 読了時間
- 約16分で読めます
かんたんに言うと: DisPatchは、自分のパソコン上ですべてをホストする、3つの機能がひとつになったアプリです。同じパソコン上で動くAIアシスタントと会話するチャットアプリであり、ファイルサーバーであり、ターミナルでもあります。手持ちのすべてのデバイスからいつでも使え、クラウドサービスには何も送りません。ファイルサーバーはどのデバイスからでもファイルをアシスタントが動くパソコンへ移せるので、アップロードした内容をそのまま扱わせられます。共用端末では、PINなしでも安全なアシスタントの一部が使え、PINを入れるとフルパワーのものまでアンロックされます。
DisPatchは、自分のAIエージェントにメッセージを送るためだけに30コンテナ構成のRocket.Chatを動かし続ける生活をやめたくて作ったアプリです。結果的に3つの機能がひとつになりました — ローカルでホストするLLMチャットアプリ、ファイルサーバー、そしてターミナルです。手持ちのすべてのデバイスから、いつでも使えます。約720KBのzipで、無料。いまや私が使う唯一のチャットフロントエンドです。
TL;DR
- これは何:セルフホストで3つの機能がひとつに。手持ちのローカルAIエージェント軍団(OpenClawゲートウェイ)向けのWebチャット、ドラッグ&ドロップのファイルサーバー、そして本物のターミナル。LANやTailscaleネットワーク上の全デバイスから、いつでも。
- 費用:$0。SaaSの席料もアカウントも不要。
- 必要なもの:Linux、Python 3.12+、
uv。返信が欲しければOpenClawゲートウェイ。それ以外はゲートウェイなしで全部動きます。 - 得られるもの:ボットごとのスレッド、ストリーミングMarkdown、ドラッグ&ドロップのファイルサーバー、アプリ内のコーディングエージェント用ターミナル、PINでロックできる「セーフモード」、cronジョブ用のプッシュAPI、そしてzipから出してそのまま通る187件のテスト。
最終的にこうなる
セットアップの話の前に、仕事をしている姿から。スクリーンショットはダミーデータ入りのサンドボックス環境のものです。本物のボットのログを世間にお見せする必要はないので。



実際にできること
- ボットごとのスレッドが、どの端末でも同じ。会話の途中でスマホから開いても同じスレッド、同じ履歴。再ログインの儀式はありません。
- ファイルサーバー。すべてのデバイスとこのパソコンの間でファイルをドラッグ&ドロップ。いちばん使っている機能で、後ろに専用のセクションを設けています。
- ターミナル(Terminex)。ブラウザ内でコーディングエージェントが動く本物のシェル。こちらも後ろに専用セクションがあります。
- ストリーミングMarkdownの返信。シンタックスハイライトつきコード(コピーボタン込み)、ズームライトボックスつきの画像・動画、PWAとしてインストール可能。
- ライブの「作業中」パネル。入力中ドットをクリックすると、モデルのツール呼び出しと思考をリアルタイムで覗けます。スピナーの演出ではなく、エージェントのトランスクリプトを実際にtailしています。
- 二段構えのロック。PINが、フルパワーのエージェントと、安全な一般情報専用のサブセットとを分けます。詳しくは後ろの二段ロックのセクションで。
- プッシュAPI(
POST /api/inject)。cronジョブや別のエージェントが、スレッドにメッセージ(や画像)を落とせます。頼んでもいないのに朝のブリーフィングがスマホに届くのは、この仕組みです。 - スレッド内でのローカル画像生成。エージェントに絵を頼むと、ローカルのComfyUIを呼び出せます。結果は他のメディアと同じようにチャットに届き、ライトボックスで拡大もできます。画像APIなし、どこへのアップロードもなし。
- オプション機能(デフォルトはオフ):その画像生成サービスをチャットから起動・停止できるComfyUIパネル。
ファイルサーバー
DisPatchの機能の中で、いちばん使っているのがこのファイルサーバーです。同じパソコンから配信される、ドラッグ&ドロップのファイル置き場です。どのデバイスからでもDisPatchを開いてファイルを落とせば、それでサーバー上にファイルが入ります。日付ごとにまとまり、画像や動画にはサムネイルがつきます。

ただの便利機能にとどまらないのは、ファイルがどこに落ちるかです。エージェントたちが動いているのと同じマシンに落ちます。スマホからアップロードした写真は、数秒後にはどのエージェントもディスクから読めます。説明させたり、整理させたり、Webサイトに載せたり、画像ツールに食わせたり。「いま手に持っている端末の中のファイル」と「エージェントが扱えるファイル」との橋渡しを、間にクラウドドライブを挟まず、ケーブルもなしでやってくれます。
地味な部分もちゃんと面倒を見ています。アップロードはバッファに載せる前にサイズを検査し(スマホがパソコンをOOMで落とせないように)、全体のストレージ上限があり、書き込みはアトミックなので中断されたアップロードが半端に書きかけのファイルを残すことはなく、ファイルストアはチャット履歴と同じローテーション式バックアップにミラーされます。設定フラグひとつで、全体を一方通行のアップロード専用ボックスに切り替えることもできます。デバイスはファイルを追加できるが閲覧はできない、という具合で、アプリに届く人数がその保管庫を読んでよい人数より多いような構成向けです。
Terminex:チャットの中のターミナル
毎日きっちり働くもうひとつの機能。DisPatchはブラウザの中に本物のターミナルを置けます。ボットレールには他のボットと同じように現れますが、開くとサーバー側のPTYに接続され、それがWebSocket経由でxterm.jsへストリーミングされます。モックではなく、パソコン上の本物のシェルセッションです。

うちのはそのままReasonixを立ち上げます。DeepSeekで動かしているClaude Codeスタイルのコーディングエージェントで、アシスタントの入り口ページではこの構成をTerminexと呼んでいます。つまりチャットのタブが、まるごとコーディングエージェントのコンソールになります。仕事を渡して、動いているのを眺める。それをアプリを開けるどのデバイスからでも。ファイルサーバーと組み合わせれば、パソコンをスマホから完全に操れます。どこにいてもファイルを放り込み、ターミナルを開いて、それをどうしてほしいかエージェントに伝えるだけです。
- アンロックモード専用。ターミナルはセーフモードには存在せず、有効化(
LOCAL_CHAT_TERMINAL)しない限りまるごとオフのままです。 - セッションはメモリ上。アプリからStart / Restart / Stopでき、サーバーを再起動すると終了します。裏で居座り続ける永続シェルはありません。
なぜ存在するのか
置き換えた相手はフル構成のRocket.Chatでした。データベースにコンテナの山。それで一人がローカルモデルと話すだけです。動いてはいましたが、「テキストを送って、返事をもらって、全端末で同期する」ためのインフラとしては、いくらなんでも大げさでした。
そして「そもそもなぜチャットアプリまでセルフホストするのか」への本当の答えはこうです。モデルをローカルで動かすようになると、最後にクラウドサービスへ誘惑してくるのがチャットUIなんです。そこもセルフホストすれば、プロンプトも、エージェントの出力も、家族のメッセージも家から一歩も出ません。そしてUI・ゲートウェイ・モデル・画像生成というループ全体が、インターネットを抜いても動き続けます。すでにローカルLLMに舵を切っているなら、ホスティング型のチャットフロントエンドだけが浮いた存在です。
設計判断でひとつ言っておきたいのは、DisPatchには意図的にビルド工程がないことです。フロントエンドはvanilla JS(ESモジュール)と素のCSS/HTML。バンドラーなし、npm run buildなし。そもそもの要件が「あとでAIエージェントが手を入れやすいこと」でした。JSファイルを編集して、リロードして、終わり。いまでも同じ判断をすると思います。
構成
| レイヤー | 中身 |
|---|---|
| バックエンド | Python 3.12+、FastAPI + uvicorn、aiosqlite(SQLiteファイル1個、WALモード)、Pillow。パッケージ管理はuv |
| バックエンド規模 | backend/app/に約7,200行(main.pyだけで約3,600行)、テストが約2,700行 |
| フロントエンド | ビルドなしのvanilla JS + HTML/CSS。JSが約4,900行、CSS/HTML/サービスワーカーが約2,100行 |
| 同梱ライブラリ | marked、highlight.js、DOMPurify、xterm.js。CDN呼び出しなし |
| データ | コードの外、~/.local/share/local-chat/に:DB、config.yaml、security.yaml、media/、files/、ローテーションされるDBバックアップ |
セットアップ
zip内のREADME-SETUP.mdの要約です(ダウンロードは下のボックスから)。
unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh
これでhttp://0.0.0.0:8765で起動し、LANとTailscaleのURLが表示されるので、スマホからタップして開けます。そこから先は:
~/.local/share/local-chat/config.yaml(自動生成、説明はconfig.yaml.exampleにあり)を編集して、ボットのidをOpenClawのエージェントidに合わせます。ホットリロードされます。- オプション:
./install.sh --systemdでユーザーsystemdユニットを生成。 - オプション:歯車アイコン → Security からPINを設定。
security.yaml.exampleにファイルの説明があり、外部から/api/injectを叩く場合のapi_tokenも載っています。 - 環境変数のつまみ:
LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY。オプションパネルを切っておくならLOCAL_CHAT_COMFY=0/LOCAL_CHAT_TERMINAL=0。
openclaw CLI自体は同梱していません。DisPatchは、手の届くところでOpenClawゲートウェイがすでに動いていることを前提にしています。ゲートウェイがまだない?それでもアプリは起動して、ログで警告しつつ、エージェントの返信以外は全部動きます。スレッド、inject API、ファイルサーバー、PINとセーフモード。実際、このzipのスモークテストはその状態でやりました。新規解凍、uv sync、空きポート、ゲートウェイなし。同梱のテストスイートの結果は187 passed in 8.4sでした。
メッセージ送信の裏側で起きること
何か入力して送信を押す。その先の経路がこれです:
これはハッピーパスです。エンジニアリングの本番はアンハッピーな方でした。CLIがタイムアウトする、クラッシュする、サブエージェントが10分遅れで答える。独立した4つの経路 — ライブウォッチャー、CLIのJSONペイロード、ターン後にトランスクリプトを再スキャンする照合器、さらに30分ほどtailし続ける「フォロワー」 — が、ターン単位の重複排除(正規化テキスト+メディアのフィンガープリント)を備えたひとつの配達漏斗に流れ込みます。どれか1つが死んでも、返信はきっちり1回だけ届きます。CLIがエラーで落ちた場合ですら先に照合器が走ります。モデルは死ぬ前に、答えをトランスクリプトに書き出していることが多いからです。
二段ロック:通常モードとセーフモード
この区別が大事なのは、アンロック側がおもちゃではないからです。あちらのエージェントは実際に手を動かせます。ソースコードを編集し、公開中のWebサイトを変更し、ツールを実行し、動いているマシンを管理する。ロック側は一般情報専用です。それでもちゃんと使えます。質問もできるし、雑談も相談もできる。ただし、その権限からは意図的にフェンスで隔てられています。理由は単純で、うちには3歳児がいて、家族のスマホがいつも大人の手にあるとは限らないからです。3歳児が私のスマホからフルアクセスを握ったらどうなるか想像してみてください。サイトはまるごとパウ・パトロールの動画になるでしょう。つまりセーフモードは、サーバー側で強制するチャイルドプルーフです。ちょっとした改善点として、ロック側のホーム画面にも、以前のレターブロックのプレースホルダーではなく、アンロック側と同じボットのアバターが表示されるようになりました。PIN未入力のリクエストが来たときの流れがこちら:
締め出されても、リカバリーコードのファイルと手で編集できるYAMLがあります。サポート窓口に問い合わせる必要はありません。それと、security.yamlが書き込み途中で壊れた場合の挙動は「ロックされたまま」であって、「LANの全員に全部公開」ではありません。これは意図した設計です。
罠
- APIのフィールドは
contentであってtextではありません。POST /api/injectが求めるのは{"bot_id": "...", "content": "..."}。統合時の間違いランキング不動の1位で、自分でも何度かやりました。 - 長いメッセージをCLI引数で渡さないこと。カーネルはargvを128KBで打ち切ります(
MAX_ARG_STRLEN)。長い返信はE2BIGでスポーンごと落ちます。コードは--message-file <tmpfile>を使っています。引数に「簡略化」して戻さないでください。 - ボットidは、ある1か所だけ大文字小文字を区別します。OpenClawのゲートウェイはインデックス内のセッションキーを小文字化しますが、idの側は混在ケースだったりします(
Doxy、DS_Brain)。ケース依存の照合だと、他が全部動いたまま、作業中パネルと遅延回答フォロワーだけが静かに死にます。気づきにくさは一級品。まず完全一致、だめなら小文字でフォールバックです。 - CLIは最後のブロックしか返しません。ツール呼び出しの合間にエージェントが言ったことは、CLIの戻り値ではなくトランスクリプトにしか存在しません。ウォッチャーと照合器があるのはそのためで、サブエージェント待ちの「とりあえず途中経過ですが」への特別扱いも同様です。
- クロスサイトWebSocketハイジャックはOriginチェックで遮断。両方のWSルートは、
OriginのホストがHostと一致しないハンドシェイクを拒否します。ブラウザ以外のクライアントはOriginを送らないので普通に通ります。 - SVGはわざとメディアとして配信できないようにしてあります。ブラウザはインラインSVGを同一オリジンのスクリプトとして扱うので、画像として返すと持続型XSSの穴になります。メディアレスポンスには代わりにサンドボックス化CSPとnosniffがつきます。
- サービスワーカーの更新後、古いタブは古いJSのまま固まります。見た目は完全にリグレッションですが、実はタブが新しいワーカーを拾っていないだけ。
controllerchangeでの自動リロードで解決しました。
この構想を自分で再現するには
私のzipを使わないとしても、このアーキテクチャは盗む価値があります。全体は独立した4つのピースで、それぞれ差し替え可能です:
- 履歴を持つチャットバックエンド:小さなWebサーバー1つ(ここではFastAPI + WALモードのSQLiteファイル1個)。ライブ更新用のWebSocketと、メッセージを外から押し込むための素のHTTPエンドポイント。これだけで「データベースとコンテナの山」の置き換えになります。
- エージェントゲートウェイ:バックエンドがサブプロセスやHTTPで呼ぶ相手。ここではOpenClawですが、「セッションid+メッセージを入れると、テキストが返る」ものなら何でも構いません。別プロセスにしておけば、エージェント側がクラッシュしても、更新中でも、丸ごと入れ替えても、チャットアプリは生き残ります。
- ローカルのモデルサーバー:ゲートウェイの向こう側(LM Studio、vLLM、Ollamaなど。チャットアプリではなくゲートウェイの管轄)。
- オプションのメディアサービス:画像生成用のローカルComfyUIなど。エージェントがツールとして呼び、バックエンドは出力をただのメディアとして保存するだけ。
スタックを問わず持ち帰れる教訓は3つ。セーフモード/ロックはサーバー側で強制すること(CSSで隠すのは論外)。「モデルの返信がユーザーにちょうど1回届く」をハッピーパスの前提ではなく、本物の分散システム問題として扱うこと。そして、保守するのがAIエージェントになるなら、フロントエンドはビルドなしにしておくこと。
ダウンロード
- DisPatchのソースをダウンロード(zip) 716 KB
個人利用は無料です。役に立ったら、コーヒーをおごってもらえると嬉しいです。