Files
UHD_Media_Tool/docs/design/browser-provider.md
T
rootandClaude Opus 5.5 b343dddbf6 docs: 清理过程文档,CHANGELOG 补齐 0.3.8–0.3.14
- 删除 docs/plan 下 00–18 的计划、评审、验收、发布记录与升级证据,docs/spikes 与 spikes 验证代码
  (保留 spikes/movieclaw-src 本地克隆),以及 4 份一次性审查 / 调研;全部可在 ceb66f8 查到
- MP 对照移到 docs/porting/moviepilot-v3-comparison.md;经验教训改为 docs/plan/lessons.md(补第 25 条)
- docs/plan/README.md:文档放哪、当前状态、待办
- 引用改写:代码注释指向在维护的文档,其余注明已删除与 git 位置;清掉原有 6 个失效链接
- README 开头改为当前状态与入口;CHANGELOG 补齐 0.3.8–0.3.14 与工程变更

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

102 lines
12 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.
# BrowserProvider 合同与 camofox-browser 适配
对应 REQ v0.7 §4.4、§4.7.2–4.7.4、§12.1 浏览器实体、附录 S18–S20;计划 `00-first-version-plan`(已删除,见 git `ceb66f8`) §3.2:v0.1 只落接口,浏览器接入与原站视图在 M2。
代码位置:
| 层 | 路径 |
| --- | --- |
| 合同(zod) | `packages/contracts/src/browser.ts` |
| Provider 接口与实现 | `packages/adapters/src/browser/**`(`provider.ts`、`camofox/`、`none.ts`、`redact.ts`、`profile-lock.ts`、`budget.ts`) |
| mock camofox | `packages/testkit/src/browser/mock-camofox.ts` |
| 表结构 | `packages/db/migrations/0023_browser.ts` |
## 1. Playwright 不是产品浏览器能力
产品内所有浏览器动作(站点排障、原站视图、Agent 受控交互、下载捕获)只通过 `BrowserProvider` 访问浏览器服务,首个实现对接用户已经运行的 **camofox-browser**。仓库里的 Playwright 只用于前端截图/视觉测试,不作为产品能力、不在服务端启动、不作为 camofox 不可用时的回退。不得把"另起一个浏览器完成了操作"标为同会话协作(REQ §4.7.2)。
## 2. 合同要点
- **BrowserProviderKind**:`camofox | none`。`none` 为默认,能力全关,动作报 `unavailable`,界面据此显示"浏览器不可用"。
- **BrowserCapabilities**:`sessions / tabs / navigate / snapshot / screenshot / interact / extract / downloadCapture / takeover / persistentProfile`,每项是 `{ supported, verified, source, note? }`:
- `supported`:本适配器当前对该上游能提供(上游有端点但适配器尚未实现的也记 false,并注明 M2);
- `verified`:只来自真机核验 —— 适配器内置的按版本核验表 `CAMOFOX_VERIFIED_RELEASES`(探测到完全相同的上游版本才生效)
与 `config.verified` 逐项覆盖;探测本身永远不会把它设为 true;
- `source`:`static`(按文档声明)或 `probed`(从上游 `/openapi.json` 推导);
- 另有 `upstreamVersion`、`probedAt`。界面只把 `supported && verified` 视为可用(REQ §4.7.4)。
- **BrowserSession**:`providerId, siteId, accountId, profileKey, controller, currentUrl, lastActivityAt, upstreamRef, recoveryState, closedAt, closeReason`。`controller ∈ automation | human | waiting | closed`;非 `automation` 时适配器拒绝自动化动作(`control_conflict`)。
- **BrowserTab**:上游标签页映射(`upstreamTabId`、`url`、`status open|closed|lost`、`lastSnapshotAt`)。
- **BrowserControlLease**:`sessionId, holderKind(automation|human), holder, reason, status, acquiredAt, expiresAt, releasedAt, releaseReason`;每会话至多一条 `active`(DB 部分唯一索引)。
- **NavigationBudget**:`domainAllowlist[]`(含子域;空 = 全拒)、`timeoutMs`、`maxRetries`(只重试超时/网络/5xx/503)、`maxBytes`(快照截断、截图超限报 `budget_exceeded`)。导航前检查目标,导航后检查落地地址(重定向出白名单报 `domain_blocked`)。
- **BrowserSnapshot**:可直接交给模型的结构 —— 脱敏后的可访问性文本(元素以 `eN` 引用)、脱敏 URL、表单/表格摘要、`redactions` 计数、分页 `nextOffset`。不含原始 HTML/脚本。
- **DownloadArtifact**:`id, sessionId, filename, mimeType, sizeBytes, bytes: BlobRef{kind: blob|secret, ref}, sha256, capturedAt`。文件内容只以引用出现(种子可能含 passkey),交后端暂存核验后再走原有提交链路。
- **BrowserErrorKind**:`not_implemented, unavailable, auth, timeout, network, not_found, rate_limited, bad_request, upstream, protocol, domain_blocked, budget_exceeded, control_conflict, lock_timeout`。
### 安全约束
- 访问凭据(camofox `CAMOFOX_ACCESS_KEY`)只以 `browser_provider.secret_ref` 保存,每次请求经 `BrowserSecretResolver` 解出;日志只记方法/路径(不含 query)/状态/耗时;错误信息不含凭据。
- 站点登录凭据经 `TypeInput { kind: 'secret' }` 由服务端解析后直接下发到输入框,不进入模型(M2)。
- `redactSnapshot`:遮蔽密码框值(a11y 文本、HTML、structure JSON 三种形态)、Cookie 值(`Cookie:` 行与常见站点会话 Cookie 名)、URL 凭据参数(passkey/authkey/rsskey/token/…)、路径形式 passkey 与 URL userinfo。camofox 快照中密码框的真实呈现方式未核验,规则宁可多遮。
- 截图暂不做像素级遮蔽:登录页或含凭据地址的截图标 `modelSafe: false`,只给人看不喂模型。
- 同一 `profileKey` 的操作串行(`ProfileLock`,当前进程内实现;多进程部署需换 PG advisory lock 或基于 `browser_control_lease` 的持久化实现)。
- 浏览器没有媒体库写权限;人工接管不是对其他站点、宿主终端或文件系统的授权。
## 3. camofox-browser OpenAPI 对照
来源:`https://raw.githubusercontent.com/jo-inc/camofox-browser/master/openapi.json`(`info.version` 1.17.0)与同仓 README,2026-09-27 取得。
**2026-09-28 已对 v1.17.0(commit `389c996`)真实容器逐项核验**,部署随 compose 自带该版本(`docker/camofox/Dockerfile`);
实测与下表文档描述的差异(截图是 `image/png` 二进制、click 不接受坐标、焦点输入要 `mode=keyboard`、ARIA 快照里密码框带明文值、
downloads 字段名、重启后旧标签页 410)已在适配器里修正,见 `docs/runbook/browser.md` §7。
下表"状态"列是 C1-E 当时的记录;现在全部方法已实现并在真机上通过。
上游模型:一个 Camoufox 实例 → `userId` 对应一个隔离 BrowserContext(cookies/storage)→ `sessionKey` 为标签组 → 标签页。本项目以 `profileKey`(站点/账号)作 `userId`,以 `browser_session.id` 作 `sessionKey`。认证:设置 `CAMOFOX_ACCESS_KEY` 后,除 `/health`、cookie 导入、`/stop` 外全部路由要求 `Authorization: Bearer <key>`;`CAMOFOX_API_KEY` 另管 cookie 导入与 traces。
| 合同方法 | camofox 端点 | 状态 |
| --- | --- | --- |
| `health` | `GET /health`(200 `{ok, engine, browserConnected, browserRunning, activeTabs, activeSessions, consecutiveFailures}`;503 `{ok:false, recovering}`) | 已实现(mock 验证) |
| `capabilities` | `GET /openapi.json`(按 paths 推导) | 已实现(mock 验证) |
| `openSession` | `POST /tabs {userId, sessionKey, url?}` → `{tabId, url, httpStatus, navigationOk}`;上游无显式建会话端点 | 已实现(mock 验证) |
| `navigate` | `POST /tabs/{tabId}/navigate {userId, url}` → `{ok, tabId, url, httpStatus, navigationOk}` | 已实现(mock 验证) |
| `snapshot` | `GET /tabs/{tabId}/snapshot?userId&format=text&offset` → `{url, snapshot, structure?, refsCount, truncated, hasMore, nextOffset}` | 已实现(mock 验证) |
| `screenshot` | `GET /tabs/{tabId}/screenshot?userId` → `{screenshot:{data(base64), mimeType}}`;兼容直接返回 `image/*` | 已实现(mock 验证) |
| `closeSession` | `DELETE /sessions/{userId}`(关闭该 userId 全部标签);或 `DELETE /tabs/group/{listItemId}` | M2 |
| `listSessions` | `GET /tabs?userId`(只有标签页) | M2,以 DB 为准 |
| `click` / `type` | `POST /tabs/{tabId}/click {ref|selector}`、`/type {ref|selector, text, clear, submit}`、`/select`、`/press` | M2 |
| `extract` | `POST /tabs/{tabId}/extract {schema}`(属性带 `x-ref`,需先 snapshot) | M2 |
| `waitForDownload` | `GET /tabs/{tabId}/downloads?includeData&consume&maxBytes`(README;OpenAPI 未列这些 query) | M2 |
| `acquireControl` / `releaseControl` | 无;VNC 插件(`ENABLE_VNC`、noVNC 6080)提供整块虚拟显示,不在 OpenAPI 中 | M2,原型验证 |
### 与设计的差异 / 待核验
1. 上游**没有会话概念的建立端点**:会话在首个 `POST /tabs` 时隐式创建,由 `SESSION_TIMEOUT_MS`(默认 10 分钟,README 架构节写 30 分钟,自相矛盾)空闲回收;标签页 `TAB_INACTIVITY_MS` 默认 5 分钟回收,且达到上限时"最旧标签被自动回收"。本项目的会话/标签状态必须以 DB 为准并定期对账,不能假定上游会话长期存在。
2. `userId` 允许字符集、长度未在 OpenAPI 定义(`profileKeyFor` 生成 `umt-site-<uuid>-acct-<uuid>`),待核验。
3. 快照是可访问性树文本(`[button e1] Submit`),**密码框的值是否出现、以什么形式出现未核验**;`format=json` 与 `structure` 的具体字段也未核验。
4. 截图 OpenAPI 为 JSON base64;是否有版本直接返回二进制未核验(适配器两者兼容)。
5. README 的 type 示例用 `pressEnter`,OpenAPI 用 `submit`。
6. 接管:上游没有 API;VNC 插件是整块显示,能否定位到同一会话/标签、画面认证能否接产品认证,需原型;`CAMOFOX_INTERACTIVE=desktop` 只适用于本机桌面。
7. 持久化:持久化插件默认保存 cookies + localStorage 到 `~/.camofox/profiles`,sessionStorage 不保存,IndexedDB 需显式开启;"重启后仍登录"要逐站验证(REQ §4.4)。
8. 下载捕获的 `includeData` 返回 base64,大文件内存占用与 `maxBytes` 行为待验证。
## 4. 表结构(0023)
- `browser_provider`:`name, kind, base_url, secret_ref, enabled, capabilities jsonb, verified_at, health_status, last_health_at, config`;`kind <> 'none'` 时必须有 `base_url`。
- `browser_session`:`provider_id, site_id, account_id, profile_key, controller, current_url(脱敏), last_activity_at, upstream_ref, recovery_state, closed_at, close_reason`;`(provider_id, profile_key)` 在未关闭会话中唯一;`controller = 'closed'` 与 `closed_at` 一致。
- `browser_tab`:`session_id, upstream_tab_id, url, title, status, last_snapshot_at, closed_at`;`(session_id, upstream_tab_id)` 唯一。
- `browser_control_lease`:`session_id, holder_kind, holder, reason, status, acquired_at, expires_at, released_at, release_reason`;`session_id` 在 `status = 'active'` 中唯一,`expires_at > acquired_at`。
## 5. M2 原型验证清单(REQ §4.7.4)
先固定用户现有 camofox-browser 的**仓库、版本(`/openapi.json` 的 info.version)、启动方式与认证配置**,再逐项验证,结果写入 `browser_provider.capabilities` 的 `verified` 与 `verified_at`。未通过项在界面标为不可用,不得用另一浏览器窗口代替后宣称通过。
| # | 项目 | 验证方法 | 通过标准 |
| --- | --- | --- | --- |
| 1 | 登录保持 | 观众/馒头/HHClub/CHD 各一账号:导入 Cookie 或经接管登录后,闲置超过 `SESSION_TIMEOUT_MS` 再访问 | 无需重新登录;失败时准确识别"登录过期"而非空列表 |
| 2 | 结构化快照 | 列表页/详情页/登录页 `snapshot` | 元素引用稳定可点;密码框值、Cookie、passkey 经 `redactSnapshot` 后不可见;大页面分页正确 |
| 3 | 截图 | 列表/详情/登录页 | 返回格式与大小;登录页 `modelSafe=false` |
| 4 | 列表/详情提取 | 快照 + `extract`(x-ref)对比现有 HTTP 适配器结果 | 标题/分类/副标题/促销/做种字段一致,原始字段不丢 |
| 5 | 下载捕获 | 详情页点击下载 .torrent | `downloads` 能拿到文件与文件名;sha256 与直接 HTTP 下载一致;进入后端暂存核验,不直写媒体库 |
| 6 | 重启后会话 | 重启 camofox 容器/浏览器(`BROWSER_IDLE_TIMEOUT_MS` 触发与强制重启各一次) | 持久化 profile 恢复登录;旧 tabId 失效被识别为 `lost`,不重放旧元素引用 |
| 7 | 人机同标签页连续操作 | Agent 打开详情页 → 转人工(VNC)在**同一标签页**操作 → 交还 Agent 重新 snapshot | 人工看到的是同一 Camoufox 会话与页面;交还后 Agent 读到人工操作后的状态;不重复提交下载 |
| 8 | 异常路径 | 登录过期、页面结构变化、需人工验证(验证码/2FA)、服务不可用(503/断连) | 分别落为 `auth` / 提取失败标过期 / `waiting` 待处理 / `unavailable`,均不无限重试 |
| 9 | 性能与资源 | 四站并发 1/2/4 会话 | 记录启动延迟、实时查询延迟、内存、失败恢复时间,作为默认并发与空闲释放参数依据 |