- 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]>
11 KiB
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 |
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,原型验证 |
与设计的差异 / 待核验
- 上游没有会话概念的建立端点:会话在首个
POST /tabs时隐式创建,由SESSION_TIMEOUT_MS(默认 10 分钟,README 架构节写 30 分钟,自相矛盾)空闲回收;标签页TAB_INACTIVITY_MS默认 5 分钟回收,且达到上限时"最旧标签被自动回收"。本项目的会话/标签状态必须以 DB 为准并定期对账,不能假定上游会话长期存在。 userId允许字符集、长度未在 OpenAPI 定义(profileKeyFor生成umt-site-<uuid>-acct-<uuid>),待核验。- 快照是可访问性树文本(
[button e1] Submit),密码框的值是否出现、以什么形式出现未核验;format=json与structure的具体字段也未核验。 - 截图 OpenAPI 为 JSON base64;是否有版本直接返回二进制未核验(适配器两者兼容)。
- README 的 type 示例用
pressEnter,OpenAPI 用submit。 - 接管:上游没有 API;VNC 插件是整块显示,能否定位到同一会话/标签、画面认证能否接产品认证,需原型;
CAMOFOX_INTERACTIVE=desktop只适用于本机桌面。 - 持久化:持久化插件默认保存 cookies + localStorage 到
~/.camofox/profiles,sessionStorage 不保存,IndexedDB 需显式开启;"重启后仍登录"要逐站验证(REQ §4.4)。 - 下载捕获的
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 会话 | 记录启动延迟、实时查询延迟、内存、失败恢复时间,作为默认并发与空闲释放参数依据 |