Files
UHD_Media_Tool/packages/contracts/src/browser.ts
T
rootandClaude Opus 5.5 effaa32b70 feat(browser): W4-C BrowserProvider contract and camofox-browser adapter skeleton
- 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]>
2026-09-27 21:39:29 +08:00

224 lines
8.5 KiB
TypeScript
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.
import { z } from "zod";
import { EntityBase, Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
import { InstanceHealth } from "./instances.ts";
// 浏览器能力合同(REQ §4.4、§4.7.2–4.7.4、§12 浏览器实体)。
// 这是本项目自有接口,不等于上游同名接口:上游对接由 @umt/adapters 的 BrowserProvider 实现
// (首个实现为用户已有的 camofox-browser;Playwright 只用于前端截图测试,不是产品浏览器能力)。
// v0.1 只落合同与骨架,会话/接管/下载捕获在 M2 原型验证后启用(docs/design/browser-provider.md)。
/** 浏览器服务实现种类;none = 未配置(默认),所有浏览器动作返回不可用 */
export const BrowserProviderKind = z.enum(["camofox", "none"]);
export type BrowserProviderKind = z.infer<typeof BrowserProviderKind>;
/**
* 单项能力声明。supported = 实现/上游声明具备;verified = 已对用户真实实例做过原型验证(REQ §4.7.4)。
* 界面只把 supported && verified 的能力当作可用,未验证的明确标注"未验证",不得用其他浏览器替代后宣称可用。
*/
export const BrowserCapabilityFlag = z.object({
supported: z.boolean(),
verified: z.boolean(),
/** static = 适配器按文档静态声明;probed = 从服务端 OpenAPI/健康端点探测到 */
source: z.enum(["static", "probed"]),
note: z.string().optional(),
});
export type BrowserCapabilityFlag = z.infer<typeof BrowserCapabilityFlag>;
export const BROWSER_CAPABILITY_KEYS = [
"sessions",
"tabs",
"navigate",
"snapshot",
"screenshot",
"interact",
"extract",
"downloadCapture",
"takeover",
"persistentProfile",
] as const;
export type BrowserCapabilityKey = (typeof BROWSER_CAPABILITY_KEYS)[number];
export const BrowserCapabilities = z.object({
/** 会话(站点/账号隔离的浏览器上下文) */
sessions: BrowserCapabilityFlag,
/** 标签页列举/新建/关闭 */
tabs: BrowserCapabilityFlag,
navigate: BrowserCapabilityFlag,
/** 结构化快照(可访问性树 + 元素引用),非原始 HTML */
snapshot: BrowserCapabilityFlag,
screenshot: BrowserCapabilityFlag,
/** click / type / select */
interact: BrowserCapabilityFlag,
/** 结构化提取(按 schema 取字段) */
extract: BrowserCapabilityFlag,
/** 捕获浏览器下载产物(.torrent 等)交后端暂存核验 */
downloadCapture: BrowserCapabilityFlag,
/** 人工接管:同一会话的画面与输入通道 */
takeover: BrowserCapabilityFlag,
/** 登录状态随 profile 持久化,重启后恢复 */
persistentProfile: BrowserCapabilityFlag,
/** 上游版本(来自 OpenAPI info.version 等);未取得为 null */
upstreamVersion: z.string().nullable(),
probedAt: Timestamp.nullable(),
});
export type BrowserCapabilities = z.infer<typeof BrowserCapabilities>;
/** 会话控制权:自动化 / 人工接管 / 等待处理(验证码、2FA 等)/ 已关闭(REQ §4.7.3) */
export const BrowserController = z.enum(["automation", "human", "waiting", "closed"]);
export type BrowserController = z.infer<typeof BrowserController>;
export const BrowserProviderRow = EntityBase.extend({
name: z.string().min(1),
kind: BrowserProviderKind,
baseUrl: z.string().nullable(),
/** 访问凭据(如 camofox CAMOFOX_ACCESS_KEY)只以 secret 引用保存 */
secretRef: Id.nullable(),
enabled: z.boolean(),
/** 最近一次声明/探测到的 BrowserCapabilities 快照 */
capabilities: JsonObject,
/** 最近一次对真实实例完成原型验证的时间;null = 从未验证 */
verifiedAt: Timestamp.nullable(),
healthStatus: InstanceHealth,
lastHealthAt: Timestamp.nullable(),
config: JsonObject,
});
export type BrowserProviderRow = z.infer<typeof BrowserProviderRow>;
/** 浏览器会话:按站点/账号绑定;同一 profileKey 同时至多一个未关闭会话 */
export const BrowserSession = EntityBase.extend({
providerId: Id,
siteId: Id,
accountId: Id.nullable(),
/** 站点/账号维度的 profile 键,决定上游登录状态隔离与串行锁 */
profileKey: z.string().min(1),
controller: BrowserController,
currentUrl: z.string().nullable(),
lastActivityAt: Timestamp,
/** 上游会话标识(camofox: userId/sessionKey);不含凭据 */
upstreamRef: JsonObject,
/** 接管/断线/重启时的可恢复状态;恢复后必须重新读取页面,不重放旧元素引用 */
recoveryState: JsonObject,
closedAt: Timestamp.nullable(),
closeReason: z.string().nullable(),
});
export type BrowserSession = z.infer<typeof BrowserSession>;
export const BrowserTabStatus = z.enum(["open", "closed", "lost"]);
export type BrowserTabStatus = z.infer<typeof BrowserTabStatus>;
export const BrowserTab = EntityBase.extend({
sessionId: Id,
/** 上游标签页 ID(camofox tabId) */
upstreamTabId: z.string().min(1),
url: z.string().nullable(),
title: z.string().nullable(),
status: BrowserTabStatus,
lastSnapshotAt: Timestamp.nullable(),
closedAt: Timestamp.nullable(),
});
export type BrowserTab = z.infer<typeof BrowserTab>;
export const BrowserLeaseHolderKind = z.enum(["automation", "human"]);
export type BrowserLeaseHolderKind = z.infer<typeof BrowserLeaseHolderKind>;
export const BrowserLeaseStatus = z.enum(["active", "released", "expired", "revoked"]);
export type BrowserLeaseStatus = z.infer<typeof BrowserLeaseStatus>;
/** 会话控制权租约:每个会话同时至多一个 active(DB 部分唯一索引保证) */
export const BrowserControlLease = EntityBase.extend({
sessionId: Id,
holderKind: BrowserLeaseHolderKind,
/** 持有者标识:agent_run id / admin 用户名等 */
holder: z.string().min(1),
reason: z.string().min(1),
status: BrowserLeaseStatus,
acquiredAt: Timestamp,
expiresAt: Timestamp,
releasedAt: Timestamp.nullable(),
releaseReason: z.string().nullable(),
});
export type BrowserControlLease = z.infer<typeof BrowserControlLease>;
/** 导航预算(REQ §4.4):域名范围、超时、重试与资源上限;空白名单 = 拒绝全部导航 */
export const NavigationBudget = z.object({
/** 允许的主机名;"example.org" 同时放行其子域 */
domainAllowlist: z.array(z.string().min(1)),
timeoutMs: z.number().int().min(1),
maxRetries: z.number().int().min(0).max(10),
/** 单次响应(快照文本/截图)字节上限 */
maxBytes: z.number().int().min(1),
});
export type NavigationBudget = z.infer<typeof NavigationBudget>;
export const SnapshotRedactionKind = z.enum(["password_field", "cookie", "credential_url", "secret_value"]);
export type SnapshotRedactionKind = z.infer<typeof SnapshotRedactionKind>;
/**
* 可交给模型的页面快照:只含经 redactSnapshot 脱敏后的结构化文本与元素引用,
* 密码框值、Cookie 值、含 passkey 等凭据的 URL 已遮蔽;不含原始 HTML/脚本。
*/
export const BrowserSnapshot = z.object({
sessionId: Id,
tabId: z.string().min(1),
/** 已脱敏的当前地址 */
url: z.string(),
format: z.enum(["a11y_text"]),
/** 已脱敏的可访问性快照文本,元素以 eN 引用 */
text: z.string(),
/** 表单/表格摘要(已脱敏) */
structure: JsonObject.nullable(),
refsCount: NonNegInt,
truncated: z.boolean(),
nextOffset: NonNegInt.nullable(),
redactions: z.array(z.object({ kind: SnapshotRedactionKind, count: NonNegInt })),
capturedAt: Timestamp,
});
export type BrowserSnapshot = z.infer<typeof BrowserSnapshot>;
/** 二进制内容的引用:blob 暂存路径或 secret(下载产物可能含 passkey,不内联进合同/日志) */
export const BlobRef = z.object({
kind: z.enum(["blob", "secret"]),
ref: z.string().min(1),
});
export type BlobRef = z.infer<typeof BlobRef>;
/** 浏览器捕获的下载产物:交后端受控暂存与核验后再走原有提交链路(REQ §4.7.1) */
export const DownloadArtifact = z.object({
id: Id,
sessionId: Id,
filename: z.string().min(1),
mimeType: z.string().nullable(),
sizeBytes: NonNegInt,
bytes: BlobRef,
/** 十六进制 sha256 */
sha256: z.string().min(1),
capturedAt: Timestamp,
});
export type DownloadArtifact = z.infer<typeof DownloadArtifact>;
export const BrowserHealth = z.object({
status: z.enum(["ok", "degraded", "down", "disabled"]),
kind: BrowserProviderKind,
detail: JsonObject,
checkedAt: Timestamp,
});
export type BrowserHealth = z.infer<typeof BrowserHealth>;
export const BrowserErrorKind = z.enum([
"not_implemented",
"unavailable",
"auth",
"timeout",
"network",
"not_found",
"rate_limited",
"bad_request",
"upstream",
"protocol",
"domain_blocked",
"budget_exceeded",
"control_conflict",
"lock_timeout",
]);
export type BrowserErrorKind = z.infer<typeof BrowserErrorKind>;