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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 21:39:29 +08:00
1 parent 269a2269a1
commit effaa32b70
23 files changed
+2290

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import type { BrowserErrorKind } from "@umt/contracts";
/**
* 浏览器适配层统一错误。message 只含方法、路径与状态,不含访问凭据、Cookie、页面正文。
* retryable:同一预算内是否值得重试(超时/网络/上游 5xx/不可用);认证、预算、控制权冲突一律不重试。
*/
export class BrowserError extends Error {
readonly kind: BrowserErrorKind;
readonly httpStatus?: number;
constructor(kind: BrowserErrorKind, message?: string, opts: { httpStatus?: number; cause?: unknown } = {}) {
super(message ?? kind, opts.cause === undefined ? undefined : { cause: opts.cause });
this.name = "BrowserError";
this.kind = kind;
if (opts.httpStatus !== undefined) this.httpStatus = opts.httpStatus;
}
get retryable(): boolean {
return (
this.kind === "timeout" || this.kind === "network" || this.kind === "upstream" || this.kind === "unavailable"
);
}
}
export const notImplemented = (method: string): BrowserError =>
new BrowserError("not_implemented", `${method} 未实现(M2 原型验证后启用)`);
/** HTTP 状态 → 错误种类(camofox OpenAPI 1.17.0 的响应码:400/401/403/404/409/422/429/500/503) */
export function classifyHttpStatus(status: number): BrowserErrorKind {
if (status === 401 || status === 403) return "auth";
if (status === 404) return "not_found";
if (status === 408) return "timeout";
if (status === 429) return "rate_limited";
if (status === 503) return "unavailable";
if (status >= 500) return "upstream";
if (status >= 400) return "bad_request";
return "protocol";
}