aps-agent/server/core/storage/db.ts

162 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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());
}
}