2026-09-28 16:03:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* SQLite 存储(任务 A3.7,依据 ADR-0007 与决策 D16)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* - 驱动用 Node 内置 node:sqlite:零原生依赖,不需要为 Electron ABI 重编译
|
|
|
|
|
|
* - 时间统一存整数毫秒,主键由应用侧生成(ADR-0007 两条约定)
|
2026-09-29 10:59:34 +08:00
|
|
|
|
* - 表按 ADR-0008 的「平台库」建:会话三表 + 幂等键 + 上下文检查点(v1/v2)、身份绑定、待确认动作、审计链(v3,A4);文件与桌面授权后续再加
|
2026-09-28 16:03:12 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
`,
|
|
|
|
|
|
},
|
2026-09-29 10:24:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
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
|
|
|
|
|
|
);
|
|
|
|
|
|
`,
|
|
|
|
|
|
},
|
2026-09-29 10:59:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
`,
|
|
|
|
|
|
},
|
2026-09-28 16:03:12 +08:00
|
|
|
|
];
|
|
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|