Files
UHD_Media_Tool/docs/design/webtop-remote-browser.md
T

320 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 原站视图:Webtop 4.0(Wayland)远程桌面 + 同一 Camoufox 会话
对应 REQ v0.7 §4.4、§4.7.1–4.7.4(S18–S20);现状见 `docs/design/browser-provider.md`、`docs/runbook/browser.md`。
状态:**方案 / 待原型验证**(2026-09-29 调研,未起容器)。文中"已核实"指读过源码或文档;"待验证"必须在 P0 原型里实测,不能当结论。
## 0. 结论先行
- **能做到同会话**。camofox-browser 1.17.0 原生支持有头运行:`CAMOFOX_INTERACTIVE=desktop` 时不再自起 Xvfb,
以 `headless:false` 启动 Camoufox,并把进程环境(`DISPLAY`/`WAYLAND_DISPLAY`/`MOZ_ENABLE_WAYLAND`)原样传给 Firefox
(`server.js` L1138–1180,camoufox-js `launchOptions` 缺省 `env = process.env`)。所以 **把 camofox-browser 装进 Selkies
Wayland 桌面容器里跑**,桌面画面上的窗口就是 Agent 经 camofox API 驱动的那个 Firefox 进程,不是另开的浏览器。
- 镜像:**以 `ghcr.io/linuxserver/baseimage-selkies:debiantrixie`(Webtop 4.0 的底座)叠加 camofox-browser**,
而不是 `webtop:latest`(Alpine/musl)或 `webtop:debian-xfce`(不支持 Wayland)。浏览器桌面单独一个容器,app 不动(§5.1)。
- 中日文输入**以用户本机输入法为主**:Selkies 客户端接管 composition 事件,把上屏文本按 Unicode keysym 注入
(已核实源码);容器内 fcitx5 只作备选。中日文显示由 Camoufox 自带字体包(含 Noto Sans SC/JP/KR)负责。
- 输入权限用 Selkies 的**双口令**做服务端强制:持有人工租约的人拿控制口令,其他人拿只读口令,由我们的反代注入。
- 硬前提:**产品必须经 HTTPS(或 localhost)访问**,否则 WebCodecs 与剪贴板 API 都不可用 → 自动回退截图模式。
- 视觉闭环(§7)可行:pi-ai 0.87.1 的工具结果支持图片块;Agent 截图继续走 camofox API(视口坐标),不抓桌面。
## 1. 调研:Webtop 4.0 / Selkies 架构(来源见 §10)
| 项 | 事实 |
| --- | --- |
| 合成器 | `PIXELFLUX_WAYLAND=true` 时 pixelflux(基于 Smithay)作采集合成器(`wayland-1`),其内嵌套 **labwc** 作应用合成器(`wayland-0`,带 Xwayland);`false` 走 X11(Xvfb+openbox)[W3][W7] |
| 渲染 | 有 GPU 时 EGL/DMABUF 零拷贝;无 GPU 用 Pixman + Mesa llvmpipe[W1] |
| 传输 | 缺省 **WebSocket**(`SELKIES_MODE=websockets`);WebRTC 为可选,才需要 STUN/TURN[W3] |
| 编码 | `SELKIES_ENCODER` 缺省 `h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg`;`SELKIES_USE_CPU` 强制软编;VAAPI/NVENC 需 `/dev/dri` 或 NVIDIA 运行时[W3] |
| 性能 | 官方演示:N97 迷你机 1080p/60fps 多应用[W1](有核显);**纯 CPU 下 1080p 帧率未见官方数据,需 P0 实测** |
| 端口 | 3000 HTTP(只给反代)、3001 HTTPS 自签;Selkies 进程在容器内 `localhost:8082`,nginx 反代 `SUBFOLDER` 与 `SUBFOLDERapi`[W2][W7] |
| 变体 | webtop `latest`=Alpine XFCE;Wayland 支持的 Debian 变体只有 `debian-i3`(sway);`debian-xfce/kde/mate` 不支持 Wayland[W2] |
| 底座 | baseimage-selkies `master` 分支 = tag `debiantrixie`,Selkies 2.0.0、pixelflux 2.1.0,**已装 `fonts-noto-cjk`、labwc、xwayland**;`HOME=/config`、运行用户 `abc`(PUID/PGID)[W7] |
| 常用变量 | `PUID/PGID/TZ`、`TITLE`、`SUBFOLDER`(前后都带 `/`)、`NO_DECOR`、`NO_FULL`、`DISABLE_IPV6`、`LC_ALL`(写 LANG/LANGUAGE)、`START_DOCKER`(底座缺省 `true`,须设 `false`)、`DRINODE`/`DRI_NODE`、`MAX_RES`[W2][W3] |
| 鉴权 | 底座 `SELKIES_ENABLE_BASIC_AUTH=false`;设 `PASSWORD` 会让 **nginx** 开 basic auth(只认一个口令);Selkies 自身 basic auth 支持第二口令 `SELKIES_BASIC_AUTH_VIEWONLY_PASSWORD`,用它登录的连接被**服务端**限为 viewer(无键鼠/剪贴板/命令)[W3][W8 `stream_server.py`/`settings.py`] |
| 反代 | 转发 Upgrade/Connection,读写超时 ≥3600s,`proxy_buffering off`;可由反代注入上游 `Authorization`[W4] |
| 加固 | `HARDEN_DESKTOP`(禁 sudo/终端/xdg-open、关文件传输/命令/打印)、`HARDEN_OPENBOX`(labwc 下同样生效:去关闭按钮、禁右/中键菜单、禁 Alt-F4 等快捷键)[W5][W7 `init-selkies-config`] |
| 自启动 | Wayland 模式把 `/defaults/autostart_wayland` 复制为 `~/.config/labwc/autostart`;s6 服务在 `/etc/s6-overlay/s6-rc.d/`;另支持 `/custom-cont-init.d`[W7] |
| iframe | 容器 nginx 未设 `X-Frame-Options`/CSP(已核实 `default.conf`),可被同源 iframe 嵌入 |
**剪贴板**(读 `clipboard-sync.js`):文本双向;Chromium 在授予 `clipboard-read` 且页面聚焦时可无手势自动同步
(`SELKIES_CLIPBOARD_SEAMLESS`);Firefox/Safari 只能在用户手势内同步(Ctrl+V 的 paste 事件送入,复制出来需一次点击),
且 **`navigator.clipboard` 只在安全上下文存在**。服务端在 Wayland 下走 data-control 原生剪贴板,不 fork wl-copy。
iframe 里还需 `allow="clipboard-read; clipboard-write"`(Chromium 权限策略)。
**IME**(读 `input.js`、`input_handler.py`、`tests/e2e/test_ime_composition.py`):浏览器端监听
`compositionstart/update/end`,预编辑按码点差分发送,提交文本按 keysym 注入;服务端对布局缺失的 keysym
(Unicode 平面 `0x01000000|cp`)临时改写合成器 keymap 绑定到空闲键码,嵌套合成器下改走 `zwp_virtual_keyboard` 文本批次。
源码注释明确:**键码 >255 的绑定只有原生 Wayland 客户端看得见,Xwayland 客户端收不到** → Camoufox 必须以原生 Wayland 运行
(`MOZ_ENABLE_WAYLAND=1`)。Camoufox 152 的 libxul 含 Wayland 后端(`libmozwayland.so`、`xdg_toplevel` 等符号,已核实)。
**容器内输入法(备选)**:labwc 新版本实现 input-method-v2/text-input-v3,Firefox Wayland 经 GTK text-input-v3 接 IME,
理论上 fcitx5(`fcitx5-chinese-addons`/`fcitx5-rime` + `fcitx5-mozc`)可用;需 `XMODIFIERS=@im=fcitx`,Wayland 下
**不要**设 `GTK_IM_MODULE=fcitx`(会绕过 text-input-v3)。与本机 IME 同时开会双重组字,缺省关闭(Ctrl+Space 切换)。待验证。
**字体**:camoufox-js 在 Linux 上给 Firefox 设 `FONTCONFIG_PATH` 指向自带 `fontconfig/<os>`,只加载自带 `fonts/<os>`
(Linux 包含 `NotoSansSC/JP/KR-Regular.otf`,Windows 包含 `simsun.ttc`/`msgothic.ttc`)——网页与浏览器 UI 的中日文由它负责,
系统 `fonts-noto-cjk` 只影响 labwc 标题栏、fcitx5 候选框等。这是反指纹设计,**不要**往 Camoufox 里硬塞系统字体。
## 2. 调研:camofox 有头运行与同会话
- 启动路径(已核实):Linux 且 `interactiveMode !== 'desktop'` 时每次启动浏览器新建 Xvfb(1280×720,VNC 插件改 1920×1080);
`desktop` 模式跳过 Xvfb,`headless = !useDesktopWindow = false`。
- 自带 VNC 插件(`ENABLE_VNC`,x11vnc+noVNC)能看同一个 Xvfb,但无中文 IME 注入、无剪贴板/HTTPS/鉴权整合、画质差,
只适合作对照,不作为方案。
- 同一 Firefox 被 Playwright(Juggler)驱动 + 人在桌面点击:Juggler 通过协议合成事件,不抓 OS 输入,也不锁输入,二者共存;
Playwright 还对每个页面做"焦点模拟",人工切窗口不影响 Agent。**待验证**:人工输入期间 Agent 的截图/快照不受干扰。
- 窗口对应:camofox 一个 `userId` = 一个 BrowserContext(我们的站点/账号 profile),`sessionKey` = 标签组。有头 Firefox 下
Playwright 新页面多半各开窗口(**待验证**);labwc 规则会把所有窗口最大化。接管时需把目标页置前:插件调
`page.bringToFront()`;Wayland 禁止客户端抢焦点时,退路是 `wlrctl toplevel focus`(labwc 支持 foreign-toplevel)。
- 生命周期:`BROWSER_IDLE_TIMEOUT_MS` 到期浏览器整体退出(窗口消失),`SESSION_TIMEOUT_MS` 到期关闭 context(窗口消失)。
人工操作期间 camofox 看不到 API 请求 → **接管期间服务端必须心跳**,否则人手里的窗口会被回收。
- 风险点:用户点窗口关闭按钮 = 关掉 Playwright 页面(→ 我们对账为 lost);用户在地址栏输入任意网址 = 越出站点白名单。
前者用 `HARDEN_OPENBOX` 去掉关闭按钮;后者见 §6。
## 3. 与现有实现的对接
| 现有 | 桌面方案 |
| --- | --- |
| 原站视图 = 1.5s 截图轮询 + 坐标点击(`site-origin-view.tsx`,`coords.ts`) | iframe 嵌 Selkies 客户端;截图模式保留为回退 |
| 接管 = `control/acquire` → 适配器拒自动化输入 → 写租约 | 不变;另外把本人的桌面连接升级为 controller 口令 |
| 人工输入走 HTTP 端点(click/type/scroll…,需持租约) | 人工输入直接进桌面;HTTP 端点保留(截图模式用) |
| Agent:snapshot/navigate/interact/download 走 camofox API | 不变。截图也走 camofox API,不抓桌面(坐标系不同,见 §7.3) |
| 下载捕获:camofox `downloads` → 暂存核验 → 计划链路 | 不变(人工在桌面点的下载同样被 Playwright 捕获,**待验证**) |
| 交还后 `needsSnapshot=true`,旧 `eN` 作废 | 不变;另外校验当前 URL 在白名单内,不在则标记并回到站点首页 |
## 4. 资源与网络安全
- 资源(**全部待 P0 实测**,以下仅为估计口径):现 camofox 空闲约 200 MB、2 会话约 390 MB(runbook §7 实测);
桌面栈另加 labwc + pixelflux + Selkies(Python)+ nginx + pulseaudio 常驻,有客户端连接时再加编码 CPU。
建议缺省 `mem_limit: 3g`、`shm_size: 2gb`,分辨率锁 1280×900,`SELKIES_FRAMERATE=8-30|locked`,纯 CPU 优先 `jpeg`
或 `h264enc`(实测后定)。无人观看时是否停止编码要实测(决定"开着原站视图空跑"的成本)。
- 网络:3000/3001/8082 **不映射到宿主**,只在 compose 内网;唯一入口是 app 的 `/browser/desktop/*` 反代。
WebSocket 模式下不需要 TURN(局域网/公网都走同一个 HTTPS 连接);不启用 WebRTC。
- 剪贴板/文件边界:只开文本剪贴板(`SELKIES_ENABLE_BINARY_CLIPBOARD=false|locked`);文件传输、打印、音频、麦克风、
摄像头、手柄、分享链接、命令通道全部关闭并 `|locked`;`HARDEN_DESKTOP=true` 去掉终端与 sudo(底座缺省终端带免密 sudo)。
- `SELKIES_ALLOWED_ORIGINS` 设为产品对外 Origin;不启用 Pelorus / `PIXELFLUX_CU`(无鉴权的 Computer Use 端口)。
## 5. 方案
### 5.1 合一方式:A(浏览器桌面独立容器)还是 B(塞进 app 镜像)
| 维度 | A:Selkies 桌面 + 有头 camofox 一个容器,app 独立 | B:Deno app + 桌面 + 浏览器一个镜像 |
| --- | --- | --- |
| 同会话 | 成立:GUI 与 Firefox 同容器同 Wayland 会话 | 同样成立,无额外收益 |
| 资源 | 桌面栈只在浏览器容器;可 `CAMOFOX_REPLICAS=0` 整体关掉 | app 常驻即背上 GUI 栈;不用浏览器也关不掉 |
| 生命周期 | 浏览器 OOM/崩溃/重启只影响浏览器;API、worker 继续(app 已按 502/410 降级) | 容器 OOM 会把 API 与 worker 一起杀掉;s6 做 PID1 与 app 的信号/落盘时序互相牵制 |
| 安全面 | Firefox 渲染不可信 PT 页面,逃逸后只拿到浏览器容器(无 DB 密钥、无媒体库挂载) | 逃逸即拿到 `SECRET_MASTER_KEY`、数据库口令与媒体库写权限,**直接违反 REQ §4.4"浏览器没有媒体库写权限"与 §4.7.2"浏览器后端独立运行"** |
| 镜像/构建 | 浏览器镜像约 3.6 GB 级别单独构建、少变;app 镜像保持 trixie-slim + Deno 小体积 | app 镜像膨胀到数 GB,每次业务发版都重推 |
| 升级独立 | Selkies/Camoufox/app 各自升级、各自回滚 | 任一升级都要整体回归 |
结论:**支持协调者的 A**。另有 A′(桌面容器与 camofox 容器分开,共享 Wayland socket 卷)技术上可行
(Unix socket 可传 fd),但要对齐 UID、`XDG_RUNTIME_DIR`、重启顺序,收益只有"桌面可单独关",不值得。
### 5.2 镜像:底座选 baseimage-selkies,而不是反过来
- 不用 `webtop:latest`:Alpine/musl,Camoufox 发行包是 glibc 构建;不用 `webtop:debian-xfce`:不支持 Wayland;
`webtop:debian-i3` 可用但带 sway/i3/Chromium,我们只需要"单应用 kiosk"。Webtop 4.0 本身就是这个底座 + 桌面环境。
- 不反过来(camofox 镜像上装 Selkies):pixelflux/pcmflux/labwc 层、s6 服务、nginx 模板都由 linuxserver 维护,自行拼装维护成本高。
- 草案 `docker/browser-desktop/Dockerfile`(沿用现有 `docker/camofox/Dockerfile` 的构建阶段与两处修复):
```dockerfile
FROM node:22-trixie-slim AS camofox-build # 与现有 Dockerfile 相同:拉 Camoufox 152.0.4-beta.28、npm ci、插件依赖
# …(略:照抄 docker/camofox/Dockerfile 的 camofox-browser 阶段,产物 /app 与 /root/.cache/camoufox)
FROM ghcr.io/linuxserver/baseimage-selkies:debiantrixie@sha256:<锁定>
RUN apt-get update && apt-get install -y --no-install-recommends \
libdbus-glib-1-2 libxt6 libasound2 wlrctl \
fcitx5 fcitx5-chinese-addons fcitx5-rime fcitx5-mozc fcitx5-frontend-gtk3 \
&& rm -rf /var/lib/apt/lists/* # fcitx5 一行在 P0 验证不通过时删除
COPY --from=camofox-build /usr/local/bin/node /usr/local/bin/node
COPY --from=camofox-build /app /opt/camofox
COPY --from=camofox-build /root/.cache/camoufox /opt/camoufox
COPY root/ / # s6 服务 svc-camofox、labwc 规则、policies.json、umt 插件
ENV CAMOUFOX_EXECUTABLE_PATH=/opt/camoufox/camoufox-bin CAMOUFOX_INSTALL_DIR=/opt/camoufox
```
### 5.3 compose(替换现有 `camofox` 服务,保留服务名,`BROWSER_PROVIDER_URL` 不变)
```yaml
camofox:
build: { context: docker/browser-desktop, additional_contexts: { upstream: https://github.com/jo-inc/camofox-browser.git#v1.17.0 } }
image: ${CAMOFOX_IMAGE:-umt-browser-desktop:1.17.0-s2.0.0}
deploy: { replicas: ${CAMOFOX_REPLICAS:-1} }
environment:
PUID: "1000"
PGID: "1000"
TZ: ${TZ:-Asia/Shanghai}
LC_ALL: zh_CN.UTF-8
PIXELFLUX_WAYLAND: "true"
START_DOCKER: "false"
SUBFOLDER: /browser/desktop/
TITLE: 原站视图
HARDEN_DESKTOP: "true"
HARDEN_OPENBOX: "true"
NO_DECOR: "true"
SELKIES_ENABLE_BASIC_AUTH: "true" # 不设 PASSWORD:nginx 层只认单口令,会拦掉只读口令
SELKIES_BASIC_AUTH_PASSWORD: ${BROWSER_DESKTOP_CTL_KEY:?}
SELKIES_BASIC_AUTH_VIEWONLY_PASSWORD: ${BROWSER_DESKTOP_VIEW_KEY:?}
SELKIES_ALLOWED_ORIGINS: ${UMT_PUBLIC_ORIGIN:?}
SELKIES_MANUAL_RESOLUTION: "true|locked"
SELKIES_MANUAL_WIDTH: "1280"
SELKIES_MANUAL_HEIGHT: "900"
SELKIES_SCALING_DPI: "96|locked"
SELKIES_FRAMERATE: "8-30|locked"
SELKIES_ENABLE_BINARY_CLIPBOARD: "false|locked"
SELKIES_FILE_TRANSFERS: "none"
# 音频/麦克风/摄像头/手柄/打印/分享/命令/侧栏应用与文件:SELKIES_*_ENABLED 或 SELKIES_UI_* 一律 "false|locked"(§4)
CAMOFOX_INTERACTIVE: desktop
CAMOFOX_PROFILE_DIR: /config/.camofox/profiles
# 其余 CAMOFOX_* / SESSION_TIMEOUT_MS / BROWSER_IDLE_TIMEOUT_MS / MAX_SESSIONS 沿用现有 camofox 服务
volumes: [ "umt-browser-desktop:/config" ] # 旧卷 umt-camofox 的 profiles 一次性迁移并 chown PUID
shm_size: 2gb
mem_limit: ${CAMOFOX_MEM_LIMIT:-3g}
stop_grace_period: 30s # s6 逐级 SIGTERM,camofox 落盘需要时间
```
截图-only 部署保留旧 `docker/camofox/Dockerfile` 与镜像(`CAMOFOX_IMAGE=camofox-browser:1.17.0`),app 按能力探测自动选模式。
### 5.4 启动脚本
- `svc-camofox`(s6 longrun,依赖 `svc-de`):等 `${XDG_RUNTIME_DIR}/wayland-0` 出现后以 `abc` 身份
`exec node --max-old-space-size=… /opt/camofox/server.js`(保持 exec,SIGTERM 直达 node),环境:
`WAYLAND_DISPLAY=wayland-0`、`MOZ_ENABLE_WAYLAND=1`、`XMODIFIERS=@im=fcitx`、`CAMOFOX_INTERACTIVE=desktop`。
设 `S6_KILL_GRACETIME`/服务 `timeout-finish` ≥15s,保证持久化插件关机落盘(runbook §7 第 7 条的教训)。
- labwc:`~/.config/labwc/autostart` 只放 `fcitx5 -d --replace`(备选 IME);`rc.xml` 增加 `camoufox` 的
`serverDecoration="no"` + Maximize 规则;不自启任何其它应用,桌面为空时前端显示"浏览器空闲,打开会话后出现"。
- `/opt/camoufox/distribution/policies.json`:禁 DevTools、`about:config`、`file://`、扩展安装、密码保存提示。
### 5.5 camofox 插件 `plugins/umt`(随镜像,只加路由不改上游代码)
`POST /umt/tabs/:tabId/focus`(bringToFront,失败回退 wlrctl 按标题)、`POST /umt/sessions/:userId/touch`(接管期心跳,
刷新会话与浏览器空闲计时)、`POST /umt/tabs/:tabId/mouse`(坐标点击/按下/移动/抬起/滚轮,§7)、
`GET /umt/tabs/:tabId/screenshot?scale=css&type=jpeg&mask=…`(§7)、`POST /umt/sessions/:userId/route-guard`(context.route 域名白名单,§6)。
全部挂 camofox 的 `auth` 中间件(同一 `CAMOFOX_ACCESS_KEY`)。
### 5.6 server 反代与鉴权
- 路由 `/browser/desktop/*`(HTTP 静态资源)与 `/browser/desktop/api`(WebSocket):要求控制台 cookie 会话 + 浏览器接管权限;
静态资源 `fetch` 透传到 `http://camofox:3000/browser/desktop/…`,补 `Content-Security-Policy: frame-ancestors 'self'`。
- WebSocket:`Deno.upgradeWebSocket` 接客户端,再 `new WebSocket(upstream, { headers: { Authorization } })` 连容器,双向转发
(Deno 2.9.7 的客户端 WebSocket 支持自定义头,**已本机实测**)。`Authorization` 由服务端决定:当前用户持有该 provider 下某会话的
active 人工租约 → 控制口令;否则 → 只读口令。两口令只在进程内(同 `CAMOFOX_ACCESS_KEY` 的处理)。
- 租约变化(acquire/release/过期)时服务端主动关闭该用户的桌面 WS,Selkies 客户端自动重连并按新角色认证。
于是"自动化中"桌面只能看不能动(Selkies 服务端强制),"人工接管"才放开输入——与 §4.7.3"接管前停止自动化输入"对称。
- 一个桌面承载所有站点会话:持有 A 站租约的人在桌面上理论上能切到 B 站窗口。缓解:接管时置前目标窗口、labwc 禁掉窗口切换快捷键;
严格隔离需每会话一个桌面实例(资源成本高,列为后续选项)。
### 5.7 前端原站视图
- 能力新增 `liveDesktop {supported, verified}`;`supported && verified && window.isSecureContext` 时默认桌面模式,否则截图模式,可手动切换。
- `<iframe src="/browser/desktop/" allow="clipboard-read; clipboard-write; fullscreen; keyboard-map">`,顶栏沿用现有
控制者/站点/连接状态 + "接管 / 交还"按钮;接管时先调 `focus` 再重连 iframe。
- 提示条:未接管时"只读观看,Agent 正在操作";接管后"用本机输入法直接输入中文/日文;Firefox 下复制粘贴需按 Ctrl+C/Ctrl+V"。
非 HTTPS 访问时说明"桌面模式需要 HTTPS"。
## 6. 越界防护(人工接管时)
1. 插件对每个 profile 的 context 装 `context.route` 白名单(站点域名 + `browserDomains`),越界请求直接 abort——对 Agent 与人工同样生效。
2. `policies.json` 限制浏览器本身的危险入口(§5.4)。
3. 交还时服务端核对当前 URL,越界则记事件、导航回站点首页,Agent 在重新快照后继续。
## 7. 视觉自动化闭环(Agent 处理登录页 / 滑块 / Cloudflare 交互挑战)
### 7.1 已核实的前提
- **pi-ai 0.87.1**:`ToolResultMessage.content: (TextContent | ImageContent)[]`,`ImageContent = {type:"image", data(base64), mimeType}`。
各上游处理(读 `dist/api/*`):Anthropic 放进 `tool_result` 内容块;OpenAI Responses 转 `input_image`;OpenAI Chat Completions
另追加一条带 `image_url` 的 user 消息;Google Gemini 3+ 嵌在 `functionResponse.parts`,更早版本另起 user 轮;Bedrock 转 image 块;
Mistral 视模型而定。全部以 `model.input.includes("image")` 为门,不支持视觉的模型得到占位文本。
- **我们的 runtime 目前把能力输出 `JSON.stringify` 成单个 text 块**(`packages/agent/src/pi/runtime.ts` L193–196),要加图片通道。
pi-agent-core 有 `transformContext`,可在每轮请求前把较早的截图替换为占位文本,控制 token 与缓存前缀。
- **camofox 1.17.0 没有坐标鼠标端点**;适配器现在的坐标点击是 `elementFromPoint` → 选择器点击,对 canvas 滑块、
跨源 iframe(Turnstile 复选框)与拖拽无效。截图是 `page.screenshot({type:'png'})`,像素 = 视口 CSS 像素 × DPR,runbook 实测 DPR=1。
### 7.2 能力设计
| 能力 | 类别 | 说明 |
| --- | --- | --- |
| `browser.screenshot` | read | 调插件 `scale=css` JPEG(质量 70),用 Playwright `mask` 盖住 `input[type=password]` 与含 passkey 的元素;返回图片块 + `{screenshotId, width, height, url(脱敏), title, refsCount}`;`modelSafe=false` 的页面(URL 含凭据等)拒绝给模型 |
| `browser.click_at` | execute | `{screenshotId, x, y, button?, clickCount?}`,坐标以截图像素计;服务端核对该截图仍是当前页(URL/导航序号未变,否则 409"先重新截图"),按记录的缩放换算成视口坐标,调插件 `mouse`(真实 `page.mouse`,跨 iframe 有效) |
| `browser.type_at` | execute | `click_at` 后以 keyboard 模式输入;登录字段仍只能用 `credential` 由服务端注入,模型不得看到或写出密码 |
| `browser.drag` | execute | `{screenshotId, from, to, durationMs}`,插件分段 `mouse.move` 生成带抖动的轨迹(滑块) |
| `browser.solve_challenge` | execute(有界子流程) | 见 §7.4 |
分辨率锁 1280×900 后视口约 1280×800,低于 Anthropic 1568 长边与 OpenAI 缩放阈值,**上游不会再缩放,坐标可直接映射**;
若视口更大,服务端先缩到长边 ≤1280 并记录比例。
### 7.3 坐标系:Agent 永远用 camofox 截图,不用桌面画面
桌面(Selkies)坐标 = 屏幕像素,含 labwc 与 Firefox 工具栏偏移,还受 Selkies 缩放影响;camofox 截图坐标 = 页面视口 CSS 像素,
与 `page.mouse` 同一坐标系。故 Agent 截图与点击只走 camofox API,Selkies 画面只给人看。锁 `SELKIES_SCALING_DPI=96` 保证 DPR=1,
截图用 `scale=css` 兜底,P0 验证"截图点 (x,y) = 页面 `elementFromPoint(x,y)`"。
### 7.4 `solve_challenge` 边界
- 触发:快照判定 `captcha/challenge` 且站点策略 `autoSolve=true` 且当前模型支持图片;否则照旧转 `waiting` + 待处理 + 接管入口。
- 允许自动的:图形字符验证码、滑块/拼图、Cloudflare Turnstile/托管挑战的复选框、普通登录表单(凭证服务端注入)。
- **永不自动**:二次认证/TOTP/短信/邮件验证码、本人设备确认、账户安全问题;密码错误一次即停(避免锁号)。
- 预算:每次挑战 ≤8 步、≤2 次整体重试、≤90 秒;每站每日自动解题次数与 token 上限可配;超限即转人工。
- 证据:每步截图(已遮蔽)+ 模型给出的动作与理由写入该会话的待处理事项/运行记录,供事后复核;截图不入模型以外的日志。
- 失败/不确定 → `waiting`(`challenge_unsolved`),待处理事项带"在原站视图接管"链接;不在未知状态下继续点击。
- 桌面方案下,人可在 iframe 里实时看到 Agent 解题过程,需要时直接接管——这是截图模式给不了的。
## 8. 验证清单(P0 原型 + 上线前)
| # | 项 | 通过标准 |
| --- | --- | --- |
| 1 | 同会话 | Agent `navigate` 打开详情页 → 桌面上可见同一页面;人工点击后 Agent `snapshot` 读到变化;tabId 不变 |
| 2 | 反向同会话 | 人工在桌面登录 → 交还 → Agent 直接访问受保护页不需再登录 |
| 3 | 中文显示 | 中/日文站点、标签页标题、地址栏、表单占位符无方块字 |
| 4 | 中文输入 | Win 微软拼音 / macOS 拼音 / 搜狗:搜索框输入"流浪地球 2",含标点与候选翻页;Chrome 与 Firefox 客户端各一次 |
| 5 | 日文输入 | macOS/Win 日文 IME:输入并变换"進撃の巨人",含片假名、全角空格 |
| 6 | 剪贴板 | 本机 → 远端(Ctrl+V 粘贴中文长文本);远端 → 本机(复制页面中文);HTTP 访问时明确提示不可用 |
| 7 | 输入权限 | 未接管时桌面键鼠无效(服务端拒绝);接管后生效;交还后立刻失效 |
| 8 | 重启保持 | `docker compose restart camofox` 后登录仍在;SIGKILL 情形与现状一致(已知限制) |
| 9 | 坐标一致 | 截图坐标点击命中预期元素;Turnstile 类跨源 iframe 与 canvas 滑块可点/可拖 |
| 10 | 越界 | 地址栏输入站外网址被拦;关闭按钮不存在;无终端、无 sudo |
| 11 | 资源 | 记录空闲/1/2/4 会话内存,观看时 1280×900 的 CPU 与帧率,无人观看时 CPU |
| 12 | 下载 | 人工在桌面点 .torrent 被 camofox 捕获、进暂存,不重复提交 |
## 9. 风险与回退
| 风险 | 影响 | 回退 |
| --- | --- | --- |
| Camoufox/Playwright 在原生 Wayland 下异常(窗口、截图、WebGL) | 同会话不成立 | 改 Xwayland(`MOZ_ENABLE_WAYLAND=0`)+ 容器内 fcitx5(XIM)输入;仍不行则保留截图模式 |
| 本机 IME 注入丢字/重复 | 中日文输入不可用 | 启用容器内 fcitx5;或前端"输入文本"框经 camofox `type` 注入(现有能力) |
| 纯 CPU 编码占用过高 | 宿主负载 | 降帧率/分辨率、改 jpeg;挂 `/dev/dri` 用 VAAPI |
| Wayland 下置前窗口失败 | 人看到别的站点窗口 | wlrctl;不行则同一时刻只保留被接管会话的窗口可见 |
| 部署只有 HTTP | 桌面模式不可用 | 自动截图模式,提示配置 HTTPS |
| Selkies/底座升级破坏行为 | 回归 | 镜像按 digest 锁定;核验表按"camofox 版本 + Selkies 版本"登记 |
截图模式代码与能力保持不删;桌面能力 `liveDesktop.verified` 只由真机核验置 true(同 runbook §6 规则)。
## 10. 实施拆分与估算(人日,不含等待构建)
| 阶段 | 内容 | 估算 |
| --- | --- | --- |
| P0 原型 | 临时构建镜像,验证 §8 的 1、3、4、5、6、9、11;不通过则按 §9 调整或止损 | 2 |
| P1 镜像与 compose | Dockerfile、s6 服务、labwc 规则、policies.json、卷迁移、runbook | 1.5 |
| P2 camofox 插件 | focus / touch / mouse / screenshot(mask, css) / route-guard + 测试 | 1.5 |
| P3 server | `/browser/desktop` 反代、WS 转发与角色切换、心跳、能力 `liveDesktop` | 2 |
| P4 前端 | iframe 模式、接管联动、提示、模式切换 | 1.5 |
| P5 真机核验 | §8 全部 + 四站登录;更新核验表与 runbook | 1.5 |
| V1 视觉通道 | runtime 图片块 + `transformContext` 截图裁剪 + 能力 `browser.screenshot` | 1.5 |
| V2 坐标动作 | `click_at` / `type_at` / `drag` + 截图有效期校验 | 1.5 |
| V3 解题子流程 | `solve_challenge` 策略、预算、证据、转人工;mock 滑块/复选框页测试 | 2.5 |
合计约 15.5 人日。P0 不通过不进入 P1;V 系列不依赖桌面,可与 P1–P4 并行(截图与鼠标插件在 P2 共用)。
## 来源
- [W1] linuxserver 博客《Webtop 4.0 – Wayland is here》:https://www.linuxserver.io/blog/webtop-4-0-wayland-is-here-engage-the-reality-engine
- [W2] linuxserver/docker-webtop README 与 `readme-vars.yml`(commit a34075b,分支 `debian-i3` Dockerfile):https://github.com/linuxserver/docker-webtop
- [W3] Selkies 配置参考:https://docs.linuxserver.io/selkies/user-guide/configuration/
- [W4] 反向代理:https://docs.linuxserver.io/selkies/user-guide/reverse-proxy/
- [W5] 安全与加固:https://docs.linuxserver.io/selkies/user-guide/security/ ;Web 客户端:https://docs.linuxserver.io/selkies/user-guide/web-client/
- [W7] linuxserver/docker-baseimage-selkies(commit 094bd40:`Dockerfile`、`root/defaults/*`、`init-selkies-config`、`init-nginx`、`svc-de`):https://github.com/linuxserver/docker-baseimage-selkies
- [W8] selkies-project/selkies(commit 3291323:`addons/selkies-web-core/lib/input.js`、`lib/clipboard-sync.js`、`src/selkies/input_handler.py`、`stream_server.py`、`settings.py`):https://github.com/selkies-project/selkies
- [W9] 本机 `camofox-browser:1.17.0` 镜像内 `server.js`、`lib/config.js`、`plugins/vnc`、`node_modules/camoufox-js/dist/utils.js`;
`node_modules/.deno/@[email protected]` 的 `dist/types.d.ts` 与 `dist/api/*`