# Linux、FreeBSD、NetBSD、Windows、macOS、Android デバイス

> HTML: https://remote.ivjn.us/docs/ja/platforms/

Linux（glibc / musl）、FreeBSD、NetBSD、Windows、macOS、Android デバイスは、`remote-agent`、Exchange、`remote-cli`、モバイル運用クライアントを共通で使用します。デバイス側と CLI は汎用の名前を使用し、環境変数は `REMOTE_*` に統一されています。

## コマンド 1 つでインストール（Windows / Linux / macOS / FreeBSD / NetBSD）

公式サイトのスクリプトは OS に応じたパッケージをダウンロードして検証し、バイナリを `~/.local/bin` に配置します。Agent はその後 `remote-agent install` を呼び出し、ログイン時の自動起動を登録します。

用途に応じてコマンドを選び、それぞれ個別にコピーして実行します。

**管理対象のデバイスに Agent をインストール**

```sh
curl -fsSL https://remote.ivjn.us/install.sh | bash -s -- agent
```

**操作端末に CLI をインストール**

```sh
curl -fsSL https://remote.ivjn.us/install.sh | bash -s -- cli
```

Windows では Git Bash で実行します。Agent の認証情報はパイプの後ろに指定してください。`bash -s -- agent --device-id NAME --token TOKEN`（または既存の `config.json` / `REMOTE_CONFIG`）を使用します。`curl` の前に指定した環境変数は bash に渡りません。Linux のシステム全体の systemd には `--system` を追加できます。スクリプトは glibc と musl（Alpine など）を自動判別し、Linux arm64（aarch64）にも対応します。`REMOTE_LIBC=musl` で musl パッケージを強制的に選択できます。FreeBSD / NetBSD：一般ユーザーのインストール先は `~/.local/bin` です。`sudo remote-agent install --system --config CONFIG` で rc.d を登録します。デスクトップ制御には `python3` と `xdotool` を備えた X11 セッションが必要です。Android と OpenWrt には専用パッケージを使用してください。

## コマンドとサービス

デバイスと CLI のコマンドは `remote-agent` と `remote-cli` です。`remote-cli` は既定で `remote-cli/config.toml` に書き込み、明示的な `REMOTE_CLI_CONFIG` が優先されます。

OpenWrt パッケージ、UCI ファイル、init サービスの名前は `remote-agent` です。状態ファイルは `/var/run/remote-agent.state`、設定ファイルは `/etc/config/remote-agent` です。LuCI プラグイン `luci-app-remote` はこのパッケージに依存します。`REMOTE_AGENT_SRC` はビルド元のパスを指定します。

| デバイス | コマンド実行 | 対話型ターミナル | 常駐方法 | ubus |
|---|---|---|---|---|
| Linux | `/bin/sh -c` | Unix PTY | systemd またはフォアグラウンドプロセス | OpenWrt のみ |
| Linux musl（Alpine 3.x など）、x64 / arm64 | `/bin/sh -c` | Unix PTY | フォアグラウンドプロセスまたは独自の OpenRC サービス（systemd なし） | 非対応 |
| FreeBSD 14+、amd64 / arm64 | `/bin/sh -c` | Unix PTY | rc.d（root/システム）；前景（ユーザー） | 非対応 |
| NetBSD 10+、amd64 | `/bin/sh -c` | Unix PTY | rc.d（root/システム）；前景（ユーザー） | 非対応 |
| Windows 10 1809+/11、Server 2019+ | Windows PowerShell 5.1 | ConPTY + PowerShell | ログイン時のスケジュールタスクまたはフォアグラウンドプロセス | 非対応 |
| macOS 11+、Apple Silicon / Intel | `/bin/sh -c` | Unix PTY | launchd LaunchAgent またはフォアグラウンドプロセス | 非対応 |
| Android 8.0+、arm64 / x86_64 | `/system/bin/sh -c` | Unix PTY | 独立 APK のフォアグラウンドサービス | 非対応 |

## 共通設定

まず `remote-cli device add <id>` でデバイスを登録し、デバイストークンを取得します。Linux/Windows では、実行中のアカウントだけが読める `config.json` を作成します。

```json
{
  "server": "wss://mgmt.example.com",
  "device_id": "my-device",
  "token": "REPLACE_WITH_DEVICE_TOKEN"
}
```

```sh
remote-agent --config /path/to/config.json
```

優先順位はコマンドラインおよび環境変数 `REMOTE_SERVER`、`REMOTE_DEVICE_ID`、`REMOTE_TOKEN`、次に JSON、最後に OpenWrt でのみ UCI へフォールバックします。JSON では `state_file` も設定でき、コマンドラインの `--state-file` / `REMOTE_STATE_FILE` で上書きできます。状態ファイルを指定しない場合、OpenWrt では引き続き `/var/run/remote-agent.state` を使用します。他のプラットフォームでは `LOCALAPPDATA`、`XDG_STATE_HOME`、`$HOME/.local/state` の順に、その配下の `remote-agent/agent.state` を使用し、ユーザーディレクトリがなければ一時ディレクトリにフォールバックします。複数インスタンスにはそれぞれ別の状態パスを使用してください。

本番接続には `wss://` を使用します。例の `ws://127.0.0.1` はローカル検証専用です。

### Agent 接続の通知

JSON に `"notify_urls": ["https://example.invalid/notify"]` を設定するか、`remote-agent --notify-url https://example.invalid/notify`（複数指定可）を使用します。環境変数 `REMOTE_NOTIFY_URL` はカンマ区切りのアドレスを受け付けます。空でないコマンドラインまたは環境変数のリストが JSON 設定を置き換えます。それ以外は JSON を使用し、最後に OpenWrt でのみカンマ区切りの UCI `remote-agent.main.notify_url` にフォールバックします。すべての指定元が省略または空の場合、POST は無効です。

登録成功後に `agent.connected`、確立済みセッションの終了時に一度だけ `agent.disconnected`、登録を明示的に拒否された場合は `agent.rejected` のみを送信します。登録前のネットワーク障害は通知しません。POST は Exchange Web Push と同じ JSON オブジェクト（`title`、`body`、`url`、`event`、`device_id`、`occurred_at`）を使用しますが、デバイスのオンライン状態ではなく agent の接続を表します。`event` の接頭辞は `device.` ではなく `agent.`、`url` は `""`（Web Push コンソールのパスなし）で、`tag` は送信しません。切断や拒否の本文には、安全に配慮した短い理由の要約が含まれ、デバイストークンは含まれません。時刻は RFC3339 UTC です。

通知はバックグラウンドで送信します。失敗時はイベント名のみを記録し、宛先アドレスは記録せず、agent の再接続を妨げません。再試行方針は Exchange Web Push と同様です。最大 5 回試行し、ネットワークエラーと HTTP 408/429/5xx は再試行可能、404/410 とその他の再試行不能な 4xx は破棄します。整数秒の `Retry-After` を優先し、なければ 5 秒からの指数バックオフを使用します。遅延は 1～900 秒に制限します。HTTP/HTTPS のみ受け付け、リダイレクトには従いません。URL にユーザー名、パスワード、その他の認証情報を含めないでください。ユーザー名またはパスワードを含むアドレスは拒否されます。

### アウトバウンドプロキシと接続維持

ヘッドレスと `--ui` はどちらも同じ `client::run` で Exchange に接続します。WebSocket は agent プロセスが継承したプロキシ環境変数を読み取り、`HTTPS_PROXY` / `https_proxy`、`ALL_PROXY` / `all_proxy`、`HTTP_PROXY` / `http_proxy` の順で最初の空でない値を使用します。`NO_PROXY` / `no_proxy` に一致するホストはプロキシを経由しません。`remote-cli` の WebSocket も同じ規則です（#36/#37、#39）。ログイン時の自動起動やバックグラウンドサービスでは、その実行環境にこれらの変数を設定する必要があります。別のターミナルで設定しても起動済みプロセスには渡りません。

デバイスの WSS がプロキシを経由する場合、まず **HTTP CONNECT** トンネルを確立してから rustls で接続先に接続します。TLS ClientHello の ALPN は CLI と同じく **`http/1.1` のみを通知します**。`h2` が選択されると、現在の HTTP/1.1 WebSocket Upgrade 経路が動作しません。Clash などのプロキシで接続中のまま進まない場合は、プロキシが CONNECT に対応するか確認し、#39/#42 の修正を含むビルドを使用してください。デバイスと CLI の WebSocket 経路はどちらも SOCKS に対応しません。HTTP CONNECT プロキシ（例：`HTTPS_PROXY=http://127.0.0.1:7890`）を使用してください。

直接接続または CONNECT 完了後、agent は `TCP_NODELAY` とアイドル 30 秒後の TCP keepalive を有効にします。JSON ハートビートのたびに WebSocket Ping も送信し、Ping を受信すると自動キューに入った Pong を明示的に flush します。接続と登録応答の待機はそれぞれ 45 秒、送信と登録後の Pong flush は 30 秒が上限です。[接続維持とタイムアウト](/docs/ja/websocket-protocol.md#キープアライブとタイムアウト)を参照してください（#42）。

`desktop-ui` が有効な Windows ビルドは、ヘッドレス実行時もマルチスレッド Tokio runtime で axum、ローカルトレイ、デバイス WebSocket を並行処理します。他のプラットフォームでも、この機能を有効にしたローカル UI（明示的な `--ui` または引数なしの起動）はマルチスレッドです。この機能のないビルドと、他のプラットフォームで設定を明示したヘッドレス実行は、小型デバイス向けの `current_thread` を使用します。

## Linux / Windows / macOS 設定画面

ビルドでは `desktop-ui` が既定で有効です。デスクトップ配布版にはローカル設定画面を含めてください。ヘッドレスサーバーや組み込み向けの場合のみ `--no-default-features` を使用します。接続中は管理サーバー、デバイス ID、トークンの入力欄を非表示にし、リモートアクセスを停止すると再表示します。接続状態、LAN 設定、ログイン時の起動設定は引き続き表示します。

```sh
cargo build --locked -p agent --release
# Linux / macOS
./target/release/remote-agent --ui
```

```powershell
# Windows
.\target\release\remote-agent.exe --ui
```

Linux / Windows / macOS で接続引数なしで起動し（例：`remote-agent.exe` をダブルクリック）、`desktop-ui` が有効な場合、使用可能な設定がなければ既定の `config.json` を書き込み（既定の Exchange のみを設定し、device_id/token は空欄）、ブラウザーの設定ページを開きます。設定済みの場合、Windows では引き続きトレイを使用し（ブラウザーを強制的に開きません）、Linux/macOS ではローカル URL を表示します（macOS にはメニューバーのアイコンもあります）。`desktop-ui` のないビルドを対話的に引数なしで起動すると、既定の設定ファイルを作成してパスを表示してから終了するため、`Missing device_id` だけが表示されることはありません。OpenWrt / systemd / スケジュールタスクなどでは、引き続き `--config`、UCI、`REMOTE_*` を使用してください。

引数なしの起動と明示的な `--ui` は同じ規則を使います。保存済み設定の `device_id`/`token` が未設定または空の場合だけ既定のブラウザーを開きます。設定済みならバックグラウンドで接続し、ログイン時の自動起動でもブラウザーは開きません。Windows はトレイから手動で設定ページを開けます。Linux/macOS はターミナルのローカル URL を使います。`--ui --no-open` は初回設定も含めて自動起動を常に抑止します。`--ui-port 8765` で固定ポートを指定でき、既定では空きポートをランダムに選びます。待ち受けは `127.0.0.1` のみです。UI リソースは埋め込みで、外部フォントや CDN は使いません。

Windows のトレイと macOS のメニューバーには、設定ページ、スクリーンセーバー設定、タスク管理への入口とクリップボード操作があります。タスク管理はローカルのタスクパネルを直接開きます。Linux ではブラウザーの設定ページを使用します。

まず `remote-cli device add <id>` でデバイス ID とトークンを取得し、**Connection settings** にサーバー、デバイス ID、デバイストークンを入力します。

- **Save only**：設定を保存し、サーバーには接続しません。
- **Save and connect**：保存して接続します。実行中に設定を変更すると旧接続を切断し、新しい設定で再接続します。
- **Stop remote access**：切断し、現在のリモート実行とターミナルセッションを取り消します。設定は保持します。
- ステータスバーには登録成功後の **Connected**、接続中、再接続中、停止済みの状態と、最新の接続エラーが表示されます。

接続開始時、UI は自身の状態ファイルにまず **Connecting** を書き込んでから `client::run` タスクを開始します。これにより状態ファイルがまだ存在しない時点の既定値がページに残るのを防ぎます（#42）。

保存後のトークンはページに返されません。空欄は元のトークンを保持する意味です。設定は一時ファイルを使ってアトミックに置き換えます。Linux の権限は `0600`、Windows の ACL は現在のユーザーのみに許可します。設定 JSON が破損している場合や読めない場合、画面で修復を案内し、元のファイルを上書きしません。同じ設定ファイルを開ける UI プロセスは 1 つだけです。

既定の設定パスは Linux/macOS では `$XDG_CONFIG_HOME/remote-agent/config.json`（未設定なら `~/.config/remote-agent/config.json`）、Windows では `%LOCALAPPDATA%\remote-agent\config.json` です。`--ui --config <path>` または `REMOTE_CONFIG` で既存の JSON ファイルを指定できます。UI は、保存値と実際の接続の不一致を防ぐため、`--server`、`--device-id`、`--token`、`--state-file` および対応する環境変数による上書きを受け付けません。JSON の `state_file` は UI にも適用されます。未指定の場合は headless と同じ永続的な既定パスを使い、密封の識別情報と信頼設定を保持します。

設定ページには **Start at login / ログイン時に自動起動** スイッチがあります（`GET /api/status` の `autostart: { supported, enabled, detail }`、`POST /api/autostart` に `{ "enabled": true|false }` を渡します）。権限昇格は不要です。

- **macOS**：`~/Library/LaunchAgents/us.ivjn.remote-agent.plist` をインストール/削除します（テンプレートは `install/macos/install.sh` と同じ）。`launchctl bootstrap` / `bootout` を実行し、バイナリは優先的に `~/.local/bin/remote-agent` にコピーします。既定の設定は `~/.config/remote-agent/config.json` です（既存なら保持）。
- **Windows**：現在のユーザーのスケジュールタスク `RemoteAgent`（AtLogOn）を登録/解除します。処理は `install/windows/install.ps1` に合わせてあり、インストール先は `%LOCALAPPDATA%\remote-agent` です。
- **Linux**：ユーザー systemd ユニット（`install/linux/remote-agent.service`）をインストール/削除し、`systemctl --user enable --now` / `disable` を実行します。

有効化時には、必要に応じて現在の実行ファイルを規定のインストール先にコピーし、既存の `config.json` を保持します。応答とログにトークンは表示しません。UI が管理するのは現在のプロセスです。自動起動を有効にして UI を終了した後、次回ログイン時に LaunchAgent / スケジュールタスク / systemd がバックグラウンド agent を起動します。

**ブラウザーのタブを閉じても Agent は停止しません。Agent プロセスを終了すると切断されます。設定済みデバイスは UI 起動時に自動接続し、初回設定は Save and connect が必要です。** UI は現在のプロセスを管理し、ログイン時の自動起動も切り替えられます。同じデバイスの別プロセスを起動する前に既存のバックグラウンドサービスを停止してください。設定と自動起動の有効化後はタブを閉じられます。遠隔操作には Agent 実行アカウントの権限が適用されます。

ローカル URL には今回の UI のランダムなアクセスキーが含まれるため、共有しないでください。API は Host と Origin も検証し、外部ページからの呼び出しを拒否します。デバイストークンは設定ファイルに保存され、暗号化保存ではありません。ローカルの現在のアカウントと管理者は読み取れます。`desktop-ui` 機能のない軽量ビルドは従来のコマンドライン動作を維持します。OpenWrt と Android は各自の既存画面を使用します。

## FreeBSD

公式サイトでは `remote-freebsd-x64.tar.gz` と `remote-freebsd-arm64.tar.gz` を提供しています（Linux 上で `cargo zigbuild` によりクロスコンパイルし、FreeBSD 14 libc にリンク）。展開後は次のように実行します。

```sh
mkdir -p ~/.local/bin
cp remote-agent remote-cli ~/.local/bin/
sudo remote-agent install --system --device-id NAME --token TOKEN
```

現在のソースは rc.d システムインストール、sysctl メトリクス、`netstat -ibn` の通信量、FreeBSD の温度/ファン、NetBSD の `envstat` に対応します。`DISPLAY`、`python3`、`xdotool` がある場合に X11 デスクトップ制御を通知します。画像取得には Pillow も必要です。メディアとシステム設定は未対応です。自動更新は検証済みの対応パッケージがリリースマニフェストにある場合に BSD x64/arm64 アーカイブを選択します。変更を含む新しいビルドと BSD 上での実行検証が必要です。

## NetBSD

公式サイトでは `remote-netbsd-x64.tar.gz` を提供しています（amd64、Linux 上で zig を使い NetBSD 10 libc にリンクしてクロスコンパイル。動的依存は libc / libpthread / libutil のみ）。使い方は FreeBSD と同じです。

```sh
mkdir -p ~/.local/bin
cp remote-agent remote-cli ~/.local/bin/
sudo remote-agent install --system --device-id NAME --token TOKEN
```

機能は Exchange 接続、`exec` / PTY / ファイル転送です。メトリクスは sysctl（`kern.cp_time`、`hw.physmem64`、`vm.uvmexp2`）で取得します。デスクトップ GUI 制御はスタブ実装で、機能を通知しません。デバイスのプラットフォームは `netbsd`自己更新と rc.d による再起動は FreeBSD と同じです（`remote-netbsd-x64.tar.gz` / `remote-netbsd-arm64.tar.gz`）。ただしマニフェストにアーカイブが必要です。

## OpenWrt 公式 25.12 パッケージ

公式サイトの `remote-agent` は公式 OpenWrt 25.12.5 SDK でパッケージ化されています（apk-tools、パッケージバージョン `0.1.29-r1`、agent 0.1.29 を同梱）。

| target | パッケージアーキテクチャ | ファイル |
|---|---|---|
| ipq40xx/generic | arm_cortex-a7_neon-vfpv4 | `remote-agent-openwrt-ipq40xx-official.apk` |
| x86/64 | x86_64 | `remote-agent-openwrt-x86_64-official.apk` |
| armsr/armv8 | aarch64_generic | `remote-agent-openwrt-armsr-armv8-official.apk` |
| mediatek/filogic | aarch64_cortex-a53 | `remote-agent-openwrt-mediatek-filogic-official.apk` |
| bcm27xx/bcm2711 | aarch64_cortex-a72 | `remote-agent-openwrt-bcm27xx-bcm2711-official.apk` |
| ramips/mt7621 | mipsel_24kc | `remote-agent-openwrt-ramips-mt7621-official.apk` |
| ath79/generic | mips_24kc | `remote-agent-openwrt-ath79-official.apk` |

LuCI 画面は共通の `luci-app-remote-openwrt-official.apk` です。`apk --print-arch` でアーキテクチャを確認し、`apk add --allow-untrusted ./文件.apk` でインストールします。MIPS パッケージは追加で `libgcc1` に依存します（公式イメージには既定で含まれます）。

## Linux musl（Alpine）と arm64

- `remote-linux-arm64.tar.gz`：aarch64、glibc 2.28+（Debian 10+/Ubuntu 18.04+/RHEL 8+、Raspberry Pi の 64 ビット OS など）。`remote-agent install` による systemd 登録と自動更新に対応します。
- `remote-linux-musl-x64.tar.gz` / `remote-linux-musl-arm64.tar.gz`：musl を静的リンクしており、Alpine など glibc のないディストリビューションで動作します。Alpine には既定で systemd がないため、`remote-agent install --no-start` で設定を書き込んでから独自の OpenRC サービス（`/etc/init.d/remote-agent`）を作るか、フォアグラウンドで実行してください。musl ビルドは `update self` とデスクトップ自動更新に対応し、musl アーカイブのみをインストールします。単発の更新後は systemd、OpenRC（`rc-service remote-agent restart`）、プロセス再起動の順に試します。OpenWrt ホスト上の musl アーカイブは引き続き opkg/apk で更新します。

## Linux

組み込みサブコマンドの利用を推奨します（現在の実行ファイルをコピーし、アクセスを制限した設定を書き込み、systemd を登録します。リポジトリ内の `install/` ファイルには依存しません）。

```sh
cargo build --locked -p agent --no-default-features --profile release-small
# 用户级（默认，非 root）
./target/release-small/remote-agent install \
  --config /path/to/config.json
# 或直接传凭证（无需先有文件；安装过程不访问网络）
./target/release-small/remote-agent install \
  --device-id NAME --token TOKEN

# 系统级（root 或 --system）：/usr/local/bin + /etc/remote-agent + multi-user.target
sudo ./target/release-small/remote-agent install --system \
  --device-id NAME --token TOKEN

remote-agent status                    # 安装布局、服务、进程、连接（不打印令牌）
systemctl --user status remote-agent   # 用户级
# systemctl status remote-agent        # 系统级
remote-agent uninstall                 # 卸服务；加 --force 同时删二进制与配置
```

同等の手動手順も引き続き利用できます。

```sh
install -Dm755 target/release-small/remote-agent ~/.local/bin/remote-agent
install -Dm600 /path/to/config.json ~/.config/remote-agent/config.json
install -Dm644 install/linux/remote-agent.service ~/.config/systemd/user/remote-agent.service
systemctl --user daemon-reload
systemctl --user enable --now remote-agent
```

ユーザーサービスは既定でログインセッション中に動作します。OS 起動時から常駐させるには管理者が `loginctl enable-linger <user>` を実行するか、上記の `--system` システムユニット（`RuntimeDirectory=remote-agent`、`WantedBy=multi-user.target`）を使用してください。リモートコマンドはサービスアカウントの権限で実行します。サービス停止時はサービス内のすべてのプロセスを回収します。`install/linux/install.sh` は `remote-agent install --force` を呼び出す薄いラッパーです。

## Windows

Rust MSVC ツールチェーンと Visual Studio C++ Build Tools をインストールした後、次を実行します。

```powershell
cargo build --locked -p agent --release
.\target\release\remote-agent.exe --config C:\path\config.json

# 推荐：为当前登录用户安装常驻计划任务
.\target\release\remote-agent.exe install --config C:\path\config.json
# 已存在任务/文件时加 --force；仅注册不启动加 --no-start
Get-ScheduledTaskInfo -TaskName RemoteAgent
remote-agent status
remote-agent uninstall
remote-agent uninstall --force   # 同时删除 %LOCALAPPDATA%\remote-agent 下的文件
```

`install` はバイナリと設定を `%LOCALAPPDATA%\remote-agent` にコピーし、設定ファイルの ACL を制限して、ログイン時に起動するスケジュールタスク `RemoteAgent` を登録します。既定ではタスクやコピー先ファイルがすでに存在するとエラーになります。`--force` は旧タスクを停止・登録解除してから上書きします。このモードにはユーザーのログインが必要です。Windows SCM のシステムサービスではありません。`install/windows/install.ps1` は `remote-agent install` を呼び出す薄いラッパーです（`-Force` を追加可）。

先に UI で設定を生成した場合、`--config` には独立したパスを指定できます（例：`--ui --config .\device-config.json` で生成したファイル）。

Windows のコマンドは `Get-Process` などの PowerShell 構文を使用し、出力は UTF-8 で送信します。各実行セッションとターミナルには独立した Job Object があり、タイムアウト、キャンセル、切断、agent 終了時に子プロセスツリーをクリーンアップします。

## macOS

Rust ツールチェーン（`rustup`、既定の `aarch64-apple-darwin` または `x86_64-apple-darwin` target を含む）が必要です。メトリクスは sysctl、`getloadavg(3)`、mach の `host_statistics` で収集し、静的情報（モデル、カーネル、OS バージョン）は `sysctl` と `sw_vers` で収集します。プラットフォームは `macos` と報告します。

オンデマンドのセンサー情報（ハートビートには含まれません）は sudo や追加の SMC 依存なしで可能な範囲で取得します。`pmset -g batt` のバッテリー（ノート PC）、`pmset -g therm` の CPU_Speed_Limit / Scheduler / Available CPUs とサーマルプレッシャーの説明、`ioreg` の AppleSmartBattery の `Temperature` とファン RPM（ファームウェアが公開している場合）が対象です。デスクトップ（Mac mini など）には通常バッテリーがなく、Apple Silicon では数値の CPU_Speed_Limit がない場合もあります。生の CPU die / proximity 温度には非公開 IOKit/SMC が必要で、現在は取得しません。サーマルプレッシャー、速度制限、ファンのいずれかの値を取得できれば、`thermal` を unavailable にはしません。`desktop-ui` を有効にすると `remote-agent --ui` で設定し、ログイン時の自動起動（LaunchAgent）を切り替えられます。

```sh
cargo build --locked -p agent --release

# 推荐：安装为随登录会话运行的 LaunchAgent（无需 sudo）
./target/release/remote-agent install --config ~/.config/remote-agent/config.json
# 或：install/macos/install.sh ~/.config/remote-agent/config.json
remote-agent status
launchctl print gui/$(id -u)/us.ivjn.remote-agent | grep state
```

`remote-agent install` は現在の実行ファイルを `~/.local/bin/remote-agent` にコピーし、`~/.config/remote-agent/config.json`（権限 600）を書き込み、`~/Library/LaunchAgents/us.ivjn.remote-agent.plist` を生成します。バイナリに `codesign -s - --force` を実行し（macOS は署名なしで同じ場所に置き換えたバイナリを強制終了します）、`launchctl bootstrap` / `kickstart` を実行します。`RunAtLoad` + `KeepAlive` により、ログイン時に agent が起動し、クラッシュや切断後は launchd が再起動します。ログは `~/Library/Logs/remote-agent.log` に書き込みます。`install/macos/install.sh` は薄いラッパーです。状態ファイル未指定時は `$HOME/.local/state/remote-agent/agent.state` を使用します。リモートコマンドはそのログインユーザーの権限で実行します。

停止/アンインストール：

```sh
remote-agent uninstall
# 或手动：
# launchctl bootout gui/$(id -u)/us.ivjn.remote-agent
# rm ~/Library/LaunchAgents/us.ivjn.remote-agent.plist
```

OS 起動時（誰もログインしていない状態）から常駐させるには、`/Library/LaunchDaemons` 配下の LaunchDaemon に変更して root として読み込みます。このモードではリモートコマンドが root 権限で実行されるため、それに応じてリスクを評価してください。

## Android 独立 APK

デバイス側のプロジェクトは `android-agent/` です。`mobile/` は引き続き運用クライアントです。両者は個別にインストールでき、Expo Go、Termux、root は不要です。

すべてのプラットフォームの Agent の既定アドレスは `wss://exchange.ivjn.us` です。Android では **Connect with a six-digit code** を押すと、管理者が CLI で 6 桁のコードを入力して承認した後、自動的にデバイス ID 情報を取得します。ネイティブ Agent（Windows/macOS ローカルページ、OpenWrt LuCI の「デバイスを追加」、コマンドラインの `remote-agent pair` / `remote-agent join <邀请>`）は、Console でスキャンして承認できる 6 桁コードと QR コードを表示するか、使い捨て招待を貼り付けて参加できます。詳細は[初期化とペアリング](/docs/ja/pairing-and-enrollment.md)を参照してください。

### 初回接続とホーム画面

1. 管理対象の Android デバイスで **Remote Agent** を開きます。初回は **6 桁のコードで接続** または **接続用 QR コードをスキャン** を選び、管理者の承認を受けてください。アカウント、QR コード、6 桁コードの手順は [初期設定とペアリング](/docs/ja/pairing-and-enrollment.md) を参照してください。
2. デバイス ID とデバイストークンがある場合は **設定 → 接続設定** を開き、サーバーと認証情報を入力して **保存して接続** を選びます。接続が有効な間は認証情報の入力欄が非表示になります。デバイスの識別情報を変更する前にホーム画面で切断してください。
3. ホーム画面には接続状態、画面共有操作、管理者一覧が表示されます。設定が保存済みで接続が無効な場合は **デバイスを接続** を選びます。有効な接続はアプリを開き直すと復帰します。

Android 12 以降はシステムのスプラッシュ画面、Android 8–11 は同じブランド配色の起動背景を使います。最初のフレームが準備でき次第ホーム画面へ進み、ネットワーク接続の完了を待たず、最低表示時間も設けません。システムでアニメーションが無効なら遷移を省略します。設定画面は必要時に読み込み、ホーム画面を開くだけでユーザー補助や画面共有の権限が付与されることはありません。これは現在のソースの動作です。公開 APK で利用できるかはインストール済みのバージョンを確認してください。

### バックグラウンド動作、通知、停止

バックグラウンドへの移動やアプリ画面を閉じる操作ではフォアグラウンドサービスは停止しません。ネットワーク切断時は Agent が再接続します。接続が有効なら、端末の再起動とロック解除、またはアプリ更新時にサービスの復帰を試みます。強制停止後は手動でアプリを開き直してください。メーカーの省電力設定によってはバックグラウンド動作の許可も必要です。

通知を許可した場合、**接続を管理** からアプリに戻れます。共有中の通知にある **共有を停止** は画面共有だけを終了します。リモート接続と自動復帰を停止するには、ホーム画面の **切断** を選んで確認してください。保存済みの設定は残り、後で **デバイスを接続** から再接続できます。ホーム画面の **画面共有を停止** も共有だけを終了し、リモート接続は維持します。

デバイストークンは Android Keystore AES-GCM で暗号化して保存します。設定はバックアップ対象外で、外部公開された Activity/Service 経由でトークンを渡しません。リモートコマンドは APK のアプリ UID で実行され、Android サンドボックスと SELinux の制約を受けます。他のアプリのデータや root 権限は得られず、システム設定、ユーザー補助、画面共有には個別の許可が必要です。

リモート接続中はフォアグラウンドサービスが CPU と画面のウェイクロックを保持するため、バックグラウンドや再接続中も画面が点灯し、消費電力が増えます。切断またはサービス破棄でロックを解放し、通常の画面消灯設定に戻ります。電源ボタンによるロックは通常どおり動作し、認証を回避しません。通知は接続または共有状態を表示し、切断はホーム画面から行います。

### ソースからのビルド

以下の Debug と Release の各例は、それぞれリポジトリのルートから実行します。インストール前にデバイス一覧を確認し、DEVICE_SERIAL を対象のシリアル番号に置き換えてください。パッケージ作成、単体テスト、完全な Lint は別の検査です。Lint が失敗した場合はレポートを確認し、原因を修正または記録します。タスクの省略は合格ではありません。

ビルドには JDK 17+、Android SDK Platform 36、NDK `28.2.13676358`、Python 3、Rust が必要です。`ANDROID_HOME`（または `ANDROID_SDK_ROOT`）を設定してください。独自の NDK パスは `ANDROID_NDK_HOME` で指定します。`.Aran` の全画面表示は Metafile Android SDK に依存します。`Metafile` リポジトリを `remote-control` と同じ階層のディレクトリに配置してください（Gradle は `../../Metafile/android-sdk/aran` 経由で `:aran` モジュールを参照します）。

```sh
rustup target add aarch64-linux-android x86_64-linux-android
cd android-agent
./gradlew :app:assembleDebug :app:testDebugUnitTest
# Windows: .\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
./gradlew :app:lintDebug
# Windows: .\gradlew.bat :app:lintDebug
adb devices -l
adb -s DEVICE_SERIAL install -r app/build/outputs/apk/debug/remote-agent-debug.apk
```

Gradle は自動的に `scripts/build-android-agent.py` を呼び出し、2 つの ABI の Rust 実行ファイルを APK のネイティブライブラリディレクトリに格納します。インストール後はこの読み取り専用ディレクトリから直接起動し、アプリデータディレクトリからの実行を禁止する Android の制限に対応します。debug APK は開発検証用です。正式配布には組織独自の署名設定で release をビルドし、今後の更新に備えて署名鍵を保管してください。

Release ビルドは `android-agent/keystore.properties`（Git の無視対象）または環境変数 `REMOTE_AGENT_SIGNING_PROPERTIES` が指定する外部設定を読み込みます。項目は `android-agent/keystore.properties.example` を参照してください。鍵はリポジトリ外に保存してください。署名設定がない場合、Release ビルドは失敗し、Debug 署名にはフォールバックしません。

```powershell
cd android-agent
$env:REMOTE_AGENT_SIGNING_PROPERTIES = "$env:USERPROFILE/.remote-control/signing/remote-agent.properties"
.\gradlew.bat :app:assembleRelease :app:lintRelease :app:testReleaseUnitTest --no-daemon
# Output: app/build/outputs/apk/release/remote-agent-release.apk
```

リリース鍵とパスワードを安全にバックアップしてください。後続バージョンを上書き更新するには同じ鍵を使う必要があります。Debug と Release は署名が異なるため、既存の Debug インストールにはそのまま上書きできません。アンインストールするとローカル設定とデバイス認証情報が削除されるので、先に再ペアリングを手配してください。`scripts/build-site-downloads.py` は署名済み Release APK を Agent のダウンロードパッケージとして使用します。Release 署名によって Play Protect のブロックが解除される保証はありません。

### Android QR コード接続

Remote Agent ホーム画面の **Scan connection QR code** は、Google Play サービスなしでカメラを使って接続 QR コードをオフライン認識します。初回使用時にカメラ権限を許可してください。スキャンをキャンセルしても設定は変わりません。成功するとサーバーとデバイス ID が表示され、トークンは非表示のままです。**Connect** を押した時点で設定を暗号化保存して接続を開始します。既存の接続が実行中なら、新設定を取り込む前に停止する必要があります。スキャンしても画面制御やユーザー補助機能は自動で有効になりません。

まず Exchange に独立したデバイスを登録し、その**デバイストークン**を取得してから、PC 上でローカルに QR コードを生成してください（運用アカウントのトークンや第三者のオンライン QR サイトは使用しないでください）。

```powershell
python -m pip install qrcode==8.2
python scripts/connection-qr.py --server wss://exchange.example.com --device-id android-phone --output "$env:TEMP\android-phone-connection.svg"
# 按提示输入设备令牌，输入不会显示。也可用 --token-stdin 从标准输入读取。
```

PC で生成した SVG を開き、Remote Agent でスキャンします。QR コードファイルにはデバイストークンが含まれるため、秘密として扱い、使用後は削除して Git にコミットしないでください。このツールは既存ファイルの上書きを拒否します。

QR コードの内容は UTF-8 JSON で、全長は最大 2048 バイトです。

```json
{"type":"remote-agent","version":1,"server":"wss://exchange.example.com","device_id":"android-phone","token":"DEVICE_TOKEN"}
```

スキャンは `wss://` アドレスのみを受け付け、`/ws/device` パスは指定できますが、URL のユーザー情報、クエリパラメーター、フラグメントは受け付けません。`server`、`device_id`、`token` は空でない文字列で、それぞれ最大 512、128、512 文字です。制御文字や前後の空白は含められません。手動設定はローカルの `ws://` テスト環境でも引き続き使用できます。

### Android ユーザー補助、スクリーンショット、テキスト入力

以下のコマンドは `remote-cli` で実行します。デバイスは APK 内部のブリッジ経由で `android_control` 機能を報告します。権限の付与状態は `status` で確認します。

1. デバイスを接続してから **設定 → 画面と操作** を開き、**リモート画面操作を許可** を有効にします（既定では無効）。
2. **ユーザー補助の設定** を選び、Android 設定で Remote Agent を有効にします。アクセス可能な UI ツリーの読み取り、タップ、スワイプ、システムナビゲーション、フォーカス中の入力欄のテキスト置換を許可します。
3. Android 11+ は有効なユーザー補助機能でスクリーンショットを取得できます。Android 8–10、または画面キャプチャだけが必要な場合は、ホーム画面の **画面を共有** または設定の **標準の画面共有** を選び、システムの共有ダイアログを承認してください。共有終了後やプロセス再起動後は再承認が必要です。

```sh
remote-cli ui my-device status
remote-cli ui my-device tree
remote-cli ui my-device xml > screen.xml
remote-cli ui my-device click --view-id com.example:id/search
remote-cli ui my-device click --text "Search" --description "Open search"
remote-cli ui my-device screenshot --output screen.jpg
remote-cli ui my-device tap 300 500
remote-cli ui my-device swipe 500 1500 500 500 --duration-ms 400
# 多段滑动（折线，2-64 个点）
remote-cli ui my-device swipe-path 300,1500 300,800 900,800 --duration-ms 800
remote-cli ui my-device key back
remote-cli ui my-device text "Hello"
remote-cli ui my-device text --stdin < message.txt
remote-cli ui my-device type --stdin < message.txt
```

`text` は Android の `ACTION_SET_TEXT` で**現在フォーカス中の入力欄のテキスト全体を置き換えます**。中国語、絵文字、複数行に対応し、最大 8192 UTF-8 バイト、NUL は不可です。先に編集可能な入力欄をタップしてください。フォーカスがない、編集不能、この操作に非対応のコントロールではエラーになります。`--stdin` は UTF-8 を読み込み、改行を保持し、テキストをコマンド履歴に残すことも避けられます。`key` は `back`、`home`、`recents`、`notifications`、`quick-settings` に対応します。

`xml` は入れ子の `<hierarchy><node ...>` UI ツリーを返し、`resource-id`、`text`、`content-desc`、`bounds`、コントロールの状態を含みます。最大 200 ノード、50 階層、属性は最大 256 文字です。応答とルート要素の両方に切り詰めの有無を示します。`click` はこれらの属性の**値全体を完全一致**で照合します（XPath ではありません）。`--view-id` は `resource-id`、`--description` は `content-desc` に対応し、複数条件は AND で組み合わせます。表示中で有効なコントロールだけを対象とし、該当なしや複数一致はエラーです。走査の上限を超えた場合もクリックしません。テキストラベルではクリック可能な親コントロールを探します。クリックできなければ `tap X Y` を使用できます。画面変更後は XML を取得し直してください。長い属性はリソース ID による指定に切り替えられます。

入力メソッドを使うには **設定 → Remote Input** を開きます。**Remote Input を有効にする** で Android の Remote Input を有効にし、**入力方法を選択** で選択します。対象アプリの入力欄にフォーカスした後、`type` は `InputConnection.commitText` を通じてカーソル位置または選択範囲に文字を挿入します。中国語、Emoji、複数行に対応し、`text` と同じ最大 8192 UTF-8 バイトの制限があります。送信ボタンの自動クリックやフォーム送信は行わず、Agent 自身の設定も変更できません。キーボードの **Switch keyboard** で普段の入力メソッドに戻せます。`status` は `ime_enabled`、`ime_selected`、`ime_connected` を返します。UI の読み取り、タップ、文字入力の前に、画面を点灯してデバイス上でロックを解除する必要があります。

Remote Console の**デバイス画面**ページでは、座標タップ、XML 取得と属性指定のクリック、Remote Input のテキスト入力ができます。権限や画面状態が利用不能ならボタンは無効になります。入力成功後は本文を消去し、ページがバックグラウンドに移るとスクリーンショット、XML、入力待ちの内容を消去します。

### システム機能

システム設定はデバイスが報告するプロバイダーに基づいて動作し、名前空間は固定の列挙型ではなく文字列です。Android は引き続き `android_settings` 機能を使用し、プロバイダーは `system`、`secure`、`global` です。Linux（OpenWrt を除く）と macOS は `system_settings` を使用します。Linux は現在のユーザーの gsettings、macOS は現在のユーザーの `/usr/bin/defaults` です。ブラウザー拡張は `browser_settings` 機能を使用し、暗号化セッション経由で `privacy.network`、`privacy.services`、`privacy.websites` を読み書きします。Windows と OpenWrt はこれらの設定を提供しません。新しいキーと値の設定を追加する際は、Agent または拡張でプロバイダーを登録すれば、操作側はその一覧に従って表示します。

Android の 3 つの名前空間は、画面制御スイッチ、ディスプレイ、ロック状態と無関係です。shell の文字列連結ではなく ContentResolver を通じて操作します。通常の APK から `system` に書き込むには、Agent の**システム設定の変更を許可**を押して許可します。`secure/global` の書き込みには追加のシステム権限 `WRITE_SECURE_SETTINGS` が必要です。状態には現在の権限と `providers` が返されますが、Android やメーカーのポリシーが個別の読み書きを拒否する場合があります。

`settings list --prefix` は Android では設定名をフィルターします。Linux / macOS では prefix なしなら schema または domain の名前だけを返し、prefix があればその schema または domain のトップレベルの文字列、整数、真偽値を返します。`settings clear` は現在、ブラウザーのプライバシー項目だけが受け付け、他のプロバイダーは明示的なエラーを返します。監査には名前空間、設定名、文字数を記録し、値は記録しません。

```sh
remote-cli ui realmev5 settings
remote-cli ui realmev5 settings list --namespace system --limit 100
remote-cli ui realmev5 settings get screen_off_timeout
remote-cli ui realmev5 settings set screen_off_timeout 600000
remote-cli ui realmev5 screen-timeout 600
remote-cli ui realmev5 settings get device_name --namespace global
remote-cli ui realmev5 settings set vendor.custom_setting --stdin
```

`settings set` の値は文字列で、`--stdin` は改行を保持します。設定値は監査に記録しません。値は最大 8192 UTF-8 バイトで NUL 不可、設定名は最大 256 ASCII 文字（英字、数字、アンダースコア、ピリオド、コロン、ハイフン）です。一覧は最大 500 件、512 KiB で、上限超過時は `truncated` を返します。単一値は最大 64 KiB です。存在しない設定は `value: null` を返します。制限された設定を通知なく強制変更することはなく、書き込みのたびに再読み取りして確認し、失敗時は明示的なエラーを返します。変更は永続化され、Agent を切断しても元の値には戻りません。`screen-timeout` ショートカットコマンドの単位は秒（15～86400）、内部の `screen_off_timeout` はミリ秒です。自動画面消灯と、消灯後のロック遅延は別です。

### メディア再生（media）

読み上げ、音声、画像（全画面テキストを含む）、動画、Web ページは共通の `media` 機能を使用します。Android と Windows/Linux/macOS Agent が通知し、OpenWrt は通知しません。プロトコルの操作は `media_play`（`media` オブジェクトを指定）、`media_stop`、`media_status` の 3 つで、いずれも同じ `media` 状態を返します。

| `media.type` | フィールド | チャンネル |
| --- | --- | --- |
| `speech` | `text`、任意の `language`（BCP-47、例：`zh-CN`） | 音声 |
| `audio` | `url` または `path` のいずれか、任意の `loop` | 音声 |
| `image` | `url`、`path`、`text` のいずれか、任意の `p2p`（`text` では指定不可） | 画面 |
| `video` | `url` または `path` のいずれか、任意の `p2p` | 画面 |
| `web` | `url`（https のみ） | 画面 |

デバイスには独立した 2 つのチャンネルがあります。**音声**（読み上げ、音声）と**画面**（画像、テキスト、動画、Web ページ）です。新しい内容は同じチャンネルで再生中の内容だけを置き換えるため、BGM と画像スライドショーを同時に実行できます。`media_stop` の `channel` は `sound`、`screen`、`all`（既定）を指定できます。

```sh
remote-cli ui realmev5 media speak "Remote 连接正常" --language zh-CN
remote-cli ui realmev5 media speak --stdin --language zh-CN
remote-cli ui realmev5 media audio --path /sdcard/Music/doorbell.mp3
remote-cli ui realmev5 media audio --url https://cdn.example.com/notice.mp3 --loop
remote-cli ui realmev5 media image --text "值班中"
remote-cli ui realmev5 media image --url https://example.com/poster.jpg
remote-cli ui realmev5 media video --path /sdcard/loop.mp4
remote-cli ui realmev5 media video --url https://files.example.org/library/clip.Aran --p2p-group library --p2p-discovery https://p2p.ivjn.us --p2p-token secret --p2p-peer 192.0.2.10:7300
remote-cli ui realmev5 media web https://status.example.com/board
remote-cli ui realmev5 media status
remote-cli ui realmev5 media stop sound      # 或 screen；不写则两个通道都停
remote-cli ui laptop media audio --path 'C:\Users\me\Music\alert.wav'
remote-cli ui laptop media image --text "Kiosk"
```

`media status` の応答：

```json
{"kind":"media",
 "sound":{"type":"audio","id":"…","state":"playing","language":null,"loop":true},
 "screen":{"type":"web","keep_awake":true},
 "speech_available":true,"media_volume":7,"media_volume_max":15}
```

`sound` は音声チャンネルの最新項目です（結果確認のため終了後も保持）。状態は `queued` / `speaking` / `loading` / `playing` / `completed` / `stopped` / `error` です。一度も再生していなければ `null` です。`screen` は画面表示中だけ現れ、それ以外は `null` です。`speech_available=false` は利用可能な音声エンジンがデバイスにないことを示します。メディア音量は Android のみ値があり、デスクトップでは 0 です。

検証と監査：テキスト（読み上げや画像テキスト）は最大 4000 UTF-8 バイトで NUL 不可です。URL は `https` 必須、最大 2048 バイトで空白や制御文字を含められません。パスは最大 4096 バイトで制御文字を含められません。監査では種類、ソース区分（`url` / `path` / `text`）、文字数、ループの有無、P2P の使用有無だけを記録します。URL、パス、本文は一切記録しません（URL には署名トークンが含まれる場合があります）。`readonly` アカウントはどのメディア操作も呼び出せません。

#### 音声：読み上げと音声ファイル

- 読み上げ：Android はシステム TTS エンジン、Windows は `System.Speech`、macOS は `say`、Linux は `spd-say` / `espeak-ng` / `espeak` を使用します（すべて未インストールなら `speech_available=false` で、読み上げは明示的なエラーを返します）。デスクトップでは可能な範囲で言語タグをローカル音声に対応付けます。Android はエンジンが発声を開始してから応答します。システムエンジンの初期化や言語データが利用不能ならエラーを返し、Agent 停止時に TTS を解放します。
- 音声：デバイス上の絶対パスまたは https URL をソースにします。デバイスにないファイルは先に `remote-cli files upload` で転送してからパスで再生します。`loop` は停止するまで繰り返します。
  - Android は `MediaPlayer` でメディア音量を使用し、URL は直接ストリーミング再生します。発声開始後に応答し、10 秒以内に開始できなければエラーを返します。
  - デスクトップでは先に URL を現在のユーザーの一時状態ディレクトリにダウンロードします（上限 50 MB、リダイレクトは https のみ）。再生終了、停止、エラー時に削除します。インターフェースは再生プロセスの起動後すぐ応答し、ダウンロードやデコードの失敗は `media status` の `sound.state=error` に反映します。Windows は `MediaPlayer`（mp3、wav、m4a、aac、wma、flac 対応）、macOS は `afplay` を使用します。Linux は `mpv`、`ffplay`、`paplay`、`pw-play`、`aplay` の順に探し、どれもなければエラーです（`paplay` / `aplay` は mp3 非対応）。
  - パスは存在して読み取り可能な通常ファイルである必要があります。Windows では、リモート SMB サーバーに認証情報を送らないよう、`\\server\share` のようなネットワークパスを拒否します。
- デバイスの音量がゼロの場合、音が聞こえないことがあります。

#### 画面：画像、テキスト、動画、Web ページ

- **image**：ローカルの `path`、`https` URL、または `text`（暗い背景に大きな UTF-8 テキストを表示し、静止画像として扱います）を使用します。Android では拡張子 `.Aran` のソースを Metafile Android SDK で復号して表示します。
- **video**：ローカルの `path` または `https` URL をループ再生します。Android の `.Aran` は通常の HTTPS メディアストリームとして扱えません。未キャッシュの場合はブロック単位で復号しながらダウンロード・再生し、平文をローカルキャッシュに保存して次回は直接再生します。HTTP ブロックのアドレスは、その origin のルートパス配下の `/meta/<SHA1>` です。ローカルファイルではマニフェストの隣の `meta/` ディレクトリを読み取ります。既定ではマニフェスト内の鍵または SDK の既定鍵を使用します。任意で P2P を利用でき、ブロックソースはローカル `meta/`、UDP P2P（`LOOKUP_ONLY`、metastore には登録しません）、HTTP フォールバックの順です。ディスカバリーサービスと静的 peer は `media.p2p` で指定します。例：`--p2p-group library --p2p-discovery https://p2p.ivjn.us`。P2P 起動に失敗しても HTTP/ローカルを使用し、再生を中断しません。
- **web**：`https` ページを全画面で開きます。Android は全画面 Activity 内の WebView を使用します（JavaScript と DOM storage を有効にし、ページ内の移動は https のみ。レンダラープロセスのクラッシュ時は Agent に影響せず表示を閉じます）。デスクトップは一時プロファイルディレクトリを使う独立したブラウザープロセスを kiosk モードで起動し、停止時にブラウザーごと終了します。Windows は Edge、Chrome、Firefox、macOS は Chrome、Edge、Chromium、Firefox（Safari に kiosk モードなし）、Linux は Chromium、Chrome、Edge、Firefox の順に探します。ブラウザーがなければ明示的なエラーを返します。

Android は `https://cdn.ivjn.cn` と `https://cdn.ivjn.us` の `.Aran` マニフェストとデータブロックで、もう一方のドメインを自動的に予備ソースとして使用します。マニフェストのパスは保持し、データブロックは各ソースの `/meta/<SHA1>` から読み取ります。他のドメインは自動対応付けしません。一時的なエラー（HTTP 554 を含む）、タイムアウト、データブロック検証失敗時には、先に予備ソースを試してから 250/500 ms のバックオフを行い、最大 3 ラウンド試行します。成功後はそのソースを優先します。各接続のタイムアウトは 5 秒、読み取りは 30 秒です。403/404 などの恒久エラーでは、同じ読み取り中に同じソースへ繰り返し要求しません。ブロックサイズと SHA-1 の検証は維持し、リダイレクトに従わず、HTTPS 検証も緩和しません。この動作には Metafile `ab2b92f` 以降の Android SDK（`MirroredHttpSource` を含む）が必要です。

画面チャンネルは一度に 1 件だけ表示し、新しい画面は前の画面を置き換えます。表示中は画面を点灯したままにします（Android：`FLAG_KEEP_SCREEN_ON` + `turnScreenOn` / `showWhenLocked`、Windows：`SetThreadExecutionState`、macOS：`caffeinate -d`、Linux：`systemd-inhibit` または `xdg-screensaver inhibit`）。静止画像とテキストは焼き付き防止のためゆっくりピクセル移動します。動画はループのみ、Web ページは移動しません。静的な強調枠はありません。Android は独立した全画面 Activity を使用します（ユーザー補助は不要）。Linux の画面表示にはロック解除済みの X11 セッション、動画にはローカルの `mpv` または `ffplay` が必要です。macOS の動画は mpv を優先し、なければ `open` を使用します。

#### 制御：再生位置と Web ページ

`media_control` はチャンネルごとに再生中の内容を制御し、同じ `media` 状態を返します。`channel` は `sound` または `screen`、`op` は次のいずれかです。

| `op.type` | パラメーター | 対象 |
| --- | --- | --- |
| `pause` / `resume` | — | 音声（sound）、動画（screen） |
| `seek` | `position_ms`（絶対位置、最大 7 日） | 音声、動画 |
| `navigate` | `url`（https） | Web ページ |
| `reload` / `back` / `forward` | — | Web ページ |
| `scroll` | `dx`、`dy`（ピクセル、ゼロ以外、それぞれ最大 100000） | Web ページ |
| `eval` | `script`（最大 16 KiB） | Web ページ、`script_result`（結果の JSON エンコード、最大 64 KiB）を返す |

```sh
remote-cli ui realmev5 media pause sound
remote-cli ui realmev5 media seek 1:30 screen      # 也接受 90、1:02:03.5
remote-cli ui realmev5 media resume screen
remote-cli ui realmev5 media web go https://status.example.com/next
remote-cli ui realmev5 media web back
remote-cli ui realmev5 media web scroll 0 800
remote-cli ui realmev5 media web eval "document.title"
```

状態には、`sound` / `screen` の `position_ms`、`duration_ms`、`paused`（音声と動画で、プレーヤーが報告可能な場合）、Web ページでは `screen` の `url` と `title` が追加されます。読み上げ、画像、テキストは制御できません。チャンネルの内容と操作が一致しなければ明示的なエラーを返します。監査にはチャンネル、操作の種類、スクリプトの文字数だけを記録し、URL やスクリプト本文は記録しません。

- Android：音声と動画は `MediaPlayer` / `VideoView` を直接制御し、Web ページは全画面 Activity 内の WebView を制御します。
- Windows：音声は付属の `MediaPlayer` worker が実行します。動画は mpv では IPC パイプ経由で制御し、WPF フォールバックプレーヤーでも制御可能ですが、ffplay は非対応です。
- Linux / macOS：音声・動画はプレーヤーが mpv の場合だけ制御可能です（JSON IPC）。それ以外は「Install mpv to pause or seek desktop media」を返します。macOS で mpv がインストール済みなら、音声も mpv で再生します。
- デスクトップの Web ページ：Chrome / Edge / Chromium の kiosk を `--remote-debugging-port=0` で起動します（127.0.0.1 のみ、ランダムポート、一時プロファイルディレクトリ）。DevTools プロトコル経由で実行します。Firefox は `navigate` のみ対応します（新アドレスで kiosk を開き直します）。

Remote Console モバイル版では、デバイス詳細の**システム機能**ページに「システム設定」と「メディア」タブがあります。メディアページは 2 チャンネルの状態確認、音声と画面の個別停止、読み上げ、音声 URL、テキスト、画像、動画、Web ページの再生に対応し、音声・動画の再生位置と Web ページ（移動、進む/戻る、更新、スクロール、JS 実行）も制御できます。ページがバックグラウンドに移ると、設定結果、送信待ちの設定値、読み上げ本文、表示入力を消去します。Web コンソールも同じ設定フォームを提供し、ブラウザー設定は確立済みの暗号化セッションを再利用します。Web 版には別のメディア再生ページはありません。

### Android テキスト検索と XML

```sh
remote-cli ui realmev5 xml
remote-cli ui realmev5 find-text "设置"
remote-cli ui realmev5 find-text "Settings" --exact --ignore-case
```

テキスト検索は可視ノードのテキストと説明の両方を対象にし、既定は部分一致、任意で完全一致や大文字小文字の無視を指定できます。各一致の `bounds` と `center`（元の画面ピクセルで、`tap X Y` でタップ可能）、リソース ID、テキスト、有効状態を返します。中心点は画面との交差領域から算出し、画面外のタップ座標を返さないようにします。最大 200 ノードを走査し、切り詰めを報告します。パスワードノードと Agent 自身の設定サブツリーは検索対象外です。独自描画の文字は Android ユーザー補助から読めない場合があります。このインターフェースは OCR を実行しません。画面変更後は検索し直してください。

スクリーンショットは JPEG で、最長辺は最大 1600 ピクセルです。CLI は既存ファイルの上書きを拒否します。CLI は画像サイズと元の画面サイズを両方表示します。タップ、スワイプ、ツリーノードの `bounds` は元の画面ピクセルを使用します。縮小画像から位置を求める場合は `屏幕坐标 = 图像坐标 × screen_width / width`（縦方向は高さの比率）を使用してください。回転後はサイズを取得し直します。UI ツリーは最大 200 ノード、各文字列は最大 256 UTF-16 単位です。`truncated` はノード数の制限を示します。アプリ独自の描画 UI ではアクセス可能なノードがない場合があります。

パスワードノードのテキストと説明、Remote Agent 自身の設定サブツリーは返しません。リモートのテキスト入力で自身の設定は変更できません。Android の `FLAG_SECURE` で保護されたウィンドウはスクリーンショットで黒く塗りつぶされるか、システムエラーになります。Android 15 の画面共有では、パスワード欄などの機密情報を含むアプリが自動的に黒くなる場合もあります。画面制御は既存の運用認証を再利用し、`readonly` アカウントは操作を開始できません。監査には操作、座標、入力文字数だけを記録し、入力本文、UI ツリー、スクリーンショットは保存しません。

**Stop screen sharing** は MediaProjection の共有セッションを終了します。Android 11+ のユーザー補助が有効なら、ユーザー補助経由のスクリーンショットは引き続き利用できます。**Allow remote screen controls** を無効にすると、以後のすべての画面操作を拒否して共有を終了します。**Stop remote access** は agent も切断します。実行済みのタップや入力は取り消せません。ユーザー補助の権限は Android 設定で個別に管理します。スイッチを無効にした場合やサービス停止時もシステムの許可は保持しますが、リモート画面操作は受け付けません。

ローカルブリッジはランダム名の Unix socket を使用し、呼び出し元の UID が APK と同じか検証します。これは他の通常アプリからのアクセス境界を保護します。許可済みのリモート shell は APK と同じアプリ UID を共有するため、画面スイッチをその shell やデバイス管理者に対する権限分離として扱わないでください。

## システム情報とメトリクス

`DeviceInfo.platform` は `linux`、`freebsd`、`netbsd`、`windows`、`android`、`macos`、`openwrt` のいずれか、`os_version` は汎用の OS バージョンです。`capabilities` は `exec`、`pty`、`ubus`、`android_control` を表します。OpenWrt は `openwrt_version` を報告します。

Linux/OpenWrt は `/proc` からメトリクスを収集します。Android では SELinux の制限に遭遇しても他の読み取れるメトリクスを保持し、稼働時間は `CLOCK_BOOTTIME` から取得できます。Windows はネイティブシステム API でメモリー、CPU、稼働時間、ネットワークインターフェースのカウンターを取得します。取得できないメトリクスは `unavailable` リストで示し、CLI とモバイル側は欠損項目として表示します。Windows には Unix の load average がないため、`load` は常に利用不能とされます。

## 検証

```sh
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p agent --features desktop-ui
cargo clippy -p agent --features desktop-ui --all-targets -- -D warnings
cd mobile && npm run typecheck && npm run lint && npm test
```

`scripts/agent-smoke.py` は Python の `websockets` を使用するローカルテスト relay です。実際の agent の登録、メトリクス、実行、タイムアウト、キャンセル、PTY、resize、非対応の ubus を検証します。Exchange の認証とデータベースのテストを置き換えるものではありません。スクリプトの説明に従って固定のテスト認証情報を使い、本番デバイスやサービスには接続しないでください。Windows と Linux ではそれぞれネイティブテストを実行してください。Android は APK をインストールし、実際の実行環境でフォアグラウンド、バックグラウンド、停止、再接続も検証してください。

`scripts/agent-ui-smoke.py --agent <binary> --platform linux|windows` は、UI の初回設定、認証とオリジン制限、プライベートファイル保存、トークン保持、実際の登録と実行、停止時の回収、切断後の再接続、再オープンを別途検証します。Python 3.11+ と `websockets` が必要で、テスト対象 agent をあらかじめ `desktop-ui` 付きでビルドする必要があります。ブラウザー検証では保存ボタン、入力エラー、トークンの表示/非表示、更新後の復元、狭い画面のレイアウトも確認してください。

`scripts/android-control-smoke.py` は、実際の Exchange、CLI、独立した Android テストアプリで画面操作を検証します。専用の破棄可能なエミュレーターだけで実行してください。スクリプトは Remote Agent のアプリデータを消去し、ユーザー補助設定を変更します。先に `:app:assembleDebug` と `:control-fixture:assembleDebug` の 2 つの APK をビルドしてインストールし、独立した Exchange/データベースを用意して、テストデバイスを登録し、デバイストークンと CLI 設定を保存してください。Python 3.11+、Pillow、`adb` が必要です。

```sh
adb -s emulator-5580 install -r android-agent/app/build/outputs/apk/debug/remote-agent-debug.apk
adb -s emulator-5580 install -r -t android-agent/control-fixture/build/outputs/apk/debug/control-fixture-debug.apk
python scripts/android-control-smoke.py --serial emulator-5580 \
  --cli target/debug/remote-cli --cli-config /private/qa-cli.toml \
  --device-token /private/qa-device-token.txt \
  --device-id android-control-qa --device-server ws://10.0.2.2:18820/ws/device
```

Windows では CLI のパスを `target/debug/remote-cli.exe` に変更します。テスト成果物の既定の保存先は `target/android-control-qa/screenshots/` です。fixture はテスト専用で、デバイス側 APK には含めません。

### Android ログ、ディスプレイ状態、画面の点灯

Remote Console はデバイス詳細に **Device logs** と **Device screen** を追加します。画面ページにはディスプレイの有無、電源状態、ロック状態、サイズ、権限を表示し、オンデマンドの JPEG プレビューと **Wake screen** を提供します。ディスプレイがないデバイスや Android 制御インターフェースのないデバイスでは画面操作が無効になります。キャプチャ権限の不足は別に表示されます。ローカルの画面制御が有効なら、画面が消灯しているデバイスを点灯できます。点灯操作でキーロックを解除することはありません。

Android Agent はローカルに **Screen status and device logs** を備えています。リモートアクセスが有効な間は、接続欄、QR 登録、接続コードのボタンが非表示になります。切断には確認が必要で、保存済み認証情報は保持します。ユーザー補助の許可とサービスの実際のバインド状態は別々に報告します。許可済みでバインド待ちの場合は、システムのユーザー補助設定で Remote Agent だけを切り替えてください。

```powershell
remote-cli ui realmev5 status
remote-cli ui realmev5 logs --limit 100
remote-cli ui realmev5 wake
remote-cli ui realmev5 screenshot --output screen.jpg
```

既存の `open_android` WebSocket インターフェースは `{action:"logs",limit:100}`（1～300 件）と `{action:"wake"}` を受け付けます。状態には任意の `display_present`、`screen_on`、`locked`、`screen_width`、`screen_height`、`accessibility_authorized` が追加されます。`accessibility_enabled` は引き続きサービス接続済みを意味します。Android のネイティブ画面 API には引き続き APK が必要で、Android 以外の agent はこのインターフェースを通知しません。

ログは現在 `logcat -b all` から取得し、PID、パッケージ、タグによるフィルターは行いません。Android logd から利用可能な system、application、events、crash バッファが対象です。各スナップショットは UI では最大 100 件、CLI では 1～300 件を返し、タイムスタンプ、レベル、バッファ名、PID/TID、タグ、元のメッセージを保持します。これはデバイスの現在の logcat 履歴であり、任意のログファイルや Android がすでにローテーションで削除したログのアーカイブではありません。デバイス APK を更新してください。

許可された ADB 接続を通じて、デバイス APK にシステムログ権限を付与します。

```sh
adb shell pm grant com.remote.agent android.permission.READ_LOGS
```

ログアクセスのたびに許可が必要な Android では、Agent の診断ページを開いてシステムの確認を承認し、リモート要求を再試行します。Android/OEM ポリシーにより、バックグラウンドアクセスが拒否されたり許可が失効したりすることがあります。Android は拒否後に呼び出し元の UID だけを通知なく返す場合があるため、コレクターは要求された末尾部分を選ぶ前に最新 301 件の logcat エントリーの UID メタデータを調べます。他の UID が現れない場合（空ログを含む）、アプリ限定スナップショットを提示せず、デバイス全体へのアクセスを確認できなかったと報告します。権限不足や logcat 失敗も明示的なエラーを返し、Agent のイベントリングにはフォールバックしません。収集は UI スレッド外で行い、キャンセル時に停止します。期限は 6 秒で出力量にも上限があり、大きすぎるスナップショットは切り詰めまたはエラーを報告します。ログは既存の認証済みオペレーターセッション、テナントルーティング、ロール制限を使用します。デバイスログには他アプリが書き込んだ内容が含まれる場合があります。Web とモバイルのログ表示はページを離れたりバックグラウンドに移ったりすると消去され、スナップショットをディスクには保存しません。Remote Console は終了時やバックグラウンド移行時にスクリーンショットも消去します。

**透明オーバーレイによる全画面共有：** Android Agent で **Share full screen with transparent overlay** を選択します。Android の「他のアプリの上に表示」権限と画面共有を許可してください。Agent はバックグラウンドに移り、不透明度ゼロ、`FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE` の全画面 `TYPE_APPLICATION_OVERLAY` を配置します。すべてのタッチは下のアプリに渡ります。MediaProjection は画面全体をキャプチャします（Android 14+ では既定ディスプレイを明示的に要求します）。Agent ページまたは通知から共有を停止できます。共有停止は Agent を切断しません。共有終了またはサービス停止時にオーバーレイを削除します。

画面プレビューと接続済み Agent の状態ページではキャプチャを許可します。接続設定、ログインチケット、トークン QR のページは引き続き保護されます。他アプリの保護されたウィンドウでは Android が黒い画像を返す場合があり、透明オーバーレイでそのアプリの保護を回避することはできません。

キャプチャ前に Android Agent はディスプレイ状態と PowerManager のインタラクティブ状態を両方確認します。消灯中のディスプレイだけを点灯し、操作可能になったことを確認し、キーロックが残っていればキャプチャを拒否します。MediaProjection のリーダーはフレームを継続的に読み出し、最新の合成画像を 1 枚保持します。スクリーンショットごとにサーフェスを作り直すことはありません。全面が黒いフレームでは利用可能なフレームを待ち、タイムアウト時には正常な黒い JPEG を返すのではなく、対処方法の分かるエラーを返します。

## Agent モードの比較

起動モードとデバイスのプラットフォームは別の要素です。ネイティブの headless、ローカル UI、システムサービスは接続コアを共有します。`desktop-ui` はローカル設定画面を追加するだけで、リモート権限を付与しません。

| モード/プラットフォーム | 機能 | 条件と制限 |
|---|---|---|
| headless / ローカル UI | 共通のネイティブプロトコル、コマンド、端末、ファイル、メトリクス、通知、任意の LAN | UI は Linux/Windows/macOS のみ。Windows はトレイ対応。デスクトップ操作には有効なグラフィカルユーザーセッションが必要 |
| システムサービス / ログイン時起動 | 同じネイティブ Agent | サービスのアカウントと環境を継承。起動時の上書きは別のサービスへコピーされない |
| OpenWrt | ネイティブコア、ubus | ubus が存在する場合に通知。procd が監視。デスクトップ制御は通知しない |
| Linux / Windows / macOS | ネイティブコア、デスクトップ制御、システムログ、メディア | OS 権限とツールが必要。システム設定は Linux/macOS のみ通知 |
| FreeBSD / NetBSD | ネイティブコア、端末、ファイル、ネットワーク、メトリクス、任意の LAN | 電池未対応；X11 依存関係が必要；メディア/設定は未対応 |
| Android APK | Rust コア + Java ブリッジ、画面、設定、メディア、OTA | アプリ UID と OS 権限の範囲内。単体 Rust バイナリには APK 制御ブリッジがない |
| Chromium 拡張機能 | URL、Cookie、ページ、ブラウザー設定、CDP、メトリクス | OS コマンド/PTY/任意のホストファイル/LAN リスナーなし。完全な CDP にはローカル許可が必要 |

UI と headless は実行設定の解決処理を共有します。LAN、watchdog、TLS、通知は CLI/環境変数が JSON より優先され、自動更新は CLI > JSON > `REMOTE_AUTO_UPDATE` > 既定で有効の順です。`REMOTE_WATCHDOG=1` と 0、true/false、yes/no、on/off を受け付けます。UI では接続認証情報と状態パスの起動時上書きを引き続き拒否します。実行時の上書きは JSON に保存されず、自動起動プロセスには別途設定が必要です。LAN 状態には有効値を表示し、画面で保存しても起動時の上書きは解除されません。

UI と headless は JSON の `state_file`、未指定なら共通の永続的なシステム既定パスを使用します。複数のデバイスインスタンスには別々のパスが必要です。旧 UI の一時的な識別情報が削除された場合は、指紋を再確認し信頼を再設定してください。停止すると接続の自動更新 future をキャンセルし、watchdog の状態を解放します。監視処理は次の確認時に終了し、完了済み操作は取り消せません。 再接続のたびにデバイスの識別情報と信頼ポリシーも再読み込みします。新しい識別情報の読み込みに失敗した場合は、古いデバイス認可を残さず識別情報を消去します。

すべてのデバイス操作に密封セッションとワークスペースルートの認可が必要です。完全なブラウザー CDP の都度確認/無人運用許可は型付き Cookie スコープで隔離されず、Chrome のポリシーも回避できません。拡張機能の終了や切断後は再ハンドシェイクが必要です。デスクトップ更新パッケージは Windows x64、Linux glibc x64/arm64、macOS arm64 に対応します。他のネイティブ環境はインストーラー/パッケージ管理、Android は APK OTA、ブラウザーは拡張機能パッケージを使用します。機能の通知はハードウェア、権限、公開パッケージの利用可能性を保証しません。

## 自動スクリーンセーバー

ローカル Agent とブラウザー拡張の設定で、無操作（既定 300 秒）、制御開始・終了、即時起動を選択できます。文字、HTTPS 画像・動画、隔離された静的 HTML を表示します。動画は無音ループ、文字と画像は広い範囲の移動に対応します。デスクトップと Android は終了パスワードを設定できますが、ブラウザーの全画面ウィンドウはパスワード不要で、キー・マウス移動・ウィンドウ終了で閉じます。対象はアクティブな画面のみで、OS ロックではありません。Linux は X11 と xprintidle が必要です。ヘッドレス OpenWrt、Wayland、BSD は現在非対応です。

単色モードは文字やメディアを表示せず、#RRGGBB の背景色と 0～100% の明るさを設定できます。0% は黒表示です。Android はバックライトも下げ、入力時にシステムの明るさに戻します。他の環境は画像のみを暗くし、LCD の省電力はバックライトに依存します。終了画面は暗くなりません。

0.1.34 以降、移動を有効にすると文字と画像だけが背景上の広い範囲を移動します。大きな内容は必要に応じて表示可能な幅と高さの 44% 以下に縮小され、対角の位置が重ならないようにします。HTML、動画、単色は静止し、動きを減らす設定では移動を停止します。全モードで最初のフレームからポインターを隠し、終了パスワード画面では表示し、キャンセルすると再び隠します。Windows はシステムのカーソル設定を変更せず、画面保護ウィンドウ内の状態を 100 ms ごとに更新します。まず Windows と Browser Agent の 0.1.34 パッケージで提供し、他の環境の実際のバージョンはダウンロード一覧で確認してください。
