Files
UHD_Media_Tool/docs/runbook/browser.md
T
rootandClaude Opus 5.5 7dc7b1c5fa fix(agent): solve_challenge authorization covers its child's coordinate steps
With only a browser.solve_challenge grant, the vision child run was
denied browser.click_at, so the loop stopped after the first screenshot
(REAL-2 task 4). Steps inside a run spawned by solve_challenge are now
allowed; ancestors' allowlist and step budget still apply. Needs image
rebuild.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 03:06:05 +08:00

343 lines
32 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.
# 浏览器(camofox)与原站视图运行手册
对应 REQ v0.7 §4.4、§4.7.1–4.7.4;合同与 camofox OpenAPI 对照见 `docs/design/browser-provider.md`。
代码:`packages/adapters/src/browser/**`(适配器)、`apps/server/src/modules/browser/**`(会话/接管/能力)、
`apps/web/app/components/browser/**`(原站视图)。
> 当前状态:已对真实 camofox-browser **v1.17.0**(jo-inc,commit `389c996`)容器逐项核验(§7),十项能力全部通过;
> 探测到其它版本 / fork 时一律显示"未核验",需按 §6 重新核验。
## 1. 浏览器服务从哪来
### 1.1 部署自带(缺省)
`docker compose up -d` 同时起 `camofox` 服务(`docker-compose.yml`,只在内网、不映射端口,卷 `umt-camofox`)。
app 的 `BROWSER_PROVIDER_URL` 缺省 `http://camofox:9377`,启动后第一次用到浏览器时登记为 **`camofox-env`**
(`browser_provider.config.managedBy = "env"`,访问凭据取 `BROWSER_ACCESS_KEY` / `CAMOFOX_ACCESS_KEY`,只在进程内,不入库),
并在后台探测一次能力。地址与凭据只能改部署配置;设置页只能改它的启用。
```sh
# .env(部署目录)
CAMOFOX_ACCESS_KEY=$(openssl rand -hex 24) # camofox 与 app 共用
docker compose --env-file .env.deploy up -d --build
docker compose ps camofox # healthy 即可
```
上游不发布镜像。compose 缺省用的是**浏览器桌面镜像** `umt-browser-desktop:1.17.0`(`docker/browser-desktop`,§8),
它从纯截图镜像 `camofox-browser:1.17.0` 复制 camofox,所以**先构建旧镜像**:`docker/camofox/Dockerfile` 从 git tag `v1.17.0`
构建(修了上游 Dockerfile 的两个问题,见文件头)。首次构建要从 GitHub 拉源码与约 630 MB 的 Camoufox 浏览器包:
```sh
git clone --depth 1 --branch v1.17.0 https://github.com/jo-inc/camofox-browser.git /tmp/camofox-src
docker build -t camofox-browser:1.17.0 --build-arg APT_MIRROR=mirrors.ustc.edu.cn \
--build-context upstream=/tmp/camofox-src docker/camofox
# 走宿主代理时加 --network host --build-arg https_proxy=http://127.0.0.1:7890 --build-arg http_proxy=...
docker compose build camofox # 再构建桌面镜像(或 docker build -t umt-browser-desktop:1.17.0 docker/browser-desktop)
```
只要截图模式(不要桌面、省约 400 MB 常驻内存与 3 GB 磁盘):`.env` 里 `CAMOFOX_IMAGE=camofox-browser:1.17.0`,同一个卷,登录状态通用。
### 1.2 接自有实例
`.env` 里 `BROWSER_PROVIDER_URL=http://<host>:9377`、`BROWSER_ACCESS_KEY=<它的 CAMOFOX_ACCESS_KEY>`、`CAMOFOX_REPLICAS=0`
(不再起自带容器)。或者不设环境变量,在 **设置 → 站点默认 → 浏览器服务** 里"添加浏览器服务":名称、服务地址、访问凭据
(加密存凭证库,只写不回显)、启用。自有实例的版本不是 1.17.0 时能力一律显示"未核验"。
### 1.3 不用浏览器
`CAMOFOX_REPLICAS=0` + `BROWSER_ENABLED=false`:`camofox-env` 停用,原站视图显示"没有启用的浏览器服务"。
### 1.4 设置页与 API
设置 → 站点默认 → 浏览器服务:列表(地址、来源、凭据是否设置、启用、健康、核验)、测试连通(探测 `/health` 与
`/openapi.json`,凭据错误显示"访问凭据不对")、编辑、删除(有未关闭会话时拒绝;`camofox-env` 只能停用);
点行打开的抽屉里是逐项能力表(不可用 / 未核验 / 已核验 + 说明)。
| 端点 | 说明 |
| --- | --- |
| `GET /browser/providers[?probe=true]` | 列表;`probe=true` 探测全部启用的服务并写回 |
| `POST /browser/providers` | 登记 `{name, baseUrl, accessKey?, enabled?}`;`camofox-env` 名称保留 |
| `PATCH /browser/providers/{id}` | `{baseUrl?, accessKey?(null 清除), enabled?}` |
| `POST /browser/providers/{id}/probe` | 测试连通(停用的也可以测) |
| `DELETE /browser/providers/{id}` | 删除 |
`browser_provider.config` 可调项(都可省略):
| 键 | 缺省 | 作用 |
| --- | --- | --- |
| `verified` | `{}` | 逐项覆盖核验结果,如 `{"takeover": false}`;缺省取适配器内置的真机核验表(§7,按上游版本);探测永远不会把它置 true |
| `navigationTimeoutMs` | 30000 | 单次导航超时 |
| `maxRetries` | 2 | 导航只重试超时 / 网络 / 5xx / 503;401、白名单、控制权冲突不重试 |
| `snapshotMaxBytes` | 524288 | 快照文本上限(超出截断并标 truncated) |
| `requestTimeoutMs` | 30000 | 其它请求超时 |
站点域名白名单 = 站点 `base_url` 的主机(含子域)+ `site.config.browserDomains`(字符串数组,给跳转到独立下载域的站点用)。
站点限流与快照 / 实时搜索共用同一个限流器(导航、开标签页、历史导航取令牌)。
## 2. 会话与控制权
- 会话按站点 / 账号隔离:`profileKey = umt-site-<siteId>-acct-<accountId|anon>`,同时作为 camofox `userId`
(上游的登录状态隔离单元);`browser_session.id` 作 camofox `sessionKey`。同一 provider + profile 至多一个未关闭会话。
- 数据库为准:服务重启后按 `browser_session` / `browser_tab` 恢复适配器内存,`needsSnapshot` 置 true,旧元素引用全部作废。
上游回收的标签页(404)对账标 `lost`,返回 409"标签页已被浏览器服务回收"。
- 控制者:`automation`(Agent/任务)→ `human`(人工接管)→ `automation` 或 `waiting`;`closed` 终态。
| 事件 | 结果 |
| --- | --- |
| 接管 `POST /browser/sessions/{id}/control/acquire` | 先把适配器控制者切到 human(新的自动化输入立即被拒),等该 profile 上进行中的请求收尾,再写 `browser_control_lease`(缺省 30 分钟) |
| 他人已接管 | 409 |
| 交还 `.../control/release` | 租约 released;会话回 automation(或 `to: "waiting"`);`needsSnapshot = true`;待处理事项关闭 |
| 租约到期未交还 | 租约 expired,会话转 `waiting`(`lease_expired`),不自动从未知页面继续 |
| Agent 快照看到验证码 / 二次认证 / Cloudflare 挑战 | 会话转 `waiting`(`captcha` / `challenge`),生成待处理事项(kind `other`,subject `browser_session`) |
| Agent `browser.interact action=request_human` | 同上,原因 `agent_request` |
| 站点停用 / 浏览器服务停用 | 下一次访问时会话关闭(`site_disabled` / `provider_disabled`) |
HTTP 输入类端点(navigate / click / type / scroll / select / 开关标签页 / 带 target 的 download)只接受持有自己人工租约的调用者;
快照、截图、会话详情随时可读。Agent 看截图走 `browser.screenshot`(§3.1),敏感截图默认不进模型。
## 3. Agent 能力
| 能力 | 类别 | 说明 |
| --- | --- | --- |
| `browser.snapshot` | read | 结构化快照(已脱敏);`sessionId` 或 `siteId`;清掉 `needsSnapshot` |
| `browser.navigate` | execute(admin) | 站点域名内;没有会话时按站点打开 |
| `browser.interact` | execute(admin) | click / type / select / scroll / press / request_human;登录字段用 `credential` 由服务端注入 |
| `browser.download` | execute(organize.execute) | 捕获下载 → 暂存 → 解析种子、匹配本地资源;**不提交**,提交用 `download.plan` / `download.submit` |
execute 类需要持续授权(`automation_grant`)或审批。元素引用只来自最近一次快照:导航、点击、按键提交、控制权交接、
服务重启之后,旧的 `eN` 报 409 `元素引用已失效…先重新读取页面`。
### 3.1 视觉自动化(VIS-1)
截图作为图片交给视觉模型 → 模型给出截图上的坐标 → 服务端换算到页面并操作 → 再截图,循环。用于登录表单、勾选框 / 滑块 /
点选类挑战等快照引用够不着的页面;对应 REQ v0.7 §4.4、§10。代码:`apps/server/src/modules/browser/{service,capabilities}.ts`
(截图、坐标、证据)、`apps/server/src/modules/agent/challenge.ts`(有界子流程)、`packages/adapters/src/browser/image.ts`(缩放编码)。
| 能力 | 类别 | 说明 |
| --- | --- | --- |
| `browser.screenshot` | read | 截图缩到 ≤1280 像素宽(缺省 JPEG 质量 70),以 image content 交给模型;同时登记"模型坐标系"并写证据缩略图 |
| `browser.click_at` | execute | `{sessionId, x, y, button?}`:坐标 = 最近一张截图的像素;只支持左键;返回点击后的新截图 |
| `browser.type_at` | execute | `{sessionId, x?, y?, text, pressEnter?}`:先点再输(不给坐标 = 输入到当前焦点);账号密码仍用 `browser.interact` 的 `credential` |
| `browser.scroll` | execute | `{sessionId, dx, dy}` 像素,返回新截图 |
| `browser.wait` | read | `{sessionId, ms ≤ 5000}`,返回等待后的新截图 |
| `browser.solve_challenge` | execute | `{sessionId, goal, success, maxSteps ≤ 12, budget}`:有界视觉子流程,见下 |
- **上游必须支持图片输入**:按上游模型的 `input` 是否含 `image` 判定(内置 provider 的目录自带;自定义 openai-compatible 端点目前按纯文本处理)。
不支持时截图不发送,能力改给已脱敏的文字快照并提示;`solve_challenge` 直接判定需人工。fake 上游按支持图片处理。
- **坐标系**:服务端对每次截图读页面视口与 DPR(`evaluate`),`页面 CSS 像素 = 截图像素 × 视口宽 / 截图宽`;
输出的 `coordinates` 记录截图尺寸、原始尺寸、视口、DPR(取不到时 `assumed = true`,按原图、DPR 1)。动作之后旧坐标系作废,
必须用新截图的坐标。点击沿用 1.17.0 的做法:`elementFromPoint` 打标记再按选择器点,**画布(canvas)内的位置、跨域 iframe
里的控件(如 Cloudflare Turnstile)点不到具体位置**,这类挑战实际只能转人工。
- **动作前提**:会话在自动化控制下(人工接管 / 等待处理中一律拒绝),当前页面主机在站点域名白名单内。
- **敏感截图**:登录页地址、页面上有密码框、地址带凭据 → `sensitive = true`,默认不交给模型(不做像素遮蔽),改给已脱敏的文字快照;
要交给模型需要 `automation_grant.action = browser.sensitive_screenshot`(或 `*`)的持续授权,且调用时传 `allowSensitive: true`。
- **二次验证 / 短信 / 本人设备确认**:截图时读页面文字、快照时读快照文本,命中即把会话转等待处理(原因 `two_factor`),
之后的自动化动作全部被拒;系统提示也要求模型立即停止。视觉子流程**不能**接手 `two_factor` 等待。
- **证据**:每次截图(含动作后的截图)写 480 像素宽的 JPEG 缩略图到 `DATA_DIR/tmp/evidence/<id>.jpg`(0600,7 天后清理);
能力输出只给引用 `evidence/<id>.jpg`,随工具结果摘要进 `agent_run_event`。图片数据不进 JSON 文本、不进操作账本。
- **授权**:`click_at / type_at / scroll / solve_challenge` 由 `browser.interact` 的持续授权覆盖(也可单独授权);
父会话白名单里有 `browser.solve_challenge` 即覆盖子流程用到的截图与坐标能力;授权同理,solve_challenge 通过授权后,子流程(`spawn_reason = browser.solve_challenge`)内的坐标动作不再单独要持续授权。坐标动作按调用区分幂等键,崩溃恢复不重放。
- 交给模型的截图参数可在 `browser_provider.config` 调:`screenshotMaxWidth`(≤1280)、`screenshotFormat`(`jpeg` / `png`)、
`screenshotQuality`(20–95)。
`browser.solve_challenge` 流程:
1. 会话须在自动化控制下,或因 `captcha` / `challenge` / `challenge_unsolved` 在等待处理(收回自动化控制,原待处理事项保留);
其它等待原因(二次验证、租约过期、Agent 请求人工)与人工接管中一律拒绝。
2. 在调用进程里起一个子 `agent_run`(父会话 = 调用它的 Agent 运行,`spawn_reason = browser.solve_challenge`;
上游与父会话相同;白名单只有截图 / 坐标点击 / 输入 / 滚动 / 等待 / `browser.snapshot`),一步 = 一次工具调用,
`maxSteps` 即工具调用上限;token / 费用 / 时长按 `budget`(时长缺省 180 秒),用量计入父会话。
3. 子流程结束后服务端按 `success`(`urlContains` / `urlChanged` / `textPresent` / `textAbsent`,全部满足)读页面核实,
不信模型自述。结果:`solved`(关闭原待处理事项)/ `needs_human`(二次验证、模型最后一行写 `NEEDS_HUMAN: 原因`、上游不支持图片)/
`exhausted`(步数或预算用尽)/ `failed`。
4. 没成功:会话转等待处理(`two_factor` 或 `challenge_unsolved`),待处理事项的 `evidence.challenge` 附最近 12 张证据截图引用与子运行 ID,
返回 `takeover.path = /console/sites/<siteId>?tab=origin`(原站视图接管入口)。不自动重试;人可以再次调用或接管处理。
不承诺通过 Cloudflare 等挑战(REQ §4.4):上游浏览器指纹与点击方式都可能被识别,失败按上面第 4 步转人工。
## 4. 下载产物
- 暂存:`DATA_DIR/tmp/browser-downloads/<artifactId>.bin`(目录 0700、文件 0600),超过 24 小时的在下一次捕获时清理。
合同里只出现引用(`bytes.ref`),来源地址去掉 passkey。
- 核验:bencode 解析、infohash v1/v2、私有标记;按 `source_release.infohash_v1/v2`,再按下载地址 `id` 参数匹配本站资源。
- 提交:HTTP `POST /browser/sessions/{id}/download` 带 `submit` 时转调 `POST /acquisition-plans`(同一鉴权、计划、去重、
缓存阈值、授权检查);被拒时产物照常返回,`note` 写拒绝原因。同一会话同一 sha256 第二次捕获标 `duplicate`,不再提交。
- 浏览器没有媒体库写权限;提交链路下载的是站点适配器取到的种子,暂存文件只作核验证据。
## 5. 排障
| 现象 | 查 |
| --- | --- |
| 原站视图"没有启用的浏览器服务" | `browser_provider` 是否有 `enabled = true` 且 `kind = 'camofox'` 的行 |
| 412"浏览器服务认证失败" / camofox 401 | 访问凭据与 `CAMOFOX_ACCESS_KEY` 是否一致;`GET /browser/providers?probe=true` |
| 502 浏览器服务不可用 | camofox `/health`(503 = 恢复中);容器日志 |
| 409 先接管会话再操作 | 界面点"接管";或租约已被他人持有 / 已过期 |
| "连接中断" | 截图请求失败;看 API 日志 `component: browser` 的方法 / 路径 / 状态(日志不含正文与凭据) |
| 测试连通提示"访问凭据不对" | `/health` 免认证所以能连上,但 `/openapi.json` 401:`BROWSER_ACCESS_KEY` 与 camofox 的 `CAMOFOX_ACCESS_KEY` 不一致 |
| 409"标签页已被浏览器服务回收" | camofox 重启(旧标签页返回 410)或会话空闲回收(404);Agent 再次 `browser.navigate` 会在同一会话开新标签页 |
| 重启 camofox 后要重新登录 | 容器是被 SIGKILL / OOM 杀掉的(`docker inspect` 看 `OOMKilled`、退出码 137):只有 SIGTERM 正常停止、会话空闲回收、关闭会话时才落盘登录状态;加大 `CAMOFOX_MEM_LIMIT` |
| 设置页显示"未核验" | 探测到的上游版本不在核验表里(不是 1.17.0,或 fork);按 §6 核验后在适配器 `CAMOFOX_VERIFIED_RELEASES` 加一行,或临时写 `config.verified` |
## 6. 真实实例核验清单(REQ §4.7.4)
先固定 camofox-browser 的**仓库、版本(`/openapi.json` 的 `info.version`)、启动方式与认证配置**,记在本节末尾。
通过的能力登记进适配器的真机核验表 `CAMOFOX_VERIFIED_RELEASES`(`packages/adapters/src/browser/camofox/capabilities.ts`,
按上游版本,只在探测到完全相同的版本时生效,并把 `verified_at` 写成核验日期);个别部署要撤销某项写
`browser_provider.config.verified = {"<键>": false}`。不通过的保持 false,界面继续标"未核验"。
不得用另一个浏览器窗口代替后宣称通过。
自动化核验脚本:`scripts/browser-verify/run.sh`(临时栈 `umtbr`:postgres 127.0.0.1:55447、camofox 127.0.0.1:49377、
compose 网络里的 mock NexusPHP 站 `mocknexus:8080`;服务层在宿主进程里装配,与生产同一套代码;
阶段之间重启 / SIGKILL camofox;结果写 `.browser-verify-out/results-*.json` 与截图)。跑完 `docker compose -p umtbr down -v`。
| # | 项目 | 做法 | 通过标准 | 对应 verified 键 |
| --- | --- | --- | --- | --- |
| 1 | 登录保持 | 观众 / 馒头 / HHClub / CHD 各一账号:原站视图接管登录;闲置超过 `SESSION_TIMEOUT_MS` 后再 `browser.snapshot` | 不需重新登录;过期时落到登录页、`loginRequired = true`,而不是空列表 | `sessions`、`persistentProfile` |
| 2 | 截图 / 结构化快照 | 列表页、详情页、登录页各取 `screenshot` 与 `snapshot` | 截图格式(JSON base64 还是 image/*)、尺寸与坐标一致;快照 `eN` 引用可点;密码框值、Cookie、passkey 不出现;大页面 `offset` 分页正确 | `screenshot`、`snapshot` |
| 3 | 列表 / 详情提取 | 快照 + `POST /extract`(属性带 `x-ref`)与现有 HTTP 适配器结果对比 | 标题、分类、副标题、促销、做种一致,原始字段不丢 | `extract` |
| 4 | 文件下载捕获 | 详情页点"下载"后 `POST /browser/sessions/{id}/download` | `GET /tabs/{id}/downloads` 的字段形状(filename / data / size / status)与适配器兼容;sha256 与直接 HTTP 下载一致;进暂存核验,不直写媒体库 | `downloadCapture` |
| 5 | 重启后会话 | 分别触发 `BROWSER_IDLE_TIMEOUT_MS` 回收与强制重启 camofox 容器 | 持久化 profile 恢复登录;旧 tabId 识别为 `lost`,不重放旧引用 | `persistentProfile`、`tabs` |
| 6 | 人与 Agent 同标签页连续操作 | Agent 打开详情页 → 原站视图接管,在**同一标签页**点击、输入 → 交还 → Agent 重新 `snapshot` | 人看到的是同一会话同一页面;交还后 Agent 读到人工操作后的状态;下载不重复提交 | `takeover`、`interact` |
核验记录:
| 日期 | 仓库 / fork | 版本 | 启动方式 / 认证 | 通过项 | 备注 |
| --- | --- | --- | --- | --- | --- |
| 2026-09-28 | jo-inc/camofox-browser | v1.17.0(`389c996`),Camoufox 152.0.4-beta.28 | `docker/camofox/Dockerfile` + compose;`CAMOFOX_ACCESS_KEY` Bearer | 十项全部 | mock NexusPHP 站 + example.com;四个真实站点未测,见 §7 |
## 7. 真机核验(版本 1.17.0)
环境:`camofox-browser:1.17.0` 容器(本机构建,镜像 3.6 GB,空闲内存约 200 MB,打开 2 个会话后约 390 MB),
`CAMOFOX_ACCESS_KEY` 已设置,`SESSION_TIMEOUT_MS=60000`、`BROWSER_IDLE_TIMEOUT_MS=30000`(空闲回收阶段用短超时);
目标页:testkit mock NexusPHP(观众风格,`mocknexus:8080`,与 camofox 同一 compose 网络)与公开静态页 `https://example.com/`。
浏览器服务经环境变量登记为 `camofox-env`。每项的原始输出见 `scripts/browser-verify/run.sh` 的结果文件。
| # | 项目 | 结果 | 实际观测 |
| --- | --- | --- | --- |
| 0 | 认证 / 探测 | 通过 | 无 Bearer 请求 401;`/openapi.json` `info.version = 1.17.0`,十项能力的端点都在 |
| 1 | 登录保持 | 通过 | 新 profile 打开 `torrents.php` → 302 到 `login.php?returnto=…`,`loginRequired = true`;人工接管登录后 Agent 直接打开受保护列表页,不再登录(站点 takelogin 只收到 1 次);Agent 用服务端凭证注入登录第二个账号也通过,快照与返回值里没有密码 |
| 2 | 截图 / 结构化快照 | 通过(修了遮蔽) | 截图直接返回 `image/png` 二进制(不是 OpenAPI 写的 JSON base64),视口随指纹变化(实测 1433×1145、1691×867),截图像素 = 点击坐标;登录页 `modelSafe = false`。快照是 Playwright ARIA 格式(`- textbox [e2]: 值`,引用写作 `[eN]`),**密码框与普通文本框同形且带明文值**,原有按标签名的遮蔽规则抓不到 → 已修(见下);列表页 45 个引用、8.6 KB,未触发分页 |
| 3 | 列表 / 详情提取 | 通过 | 列表页快照含 fixture 的标题、副标题、大小、做种数(5 条中 4 条标题完整出现,1 条是站点截断显示);`POST /extract` 按 `x-ref` 取回标题;详情页取回下载链接文字。与 HTTP 适配器同源 fixture 对照,未逐字段比对促销标记 |
| 4 | 下载捕获 | 通过(修了字段名) | Agent 点详情页 `.torrent` 链接 → `downloads` 返回 `{id, url, suggestedFilename, mimeType, bytes, createdAt, failure, dataBase64}`;sha256 与直接 HTTP 下载一致(`9fcf61f7…`),infohash 与站点一致并匹配到本地资源,约 2 秒;`consume=true` 后不再返回同一文件 |
| 5 | 重启后会话 | 通过(修了 3 处),有已知限制 | SIGTERM 重启:旧标签页返回 **410** `browser_restarted`(不是 404)→ 已按 lost 处理;登录状态恢复,Agent 再次导航在同一会话开新标签页(约 1.3 秒,含浏览器冷启动)。会话空闲回收(60 秒 + 上游 60 秒扫描周期):旧标签页 404 → lost,重新打开仍登录(约 0.4 秒)。**SIGKILL**:上次落盘之后的新登录丢失(需重新登录),这是上游落盘时机决定的(只在会话空闲回收、关闭、SIGTERM 停止时写),界面照常识别为登录页 |
| 6 | 人与 Agent 同标签页 | 通过(修了坐标点击与焦点输入) | Agent 打开登录页(上游 tabId T)→ 接管:自动化输入被拒(409)、只读快照允许 → 截图坐标点击用户名框 → 焦点输入 → 点密码框 → 输入 → 回车 → 交还;交还前的旧引用 `e1` 被拒("先重新读取页面");Agent 重新读取,**仍是 tabId T**,看到人工登录后的页面。后退 / 前进 / 刷新 / 滚动也在同一标签页通过。核验走原站视图调用的同一组 HTTP 端点,没有在浏览器里点原站视图界面 |
对接中发现并已修的差异:
1. **截图格式**:1.17.0 直接返回 `image/png`;适配器原本两种都兼容,无需改。
2. **坐标点击**:`POST /click` 只认 `ref` / `selector`,不接受 `{x, y}` → 适配器先 `POST /evaluate` 用 `elementFromPoint` 给该点元素打
`data-umt-pt` 标记,再按属性选择器点击(仍走上游的完整鼠标事件序列与下载守卫)。坐标处没有元素报 400。
3. **焦点输入**:`POST /type` 缺省 `mode=fill` 必须带 `ref`/`selector` → 无目标时发 `mode=keyboard`;回车用 `/press`。
4. **密码框遮蔽**:快照里出现带值的输入框时,经 `evaluate` 取页面上 `input[type=password]` 的当前值按字面量遮蔽;
`evaluate` 失败时遮蔽全部输入框的值。`extract` 结果同样处理。
5. **下载字段**:`suggestedFilename`、`bytes`(大小)、`dataBase64`、`dataSkipped`(超 `maxBytes`)、`failure`。
6. **410**:浏览器重启后 5 分钟内旧标签页返回 410,与 404 同样按"标签页已不存在"对账为 lost;Agent 下一次导航在同一会话开新标签页。
7. **上游镜像**:① v1.17.0 的 Dockerfile 在 `npm ci` 前没复制 `postinstall.js`,构建必然失败;② `CMD sh -c "node …"`
不转发 SIGTERM,`docker stop/restart` 等满宽限期后 SIGKILL,**关机落盘不执行,重启后登录全丢**(修前实测)。
`docker/camofox/Dockerfile` 两处都改了(`exec node`)。
8. 其它:关掉上游缺省开启的崩溃报告外发(`CAMOFOX_CRASH_REPORT_ENABLED=false`)与每次启动从 addons.mozilla.org 下载
uBlock(`CAMOFOX_DISABLE_DEFAULT_ADDONS=true`,内网下载失败会拖慢启动)。
`userId`(`umt-site-<uuid>-acct-<uuid>`,约 90 字符)上游原样使用(持久化目录按 SHA-256 取名),无字符限制问题;
`DELETE /tabs/{id}` 的 `userId` query / body 都认;`DELETE /sessions/{userId}` 关闭该 profile 全部标签页并落盘。
视觉自动化(VIS-1,2026-09-29,`scripts/browser-verify/vision-real.ts`,只读使用现有 `umt-camofox-1`):打开 `https://example.com/`,
截图 `image/png` 1920×992(本次指纹的视口 1920×992、DPR 1;此前两次观测到 1600×819、1433×1145,视口随指纹变化),
缩成 1280×661 JPEG 交给模型;在缩放图上取"Learn more"链接中心 (640, 343),按服务端换算得页面 (960, 515),
与 `getBoundingClientRect` 的中心 (960.0, 514.8) 一致;点击后跳到 `https://www.iana.org/help/example-domains`,通过。
另:1.17.0 的 `POST /click` 在 OpenAPI 里写了 `coordinates: {x, y}`,实测仍报 `ref or selector required`,坐标点击继续走 `elementFromPoint`。
没有核验的:四个真实站点(观众 / 馒头 / HHClub / CHD)的登录保持、Cloudflare / 验证码挑战、真实站点页面上的提取对比;
并发 1 / 2 / 4 会话的性能与资源;原站视图界面本身在浏览器里的点击体验(1.5 秒截图刷新);VNC 插件(镜像已装,未启用、未接入)。
## 8. 原站视图桌面模式(Selkies 远程桌面,WT-1)
方案与取舍见 `docs/design/webtop-remote-browser.md`。桌面镜像 = linuxserver `baseimage-selkies:debiantrixie`(Webtop 4.0 的底座,
pixelflux + labwc Wayland 桌面 + Selkies 2.0 Web 客户端)+ camofox 1.17.0 **有头**运行(`CAMOFOX_INTERACTIVE=desktop`,
`MOZ_ENABLE_WAYLAND=1`)。原站视图里看到的窗口就是 Agent 经 camofox API 驱动的同一个 Firefox 进程:Agent 导航,画面跟着变;
人在画面里点,Agent 下一次快照读到的就是点过之后的页面。
### 8.1 组成
| 部件 | 位置 | 说明 |
| --- | --- | --- |
| 镜像 | `docker/browser-desktop/Dockerfile` | 底座按 digest 锁定;从 `camofox-browser:1.17.0` 复制 node、`/app`、Camoufox;补 `fonts-noto-cjk-extra`;可选 fcitx5(拼音 + mozc,`--build-arg WITH_FCITX5=0` 不装) |
| 启动 | `root/etc/s6-overlay/s6-rc.d/svc-camofox` | s6 服务,等 labwc 的 `wayland-0` 出现后以 `abc` 身份 `exec node server.js`;依赖 `svc-de` 与 `svc-dbus`(Firefox 连着 D-Bus,dbus 先停会把浏览器带走,关机落盘失败) |
| 初始化 | `init-umt-desktop` | 由 `CAMOFOX_ACCESS_KEY` 派生 Selkies 控制 / 只读口令;覆盖 labwc autostart;把 `/root/.camofox` 交给 `abc` |
| 反代 | `apps/server/src/modules/desktop` | `/api/v1/browser/desktop/{sessionId}/*` → 容器 `:3000`(静态客户端 + `api/websockets`);只接受控制台会话 |
| 前端 | `apps/web/app/components/browser/desktop-view.tsx` | 原站视图 Tab:桌面可用且是安全上下文 → 桌面模式;否则截图模式并说明原因;可手动切换 |
| HTTPS | compose `caddy`(profile `tls`)+ `docker/browser-desktop/Caddyfile` | 可选;已有反代终结 TLS 时不用 |
窗口:无标题栏、最大化铺满 1280×900(`BROWSER_DESKTOP_WIDTH/HEIGHT`),`HARDEN_DESKTOP` 去掉终端 / sudo / xdg-open,
`HARDEN_OPENBOX` 去掉关闭按钮与右键 / 中键菜单、禁 Alt+F4 等。音频、麦克风、摄像头、手柄、文件传输、打印、命令、共享全部关闭并锁定,
剪贴板只允许文本。
### 8.2 鉴权与输入权限
- Selkies 端口(3000/8082)不映射到宿主,唯一入口是 app 的反代;外部 API Key 与 DEV key 一律 403。
- 口令不经浏览器:app 按请求者身份在上游请求上注入 `Authorization: Basic`。持有该会话 active 人工租约的本人 → 控制口令;
其他人(包括未接管的本人)→ 只读口令,Selkies 服务端丢弃只读连接的键鼠与剪贴板输入。
- 口令 = `HMAC-SHA256(CAMOFOX_ACCESS_KEY, "umt-desktop/control" | "umt-desktop/view")` 前 48 位 hex,app 与容器各算一遍;
没设 `CAMOFOX_ACCESS_KEY` 时容器用随机口令,桌面模式不可用(界面回退截图模式)。
- WebSocket 每 3 秒复查一次角色:交还 / 租约过期 / 被他人接管 / 控制台会话失效时服务端主动断开,Selkies 客户端自动重连并按新口令认证
(实测交还后 5 秒内点击已无效)。WebSocket 还要求 `Origin` 与 `Host` 同源(cookie 会话 + WS 无 CORS)。
- 反代响应带 `Content-Security-Policy: frame-ancestors 'self'` 与 `X-Frame-Options: SAMEORIGIN`,只能被本站 iframe 嵌入;
上游的 `WWW-Authenticate` 不外露(否则浏览器弹 Basic 登录框)。
- 只支持环境变量登记的 `camofox-env`(compose 自带容器);桌面地址缺省由 `BROWSER_PROVIDER_URL` 推出(端口换 3000),
或显式 `BROWSER_DESKTOP_URL`。
### 8.3 界面
原站视图 Tab 调 `GET /api/v1/browser/desktop/{sessionId}/_status` 决定模式:`available`(容器 3000 能连上且有访问凭据)
且 `window.isSecureContext` → 桌面模式(iframe);否则截图模式,顶部写原因:"桌面模式不可用:…" 或 "桌面模式需要通过 HTTPS 访问控制台"。
桌面模式顶栏:连接状态(已连接,只读 / 可操作)、当前控制者、接管 / 交还(沿用 `control/acquire|release`)、重新连接、截图模式、关闭会话。
接管后直接在画面里点和输入;中文、日文用**本机输入法**(Selkies 接收 composition 事件,按 Unicode 注入上屏文本);
复制粘贴用 Ctrl+C / Ctrl+V。
### 8.4 HTTPS
WebCodecs 视频解码与剪贴板 API 只在安全上下文(HTTPS 或 `localhost`)可用;用 IP / 局域网主机名经 HTTP 访问时只能用截图模式。
```sh
# 已有反代(nginx / Caddy / Traefik)终结 TLS:无需改动,WebSocket 需透传 Upgrade、读超时 ≥ 1 小时、不缓冲
# 没有反代:启用 compose 自带 Caddy
docker compose --profile tls up -d # 自签:Caddy 内部 CA 按访问的主机名现场签发,浏览器需放行或导入根证书
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./umt-root.crt # 导入到客户端受信任根
# 有公网域名:.env 里 TLS_DOMAIN=media.example.com、[email protected](ACME,80/443 需可达)
```
`TLS_PORT`(缺省 443)、`TLS_BIND` 可改;4380 仍可直连(截图模式)。
### 8.5 回退
- 桌面容器起不来 / 桌面异常:`.env` 设 `CAMOFOX_IMAGE=camofox-browser:1.17.0` 后 `docker compose up -d camofox`,原站视图自动回到截图模式,
Agent 能力不受影响(实测同一个卷,切换前后登录状态都在)。
- 只关桌面、保留镜像:不设 `CAMOFOX_ACCESS_KEY` 即可(桌面口令随机,app 探测为不可用)。
- 容器内输入法(本机输入法不可用时的备选):`BROWSER_DESKTOP_IME=fcitx5`,Ctrl+Space 切换;与本机输入法同时开会重复组字,缺省关闭。
### 8.6 原型核验(2026-09-29,临时栈 `umtwt`)
环境:本机 96 核、无 GPU(pixelflux 软件渲染 + CPU H.264);客户端 headless Chromium 145(Playwright),经 Caddy 自签 HTTPS;
上游 camofox 1.17.0 / Camoufox 152.0.4-beta.28,Selkies 2.0.0,labwc 0.8.3。脚本 `apps/web/test/screenshots/webtop/{p0,verify}.mjs`(截图不进 git)。
| # | 项 | 结果 | 观测 |
| --- | --- | --- | --- |
| 1 | 同会话 | 通过 | camofox API 导航 example.com → iana,桌面画面同步;在桌面里点链接,API 快照 URL 变为 `iana.org/domains/reserved`,tabId 不变 |
| 2 | 中日文显示 | 通过 | zh / ja 维基首页正文、标题栏、地址栏无豆腐块(Camoufox 自带 Noto CJK) |
| 3 | 本机输入法 | 通过 | CDP `Input.imeSetComposition` + `insertText` 模拟组字:搜索框得到「流浪地球」「進撃の巨人」(camofox `evaluate` 读回) |
| 4 | 剪贴板 | 通过 | 本机 → 远端:写本机剪贴板,焦点回到画面后 Ctrl+V 粘贴中日文;远端 → 本机:远端 Ctrl+C 后本机 `readText` 读回中文 |
| 5 | 只读口令 | 通过 | 未接管时在画面点链接,URL 不变;接管后同一坐标点击生效;API 交还后 5 秒内再点无效 |
| 6 | 重启保持 | 通过(修了 1 处) | `docker compose restart camofox` 后 httpbin cookie 仍在。修前 dbus 与 camofox 并行停止,Firefox 先退出导致关机落盘失败 |
| 7 | HTTP 回退 | 通过 | 经 HTTP 访问显示"桌面模式需要通过 HTTPS 访问控制台",截图模式正常;旧镜像下显示"浏览器服务没有桌面…" |
| 8 | 资源 | 见右 | 容器内存:1 会话无人观看约 570–670 MB,观看约 610 MB,滚动维基首页约 730 MB;CPU:无人观看 0.3%(不编码),观看静止页 1–7%,持续滚动约 75–100%(约 1 核) |
| 9 | 镜像 | — | `umt-browser-desktop:1.17.0` 解压 6.9 GB / 压缩 2.0 GB(旧镜像 3.6 GB / 1.1 GB) |
已知问题与未做:
- Firefox 工具栏里有 Playwright 的 "JUGGLER" 标识;用户可在画面里打开 `about:addons`、新标签页、输入任意网址——越界防护
(`policies.json`、`context.route` 白名单、交还时核对 URL,方案 §6)未做。
- 接管期间 camofox 看不到 API 请求,`SESSION_TIMEOUT_MS`(缺省 30 分钟)到期会回收会话窗口;接管心跳(方案 §5.5 插件)未做。
- 一个桌面承载所有站点会话:持有 A 站租约的人在画面里能看到 / 操作切到前台的其它窗口;目前同一时刻一般只有一个会话窗口。
- 真实输入法(Windows 微软拼音、macOS 日文)、Firefox / Safari 客户端、真实 PT 站点未测;容器内 fcitx5 只验证能启动,未验证组字。
- 画面坐标与 camofox 截图坐标不同(桌面含 56 px 工具栏,Camoufox 伪装了 `screenX` / `outerHeight`),Agent 视觉操作继续用 camofox 截图。