# Linux、FreeBSD、NetBSD、Windows、macOS 與 Android 裝置

> HTML: https://remote.ivjn.us/docs/zh-TW/platforms/

Linux（glibc / musl）、FreeBSD、NetBSD、Windows、macOS 與 Android 裝置複用 `remote-agent`、Exchange、`remote-cli` 和移動維運用戶端。裝置端與 CLI 使用通用名稱；環境變數統一為 `REMOTE_*`。

## 一條命令安裝（Windows / Linux / macOS / FreeBSD / NetBSD）

官網指令碼會按系統下載校驗包，把二進位檔放到 `~/.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` 字首為 `agent.` 而非 `device.`，`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 中放使用者名稱、密碼或其它憑據，含使用者名稱/密碼的地址會被拒絕。


### 出站代理與連線保活

無介面（headless）與 `--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 **僅宣告 `http/1.1`**，與 CLI 一致。協商成 `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 後顯式 flush 自動排隊的 Pong。連線與註冊應答等待各限時 45 秒，傳送及註冊後的 Pong flush 限時 30 秒，詳見 [保活與逾時](/docs/zh-TW/websocket-protocol.md#保活與超時)（#42）。

啟用 `desktop-ui` 的 Windows 建置使用多執行緒 Tokio runtime，讓 axum、本機系統匣與裝置 WebSocket 並行排程；同一建置以 headless 方式執行也使用多執行緒。其他平台啟用此功能時，本機 UI（明確指定 `--ui` 或無參數啟動）同樣使用多執行緒。未啟用此功能的建置，以及其他平台明確指定設定檔的 headless 執行，使用 `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 程序開啟。

預設設定路徑為 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 管理目前程序，也可在頁面切換登入時自動啟動。使用 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 軟體包

官網為官方 OpenWrt 25.12.5 SDK 打包的 `remote-agent`（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+、樹莓派 64 位系統等），支援 `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
```

使用者服務預設隨登入會話執行。需要開機常駐時由管理員執行 `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 命令使用 PowerShell 語法，例如 `Get-Process`，輸出以 UTF-8 傳送。每個執行會話和終端都有獨立 Job Object；逾時、取消、斷開或 agent 退出時清理其子程序樹。

## macOS

需要 Rust 工具鏈（`rustup`，含預設的 `aarch64-apple-darwin` 或 `x86_64-apple-darwin` target）。指標經 sysctl、`getloadavg(3)` 與 mach `host_statistics` 採集，靜態資訊（型號、核心、系統版本）經 `sysctl` 與 `sw_vers` 採集；平臺上報為 `macos`。

按需感測器（非心跳）在無 sudo、無額外 SMC 依賴下盡力採集：`pmset -g batt` 電池（筆記本）、`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
```

需要開機（無人登入時）常駐時，改用 `/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 輸入六位碼授權後自動取得裝置身份；原生 Agent（Windows/macOS 本地頁面、OpenWrt LuCI「新增裝置」、命令列 `remote-agent pair` / `remote-agent join <邀请>`）可顯示六位碼與二維碼供 Console 掃描批准，或貼上一次性邀請加入。詳見 [初始化與配對](/docs/zh-TW/pairing-and-enrollment.md)。

### 首次連線與首頁

1. 在需要被管理的 Android 裝置上開啟 **Remote Agent**。首次連線優先選擇 **使用六位驗證碼連線** 或 **掃描連線二維碼**，由管理員授權；帳號、QR 碼與六位碼的完整流程見 [初始化與配對](/docs/zh-TW/pairing-and-enrollment.md)。
2. 已有裝置 ID 與裝置權杖時，進入 **設定 → 連線設定**，填寫伺服器、裝置 ID 與權杖，再點 **儲存並連線**。啟用連線後會隱藏憑證輸入區；修改身分前先回首頁中斷連線。
3. 首頁顯示連線狀態、螢幕共享操作與管理者清單。已儲存設定但尚未啟用連線時，點 **連線裝置**；已啟用的裝置重新開啟應用程式時會恢復連線。

Android 12 以上使用系統啟動畫面，Android 8–11 使用相同品牌配色的啟動背景。首個畫面就緒後即進入首頁，不等待網路連線完成，也不刻意延長啟動畫面；系統關閉動畫時跳過轉場。設定頁按需載入，開啟首頁不會自動授予無障礙或螢幕共享權限。文件描述目前原始碼；公開下載包是否包含此體驗，以實際安裝版本為準。

### 背景執行、通知與停止

切到背景或關閉應用程式介面不會停止前景服務。網路中斷時 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`，把兩個 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 掃碼連線

Remote Agent 首頁的 **Scan connection QR code** 使用手機相機離線識別連線二維碼，無需 Google Play 服務。首次使用時允許相機許可權；取消掃碼不會修改設定。掃碼成功後會顯示伺服器端和裝置 ID，權杖保持隱藏。點選 **Connect** 後才加密儲存設定並啟動連線；已有連線執行時需先停止，才能匯入新設定。掃碼不會自動開啟螢幕控制或無障礙功能。

先在 Exchange 登記一個獨立裝置並取得它的**裝置權杖**，然後在電腦本地生成二維碼（不要使用維運賬號權杖或第三方線上二維碼網站）：

```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 从标准输入读取。
```

在電腦上開啟生成的 SVG，讓 Remote Agent 掃描。二維碼檔案包含裝置權杖，應保密並在使用後刪除，不要提交到 Git。工具拒絕覆蓋已有檔案。

二維碼內容為 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。它允許讀取可存取的介面樹、點選、滑動、系統導覽與取代焦點輸入欄的文字。
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 ...>` 介面樹，包含 `resource-id`、`text`、`content-desc`、`bounds` 和控制項狀態；最多 200 個節點、50 層，屬性最多 256 個字元，響應和根元素都會標明是否截斷。`click` 使用這些屬性的**完整值精確匹配**（不是 XPath）：`--view-id` 對應 `resource-id`，`--description` 對應 `content-desc`；多項條件按 AND 組合。只匹配可見、啟用的控制項，零個或多個匹配都會報錯；遍歷超限也不會執行點選。文字標籤會尋找可點選的父控制項，無法點選時可用 `tap X Y`。介面變化後應重新獲取 XML，長屬性可改用資源 ID 定位。

輸入法入口位於 **設定 → 遠端輸入**：先點 **啟用遠端輸入** 在 Android 中啟用 Remote Input，再點 **選擇輸入法** 選取它。聚焦目標應用輸入框後，`type` 透過 `InputConnection.commitText` 在游標/選區插入文字，支援中文、Emoji 和多行，使用與 `text` 相同的 8192 UTF-8 位元組限制。它不會自動點選傳送或提交表單，也不能修改 Agent 自身設定。鍵盤上的 **Switch keyboard** 可恢復日常輸入法。`status` 返回 `ime_enabled`、`ime_selected` 和 `ime_connected`。讀取和點選介面、輸入文字前必須亮屏並在裝置上解鎖。

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 的三個名稱空間與螢幕開關、顯示器和鎖屏狀態無關，透過 ContentResolver 操作而非 shell 拼接。普通 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）

朗讀、音訊、圖片（含全屏文字）、影片和網頁統一走 `media` 能力，在 Android 與 Windows/Linux/macOS Agent 上通告；OpenWrt 不通告。協議只有三個動作：`media_play`（帶一個 `media` 物件）、`media_stop`、`media_status`，三者都返回同一個 `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） | 畫面 |

裝置上有兩個互相獨立的通道：**聲音**（朗讀、音訊）和**畫面**（圖片、文字、影片、網頁）。新的內容只替換同一通道里正在播放的那個，所以背景音樂可以和圖片輪播同時進行。`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 拒絕 `\\server\share` 這類網路路徑，避免向遠端 SMB 伺服器傳送憑據。
- 裝置音量為零時可能聽不到聲音。

#### 畫面：圖片、文字、影片與網頁

- **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`）。

畫面通道一次只展示一項；新的畫面替換上一項。展示期間保持亮屏（Android：`FLAG_KEEP_SCREEN_ON` + `turnScreenOn` / `showWhenLocked`；Windows：`SetThreadExecutionState`；macOS：`caffeinate -d`；Linux：`systemd-inhibit` 或 `xdg-screensaver inhibit`）。靜態圖片與文字緩慢畫素漂移以防灼屏；影片僅迴圈；網頁不漂移。無靜態高亮邊框。Android 透過獨立全屏 Activity（無需無障礙功能）。Linux 畫面需要未鎖屏的 X11 會話，影片需本機安裝 `mpv` 或 `ffplay`；macOS 影片優先 mpv，否則 `open`。

#### 控制：進度與網頁

`media_control` 按通道控制正在播放的內容，返回同樣的 `media` 狀態。`channel` 必須是 `sound` 或 `screen`；`op` 取下表之一：

| `op.type` | 引數 | 適用 |
| --- | --- | --- |
| `pause` / `resume` | — | 音訊（sound）、影片（screen） |
| `seek` | `position_ms`（絕對位置，最多 7 天） | 音訊、影片 |
| `navigate` | `url`（https） | 網頁 |
| `reload` / `back` / `forward` | — | 網頁 |
| `scroll` | `dx`、`dy`（畫素，非零，各不超過 100000） | 網頁 |
| `eval` | `script`（最多 16 KiB） | 網頁，返回 `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`（音訊和影片，播放器能報告時），網頁時 `screen` 的 `url` 與 `title`。朗讀、圖片和文字不能控制；通道里放的內容與操作不匹配時返回明確錯誤。審計只記錄通道、操作型別和指令碼字元數，不記錄網址或指令碼內容。

- Android：音訊與影片直接控制 `MediaPlayer` / `VideoView`，網頁控制全屏 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 播放。
- 桌面網頁：Chrome / Edge / Chromium 的 kiosk 以 `--remote-debugging-port=0` 啟動（只綁 127.0.0.1、隨機埠、臨時設定目錄），經 DevTools 協議執行；Firefox 只支援 `navigate`（以新地址重開 kiosk）。

Remote Console 手機端在裝置詳情的 **系統能力** 頁面提供「系統設定」與「媒體」頁籤；媒體頁可讀取兩個通道的狀態、分別停止聲音或畫面，播放朗讀、音訊 URL、文字、圖片、影片和網頁，並控制音影片進度與網頁（跳轉、前進後退、重新整理、滾動、執行 JS）。頁面進入背景會清空設定結果、待傳送設定值、朗讀正文和展示輸入。網頁控制檯提供同一份設定表單，瀏覽器設定複用已經建立的加密會話；網頁端不另做媒體播放頁。

### 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`（縱向使用高度比例）。旋轉後重新讀取尺寸。介面樹最多 200 個節點，每個字串最多 256 個 UTF-16 單元，`truncated` 表示節點數量受限。應用自繪介面可能沒有可訪問節點。

密碼節點的文字和描述、Remote Agent 自身的設定子樹不會返回；遠端文字輸入不能修改自身設定。受 Android `FLAG_SECURE` 保護的視窗在截圖中被遮黑或返回系統錯誤；Android 15 的螢幕共享還可能自動遮黑含密碼欄位等敏感內容的應用。螢幕控制複用現有維運身分驗證與授權，`readonly` 賬號不能發起這些操作。審計只記錄動作、座標和輸入字元數，不儲存輸入正文、介面樹或截圖。

**Stop screen sharing** 結束 MediaProjection 共享會話；若 Android 11+ 無障礙功能仍啟用，無障礙功能截圖仍可用。關閉 **Allow remote screen controls** 會阻止後續所有螢幕操作並結束共享；**Stop remote access** 同時斷開 agent。已經執行的點選或輸入無法撤回。無障礙功能許可權由 Android 設定單獨管理；開關關閉或服務停止時仍保留系統授權，但不接受遠端螢幕操作。

本地橋接使用隨機名稱的 Unix socket，並校驗呼叫者與 APK 的 UID 相同。這保護其它普通應用的訪問邊界；已授權的遠端 shell 與 APK 共用應用 UID，螢幕開關不應被當作針對該 shell 或裝置管理員的許可權隔離。

## 系統資訊與指標

`DeviceInfo.platform` 為 `linux`、`freebsd`、`netbsd`、`windows`、`android`、`macos` 或 `openwrt`，`os_version` 為通用系統版本，`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` 是本地測試 relay，使用 Python `websockets`，驗證真實 agent 的註冊、指標、執行、逾時、取消、PTY、resize 和不支援的 ubus。它不替代 Exchange 身分驗證與授權與資料庫測試。按指令碼說明使用固定測試憑證，勿連線正式裝置或服務。Windows 和 Linux 都應執行各自的原生測試；Android 還應安裝 APK 後在實際執行環境驗證前景、背景、停止及重連。

`scripts/agent-ui-smoke.py --agent <binary> --platform linux|windows` 另行驗證 UI 的首次設定、身分驗證與授權與來源限制、私有檔案儲存、保留權杖、實際註冊和執行、停止回收、斷線重連及重新開啟。要求 Python 3.11+ 和 `websockets`；必須先用 `desktop-ui` 建置待測 agent。瀏覽器驗證還應覆蓋儲存按鈕、輸入錯誤、權杖顯隱、重新整理恢復與窄屏佈局。

`scripts/android-control-smoke.py` 使用真實 Exchange、CLI 和獨立 Android 測試應用驗證螢幕操作。僅在專用、可丟棄的模擬器執行：指令碼會清除 Remote Agent 應用資料並修改無障礙功能設定。先建置並安裝 `:app:assembleDebug` 與 `:control-fixture:assembleDebug` 兩個 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 可提供的系統、應用程式、事件與當機緩衝區。每次快照在 UI 最多回傳 100 筆，在 CLI 可回傳 1–300 筆，保留時間戳記、層級、緩衝區名稱、PID/TID、標籤與原始訊息。這是裝置目前的 logcat 歷史，並非任意日誌檔案或 Android 已輪替移除日誌的封存。

透過已授權的 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 執行緒之外執行，取消時停止，並設有六秒期限與輸出上限；過大的快照會回報截斷或錯誤。日誌沿用既有的已驗證操作員工作階段、租戶路由與角色限制。裝置日誌文字可能包含其他應用程式寫入的內容。網頁與行動端日誌檢視在離開頁面或切換至背景時會清空，不會將快照存入磁碟。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 讀取器會持續取出影格並保留一張目前的合成影像，不會為每次擷取畫面重新建立 surface。遇到全黑影格時會等待可用影格，逾時後回傳可據以處理的錯誤，而不會將全黑 JPEG 當作成功結果。

## Agent 模式對照

啟動方式與裝置平台是兩個維度。原生 headless、本機 UI 和系統服務共用連線核心；`desktop-ui` 只增加本機設定介面，不增加遠端權限。

| 模式/平台 | 提供的能力 | 條件與邊界 |
|---|---|---|
| headless / 本機 UI | 相同原生協定、命令、終端、檔案、指標、通知、可選 LAN | UI 僅 Linux/Windows/macOS；Windows 有系統匣；桌面操作需要可用的使用者圖形工作階段 |
| 系統服務 / 登入自啟動 | 相同原生 Agent | 繼承服務帳號及環境；啟動參數不會自動同步到另一個服務 |
| OpenWrt | 原生核心、ubus | ubus 存在時通告；procd 監管；不通告桌面控制 |
| Linux / Windows / macOS | 原生核心、桌面控制、系統記錄、媒體 | 依賴系統權限/工具；系統設定僅 Linux/macOS 通告 |
| FreeBSD / NetBSD | 原生核心、終端、檔案、網路、指標、可選 LAN | 電池不可用；需要 X11 相依元件；無媒體/系統設定 |
| Android APK | Rust 核心 + Java 控制橋、螢幕、設定、媒體、OTA | 應用程式 UID 和系統授權範圍；單獨執行 Rust 二進位檔沒有 APK 控制橋 |
| Chromium 擴充功能 | URL、Cookie、網頁、瀏覽器設定、CDP、指標 | 無系統命令/PTY/任意主機檔案/LAN 監聽；完整 CDP 需要本機許可 |

UI 和 headless 共用執行設定解析：LAN、watchdog、TLS 及通知遵循命令列/環境變數優先於 JSON；自動更新維持命令列 > JSON > `REMOTE_AUTO_UPDATE` > 預設開啟。`REMOTE_WATCHDOG=1` 和 0、true/false、yes/no、on/off 均可解析。UI 仍拒絕連線憑證及狀態路徑的啟動覆寫；執行覆寫不寫回 JSON，自啟動程序需另行設定。LAN 狀態顯示目前生效值，頁面儲存不會取消啟動覆寫。

UI 與 headless 使用 JSON 的 `state_file`，否則使用相同系統預設持久路徑；多個裝置實例應各自指定路徑。連線開始時更新 connecting，停止時顯示 stopped。停止連線會取消該連線的自動更新 future，並釋放 watchdog 狀態；背景監控在下次檢查時退出，已完成的操作無法撤銷。 每次重新連線也會重新載入裝置身分與信任策略；新身分載入失敗時清空舊身分，避免沿用舊裝置授權。

所有裝置操作都要求密封工作階段與工作區根授權。瀏覽器完整 CDP 的逐次確認/無人值守許可不受型別化 Cookie 範圍隔離，也不能繞過 Chrome 政策。擴充功能關閉或斷線後要重新握手。桌面自動更新套件對應 Windows x64、Linux glibc x64/arm64、macOS arm64；其他原生平台使用安裝套件/套件管理員，Android 使用 APK OTA，瀏覽器使用擴充功能套件。能力通告不代表硬體、權限或發布套件一定可用。

## 自動螢幕保護

本機 Agent 或瀏覽器擴充功能的設定支援閒置（預設 300 秒）、控制開始/結束和立即啟動，內容可選文字、HTTPS 圖片/影片或隔離的靜態 HTML。影片靜音循環，文字和圖片可大範圍移動。桌面和 Android 可設定退出密碼；瀏覽器全螢幕視窗不設密碼，按鍵、移動滑鼠或關閉視窗即可退出。僅覆蓋活動顯示器，不取代系統鎖定。Linux 需要 X11 與 xprintidle；無桌面 OpenWrt、Wayland 和 BSD 目前不支援螢幕展示。

純色模式僅顯示所選底色，不顯示文字或媒體。底色使用 #RRGGBB，亮度為 0–100%（0% 全黑）。Android 同時降低視窗背光，觸控或按鍵時恢復系統亮度；其他平台僅調暗畫面，LCD 節能取決於背光。退出密碼提示保持可讀。

從 0.1.34 起，啟用移動時僅文字和圖片在全螢幕黑底區域內大範圍移動；大型內容按需縮小至可用區域寬、高的 44% 以內，使相對角落的位置不重疊。HTML、影片與純色保持靜止，系統「減少動態效果」設定會暫停移動。所有模式從首幀隱藏滑鼠，退出密碼框顯示時恢復，取消後再次隱藏；Windows 每 100 毫秒更新螢幕保護視窗內的游標狀態，不修改系統游標。首批提供 Windows 與 Browser Agent 0.1.34 套件，其他平台請以下載清單的實際版本為準。
