/** * SQLite 存储(任务 A3.7,依据 ADR-0007 与决策 D16)。 * * - 驱动用 Node 内置 node:sqlite:零原生依赖,不需要为 Electron ABI 重编译 * - 时间统一存整数毫秒,主键由应用侧生成(ADR-0007 两条约定) * - 表按 ADR-0008 的「平台库」建:会话三表 + 幂等键 + 上下文检查点(v1/v2)、身份绑定、待确认动作、审计链(v3,A4);文件与桌面授权后续再加 */ import { mkdirSync } from 'node:fs'; import { dirname } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; export type Database = DatabaseSync; const MIGRATIONS: Array<{ version: number; sql: string }> = [ { version: 1, sql: ` create table if not exists projects ( project_id text primary key, name text not null, status text not null, writable integer not null default 1, created_at integer not null ); create table if not exists sessions ( session_id text primary key, project_id text, scope text not null, status text not null, title text not null, owner_user_id text not null, owner_tenant_id text not null, owner_display_name text not null, owner_client_kind text not null, created_at integer not null ); create index if not exists idx_sessions_project on sessions(project_id, created_at); create table if not exists messages ( message_id text primary key, session_id text not null, role text not null, text text not null, ts integer not null ); create index if not exists idx_messages_session on messages(session_id, ts); create table if not exists idempotency_keys ( idempotency_key text primary key, session_id text not null, message_id text not null, run_id text not null, created_at integer not null ); `, }, { version: 2, sql: ` create table if not exists context_checkpoints ( session_id text primary key, summary text not null, summary_source_ids text not null, included_message_ids text not null, last_message_id text, pinned text not null default '[]', estimated_chars integer not null default 0, updated_at integer not null ); `, }, { // A4 控制面:身份绑定(含加密凭据)、待确认动作、审计链三张表(ADR-0008 补记) version: 3, sql: ` create table if not exists identity_bindings ( user_id text not null, tenant_id text not null, provider_id text not null, account_id text not null, display_name text not null, status text not null, credential_ref text, credential_iv text, credential_ciphertext text, credential_tag text, created_at integer not null, updated_at integer not null, primary key (user_id, tenant_id, provider_id) ); create table if not exists pending_actions ( execution_id text primary key, approval_id text not null unique, session_id text not null, project_id text, owner_user_id text not null, tenant_id text not null, capability_id text not null, provider_id text not null, params_digest text not null, params_preview text not null default '{}', params_iv text, params_ciphertext text, params_tag text, impact_summary text not null, revision integer not null, status text not null, decision text, decided_by text, decided_at integer, idempotency_key text, decision_idempotency_key text, grant_id text, receipt text not null, expires_at integer not null, created_at integer not null, updated_at integer not null ); create index if not exists idx_pending_actions_session on pending_actions(session_id, created_at); create table if not exists audit_log ( seq integer primary key autoincrement, audit_id text not null unique, ts integer not null, actor_user_id text not null, tenant_id text not null, event text not null, execution_id text, approval_id text, session_id text, capability_id text, provider_id text, params_digest text, decision text, detail text not null default '{}', prev_hash text not null, hash text not null ); create index if not exists idx_audit_execution on audit_log(execution_id, seq); `, }, ]; export function openDatabase(path: string): Database { if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true }); const db = new DatabaseSync(path); try { db.exec('pragma journal_mode = WAL'); } catch { /* 内存库不支持 WAL */ } db.exec('pragma foreign_keys = ON'); migrate(db); return db; } export function migrate(db: Database): void { // 先建迁移表本身,否则第一次检查版本号就会报 no such table db.exec('create table if not exists schema_migrations (version integer primary key, applied_at integer not null)'); for (const migration of MIGRATIONS) { const applied = db.prepare('select version from schema_migrations where version = ?').get(migration.version); if (applied) continue; db.exec(migration.sql); db.prepare('insert into schema_migrations (version, applied_at) values (?, ?)').run(migration.version, Date.now()); } }