Files
UHD_Media_Tool/docs/runbook/browser.md
T
rootandClaude Opus 5.5 40dd0e69e4 feat(browser): ship camofox v1.17.0 in compose, verified on a real instance
- compose: camofox service on by default (internal only, volume umt-camofox,
  healthcheck, CAMOFOX_REPLICAS=0 to skip); app depends on it with
  service_started/required=false and auto-registers camofox-env from
  BROWSER_PROVIDER_URL / BROWSER_ACCESS_KEY / BROWSER_ENABLED
- docker/camofox/Dockerfile: builds upstream tag v1.17.0 (389c996); fixes the
  upstream Dockerfile (missing postinstall.js before npm ci, sh -c not
  forwarding SIGTERM so logins were never persisted on restart)
- adapter fixes from the real container: coordinate click via evaluate +
  selector, focus typing with mode=keyboard, password values redacted from
  ARIA snapshots/extract, downloads field names, 410 after browser restart
  treated as lost tab; navigate reopens a tab in the same session
- verified-capability table keyed by upstream version (1.17.0 all ten);
  other versions stay unverified; probe reports auth failures
- settings: browser providers block (create/edit/delete/probe, capability
  table); API POST/PATCH/DELETE /browser/providers, POST .../probe
- scripts/browser-verify: umtbr stack + mock NexusPHP run of REQ 4.7.4 items
- docs: runbook browser §1/§7 (real-instance record), deploy §2.1

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

17 KiB
Raw Blame History

浏览器(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,只在进程内,不入库), 并在后台探测一次能力。地址与凭据只能改部署配置;设置页只能改它的启用。

# .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 用 docker/camofox/Dockerfile 从 git tag v1.17.0 构建(修了上游 Dockerfile 的两个问题,见文件头)。 首次构建要从 GitHub 拉源码与约 630 MB 的 Camoufox 浏览器包;网络不好时先在本机构建同名镜像,compose 会直接用:

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=...

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)只接受持有自己人工租约的调用者; 快照、截图、会话详情随时可读。截图只给原站视图,modelSafe=false 的不进模型。

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 元素引用已失效…先重新读取页面。

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 全部标签页并落盘。

没有核验的:四个真实站点(观众 / 馒头 / HHClub / CHD)的登录保持、Cloudflare / 验证码挑战、真实站点页面上的提取对比; 并发 1 / 2 / 4 会话的性能与资源;原站视图界面本身在浏览器里的点击体验(1.5 秒截图刷新);VNC 插件(镜像已装,未启用、未接入)。