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): Promise { 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): Promise { await execDdl(db, dropTables(["browser_control_lease", "browser_tab", "browser_session", "browser_provider"])); }