# 原站视图: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/`,只加载自带 `fonts/` (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` 时默认桌面模式,否则截图模式,可手动切换。 - `