- contracts/browser.ts: provider kind, per-capability {supported, verified} flags,
session/tab/control-lease rows, NavigationBudget, redacted BrowserSnapshot,
DownloadArtifact with blob/secret refs, BrowserErrorKind
- adapters/browser: BrowserProvider interface, camofox client (health, capabilities
static + /openapi.json probe, openSession/navigate/snapshot/screenshot; rest
not_implemented for M2), none provider, redactSnapshot, profile serial lock,
domain allowlist budget; Bearer access key via BrowserSecretResolver
- testkit/browser: mock camofox server (auth, fixtures with secrets, faults)
- db 0023: browser_provider/session/tab/control_lease with one-active-lease and
one-live-session-per-profile partial unique indexes; types regenerated
- docs/design/browser-provider.md: contract, camofox OpenAPI 1.17.0 mapping,
unverified items and M2 prototype checklist; Playwright is not a product browser
Co-Authored-By: Claude Opus 5.5 <[email protected]>
97 lines
11 KiB
Markdown
97 lines
11 KiB
Markdown
# BrowserProvider 合同与 camofox-browser 适配
|
||
|
||
对应 REQ v0.7 §4.4、§4.7.2–4.7.4、§12.1 浏览器实体、附录 S18–S20;计划 `docs/plan/00-first-version-plan.md` §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`:仅在真实实例原型验证通过后由配置注入为 true,探测永远不会把它设为 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 取得,**仅读文档,未连接真实实例**;用户实际部署的仓库/fork/版本尚未确认。
|
||
|
||
上游模型:一个 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 会话 | 记录启动延迟、实时查询延迟、内存、失败恢复时间,作为默认并发与空闲释放参数依据 |
|