import type { BrowserCapabilities, BrowserControlLease, BrowserCookie, BrowserEgressProxy, BrowserHealth, BrowserIdentity, BrowserLeaseHolderKind, BrowserProviderKind, BrowserSession, BrowserSnapshot, BrowserTab, DownloadArtifact, NavigationBudget, } from "@umt/contracts"; /** * BrowserProvider:业务侧访问浏览器服务的唯一合同(REQ §4.7.2)。 * 这是本项目接口;上游(camofox-browser 等)的同名能力由各实现映射,不假定上游具备同名端点。 * 约束: * - 同一 profileKey(站点/账号)的操作串行(ProfileLock); * - 导航受 NavigationBudget 约束(域名白名单、超时、重试、字节上限); * - 进入模型的快照/截图先经脱敏钩子;凭证经 BrowserSecretResolver 注入,不落日志、不进模型; * - controller 不是 automation 的会话拒绝自动化输入(接管期间,REQ §4.7.3); * - 浏览器没有媒体库写权限,下载产物只交后端暂存核验。 */ export interface BrowserProvider { readonly kind: BrowserProviderKind; health(): Promise; capabilities(opts?: { probe?: boolean }): Promise; openSession(req: OpenSessionRequest): Promise; closeSession(sessionId: string, reason?: string): Promise; listSessions(): Promise; navigate(ref: TabRef, url: string, budget: NavigationBudget): Promise; snapshot(ref: TabRef, opts?: SnapshotOptions): Promise; screenshot(ref: TabRef, opts?: { maxBytes?: number }): Promise; click(ref: TabRef, target: ElementTarget): Promise; /** text 为明文或凭证引用;凭证引用由服务端解析后直接下发,不进入模型与日志 */ type(ref: TabRef, target: ElementTarget | null, text: TypeInput): Promise; extract(ref: TabRef, schema: Record): Promise>; waitForDownload(ref: TabRef, opts: { timeoutMs: number; maxBytes: number }): Promise; acquireControl(req: AcquireControlRequest): Promise; releaseControl(leaseId: string, reason?: string): Promise; // ── C1-E:标签页、历史导航、其余交互与状态同步 ── openTab(sessionId: string, opts: { url?: string; budget: NavigationBudget; as?: ActorKind }): Promise; closeTab(ref: TabRef): Promise; /** 后退/前进/刷新(camofox /back /forward /refresh) */ history(ref: TabRef, action: "back" | "forward" | "reload", budget: NavigationBudget): Promise; select(ref: TabRef, target: ElementTarget, value: string): Promise; scroll(ref: TabRef, opts: { direction: "up" | "down" | "left" | "right"; amount: number }): Promise; press(ref: TabRef, key: string): Promise; /** 服务层把控制权变更同步给适配器;任何变更都让旧元素引用失效 */ setController(sessionId: string, controller: BrowserSession["controller"]): void; /** 服务重启后按数据库记录恢复内存状态(旧元素引用一律失效) */ restoreSession(state: RestoreSessionState): void; /** 对账:上游已不存在的标签页标 lost */ reconcileTabs(sessionId: string): Promise<{ alive: string[]; lost: string[] }>; /** 会话内存状态摘要(不含未脱敏地址) */ sessionInfo(sessionId: string): BrowserSessionInfo | null; /** 等待进行中的操作收尾(接管前调用) */ drain(sessionId: string): Promise; /** * 页面度量(VIS-1 坐标换算与敏感判定):视口 CSS 尺寸、DPR、是否有密码框、页面文字样本(只在服务端判定用,不外传)。 * 只读,不要求自动化控制权;上游不支持 evaluate 时返回 null。 */ pageMetrics(ref: TabRef): Promise; /** * 当前页面内容(CF-2 挑战识别与浏览器代抓):地址(已脱敏)、标题、outerHTML、正文样本、navigator.userAgent。 * 只读,不要求自动化控制权;上游不支持时返回 null。HTML 只给服务端解析器,不进模型与日志。 * currentVisible 仅用于用户从桌面详情页交接资源:未指定 tabId 时发现唯一可见标签,不能确定则拒绝。 */ pageContent?(ref: TabRef, opts?: { maxHtmlBytes?: number; currentVisible?: boolean }): Promise; // ── BR-2 凭据统一 ── /** * 把 Cookie 写进该 profile 的浏览器上下文(并落盘到 profile)。上游没有可用的 Cookie 接口时报 * BrowserError("not_implemented")。Cookie 值不进日志与错误信息。 */ setCookies?(profileKey: string, cookies: BrowserCookie[]): Promise<{ count: number }>; /** * 读该 profile 作用于这些地址的 Cookie(活动上下文优先,否则读 profile 落盘文件);没有时返回空数组。 * 上游没有接口时报 BrowserError("not_implemented")。 */ getCookies?(profileKey: string, urls: string[]): Promise; // ── BR-3 浏览器服务重启后自愈 ── /** * 浏览器服务实例标识:进程重启后会变(camofox 走 umt 插件 `GET /umt/instance`,否则取 `/health` 里的 * instanceId / bootId / startedAt)。取不到(旧镜像)时返回 null,调用方只靠 404/410 被动发现。 */ instanceId?(): Promise; /** * 重建会话的上游上下文:(可选)先把 Cookie 写进该 profile,再在同一会话(同一 userId / sessionKey)开新标签页并导航到 url; * 原有打开中的标签页一律标 lost。不检查控制者(接管中的会话也能重连,控制权不变)。 */ reopenSession?(sessionId: string, req: ReopenSessionRequest): Promise; // ── AG-4 保活 ── /** * 对标签页做一次最轻的只读调用,让上游把它算作"有活动"(camofox 标签页空闲回收 TAB_INACTIVITY_MS 与会话空闲回收 * SESSION_TIMEOUT_MS 都按 API 调用计;桌面模式里人的键鼠操作不经 API,不算活动)。不检查控制者、不改页面。 * 标签页不在了报 not_found(调用方按 BR-3 自愈重连)。 */ keepAlive?(ref: TabRef): Promise; // ── BR-4 浏览器 UA 与指纹统一、出口按站点 ── /** 固定浏览器身份的公开部分(UA、OS、版本);上游没有时返回 null */ identity?(): Promise; /** * 该 profile 的出口:proxy = 站点开代理时与 HTTP 同一代理,null = 直连。上游没有接口时报 BrowserError("not_implemented")。 * restarted = 活动上下文出口不同、已被关闭(下次操作按新出口重建)。 */ setEgress?(profileKey: string, proxy: BrowserEgressProxy | null): Promise<{ restarted: boolean; mode?: string }>; } export interface ReopenSessionRequest { url: string; budget: NavigationBudget; cookies?: BrowserCookie[]; } export interface ReopenSessionResult { tab: BrowserTab; /** 被标 lost 的旧标签页(上游 tabId) */ lost: string[]; cookies?: { status: "injected" | "unsupported" | "failed"; count: number }; } export interface PageContent { url: string | null; title: string; html: string; /** document.body.innerText 前 2 万字 */ text: string; userAgent: string | null; } export interface PageMetrics { viewportWidth: number; viewportHeight: number; devicePixelRatio: number; /** 页面上有 input[type=password] */ passwordField: boolean; /** document.body.innerText 前 4000 字(判定二次验证等,不进模型、不落日志) */ textSample: string; } /** 动作发起方:自动化(Agent/任务)或持有接管租约的人 */ export type ActorKind = "automation" | "human"; export interface RestoreSessionState { session: BrowserSession; tabs: BrowserTab[]; currentTabId: string | null; budget: NavigationBudget; } export interface BrowserSessionInfo { session: BrowserSession; tabs: BrowserTab[]; currentTabId: string | null; /** 最近一次快照之后没有导航/交接:eN 引用可用 */ refsValid: boolean; /** 当前页面是登录页 */ loginPage: boolean; } export interface OpenSessionRequest { providerId: string; siteId: string; accountId: string | null; /** 缺省按 siteId/accountId 生成(profileKeyFor) */ profileKey?: string; /** 可选初始地址,同样受 budget 约束 */ initialUrl?: string; budget: NavigationBudget; /** 打开前先注入的 Cookie(站点账号登录态,BR-2);注入失败不阻止打开,结果见 OpenSessionResult.cookies */ cookies?: BrowserCookie[]; } export interface OpenSessionResult { session: BrowserSession; tab: BrowserTab; navigation: NavigationResult | null; /** 传了 cookies 时:injected = 注入个数;unsupported = 上游没有 Cookie 接口;failed = 请求失败 */ cookies?: { status: "injected" | "unsupported" | "failed"; count: number }; } /** 会话内标签页定位;tabId 缺省 = 会话当前标签页 */ export interface TabRef { sessionId: string; tabId?: string; /** 动作发起方,缺省 automation;与会话 controller 不符时报 control_conflict */ as?: ActorKind; } export interface NavigationResult { /** 已脱敏的最终地址 */ url: string; httpStatus: number | null; /** 上游报告的文档状态 < 400 */ ok: boolean; attempts: number; } export interface SnapshotOptions { offset?: number; maxBytes?: number; /** 追加脱敏规则(站点特定字段) */ extraRedactions?: RegExp[]; } export interface BrowserScreenshot { mimeType: string; data: Uint8Array; /** 已脱敏的页面地址 */ url: string | null; /** * 是否可直接进入模型。像素级遮蔽未实现(M2),当前按页面地址/已知密码框保守判断: * 含凭据的地址或登录页一律 false,调用方只能给人看、不喂模型。 */ modelSafe: boolean; capturedAt: string; } /** 元素定位:快照引用 / CSS 选择器 / 视口像素坐标(原站视图点击;camofox 坐标点击待核验) */ export type ElementTarget = { ref: string } | { selector: string } | { x: number; y: number }; export type TypeInput = | { kind: "text"; text: string; submit?: boolean; clear?: boolean } | { kind: "secret"; siteId: string; accountId: string | null; field: "username" | "password" | "totp"; submit?: boolean; clear?: boolean; }; export interface AcquireControlRequest { sessionId: string; holderKind: BrowserLeaseHolderKind; holder: string; reason: string; ttlMs: number; } /** 凭证注入:服务端从 secret store 按需解出明文;适配器不持久化、不打印、不放进错误信息 */ export type BrowserSecretRequest = | { kind: "provider_access_key"; providerId: string } | { kind: "site_login"; siteId: string; accountId: string | null; field: "username" | "password" | "totp" }; export interface BrowserSecretResolver { resolve(req: BrowserSecretRequest): Promise; } /** 测试/脚本用固定凭证解析器;键为 `provider:` 或 `site:::` */ export function staticBrowserSecrets(table: Record): BrowserSecretResolver { return { async resolve(req) { const key = req.kind === "provider_access_key" ? `provider:${req.providerId}` : `site:${req.siteId}:${req.accountId ?? "-"}:${req.field}`; return table[key] ?? null; }, }; } /** 站点/账号维度的 profile 键;同时用作 camofox userId(登录状态隔离单元) */ export function profileKeyFor(siteId: string, accountId: string | null): string { return `umt-site-${siteId}-acct-${accountId ?? "anon"}`; } /** 只记录方法/路径/状态;实现不得把请求头、正文或快照写进日志 */ export type BrowserLogger = (entry: { level: "debug" | "warn"; msg: string; method?: string; path?: string; status?: number; ms?: number; }) => void;