Files
UHD_Media_Tool/packages/db/migrations/0023_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

94 lines
4.0 KiB
TypeScript

import type { Kysely } from "kysely";
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
// W4-C 浏览器实体(REQ §12.1 浏览器域、§4.7.3 会话控制与交接):
// - browser_provider:浏览器服务(首个为用户已有的 camofox-browser);访问凭据只存 secret_ref;
// capabilities 保存声明/探测结果,verified_at 记录最近一次真实实例原型验证;
// - browser_session:按站点/账号(profile_key)隔离;同一 provider+profile 至多一个未关闭会话;
// recovery_state 保存接管/断线/重启后的可恢复状态(恢复时重新读页面,不重放旧元素引用);
// - browser_tab:上游标签页映射;
// - browser_control_lease:控制权租约,每个会话至多一条 active(部分唯一索引)。
const HEALTH = ["unknown", "healthy", "degraded", "offline"] as const;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
${entityTable(
"browser_provider",
`
name text not null,
kind text not null ${oneOf("kind", ["camofox", "none"])},
base_url text,
secret_ref uuid references secret (id) on delete restrict,
enabled boolean not null default false,
capabilities jsonb not null default '{}'::jsonb,
verified_at timestamptz,
health_status text not null default 'unknown' ${oneOf("health_status", HEALTH)},
last_health_at timestamptz,
config jsonb not null default '{}'::jsonb,
constraint browser_provider_name_key unique (name),
constraint browser_provider_base_url_required check (kind = 'none' or base_url is not null)`,
)}
${entityTable(
"browser_session",
`
provider_id uuid not null references browser_provider (id) on delete cascade,
site_id uuid not null references site (id) on delete cascade,
account_id uuid references site_account (id) on delete set null,
profile_key text not null,
controller text not null default 'automation' ${oneOf("controller", ["automation", "human", "waiting", "closed"])},
current_url text,
last_activity_at timestamptz not null default now(),
upstream_ref jsonb not null default '{}'::jsonb,
recovery_state jsonb not null default '{}'::jsonb,
closed_at timestamptz,
close_reason text,
constraint browser_session_closed_consistent check ((controller = 'closed') = (closed_at is not null))`,
)}
create unique index browser_session_live_profile_key
on browser_session (provider_id, profile_key) where controller <> 'closed';
create index browser_session_site_idx on browser_session (site_id, account_id, last_activity_at desc);
${entityTable(
"browser_tab",
`
session_id uuid not null references browser_session (id) on delete cascade,
upstream_tab_id text not null,
url text,
title text,
status text not null default 'open' ${oneOf("status", ["open", "closed", "lost"])},
last_snapshot_at timestamptz,
closed_at timestamptz,
constraint browser_tab_upstream_key unique (session_id, upstream_tab_id)`,
)}
${entityTable(
"browser_control_lease",
`
session_id uuid not null references browser_session (id) on delete cascade,
holder_kind text not null ${oneOf("holder_kind", ["automation", "human"])},
holder text not null,
reason text not null,
status text not null default 'active' ${oneOf("status", ["active", "released", "expired", "revoked"])},
acquired_at timestamptz not null default now(),
expires_at timestamptz not null,
released_at timestamptz,
release_reason text,
constraint browser_control_lease_window check (expires_at > acquired_at)`,
)}
create unique index browser_control_lease_one_active
on browser_control_lease (session_id) where status = 'active';
create index browser_control_lease_session_idx on browser_control_lease (session_id, acquired_at desc);
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(db, dropTables(["browser_control_lease", "browser_tab", "browser_session", "browser_provider"]));
}