Files
UHD_Media_Tool/docs/runbook/browser.md
T
rootandClaude Opus 5.5 ceb66f8c77 feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)
生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 15:47:17 +08:00

703 lines
66 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/**`(原站视图)。
> 当前版本:**v1.18.0**(jo-inc,commit `f94c81b`)已重新核验原有十项能力,六阶段 25 项检查和隔离重启验证通过(§16)。
> §7 等带日期的 v1.17.0 验收表保留历史证据;
> 探测到其它版本 / fork 时一律显示"未核验",需按 §6 重新核验。
## 1. 浏览器服务从哪来
### 1.1 部署自带(缺省)
当前部署配置使用 `.env.deploy`。首次启动前,先完成本节下方的基础镜像构建;已有镜像时再执行启停命令。
`docker compose --env-file .env.deploy 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.deploy(部署目录)
CAMOFOX_ACCESS_KEY=$(openssl rand -hex 24) # camofox 与 app 共用
docker compose --env-file .env.deploy up -d --build
docker compose --env-file .env.deploy ps camofox # healthy 即可
```
上游不发布镜像。compose 缺省用的是**浏览器桌面镜像** `umt-browser-desktop:1.18.0`(`docker/browser-desktop`,§8),
当前默认版本为 1.18.0,构建补丁、锁文件和 Playwright 1.60 协议兼容例外见 [镜像说明](../../docker/camofox/README.md)。以下旧日期的验收表保留历史版本。
它从纯截图镜像 `camofox-browser:1.18.0` 复制 camofox,所以**先构建基础镜像**:`docker/camofox/Dockerfile` 从 git tag `v1.18.0`
构建(修了上游 Dockerfile 的两个问题,见文件头)。首次构建要从 GitHub 拉源码与约 630 MB 的 Camoufox 浏览器包:
```sh
git clone --depth 1 --branch v1.18.0 https://github.com/jo-inc/camofox-browser.git /tmp/camofox-src
git -C /tmp/camofox-src rev-parse HEAD # 必须为 f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753
docker build -t camofox-browser:1.18.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 --env-file .env.deploy build camofox # 再构建桌面镜像(或 docker build -t umt-browser-desktop:1.18.0 docker/browser-desktop)
```
只要截图模式(不要桌面、省约 400 MB 常驻内存与 3 GB 磁盘):`.env.deploy` 里 `CAMOFOX_IMAGE=camofox-browser:1.18.0`,同一个卷,登录状态通用。
### 1.2 接自有实例
`.env.deploy` 里 `BROWSER_PROVIDER_URL=http://<host>:9377`、`BROWSER_ACCESS_KEY=<它的 CAMOFOX_ACCESS_KEY>`、`CAMOFOX_REPLICAS=0`
(不再起自带容器)。或者不设环境变量,在 **设置 → 站点默认 → 浏览器服务** 里"添加浏览器服务":名称、服务地址、访问凭据
(加密存凭证库,只写不回显)、启用。自有实例的版本不是 1.18.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 / 410)对账标 `lost`;整个会话的标签页都没了(camofox 重启、空闲回收)时自动重连,见 §10。
- 控制者:`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)。动作之后旧坐标系作废,
必须用新截图的坐标。点击(CF-2 起)先走 umt 插件的 `POST /umt/tabs/{id}/mouse-click`——Playwright `page.mouse.click` 发真实鼠标事件,
**跨域 iframe 里的控件(Cloudflare Turnstile)和画布也点得到**;插件没有这个端点(旧镜像、纯截图镜像)时退回 1.18.0 的做法:
`elementFromPoint` 打标记再按选择器点(跨域 iframe / 画布点不到具体位置)。
- **动作前提**:会话在自动化控制下(人工接管 / 等待处理中一律拒绝),当前页面主机在站点域名白名单内。
- **敏感截图**:登录页地址、页面上有密码框、地址带凭据 → `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. 详情页下载与下载产物
本轮源码新增:进入站点资源详情页后,在串流/截图窗口外点「下载当前资源」,解析当前详情并用默认下载器和目录提交。点「下载选项」可选择下载器、保存目录与一次性洗版。未识别资源会提示确认继续下载,整理阶段再补全元数据;不用点击源站下载按钮,也不用先建立订阅或本地搜索索引。此变更已于 2026-10-01 随 v0.3.10 部署,迁移 `0060` 已执行。
`POST /browser/sessions/{id}/resource {tabId?}` 返回登记后的资源与详情地址。默认读取唯一可见的已登记标签页,能发现上游登记的 popup;无法确定当前标签时明确报错,不提交旧页。显式 `tabId` 读取指定标签。下面的文件捕获接口仍保留给已有 Agent 流程使用。
- 暂存:`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` 不一致 |
| 原站视图"浏览器已重启,正在重新连接…" | camofox 重启 / 会话空闲回收后的自动重连(§10),正常几秒内恢复 |
| 503 `browser_reconnect_failed` / "浏览器连接已断开" | 自动重连失败,消息里是原因(camofox 没起来、`POST /tabs` 5xx 等);`docker compose --env-file .env.deploy ps camofox`、容器日志;恢复后点"重新连接" |
| 409"标签页已被浏览器服务回收" | 只在显式指定了已被回收的 `tabId` 时出现(会话里还有别的标签页活着);不指定 `tabId` 重试即可 |
| 重启 camofox 后要重新登录 | 容器是被 SIGKILL / OOM 杀掉的(`docker inspect` 看 `OOMKilled`、退出码 137):只有 SIGTERM 正常停止、会话空闲回收、关闭会话时才落盘登录状态;加大 `CAMOFOX_MEM_LIMIT` |
| 设置页显示"未核验" | 探测到的上游版本不在核验表里(不是 1.18.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 --env-file .env.deploy -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.18.0(`f94c81b`),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:桌面可用且是安全上下文 → 桌面模式;否则截图模式并说明原因;可手动切换 |
| 自愈(INT-3 / FIX-5) | 同上 | 打开桌面先调一次幂等 `POST /browser/sessions/{id}/reconnect`(iframe 直连 Selkies 不触发被动重连),等它返回后才加载桌面 iframe(期间显示「正在确认浏览器会话…」,FIX-5 起不再先连上黑屏);画面 5 秒未加载或视频流未出帧 → 每个会话自动重连一次并重建桌面连接,之后靠「重新连接」按钮 |
| HTTPS | compose `caddy`(profile `tls`)+ `docker/browser-desktop/Caddyfile` | 可选;已有反代终结 TLS 时不用 |
窗口:由内置 `/defaults/umt-labwc.xml` 设为无服务端装饰、最大化铺满 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`)、重新连接、截图模式、关闭会话。
「当前控制」(HH-2,两种模式共用 `coords.ts controllerLabel`):`controller = automation` 且 2 分钟内有过自动化动作(Agent 能力经
`BrowserService.run` 成功执行,写 `recovery_state.lastAutomationAt`,会话视图字段 `lastAutomationAt`)才显示「Agent」,说明写
「Agent 正在操作」;否则显示「自动化」,说明写「目前没有 Agent 在操作」。人工接管显示「人工接管(用户名)」,人工动作不刷新该时间。
接管后直接在画面里点和输入;中文、日文用**本机输入法**(Selkies 接收 composition 事件,按 Unicode 注入上屏文本);
复制粘贴用 Ctrl+C / Ctrl+V。
### 8.4 HTTPS
当前测试部署于 2026-10-01 将 **4380 改为 Caddy 自签 HTTPS**,访问 `https://服务器地址:4380`,原 443 HTTPS 入口保留。首次访问需在浏览器继续接受内部 CA 证书。app 的 HTTP 调试映射为 `127.0.0.1:4381`,容器间仍使用 `app:4380`。管理员登录、6 个页面加载与 Chromium `isSecureContext` 检查通过。此映射写在本机 `docker-compose.override.yml`,应用监听参数写入 `.env.deploy`。
WebCodecs 视频解码与剪贴板 API 只在安全上下文(HTTPS 或 `localhost`)可用;用 IP / 局域网主机名经 HTTP 访问时只能用截图模式。
```sh
# 已有反代(nginx / Caddy / Traefik)终结 TLS:无需改动,WebSocket 需透传 Upgrade、读超时 ≥ 1 小时、不缓冲
# 没有反代:启用 compose 自带 Caddy
docker compose --env-file .env.deploy --profile tls up -d # 自签:Caddy 内部 CA 按访问的主机名现场签发,浏览器需放行或导入根证书
docker compose --env-file .env.deploy cp caddy:/data/caddy/pki/authorities/local/root.crt ./umt-root.crt # 导入到客户端受信任根
# 有公网域名:.env.deploy 里 TLS_DOMAIN=media.example.com、[email protected](ACME,80/443 需可达)
```
`TLS_PORT`(缺省 443)、`TLS_BIND` 可改;标准 Compose 可通过 app 的 `UMT_PORT` 保留 HTTP 直连。本机已将 4380 用于 HTTPS,HTTP 调试改为 `127.0.0.1:4381`。
### 8.5 回退
- 桌面容器起不来 / 桌面异常:`.env.deploy` 设 `CAMOFOX_IMAGE=camofox-browser:1.17.0` 后 `docker compose --env-file .env.deploy 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 请求:桌面连接期间与人工接管期间由 app 定期保活(§12,AG-4),不再被空闲回收;
浏览器里的心跳插件(方案 §5.5)不做。
- 一个桌面承载所有站点会话:持有 A 站租约的人在画面里能看到 / 操作切到前台的其它窗口;目前同一时刻一般只有一个会话窗口。
- 真实输入法(Windows 微软拼音、macOS 日文)、Firefox / Safari 客户端、真实 PT 站点未测;容器内 fcitx5 只验证能启动,未验证组字。
- 画面坐标与 camofox 截图坐标不同(桌面含 56 px 工具栏,Camoufox 伪装了 `screenX` / `outerHeight`),Agent 视觉操作继续用 camofox 截图。
## 9. 凭据统一:站点账号登录态 ⇄ 浏览器(BR-2)
一处管理凭据:站点账号里的 Cookie(手填,或账密自动登录得到的会话 Cookie)在打开浏览器会话时注入该站点 / 账号的 profile;
人在浏览器里登录后,登录态读回站点账号,HTTP 抓取、RSS、快照下一次就用新的。截图模式与桌面模式是同一个 camofox 进程,共用。
代码:`sources-identity/service/credentials.ts`(单一读写入口)、`modules/browser/service.ts`(`prepareCookies` / `syncCookies`)、
`packages/adapters/src/browser/{cookies.ts,camofox/client.ts}`、camofox 插件 `docker/browser-desktop/root/opt/camofox/plugins/umt-cookies`。
### 9.1 注入做法
camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一个口令 `CAMOFOX_API_KEY`;没有读取端点。
所以桌面镜像带一个最小插件 **umt-cookies**(Dockerfile 把它加进 `camofox.config.json` 的插件清单),在访问凭据
(`CAMOFOX_ACCESS_KEY`)之下提供:
| 端点 | 说明 |
| --- | --- |
| `POST /umt/sessions/{userId}/cookies` `{cookies}` | `context.addCookies`,并触发持久化插件立即落盘(与上游导入端点相同) |
| `GET /umt/sessions/{userId}/cookies?urls=a,b` | 作用于这些地址的 Cookie:会话活着读 `context.cookies(urls)`,否则读 profile 落盘文件 |
| `GET /umt/instance` | `{instanceId, startedAt}`:插件加载时生成,camofox 进程重启即变(BR-3 重启检测,§10) |
| `GET /umt/identity` | 固定浏览器身份的公开部分(UA / OS / 版本,BR-4,§11.1) |
| `PUT /umt/sessions/{userId}/proxy` `{proxy}` | 该 profile 的出口:`{server, username?, password?}` 或 `null` 直连(BR-4,§11.5) |
| `POST /umt/tabs/{tabId}/mouse-click` `{userId, x, y}` | 按视口坐标发真实鼠标事件(`page.mouse.click`,CF-2,§13);标签页不在回 422 |
适配器先走插件;插件不在(404)再试上游导入端点(`CAMOFOX_API_KEY` 与访问凭据相同时可用,只能写不能读);都不行 → 会话照常打开,
`credential.status = unsupported` 并说明。profile 落盘在 `/root/.camofox/profiles/<sha256(userId) 前 32 位>/storage-state.json`
(Playwright storageState:cookies + localStorage;sessionStorage 不保存,IndexedDB 缺省不保存),桌面镜像里目录 0700、文件 0600
(`svc-camofox` 设 `umask 077`,`init-umt-desktop` 对旧卷 `chmod -R go-rwx`)。**改了镜像,需重建 `umt-browser-desktop` 后生效**
(`docker compose build camofox && docker compose up -d camofox`);纯截图镜像 `camofox-browser:1.17.0` 不带插件,注入 / 回写都显示不支持。
打开会话(`POST /browser/sessions`、Agent `browser.navigate` 首次打开)时:
1. 匿名会话、API Key 类站点 / 账号(馒头:网页登录态与 API Key 无关)→ 不注入,`not_applicable`;
2. 取账号 Cookie:用户填的 + 自动登录得到的会话 Cookie,同名以会话 Cookie 为准;账密账号还没有会话 Cookie → 先走一次 refresh-login;
3. Cookie 头串转成站点主机的 host-only Cookie(路径 `/`,https 站点标 secure,期限 30 天——头串里没有期限,站点响应会按自己的期限覆盖),
**在开第一个标签页之前**写进 profile,再导航;
4. 注入了仍落在登录页 → `credential.status = expired`;Cookie 账号(没有密码可自动登录)转等待处理 `login_required`
("登录已失效:接管后在画面里登录一次,交还后全站生效"),账密账号留给 Agent 用凭证注入登录。
会话详情 `credential`:`status`、`source`(`manual` 手填 / `login` 自动登录 / `browser` 浏览器回写)、`sourceAt`、`count`(个数,不含值)、
`message`、`syncedAt`;原站视图顶栏显示"登录态:来源 · 时间"。
### 9.2 回写
| 时机 | 做法 |
| --- | --- |
| 人工接管交还 | 先按上游 `GET /tabs` 同步当前地址(桌面里的导航不经 API),不在登录页才读回 |
| 关闭会话 | 关闭前读回一次(站点 / 浏览器服务停用导致的关闭不读) |
| 定时 | 会话有动作时,距上次读回(或创建)满 10 分钟,后台读回一次 |
| refresh-login 发现 Cookie 失效 | Cookie 账号:从浏览器 profile 读回(不要求会话开着,读落盘文件),与存的不同就拿它验证一次,有效才回写并标 active |
读回只取作用于站点主机的、未过期的 Cookie(统计 / CDN / 第三方域名一律丢弃),剔除 Cloudflare 的 `cf_clearance` / `__cf_bm`(仅浏览器,§11.4),
与存储比较(忽略顺序),有变化才写(同时结束 UA 待切换,§11.3):
Cookie 账号替换 `secret_ref`,账密账号替换(或新建)会话 Cookie;`site_account.user_info.cookieOrigin = {source: "browser", by: "browser", at}`,
`expired` / `invalid` 的账号回到 `unknown`。来源判定:`cookieOrigin.at` 不早于 secret 最近写入(`rotated_at`,留 5 秒)才算浏览器回写,
之后再手填或自动登录会让它变回 `manual` / `login`。
安全:Cookie 值只在服务端、secret 表与浏览器服务之间传;日志只记个数;会话详情 / 能力输出 / 模型上下文里没有值;快照遮蔽照旧(§3)。
### 9.3 真机核验(2026-09-29)
`scripts/browser-verify/cookie-real.ts`:临时容器 `camofox-browser:1.17.0` + 挂载 umt-cookies 插件(与用户栈同一网络、同一出口代理),
读用户栈数据库里憨憨(hhanclub)账号的手填 Cookie(5 个),注入**全新** profile 后打开首页一次:HTTP 200、落在 `/` 不是登录页、
页面有 `a[href*=logout.php]`(站点定义的登录成功标记)→ 已登录;插件读回 5 个、全部是 `hhanclub.net` 域名、包含注入的全部名字。
对真实站点只有这一次导航,未截图。
没有核验的:桌面镜像重建后的端到端(接管 → 画面里登录 → 交还回写)只在 mock 上测过;其它三站;sessionStorage / IndexedDB 登录的站点
(Cookie 注入不覆盖这类登录态)。站点账号列表里显示登录态来源待前端接 `userInfo.cookieOrigin`。
## 10. 重启恢复(BR-3)
camofox 容器重启(`docker compose up -d` 重建、OOM、手动 restart)后,上游的标签页全部消失:旧 tabId 返回 410 `browser_restarted`
(或 404),`GET /tabs` 为空。数据库里的会话还在,app 自动把它接回来,不需要关闭重开。代码:`modules/browser/service.ts`
(`checkInstance` / `reconnectIfNeeded` / `doReconnect`、`run` 里的自愈)、适配器 `reopenSession` / `instanceId`、
前端 `components/browser/reconnect.tsx`。
### 10.1 怎么发现
| 途径 | 做法 |
| --- | --- |
| 被动(总是生效) | 任一会话操作(截图、快照、导航、点击、输入、提取、下载、视觉动作)收到 404 / 410,或适配器里已没有打开的标签页 → 按 `GET /tabs` 对账;会话一个活标签页都不剩 → 标 `needsReconnect`(`trigger = tab_gone`)并立即重连 |
| 主动(需要 umt 插件) | 会话详情 / 操作前查实例标识(`GET /umt/instance`,插件不在时退回 `/health` 里的 `instanceId` / `bootId` / `startedAt`),每个浏览器服务 5 秒最多一次;和会话打开时记下的 `recovery_state.instanceId` 不同 → 该服务下全部未关闭会话标 `needsReconnect`(`trigger = provider_restarted`),下一次操作或原站视图触发重连 |
纯截图镜像 `camofox-browser:1.17.0` 不带插件,只有被动发现;桌面镜像(缺省)两种都有,桌面模式因此也能在不截图的情况下发现重启。
### 10.2 重连做什么
1. 按会话的站点 / 账号准备 Cookie(与打开会话同一套,§9.1:匿名 / API Key 类不注入;账密账号没有会话 Cookie 先 refresh-login);
2. 写进该 profile(`POST /umt/sessions/{userId}/cookies`),**再**用同一 `userId` / `sessionKey` 开新标签页;
3. 导航到会话记录的 `current_url`(不在站点域名内或是脱敏过的地址 → 站点首页);
4. 旧的打开中标签页标 `lost`,换当前标签页;`needsSnapshot = true`(旧元素引用作废),记 `recovery_state.instanceId` 与 `reconnect`;
5. 重试原操作**一次**。
同一会话并发的操作共用一次重连。控制者与租约不变:Agent 的会话仍归 Agent;人工接管中重连,租约保留,会话详情 `reconnect.humanNotice = true`,
原站视图提示"浏览器重启过,页面已重新加载,请核对"(表单里没提交的输入、滚动位置都没了)。
失败(camofox 还没起来、`POST /tabs` 5xx、浏览器服务停用等):会话保持 `needsReconnect`,`reconnect.status = failed` 记原因,
接口返回 **503 `browser_reconnect_failed`**(消息带原因,不含凭据)。之后 10 秒内的操作直接报上次的原因,不反复打上游;
显式 `POST /browser/sessions/{id}/reconnect` 不受这个限制。
### 10.3 接口与界面
| 端点 | 说明 |
| --- | --- |
| `POST /browser/sessions/{id}/reconnect` | 幂等:强制查一次实例标识;没标需要重连时先对账,当前标签页还活着就什么都不做;否则按 §10.2 重建。不要求接管。返回会话详情 |
| `GET /browser/sessions/{id}` | 多了 `needsReconnect` 与 `reconnect {status, trigger, at, reason, humanNotice}` |
原站视图:截图模式收到 404 / 409(已回收)/ 503,或会话详情 `needsReconnect = true` → 显示"浏览器已重启,正在重新连接…",
自动调一次 `reconnect`,期间停掉截图轮询;成功后继续轮询;失败显示原因与"重新连接"按钮,不再每 1.5 秒刷错误(浏览器控制台只打一次 debug)。
桌面模式:同样按 `needsReconnect` 自动重连一次,成功(或别处触发的重连成功)后重建 Selkies 连接(iframe 重新加载)。
### 10.4 核验
- 自动化:`apps/server/src/modules/browser/__tests__/browser-reconnect.test.ts`(mock camofox `restart()`:标签页清空、旧 tabId 410、
实例标识换新、可选丢 profile)——截图自愈并注入 Cookie、回到原地址;实例标识变化 + `reconnect` 幂等;接管中重连保持租约;
失败 503 与退避;前端冒烟"原站视图 → 浏览器重启后自动重连(mock)"。
- 真机:未对真实 camofox 容器重启核验。桌面镜像要重建后才有 `GET /umt/instance`(`docker compose build camofox && docker compose up -d camofox`);
重建前只有被动发现(截图 / 操作时自愈),桌面模式要等一次截图或 Agent 操作才会重连。
## 11. 浏览器身份、站点请求 UA 与出口(BR-4)
同一个站点账号的登录态在浏览器(camofox / Firefox)和 HTTP 端(Deno)之间互注(§9),所以两端要**看起来是同一台设备**:
同一个 UA、稳定的指纹、同一个出口 IP。用户决定(2026-09-30):对外伪装 **Windows**;全实例**一套固定指纹**;
浏览器与 HTTP **按站点统一出口**。调研见 `docs/design/browser-ua-research.md`。
代码:插件 `docker/browser-desktop/root/opt/camofox/plugins/umt-cookies/{index.js,identity.js}`;
`apps/server/src/modules/sources-identity/{runtime.ts,service/user-agent.ts,service/credentials.ts,service/account.ts}`;
`apps/server/src/modules/browser/service.ts`(`identityUserAgent` / `refreshIdentity` / `applyEgress`);迁移 `0056_browser_identity`。
### 11.1 固定浏览器身份(插件 `browser:launching`)
- **首次启动**:插件用 camoufox-js 的 `launchOptions({ os: "windows" })` 生成一份完整指纹——BrowserForge 的 Windows Firefox
指纹(navigator / screen / 硬件)、Camoufox 的 Windows 字族表、按 Windows 取样的 WebGL(vendor / renderer / 参数)、
字体间距 / 音频 / canvas 随机种子、`window.history.length`、canvas 抗锯齿偏移——去掉运行期字段(`humanize`、`locale:*`、
`geolocation:*`、`timezone`、`webrtc:*`、`addons`)后落盘 **`/root/.camofox/umt-identity.json`**(compose 的持久卷 `umt-camofox`,
与 profile 同一个卷;属主 abc、0600)。WebGL2 开关等配套 Firefox 首选项一起存。
- **之后每次启动**(容器重启、浏览器空闲 5 分钟退出后再启动):把上游随机生成的 config 里的指纹部分整份换成身份文件里的,
运行期字段仍取上游。浏览器进程日志 `umt identity applied {os, fingerprintId, revision}`。
- **升级**:Camoufox 版本或 Firefox 主版本与文件里记的不同 → 只改 `navigator.*` / `headers.*` 里的版本号(与 camoufox-js 生成时
同一个替换规则),新版本多出的指纹字段从同 OS 新样本补齐,其余一律保留;`revision + 1`,`fingerprintId` 不变
(日志 `umt identity upgraded`)。站点看到的是"同一台电脑升级了 Firefox"。
- **重置**(换一台"新设备",所有站点都会看到新设备):停 camofox,删 `umt-identity.json`,再启动。
- **开关与路径**:`UMT_BROWSER_IDENTITY=off` 关掉(回到上游每次随机的 Linux 指纹);`UMT_IDENTITY_FILE` / `UMT_STATE_DIR` 改路径。
文件损坏会改名 `.bad-<时间>` 后重新生成。
- **`GET /umt/identity`**(访问凭据保护):`{userAgent, os, platform, firefoxVersion, camoufoxVersion, fingerprintId, revision,
schema, createdAt, updatedAt}`,不含指纹细节。app 存进 `browser_provider.identity`(加 `fetchedAt`):打开会话时(10 分钟内读过就不读)、
重连时、设置页探测浏览器服务时读;站点请求取它时超过 6 小时会重读,读不到 5 分钟内不再试。
**Windows 伪装的一致性**(camoufox-js 0.11.5 源码 + 2026-09-30 本地临时容器实测,只开本地页面):
| 项 | 结果 |
| --- | --- |
| UA / 请求头 | `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0`;HTTP `User-Agent` 头同值 |
| navigator | `platform = Win32`、`oscpu = Windows NT 10.0; Win64; x64`;并发数、语言等来自同一份 Windows 指纹 |
| WebGL | 按 Windows 取样:`Google Inc. (Intel)` / `ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0)`(D3D11,Windows 才有) |
| 字体 | 字族白名单 = Camoufox 的 Windows 字体表;字形来自 Camoufox 自带的 Windows 字体(`/opt/camoufox/fonts/windows`:Arial、Segoe UI、微软雅黑、宋体、等线……)。实测"微软雅黑"可用且宽度与回退字体不同,Noto / DejaVu 这类 Linux 字体不可见;中文页面以微软雅黑渲染 |
| 跨重启 | 重启容器后 `fingerprintId`、屏幕尺寸、WebGL 不变;模拟 140 → 152 升级只改了 UA 版本 |
发现并修正的上游问题:camoufox-js 0.11.5 把 `FONTCONFIG_PATH` 设成 `/opt/camoufox/fontconfig/lin`(不存在;Camoufox 152 的目录是
`linux` / `macos` / `windows`),而 fontconfig 里的字体目录写的是相对当前目录的 `fonts`(浏览器进程当前目录是 `/opt/camofox`)。
插件把 `fontconfig/windows/fonts.conf` 复制到临时目录、字体目录改成 `/opt/camoufox/fonts` 绝对路径,再指给 `FONTCONFIG_PATH`。
限制:渲染仍是 Linux 上的 Firefox——字体光栅化、canvas / 音频的像素级结果与真 Windows 不同(Camoufox 用固定种子加噪,稳定但不是
Windows 的真值);TLS / HTTP2 指纹是 Firefox 152 本身(与 OS 无关)。屏幕尺寸来自指纹(如 2560×1440),桌面模式窗口是 1280×900,
相当于没最大化的窗口。时区跟容器 `TZ`(缺省 Asia/Shanghai);语言跟指纹(缺省 en-US),要中文设 `CAMOFOX_LOCALE`。
指纹是进程级的:所有站点 / 账号共用这一套(用户决定;每账号一套要每账号一个浏览器进程,不做)。
### 11.2 站点请求 UA
站点请求的 UA 按账号解析,**优先级从高到低**:
| 来源(`SiteAccount.userAgentSource`) | 取值 | 界面 |
| --- | --- | --- |
| `manual` | 账号手动填的 UA(`site_account.user_agent`) | 手动填写 |
| `pending` | 待切换:取得当前登录态时用的旧 UA(`site_account.ua_pending`,§11.3) | 待切换:… |
| `global` | 设置 → 站点默认 → User-Agent(`settings.sites.userAgent`,非空时) | 使用设置里的全局 UA |
| `browser` | 浏览器固定身份的 UA(`browser_provider.identity.userAgent`);读不到时用常量 `FALLBACK_SITE_USER_AGENT`(同型 Windows Firefox 152) | 跟随浏览器 |
- 覆盖面:站点适配器(列表 / 搜索 / 详情 / 下载 .torrent / 健康检测)、RSS、账密登录、用户数据、Cookie 失效时的浏览器读回验证——
全部经 `SourcesRuntime.siteFetch`:请求自己没设 UA 就补上;适配器每条请求现场按"与站点凭证同一个账号"(第一个启用、未失效、非 Passkey)
取 UA,与 Cookie 同一时刻读库,不缓存。之前适配器与 RSS 发的是 Deno 缺省 UA `Deno/2.9.7`,已不再出现
(测试 `apps/server/test/sources-identity/user-agent.test.ts` 逐条断言)。不挂站点的请求(通用 RSS)用全局 UA / 跟随浏览器。
- 设置里的 User-Agent **缺省留空 = 跟随浏览器**(原缺省 Windows Chrome 140 已去掉);填了会让 HTTP 与浏览器 UA 不一致,只在确有需要时用。
- 账号"更新凭证"里的 UA 留空 = 不覆盖;Cookie 是从别的浏览器复制的、且站点校验 UA 时才填那个浏览器的 UA。
- 不在范围:TMDB、图片代理(不带凭据的公开请求)、下载器、浏览器服务本身。
- 升级 camofox 镜像(Firefox 大版本变)时:浏览器身份自动升级(§11.1),app 最迟 6 小时(或下次打开会话 / 重连)跟上;
同时把 `packages/contracts/src/settings.ts` 的 `FALLBACK_SITE_USER_AGENT` 改成新版本号。
### 11.3 平滑迁移(待切换)
改 UA 一般不会让 NexusPHP 的 Cookie 失效,但前置 Cloudflare / WAF 的站点可能要求重新过挑战。所以**不立即给存量账号换 UA**:
- 迁移 `0056_browser_identity`:Cookie / 账密账号(有凭证、没手动 UA)记 `ua_pending` = 取得当前登录态时的 UA——设置里改过全局 UA
就是那个值,否则是旧缺省 Chrome 140;API Key / Passkey 账号与网页登录态无关,直接跟随浏览器;设置里的 UA 仍是旧缺省值时删掉
(= 跟随浏览器),改过的保留为全局 UA。
- 待切换期间,这个账号的所有站点请求(包括原来发 Deno UA 的适配器 / RSS)都用旧 UA,与登录态一致。
- **切换时机**(Cookie 与 UA 同一事务写,下一条站点请求两者一起生效):
1. 重新登录:账密账号 refresh-login 走 takelogin 时用切换后的 UA,拿到会话 Cookie 后清 `ua_pending`;
2. 浏览器回写 Cookie(§9.2 的交还 / 关闭 / 定时 / refresh-login 读回):浏览器是用自己的 UA 取得 / 使用这份登录态的,回写时清 `ua_pending`;
Cookie 没变但浏览器不在登录页、正用着这份登录态,也清;
3. 换主凭证(填新 Cookie / 新密码):新登录态,直接跟随浏览器。
- Cookie 账号如果一直不开原站视图、也不换 Cookie,会一直待切换(界面写"待切换:下次刷新登录,或在原站视图里用过后改为跟随浏览器")。
刷新登录对 Cookie 账号只校验,不切换。
### 11.4 Cloudflare Cookie 仅浏览器(CF-2 起有条件给 HTTP)
`cf_clearance`、`__cf_bm`(及挑战过程中的 `cf_chl_*`)与取得它的浏览器绑定(UA + IP + TLS 指纹),Deno 的 TLS 指纹与 Firefox 必然不同,
HTTP 端拿去用多半无效还可能触发风控。所以:
- 浏览器回写账号时剔除这些 Cookie,只在 `site_account.user_info.browserOnlyCookies` 记名字(不含值);它们留在浏览器 profile 里照常用;
- 比较"有没有变化"时也不算它们(CF Cookie 轮换不会触发回写);refresh-login 从浏览器读回的 Cookie 同样剔除;
- 站点账号页技术信息里显示"仅浏览器使用的 Cookie";
- 用户手填的 Cookie 里带 `cf_clearance` 不处理(用户自己的选择)。
- **CF-2 例外**:站点遇 Cloudflare 挑战、浏览器过了之后,账号 UA 来源 = 跟随浏览器、浏览器 UA 与 HTTP UA 相同、两端出口相同时,
`cf_clearance`(只有它,`__cf_bm` 仍不带)单独存进 `site_account.user_info.cloudflare`(值在 secret)并由 HTTP 带上重试 1 次,
结果按站点记 `cfReuse`;普通回写仍剔除它(见 sites.md「Cloudflare(CF-2)」)。
### 11.5 出口 IP 按站点统一
实测(本地临时容器):**Firefox 会用容器的 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量**(Firefox 缺省"使用系统代理",Linux 下没有
GSettings 就读环境变量)。所以本机栈此前的状态是:`docker-compose.override.yml` 给 camofox 设了 `HTTP_PROXY=http://172.17.0.1:7891`,
浏览器**所有**站点都走代理;HTTP 端只有开了"使用代理"的站点走代理——同一账号两端出口 IP 不同。
现在(`UMT_BROWSER_PROXY_MODE=site`,缺省):
- 插件在启动时设 `network.proxy.type = 0`:浏览器**不再用**容器环境变量里的代理,缺省直连;
- app 在打开会话 / 重连前调 `PUT /umt/sessions/{userId}/proxy`:站点开了"使用代理"且设置了代理地址 → 该站点 profile 的
BrowserContext 走同一个代理(Playwright 按上下文代理,Firefox / Juggler 原生支持;URL 里的账号口令拆成 username / password);
否则 `null` 直连。插件把这张表存在 `/root/.camofox/umt-proxies.json`(0600,代理带口令时含口令),camofox 重启后照用;
- 该 profile 已有活动上下文、出口不同 → 插件先关掉它(持久化插件先落盘登录状态),下一次操作按新出口重建;
- 实测:直连 profile 访问测试域名走不到环境变量里的代理;代理 profile 的请求到了指定代理;改出口时活动上下文被重建。
限制与注意:
- **代理地址要让 app 与 camofox 两个容器都能连**(如本机的 socat 中继 `http://172.17.0.1:7891` 可以;`http://127.0.0.1:7890` 在
camofox 容器里连不到)。
- 站点"使用代理"开关或代理地址改了,已打开的浏览器会话在**下一次打开 / 重连**时才按新出口;要立即生效就关掉会话重开。
- 匿名会话与 Agent 打开的站点会话同样按站点出口(浏览器会话都属于某个站点)。
- 旧镜像 / 插件没有出口接口:app 只记日志,浏览器照旧按环境变量出网。`UMT_BROWSER_PROXY_MODE=env` 可退回旧行为。
- SOCKS 代理的 DNS 解析位置未核验(HTTP 代理由代理端解析,与 Deno 的代理客户端一致)。
- 本机栈重建 camofox 后,憨憨(未开代理)的浏览器会话会从"经代理"变为**直连**,与 HTTP 端一致;override 里 camofox 的
`HTTP_PROXY` 可以留着(Firefox 不再用它),也可以删掉。
### 11.6 上线步骤与核验
需要重建 **app**(代码 + 迁移 0056)和 **camofox**(插件):
```sh
docker compose --env-file .env.deploy build app camofox
docker compose --env-file .env.deploy up -d app camofox
docker compose exec camofox curl -s -H "Authorization: Bearer $CAMOFOX_ACCESS_KEY" http://127.0.0.1:9377/umt/identity
```
camofox 镜像只有 `COPY root/ /` 与其后改插件清单的一层变化;前面的 apt 层与从 `camofox-browser:1.17.0` 复制 node / camofox /
Camoufox 浏览器包的层都走构建缓存,**不会重新下载 Camoufox(约 630 MB)**。2026-09-30 在本机用临时标签实测:带
`--build-arg APT_MIRROR=mirrors.ustc.edu.cn`(与 `.env.deploy` 相同)时 apt / 复制层全部 `CACHED`,只重跑插件两层(< 1 秒)。
注意**构建参数要与上次一致**(用同一个 `--env-file .env.deploy`):不带 `APT_MIRROR` 构建会让 apt 层缓存失效,重新 apt 安装(浏览器包
仍从本地 `camofox-browser:1.17.0` 复制,不从网上下)。首次启动生成身份,之后 profile 卷里的登录状态照常可用。
核验:`apps/server/test/sources-identity/user-agent.test.ts`(每条出站请求的 UA、优先级、待切换、CF Cookie)、
`migration-browser-identity.test.ts`、`apps/server/src/modules/browser/__tests__/browser-identity.test.ts`(出口下发、身份存库)、
`packages/adapters/src/browser/umt-plugin.test.ts`(插件纯函数、适配器接口);真机:本地临时容器(`--network none`,只开容器内本地
页面)核验了 §11.1 表格与 §11.5 出口行为,未对真实站点发请求。
## 12. 会话保活(AG-4)
camofox 按 **API 调用**判断活动,有三道空闲回收(`docker-compose.yml` 的 camofox 环境变量):
| 回收 | 缺省 | 判据 |
| --- | --- | --- |
| 标签页 `TAB_INACTIVITY_MS` | 5 分钟 | 每 60 秒看一次标签页的 `toolCalls` 有没有变;没变且超过 5 分钟 → 关标签页,会话空了连会话一起关 |
| 会话 `SESSION_TIMEOUT_MS` | 30 分钟 | 会话 `lastAccess`(大部分 `/tabs/{id}/*` 接口都会刷新) |
| 浏览器 `BROWSER_IDLE_TIMEOUT_MS` | 5 分钟 | 一个会话都没有之后浏览器进程退出 |
桌面模式里人的键鼠直接进 Selkies,原站视图截图(`GET /tabs/{id}/screenshot`)只刷新 `lastAccess`、不加 `toolCalls`——
接管 5 分钟后标签页被关掉,桌面画面里的窗口消失。app 在"正在用"的会话上定期保活,代码 `modules/browser/keepalive.ts`、
`BrowserService.keepAlive`、适配器 `keepAlive`。
**保活调用**:camofox 1.17 没有 keepalive / touch 接口;`GET /tabs/{id}/stats` 只刷新 `lastAccess`,挡不住标签页回收。
用 `GET /tabs/{id}/downloads?userId=…`(不带 `includeData` / `consume`):只列下载元数据,不读页面、不改页面、不清下载记录,
会让 `toolCalls` 加一、刷新 `lastAccess`。只对当前标签页做,不要求控制权,不记 `lastAutomationAt`。
**什么时候保活**(进程内登记,各进程保活自己登记的):
| 使用方 | 开始 | 停止 |
| --- | --- | --- |
| 桌面连接 | 桌面模式 WebSocket 建立(控制或只读都算,多个连接按引用计数) | 全部连接断开 |
| 人工接管 | `control/acquire` | 交还(`control/release`)、租约过期、会话关闭 |
| 智能体会话 | 智能体会话里的浏览器能力调用(`browser.*`,含 `browser.navigate` 新开会话) | 智能体会话不再是排队 / 运行中(结束、等待下载或等用户),或最后一次使用后 30 分钟 |
每 `BROWSER_KEEPALIVE_MS`(缺省 120000 = 2 分钟,0 = 关闭;要比 `TAB_INACTIVITY_MS` 短)对登记的会话各做一次;
每轮先复查登记是否还有效,没有使用方的会话不再保活,照常由 camofox 空闲回收。会话关闭(`DELETE`)立即撤销全部登记。
**保活失败**:保活走与其它动作相同的 `run()` 路径——浏览器重启过(实例标识变了)或标签页已被回收(404 / 410、适配器里没有打开的标签页)
→ 按 §10 自愈重连(注入 Cookie、同一会话新开标签页回到原地址)后在新标签页上再保活一次;重连也失败(浏览器服务不可用)只记 warn 日志
「浏览器会话保活失败」,会话照常标 `needsReconnect` / `reconnect.status = failed`,下一轮再试。
核验:`apps/server/src/modules/browser/__tests__/browser-keepalive.test.ts`(登记 / 引用计数 / 复查;对 mock camofox:接管期间保活、
不记自动化活动、交还后停;桌面连接期间标签页被回收 → 保活时自愈重连;智能体会话结束后停、会话关闭撤销)。真机未核验。
## 13. Cloudflare 挑战(CF-2)
站点 HTTP 遇挑战时由站点请求一侧(sources-identity `CloudflareGate`)经钩子调用浏览器模块,流程与状态见 sites.md「Cloudflare(CF-2)」。
浏览器这边:
- `solveSiteChallenge({siteId, accountId, url})`:该账号 profile 的自动化会话(没有就按站点打开;人工接管中 / 等人处理中不打扰)导航到
同一地址(取站点令牌)→ 用 `POST /tabs/{id}/evaluate` 读页面(地址、标题、outerHTML、正文样本、`navigator.userAgent`,适配器
`pageContent`)按 `classifyChallengePage` 分类:自动(JS 检查)/ 可点(Turnstile、「确认您是真人」)/ 真验证码(hCaptcha、reCAPTCHA、
选图、输码)→ ① 每 1.5 秒读一次,最多 15 秒 → ② 可点的派视觉子流程(`browser.solve_challenge`,系统发起:发起人 `system:cloudflare`、
trigger event、没有父会话;步数 4、时长 ≤ 120 秒、token / 费用取 `agent.maxTokensPerRun` / `maxCostUsdPerRun`;最多 2 次;
自动化总开关关闭或只读对账时不派)→ ③ 没过:会话转等待 `cf_challenge`,待处理事项「<站点>需要你在原站视图里过一次验证」
(视觉子流程失败留下的 `challenge_unsolved` 事项改成这个说法)。通过后读回 profile 里的 `cf_clearance`、UA 与会话出口(`recovery_state.egress`,
插件按站点下发出口成功才有值)。
- `fetchSiteHtml({siteId, accountId, url})`:同一会话导航(取站点令牌)后读 outerHTML;仍是挑战页 / 会话在人工处理 / 站点停用时不返回页面。
- 同一 profile 的过挑战与代抓在进程内串行(profile 锁);跨进程同站 30 分钟一次靠站点状态的条件更新。
- 交还会话(给自动化)时:站点在等人过验证 → 读回 `cf_clearance` / UA 通知站点请求继续(`CloudflareGate.afterHuman`),待处理事项随交还关闭。
- 插件新端点 `POST /umt/tabs/{tabId}/mouse-click`:`click_at`(与原站视图截图模式的坐标点击)都先走它。**需要重建 camofox 镜像**才有
(`docker compose build camofox && docker compose up -d camofox`,只重跑插件层);没重建时退回 elementFromPoint,Turnstile 点不到,
可点类挑战会转人工。
核验:mock camofox(`cloudflare` 模式:auto / interactive / captcha)见 `browser-cloudflare.test.ts`、`umt-plugin.test.ts`、E14。
插件鼠标点击的真实容器核验见下表(本地临时容器,只开本地页面,不访问真实验证页)。
| 日期 | 做法 | 结果 |
| --- | --- | --- |
| 2026-09-30 | 临时容器 `umt-cf2-camofox`(镜像 `umt-browser-desktop:1.17.0`,只读挂载新插件目录),容器内 node 起两个本地页面:父页 `localhost:8001`(标题 Just a moment...)内嵌**跨域** iframe `localhost:8002`(复选框,`change` 事件 `isTrusted` 才向父页 postMessage);适配器 `openSession` → `pageContent` → `click({x, y})` → `pageContent` | 插件路径:`POST /umt/tabs/{id}/mouse-click` 命中复选框,iframe 里收到可信事件,父页标题变为 passed;`pageContent` 读到 UA `…Windows NT 10.0; Win64; x64; rv:152.0…`。旧路径(elementFromPoint)只能标到 iframe 元素本身、按元素中心点击,点不到指定坐标。未访问任何外部站点,容器已删除 |
## 16 2026-10-01 的 1.18 升级复测
当前默认桌面镜像为 `umt-browser-desktop:1.18.0`,基础镜像 `camofox-browser:1.18.0` 锁定上游 `f94c81babf4bfd44cdd4bd46e1f2e1fb1d91c753`。Node 24.21.0、Camoufox 152.0.4-beta.30、camoufox-js 0.12.0;Playwright 保持官方协议可兼容的 1.60.0,原因和独立锁见 [构建说明](../../docker/camofox/README.md)。
用生产 profile 与桌面配置的独立副本验证了鉴权、原身份保持、Cookie 正常重启落盘、上下文/页面、evaluate、快照、PNG、选择器与可信坐标点击、profile 隔离。现有真实 BrowserService/Agent 能力核验脚本 `scripts/browser-verify/verify.ts` 的 setup/a/b/idle/c/d 共 25 项严格通过,包含登录、脱敏、提取、下载文件哈希及资源关联、正常重启与空闲回收自动重连、旧引用拒绝、SIGKILL 未落盘登录丢失识别。适配器能力表现已登记 1.18.0 的原十项能力,未知版本不继承核验结果。
完整执行与恢复/清理状态见 [升级记录](../plan/17-full-upgrade.md),机器可读结果见 [验证证据](../plan/evidence/2026-10-01-dependencies/upgrade-verification.json)。这些检查不增加真实 PT 挑战、ARM64 或精确 URL 恢复的验收声明。SIGKILL/OOM 后仅能使用上次落盘状态的限制继续保留。
### 桌面上下黑边排查记录
2026-10-01 已修复底座初始化脚本生成无效 labwc XML 的问题;部署镜像为 `umt-browser-desktop:1.18.0-umt.1`。初始化在 `init-selkies-config` 完成后写入完整 XML,浏览器窗口自动最大化。实际视频尺寸与前端 iframe 同为 1280×900;前后串流截图确认上下黑边消失。若需要调整桌面尺寸,可改 `BROWSER_DESKTOP_WIDTH/HEIGHT`,不要以拉伸或裁剪视频修正窗口布局。证据见 [发布记录](../plan/18-organize-release.md#v0311-搜索详情与桌面黑边修复)。