# Linux、FreeBSD、NetBSD、Windows、macOS 与 Android 设备

> HTML: https://remote.ivjn.us/docs/zh-CN/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-CN/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-CN/pairing-and-enrollment.md)。

### 首次连接与主页

1. 在需要被管理的 Android 设备上打开 **Remote Agent**。首次连接优先选择 **使用六位验证码连接** 或 **扫描连接二维码**，由管理员授权；账号、二维码和六位码的完整流程见 [初始化与配对](/docs/zh-CN/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 包，其他平台以下载清单中的实际版本为准。
