# 网页控制台与 Web Push

> HTML: https://remote.ivjn.us/docs/zh-CN/web-console/

浏览器控制台位于 `https://remote.ivjn.us/console/`。它支持通过邮件验证注册、账号/密码或操作员令牌登录、工作区范围内的设备、流式命令、xterm PTY、Android 截图/点击/导航/文字输入、指标、审计记录、设备创建/删除，以及主动订阅的通知。只读账号不能执行命令或查看 Android 屏幕。离线设备仍会显示。

设备通告 `ubus` 能力时显示 **ubus** 页签，可填写对象、方法和可选的 JSON 对象参数（留空发送 `null`）。调用使用密封会话，要求设备在线、已启用且具备操作权限。取消只停止等待，无法撤销设备已经执行的方法；切换设备或失去权限会清理待处理调用和结果。

注册会创建独立的工作区。登录前必须使用八位验证码验证邮箱；控制台支持重新发送验证码，并在刷新后返回验证流程。Exchange 必须配置 SMTP（`SMTP_HOST`、`SMTP_PORT`、`SMTP_FROM`，以及 `.env.example` 中的认证和安全设置）。邮件服务不可用时，注册会报告错误，不会跳过验证。

凭据保存在页面内存中。刷新后需要重新登录。退出登录会关闭会话，尝试移除此浏览器的推送订阅，并撤销通过密码登录建立的会话。主操作员令牌仍保持原有的有效期。点击通知会聚焦已打开的控制台，不会重新加载其凭据。

## 构建与部署

```sh
cd website/console
npm ci
npm test
npm run build
```

将 `dist/` 复制到网站的 `console/` 目录。确保现有的 `website/assets/` 可通过 `/assets/` 访问，以提供共享字体和标志。提供 `index.html`、`sw.js` 和清单文件时，应要求重新验证或禁用缓存。带哈希的资源可使用不可变缓存。不要缓存 `/api/*` 或 `/ws/*`。

控制台使用同源的 `/api/*` 和 `/ws/browser`。将这些路径反向代理到 Exchange，保留 Host 并允许 WebSocket 升级。不要移除 `/api` 或 `/ws` 前缀。Vite 开发服务器将 API/WS 代理到 `127.0.0.1:18082`。Rust Exchange 默认允许同源浏览器访问。通过 `REMOTE_CORS_ORIGINS` 配置精确来源；只有显式 `*` 才允许所有来源。Worker 使用 `CORS_ORIGINS` 及其自身默认值。Bearer 鉴权和一次性 WebSocket 票据仍然必需，不启用跨域 Cookie。

浏览器认证先使用带 Bearer 授权的 `POST /api/ws-ticket`，随后请求 `GET /ws/browser`，并发送首个 JSON 帧 `{"ticket":"..."}`。票据在 30 秒后过期，只能使用一次，使用时会重新检查操作员的启用状态和凭据有效性。长期有效的凭据不会放入 WebSocket URL。原生客户端继续使用 `/ws/operator`。

所有设备操作都使用原生密封 WebSocket 会话。HTTP 提供身份验证、设备元数据、密钥与授权目录，以及中转会话管理；请参阅 [Agent 操作的 HTTP API](/docs/zh-CN/http-api.md)。

## 浏览器设备面板

选择浏览器设备后会显示标签页、页面和 Cookie 工具。扩展为 0.1.45 或更高版本时，面板增加「定位与高级操作」：按定位方式点击、双击、悬停、填写、输入、勾选和选择下拉（最多等待 5 秒，匹配到多个元素又没填序号时报 `ambiguous`），带 `wait_until` 的导航，读取页面文字或 HTML，处理待确认的 JavaScript 对话框，请远程浏览器旁的人接手，预置视口尺寸，下载列表（可把已完成的下载保存到本机），标签组，书签和历史搜索（读历史前会先征求远程浏览器旁的人同意），JSON 批量命令，以及实时画面。实时画面会自动确认帧，控制台标签页转到后台时隐藏画面。旧版扩展只显示基础面板。

## 启用 Web Push

在 Exchange 环境中设置以下两个变量（两个都缺失时禁用推送）：

```text
WEB_PUSH_VAPID_PRIVATE_KEY=<base64url-unpadded-32-byte-P256-private-key>
WEB_PUSH_VAPID_SUBJECT=https://remote.ivjn.us
```

使用 `python scripts/generate-vapid-key.py` 生成私钥。将其保存在 Git 之外、访问受限的环境文件中，重启时保持不变，并安全备份。公钥会自动推导。更换密钥后，浏览器需要重新订阅。

`WEB_PUSH_ALLOWED_ORIGINS` 默认仅包含以下准确的可信源地址：`https://fcm.googleapis.com,https://updates.push.services.mozilla.com,https://web.push.apple.com`。只有验证了其他浏览器厂商的 HTTPS 推送源地址后，才能添加该厂商。客户端提供的端点不能扩展此允许列表。系统不会跟随重定向。

需要认证的路由：

| 方法与路径 | 用途 |
| --- | --- |
| GET `/api/webpush/config` | 启用状态及公开的应用服务器密钥 |
| GET/POST `/api/webpush/subscriptions` | 列出自己的订阅元数据，或订阅/更新插入 |
| DELETE `/api/webpush/subscriptions/{id}` | 移除自己的订阅 |
| POST `/api/webpush/test` | 向调用者的订阅队列加入一条可见的测试通知 |
| POST `/api/webpush/messages` | 发送给自己，或由管理员发送给同一工作区的其他账号 |
| POST `/api/webpush/devices/{id}/messages` | 设备向有管理权限且已订阅的管理员发送消息 |

订阅使用浏览器的 `PushSubscription.toJSON()`、`application_server_key` 和 `triggers`。`triggers` 最多包含一个 `online` 对象和一个 `offline` 对象。每个对象包含 `kind`、`tick_count`（默认 3，范围 1–100），以及可选的 `device_ids`。`offline` 在连续 `tick_count` 次未收到响应后通知；`online` 在连续 `tick_count` 次成功后通知。`device_ids` 可以是单个设备 ID（字符串）、设备 ID 列表，或省略/设为 `null` 以覆盖工作区中的所有设备。空的 `triggers` 数组会订阅浏览器，但不接收自动上下线通知。不接受 `missed_count` 和 `success_count`。消息使用可选的 `operator_id` 和 `notification`，后者包含 `title`、`body`，以及可选的同源相对 `url` 和 `tag`。`/webpush/` 是最小参考客户端；生产控制台使用自己的 worker，作用域为 `/console/`。

迁移 `0005_web_push.sql` 添加订阅和持久化发件箱。`0008_online_offline_triggers.sql` 将设备通知迁移到 `triggers`，并移除 `missed_count` / `success_count`。`202` 表示已入队，不代表已送达或显示。任务在 15 分钟后过期。网络错误、408、429 和 5xx 会重试，总计最多尝试五次；404/410 会移除对应版本的订阅。90 秒租约允许 worker 故障后恢复待处理工作。投递保证至少一次，浏览器是否显示取决于厂商投递、权限和操作系统策略。持久化入队前的设备事件在过载时尽力处理；已入队的事件在重启后仍保留。限制：每账号 20 个订阅，每订阅 32 条排队消息，每账号每分钟 30 次变更操作。

iOS/iPadOS 可能要求先将控制台添加到主屏幕，才能启用通知。浏览器推送服务的可达性与 Exchange 的连接情况是两回事。

## 验证

运行 `cargo test --workspace`、`cargo clippy --workspace --all-targets -- -D warnings`，以及控制台的 npm 测试和构建。进行 PostgreSQL/HTTPS 集成测试时，先安装 `psycopg[binary]`，将 `WEB_PUSH_TEST_ADMIN_URL` 设置为具有 CREATE DATABASE 权限的测试 PostgreSQL 连接，再运行 `python scripts/test-webpush.py`。该脚本会创建并删除临时数据库；切勿指向具有非预期权限的数据库角色。

## 从各端发送

Web Console 的浏览器通知面板可填写消息；移动 Console 设置提供 Web Push 发送、测试和打开 Web Console 入口。桌面 Agent 本机设置、Android Agent 设置和浏览器扩展配置页均可使用设备凭证发送。无界面/OpenWrt Agent 使用以下命令。接收端在 Web Console/PWA 启用通知；此功能不添加原生 FCM/APNs 接收器。

```sh
remote-cli webpush status
remote-cli webpush subscriptions
remote-cli webpush test
remote-cli webpush send --title "Backup ready" --body "Completed"
remote-cli webpush send --operator-id ACCOUNT_UUID --title "Notice" --body "Hello"
remote-agent --config /path/to/config.json webpush --title "Device ready" --body "Completed"
```

```json
{"notification":{"title":"Backup ready","body":"Completed","url":"/console/","device_id":"edge-1"}}
```

POST `/api/webpush/messages` 使用操作员 Bearer 凭证。省略 `operator_id` 表示发送给自己；其他接收者需要管理员权限，并且必须属于同一工作区。上方 JSON 也可通过 `remote-cli webpush send --json-file message.json` 发送。将设备状态触发器设为空数组可关闭自动上下线提醒，同时保留通用消息接收。

POST `/api/webpush/devices/{id}/messages` 使用该设备的 Bearer 令牌，直接接收通知对象（`title`、`body`、可选 `tag`）。只有已启用且管理该设备的管理员的有效订阅会接收。服务端固定 `device_id`、`event=device.message`、`occurred_at` 和 `/console/`，发送者不能指定账号或冒充其他设备。设备发送共用每工作区每分钟 30 次的预算，单次最多 256 个订阅；账号订阅数和队列限制仍生效。没有有效接收者返回 `202 {"queued":0}`；`queued` 是订阅数，不是送达数。
