# DSH Mobile — DeepSeek Harness のホストプラグイン

[English](README.md) | [简体中文](README.zh-CN.md) | 日本語

`@kinocode/dsh-mobile` は、Android アプリ DSH Mobile のための DeepSeek Harness（DSH）プラグインです。次のことができます。

- DSH のサイドバーに出る QR を読み取って、スマホとペアリングする。
- PC で動く HTTPS の接続口を通して、スマホで通常の DSH 画面を使う。
- スマホではセッション名をヘッダーの空き幅まで表示し、タップで全文を確認する。左右の操作ボタン用の余白はセッションヘッダーだけに確保する。
- 承認待ちやエージェントからの質問に、通知から答える。回答はデスクトップと同じ実行中のセッションに届き、どの端末からでも最初の回答が採用される。
- 監視中のセッションのターンが終わったら通知を受け取る。

Android アプリは別に配布していて、このリポジトリには含まれません。非公式のプロジェクトで、DeepSeek 社とは関係ありません。

ペアリングの応答と `GET /v1/status` には、このプラグインが動作確認した Android アプリの版（`app`）を含めています。
スマホ側は、その版が自分より新しいときだけ「更新できます」と表示し、SHA-256 と署名を確かめてから
システムのインストーラーに渡します。連絡先は `lib/app-release.mjs` の 1 か所です。

## 必要なもの

- DeepSeek Harness `>=0.2.1-rc.2 <0.3.0`。まだ安定していない DSH Gateway の内部 API を使うため、対応範囲を固定しています。
- DSH の Web サーバー（`webserver` サービス。DSH デスクトップ版に含まれます）。
- Node.js 22.4 以上（DSH デスクトップ版に同梱の実行環境で足ります）。
- スマホと PC が同じネットワークにいるか、両方が [Tailscale](https://tailscale.com/) につながっていること。
- 通知を使う場合は中継サーバー（[通知と中継サーバー](#通知と中継サーバー)を参照）。

## 導入

DSH の「プラグイン」画面で `@kinocode/dsh-mobile` を追加するか、次を実行します。

```bash
dsh plugin --profile desktop add @kinocode/dsh-mobile
```

そのあと DSH を再起動します。ログに `[mobile-bridge] gateway listening {"origins":[...]}` と出れば動いています。

## スマホとペアリングする

1. DSH のサイドバー下部にある **スマホを接続** を押します。
2. アプリで **QR を読み取る** を押して QR を読み取ります。カメラが使えないときは、**リンクをコピー** で取ったリンクをアプリに貼り付けます。
3. QR は 1 回だけ使え、5 分で期限が切れます。**新しい QR** で作り直せます。

同じパネルに接続中の端末が並び、ここから削除できます。削除した端末はすぐに使えなくなります。

スマホでは、会話を画面いっぱいに使えるよう DSH のサイドバーを隠します。左上の丸いボタンで会話の上に開き、セッション・プラグイン・自動化タスク・検索結果・ワークスペースごとの「新規セッション」・アカウントメニューの項目（設定など）を選ぶか、外側をタップすると閉じます（検索欄や表示オプションなど、サイドバーの中で使う操作では閉じません）。開き直すと、前にスクロールした位置（「さらに…件のセッションを表示」で広げた分も含めて）のまま表示します。セッションを長押しして指を離すと、そのセッションのメニュー（ピン留め・名前を変更・フォーク・アーカイブ・セッションIDをコピー）が開きます。セッション名が長くて行の右端のボタンが見えないときも使えます。長押しのまま動かせば、これまでどおり並べ替えになります。閉じているプロジェクトには、中のセッションでいちばん急ぎの状態を、セッションと同じ印で出します（黄色の点＝承認・確認・回答待ち、回転＝実行中、緑の点＝完了して未確認の順に優先）。DSH の「ワークスペースのツリー」表示では、中に入っているプロジェクトの状態も含めます。デスクトップ用のボタンも隠します。

スマホでワークスペースを追加するときは、PC のフォルダ選択のウィンドウではなく、スマホに PC のフォルダ一覧が出ます。フォルダをたどるか、パスを入れて **このフォルダを使う** を押します。

プラグイン・自動化タスクなどのページを開いているときは、右上に丸い × ボタンを出し、押すとチャットに戻ります。通知から開いたときも、サイドバーやこれらのページは閉じてチャットを表示します（設定画面の × も同じ位置・大きさです）。チャットのヘッダーのサブエージェント数・バックグラウンドジョブ数は、専用のアイコンと数字で表示します（実行中は色付き。押すと一覧が開きます）。

設定は、左から右へスライドしても閉じられます（タブの並びの上では、タブのスクロールになります）。

AI からの質問（選択肢）は、質問文が長くても選択肢と「スキップ」「送信」が画面に残るよう、質問文を画面の高さの約4分の1までにしてその中でスクロールします。

右サイドバー（ファイル一覧・プレビュー・変更の確認・ブラウザ・ターミナル）は、スマホでは画面いっぱいに開きます。PC のアプリや Finder で開くボタンはスマホでは出さず、代わりにファイルのプレビューに **スマホに保存** を出します（スマホのダウンロードに保存）。DSH を再起動して画面が真っ白のままになったときは、自動で読み込み直します。

左上（サイドバー）と右上（右サイドバー）の丸いボタンは、長押しすると好きな位置へ動かせます。

DSH の内蔵ブラウザ（右サイドバーの「ブラウザ」）は、アドレス欄に URL でない言葉を入れると検索します（デスクトップは Google、スマホは Bing）。スマホの内蔵ブラウザは埋め込みを許すページしか表示できない（Google などは表示されない）ため、表示されないページは ↗（システムブラウザで開く）で Chrome に開けます。

## 設定

どの項目も省略できます。プロファイルの `cordis.patch.yml` に書きます。プロファイルの差分はプラグインの設定全体を置き換えるので、残したい項目はすべて書いてください。

```yaml
- id: mobile-bridge
  config:
    relayUrl: https://relay.example.com
    relayEnrollToken: '...'
```

| 項目 | 既定値 | 意味 |
|---|---|---|
| `relayUrl` | `''`（なし） | 通知と、通知からの回答に使う中継サーバー。 |
| `relayEnrollToken` | `''` | 登録済みの PC だけを受け付ける中継サーバー用のトークン。 |
| `gatewayEnabled` | `true` | スマホ用の HTTPS 接続口を開く。 |
| `gatewayPort` | `3543` | 接続口のポート（1024〜65535）。 |
| `gatewayHosts` | 自動検出 | 待ち受ける IPv4 アドレス。プライベートなアドレスだけ指定できます（後述）。 |
| `hostLabel` | PC 名 | スマホに表示する名前。 |
| `previewMaxChars` | `400` | アプリに表示するコマンドのプレビューの最大文字数（50〜4000）。 |
| `stateDir` | `$DSH_HOME/mobile-bridge` | PC の識別情報、接続中の端末、証明書の保存場所（絶対パス）。`$DSH_HOME` の既定値は `~/.dsh`。 |

以前の版との互換性のため、設定の ID は `mobile-bridge`、ログの表示名は `dsh-mobile-bridge` のままです。

## スマホの履歴読み込み

スマホの接続口だけで、会話を開くときは最近の10メッセージと直近ターンを優先し、最大40メッセージに抑えます。上へスクロールして先頭近くで止まると、前の履歴を自動で読み込みます（1回のスワイプで1ページ。読んでいた位置はそのまま。タップだけでは読み込みません）。「さらに前の履歴を読み込む」ボタンは読み込み中の表示として残ります。1ページの量は、DSH標準の50メッセージ・2ターンから、150メッセージ・6ターン以上に増やしています（ターンへの移動で読み込むページも同じです）。デスクトップとDSH本体は変更しません。件数はユーザー・アシスタントのメッセージ数で、ツール結果やバイト数の上限ではありません。

認証済みのDSH画面にだけ早期スクリプトを入れ、取得済み履歴をスマホのIndexedDBに保存します。同じPC・端末・会話を開き直すとSHA-256で一致するレコードを参照に置き換え、未保存・変更済みの部分だけを転送します。現在のカーソル、投影、実行中の応答は毎回サーバーの値で確定してから表示し、古いキャッシュを最新データとして先出ししません。PC側のログ読み取り自体は標準DSHのままです。

小さいレコードはハッシュと参照の方が重くなるため、256バイト以上のレコードだけを選び、送信するハッシュ情報を8KiB以内に抑えます。候補がなければハッシュ情報自体を送りません。差分の採用判定にもハッシュのアップロード量を含めます。ただし候補がすべてキャッシュミスだった場合など、既に送信したハッシュ分の追加通信は取り消せず、常に総通信量が減る保証ではありません。

大きい履歴の送信中は送信完了を待って読み取りを再開します。64MiBの送信待ちを理由とする切断は行いません。標準WebSocketの1フレーム上限（100MiB）は変更せず、メッセージ件数の上限も本文のバイト数上限ではありません。

保存対象は冒頭スナップショットのみで、端末側全体で最大12会話・8MiB、再利用期限は7日です。期限切れデータは次回の保存・整理で削除され、アプリ非稼働中の物理削除は保証しません。会話本文やツール結果を含むため、共有端末では `historyCacheEnabled: false` で保存を停止してください。停止しても既存保存内容は消去されません。削除するときはAndroidのアプリ情報からWebView/アプリの保存データを消してください（再ペアリングが必要になる場合があります）。端末削除は接続と再利用を拒否しますが、端末の保存内容の遠隔消去ではありません。ストレージを利用できないときは通常のオンライン取得に戻り、参照が壊れたときはキャッシュを無効化して接続をやり直します。

| 項目 | 既定値 | 意味 |
|---|---|---|
| `historyEnabled` | `true` | スマホの最新優先取得・差分転送を有効化。`false` なら標準の通信へ戻す。 |
| `historyCacheEnabled` | `true` | 端末での履歴保存・再利用を許可。最新優先取得とは独立。 |
| `historyMaxMessages` | `40` | 最初に取得するメッセージ数の上限（1〜500）。 |
| `historyMinMessages` | `10` | ターン境界へ揃える前に確保する最近の件数（1〜上限）。上限を10未満にすると既定値も縮小。 |
| `historyOlderMinMessages` | `150` | 「さらに前の履歴を読み込む」1回で読み込むメッセージ数の下限（1〜500）。DSH標準は50。 |
| `historyOlderMinTurns` | `6` | 「さらに前の履歴を読み込む」1回で読み込むターン数の下限（1〜50）。DSH標準は2。1回の上限はDSH標準の500メッセージのまま。 |

反映にはプラグインを読み込むDSHの再起動と、スマホ画面の再読み込みが必要です。IndexedDBは接続先オリジンごとに別領域になるため、LANアドレスが変わると初回取得に戻ります。安定したTailscale接続先を使うと再利用しやすくなります。

検証: `node --test test/*.test.mjs`。実Chromiumの保存・再読み込み・差分復元の検証は、Playwrightを用意して `PLAYWRIGHT_MODULE=/absolute/path/to/playwright/index.mjs node test/history-browser.smoke.mjs` を実行します。必要なら `CHROME_PATH` にChrome実行ファイルを指定します。スマホ画面の動き（サイドバーの位置の復元、履歴の自動読み込み、設定のスワイプなど）は、DSH の画面を模したページで `PLAYWRIGHT_MODULE=… node test/phone-layout.browser.mjs` で確かめられます。

## しくみ

- **接続口:** HTTPS はプライベートな IPv4 アドレス（`10/8`、`172.16/12`、`192.168/16` と Tailscale の `100.64/10`）だけで待ち受けます。証明書はプラグインが作る認証局（CA）で発行します。QR に CA の指紋が入っていて、アプリはこの PC についてはその CA だけを信頼します。
- **ペアリング:** アプリが Android Keystore に ECDSA P-256 の鍵を作り、一回限りのコードで公開鍵を登録します。
- **制御の要求:** 承認待ちの一覧の取得、回答、設定の変更は、端末の鍵で署名します。プラグインは署名、時刻（±5 分）、ナンスを確かめ、同じ要求の使い回しを防ぎます。
- **DSH 画面:** ペアリング後、アプリは接続口用のセッションを受け取ります。12 時間有効で、PC の外に出ない秘密情報で署名しています。端末を削除するとすぐ無効になります。接続口が自分でローカルの DSH Web サーバーにログインするので、DSH 自体のログイン情報がスマホに渡ることはありません。
- **実行中のセッション:** プラグインは DSH Gateway のイベントの流れに、プロセス内の所有者クライアントとして参加します。GUI のウィンドウが一つ増えたのと同じ扱いです。承認のリスナーは登録しないので、デスクトップから承認を横取りしたり、デスクトップを飛ばしたりはしません。Gateway は最初の回答を採用し、ほかの場所の表示を閉じます。
- **デスクトップのパネル:** サイドバーのパネルは、所有者だけが使える DSH の API（`/api/dsh-mobile-bridge/*`）を使います。

## 通知と中継サーバー

モバイル回線のスマホからは接続口に届かないので、通知は中継サーバーを通します。プラグインは中継サーバーへの外向きの WebSocket をつなぎっぱなしにします。中継サーバーは Firebase Cloud Messaging でプッシュ通知を送り、スマホからの署名付きの要求を PC に届けます。署名を作ることはできず、DSH 画面や DSH のログイン情報には触れません。

公開の中継サーバーは今のところ用意していません。中継サーバーがなくても、スマホから PC に直接届く間は、通知以外はすべて使えます。

中継サーバーが読める内容は次のとおりです。

- **プッシュ通知:** 既定では、セッション名、ツール名または質問の見出し、最大 3 つの選択肢、状態、内部の ID です。アプリで **ロック画面にコマンド全文を表示** をオンにすると、コマンドのプレビューまたは質問の全文（最大 400 文字）と、ターン終了時のエージェントの最後の返答の先頭 200 文字も加わります。
- **中継サーバーを通る要求:** 暗号化されるのは各端と中継サーバーの間だけで、端から端までではありません。スマホが中継サーバー経由で要求を送ると、承認待ちの一覧（コマンドのプレビュー全体、理由、質問）、セッションの一覧（名前と作業フォルダ）、回答（自由記述を含む）を中継サーバーが読めます。

中継サーバーは、自分で運用しているものか、信頼できるものだけを使ってください。

ロック画面では、スマホが機密情報を表示する設定でない限り、Android は公開用の短い表示だけを出します。DSH を普通に終了したときは、プラグインが未回答の通知の取り消しを試みます（最大 3 秒）。DSH が落ちた場合は、次にアプリが承認待ちの一覧を読み込んだときに古い通知を消します。

## うまくいかないとき

| 症状 | 確認すること |
|---|---|
| スマホがつながらない | 同じネットワークにいるか、両方で Tailscale を使っているか。ファイアウォールがポート 3543 を止めていないか。PC の IP アドレスが変わったら QR を出し直す。 |
| 「証明書が QR の内容と一致しません」 | 別の PC の QR か、接続口の CA が作り直された。新しい QR を出して読み直す。 |
| 通知が来ない | スマホで通知を許可したか。`relayUrl` を設定し、DSH のログに `relay connected` が出ているか。セッションの権限が承認ありの設定か（「Full access」では承認は発生しない）。 |
| DSH を再起動したらスマホの DSH 画面が白紙になった | DSH の起動が終わってから、アプリを閉じて開き直す。 |

## セキュリティ

脆弱性の報告は [SECURITY.md](SECURITY.md) を見てください。

## 開発

```bash
npm test
```

テストは Node 組み込みのテストランナーで動き、ネットワークも依存パッケージも要りません。

## ライセンス

[MIT](LICENSE)。`lib/vendor/qrcode.mjs` は Kazuhiko Arase 氏の [qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) 1.4.4（MIT）です。
