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:
1 parent
269a2269a1
commit
effaa32b70
23 files changed
+2290
No files matched your search
@@ -0,0 +1,93 @@
|
||||
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"]));
|
||||
}
|
||||
@@ -73,6 +73,12 @@ export type AssetOperationStatus = "planned" | "started" | "succeeded" | "failed
|
||||
export type AssetUnitCoverageCoverage = "full" | "partial" | "segment" | "unknown";
|
||||
export type AssetUnitCoverageSegment = "full" | "upper" | "middle" | "lower" | "unknown";
|
||||
export type AutomationGrantStatus = "active" | "revoked" | "expired";
|
||||
export type BrowserControlLeaseHolderKind = "automation" | "human";
|
||||
export type BrowserControlLeaseStatus = "active" | "released" | "expired" | "revoked";
|
||||
export type BrowserProviderKind = "camofox" | "none";
|
||||
export type BrowserProviderHealthStatus = "unknown" | "healthy" | "degraded" | "offline";
|
||||
export type BrowserSessionController = "automation" | "human" | "waiting" | "closed";
|
||||
export type BrowserTabStatus = "open" | "closed" | "lost";
|
||||
export type CapabilityInvocationStatus = "running" | "succeeded" | "failed";
|
||||
export type ContentUnitKind = "season" | "episode" | "special" | "part";
|
||||
export type ContentUnitContentType =
|
||||
@@ -460,6 +466,67 @@ export interface AutomationGrantTable {
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface BrowserControlLeaseTable {
|
||||
id: Generated<string>;
|
||||
session_id: string;
|
||||
holder_kind: BrowserControlLeaseHolderKind;
|
||||
holder: string;
|
||||
reason: string;
|
||||
status: Generated<BrowserControlLeaseStatus>;
|
||||
acquired_at: GeneratedTimestamptz;
|
||||
expires_at: Timestamptz;
|
||||
released_at: Timestamptz | null;
|
||||
release_reason: string | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface BrowserProviderTable {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
kind: BrowserProviderKind;
|
||||
base_url: string | null;
|
||||
secret_ref: string | null;
|
||||
enabled: Generated<boolean>;
|
||||
capabilities: Generated<Json>;
|
||||
verified_at: Timestamptz | null;
|
||||
health_status: Generated<BrowserProviderHealthStatus>;
|
||||
last_health_at: Timestamptz | null;
|
||||
config: Generated<Json>;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface BrowserSessionTable {
|
||||
id: Generated<string>;
|
||||
provider_id: string;
|
||||
site_id: string;
|
||||
account_id: string | null;
|
||||
profile_key: string;
|
||||
controller: Generated<BrowserSessionController>;
|
||||
current_url: string | null;
|
||||
last_activity_at: GeneratedTimestamptz;
|
||||
upstream_ref: Generated<Json>;
|
||||
recovery_state: Generated<Json>;
|
||||
closed_at: Timestamptz | null;
|
||||
close_reason: string | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface BrowserTabTable {
|
||||
id: Generated<string>;
|
||||
session_id: string;
|
||||
upstream_tab_id: string;
|
||||
url: string | null;
|
||||
title: string | null;
|
||||
status: Generated<BrowserTabStatus>;
|
||||
last_snapshot_at: Timestamptz | null;
|
||||
closed_at: Timestamptz | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
export interface CapabilityInvocationTable {
|
||||
id: Generated<string>;
|
||||
idempotency_key: string;
|
||||
@@ -1225,6 +1292,10 @@ export interface DB {
|
||||
asset_unit_coverage: AssetUnitCoverageTable;
|
||||
audit_event: AuditEventTable;
|
||||
automation_grant: AutomationGrantTable;
|
||||
browser_control_lease: BrowserControlLeaseTable;
|
||||
browser_provider: BrowserProviderTable;
|
||||
browser_session: BrowserSessionTable;
|
||||
browser_tab: BrowserTabTable;
|
||||
capability_invocation: CapabilityInvocationTable;
|
||||
content_unit: ContentUnitTable;
|
||||
decision: DecisionTable;
|
||||
@@ -1314,6 +1385,18 @@ export type AuditEventUpdate = Updateable<AuditEventTable>;
|
||||
export type AutomationGrantRow = Selectable<AutomationGrantTable>;
|
||||
export type NewAutomationGrant = Insertable<AutomationGrantTable>;
|
||||
export type AutomationGrantUpdate = Updateable<AutomationGrantTable>;
|
||||
export type BrowserControlLeaseRow = Selectable<BrowserControlLeaseTable>;
|
||||
export type NewBrowserControlLease = Insertable<BrowserControlLeaseTable>;
|
||||
export type BrowserControlLeaseUpdate = Updateable<BrowserControlLeaseTable>;
|
||||
export type BrowserProviderRow = Selectable<BrowserProviderTable>;
|
||||
export type NewBrowserProvider = Insertable<BrowserProviderTable>;
|
||||
export type BrowserProviderUpdate = Updateable<BrowserProviderTable>;
|
||||
export type BrowserSessionRow = Selectable<BrowserSessionTable>;
|
||||
export type NewBrowserSession = Insertable<BrowserSessionTable>;
|
||||
export type BrowserSessionUpdate = Updateable<BrowserSessionTable>;
|
||||
export type BrowserTabRow = Selectable<BrowserTabTable>;
|
||||
export type NewBrowserTab = Insertable<BrowserTabTable>;
|
||||
export type BrowserTabUpdate = Updateable<BrowserTabTable>;
|
||||
export type CapabilityInvocationRow = Selectable<CapabilityInvocationTable>;
|
||||
export type NewCapabilityInvocation = Insertable<CapabilityInvocationTable>;
|
||||
export type CapabilityInvocationUpdate = Updateable<CapabilityInvocationTable>;
|
||||
|
||||
@@ -83,6 +83,11 @@ const EXPECTED_TABLES = [
|
||||
"admin_session",
|
||||
"login_attempt",
|
||||
"sse_event",
|
||||
// W4-C 浏览器实体(0023)
|
||||
"browser_provider",
|
||||
"browser_session",
|
||||
"browser_tab",
|
||||
"browser_control_lease",
|
||||
].sort();
|
||||
|
||||
/** 不走 entityTable() 的表:追加型/引擎自有表,没有 uuid id + created_at/updated_at 三件套 */
|
||||
|
||||
Reference in new issue
Block a user