mod decode;
mod directories;
mod events;
mod management_tasks;
mod pending_requests;
mod runtimes;
mod schema;
#[cfg(test)]
mod tests;
mod threads;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use rusqlite::Connection;
pub const PRIMARY_RUNTIME_ID: &str = "primary";
#[derive(Debug, Clone)]
pub struct Storage {
db_path: PathBuf,
}
impl Storage {
pub fn open(db_path: PathBuf) -> Result<Self> {
Self::open_with_transient_reset(db_path, true)
}
pub fn open_existing(db_path: PathBuf) -> Result<Self> {
Self::open_with_transient_reset(db_path, false)
}
fn open_with_transient_reset(db_path: PathBuf, reset_transient_state: bool) -> Result<Self> {
if let Some(parent) = db_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("创建数据库目录失败: {}", parent.display()))?;
}
let storage = Self { db_path };
storage.migrate()?;
if reset_transient_state {
storage.clear_pending_requests()?;
storage.clear_legacy_pending_approvals()?;
}
Ok(storage)
}
fn clear_legacy_pending_approvals(&self) -> Result<()> {
let conn = self.connect()?;
conn.execute("DELETE FROM pending_approvals", [])?;
Ok(())
}
fn connect(&self) -> Result<Connection> {
let conn = Connection::open(&self.db_path)
.with_context(|| format!("打开数据库失败: {}", self.db_path.display()))?;
conn.execute_batch(
"PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;",
)?;
Ok(conn)
}
fn migrate(&self) -> Result<()> {
let conn = self.connect()?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS directory_bookmarks (
path TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS directory_history (
path TEXT PRIMARY KEY,
last_used_at_ms INTEGER NOT NULL,
use_count INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS runtimes (
runtime_id TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
codex_home TEXT NULL,
codex_binary TEXT NOT NULL,
is_primary INTEGER NOT NULL,
auto_start INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS mobile_sessions (
device_id TEXT PRIMARY KEY,
last_ack_seq INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
runtime_id TEXT NULL,
thread_id TEXT NULL,
payload TEXT NOT NULL,
created_at_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS pending_approvals (
approval_id TEXT PRIMARY KEY,
runtime_id TEXT NOT NULL DEFAULT 'primary',
thread_id TEXT NOT NULL,
turn_id TEXT NOT NULL,
item_id TEXT NOT NULL,
kind TEXT NOT NULL,
reason TEXT NULL,
command TEXT NULL,
cwd TEXT NULL,
grant_root TEXT NULL,
available_decisions TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS pending_server_requests (
request_id TEXT PRIMARY KEY,
runtime_id TEXT NOT NULL DEFAULT 'primary',
request_type TEXT NOT NULL,
thread_id TEXT NULL,
turn_id TEXT NULL,
item_id TEXT NULL,
title TEXT NULL,
reason TEXT NULL,
command TEXT NULL,
cwd TEXT NULL,
grant_root TEXT NULL,
tool_name TEXT NULL,
arguments TEXT NULL,
questions TEXT NOT NULL,
proposed_execpolicy_amendment TEXT NULL,
network_approval_context TEXT NULL,
schema TEXT NULL,
available_decisions TEXT NOT NULL,
raw_payload TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS bridge_management_tasks (
task_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
updated_at_ms INTEGER NOT NULL,
raw_json TEXT NOT NULL
);",
)?;
schema::ensure_thread_index_schema(&conn)?;
schema::migrate_legacy_workspaces(&conn)?;
schema::ensure_column(
&conn,
"thread_index",
"runtime_id",
"TEXT NOT NULL DEFAULT 'primary'",
)?;
schema::ensure_column(
&conn,
"thread_index",
"archived",
"INTEGER NOT NULL DEFAULT 0",
)?;
schema::ensure_column(&conn, "events", "runtime_id", "TEXT NULL")?;
schema::ensure_column(
&conn,
"pending_approvals",
"runtime_id",
"TEXT NOT NULL DEFAULT 'primary'",
)?;
schema::ensure_column(
&conn,
"pending_server_requests",
"runtime_id",
"TEXT NOT NULL DEFAULT 'primary'",
)?;
Ok(())
}
pub fn db_path(&self) -> &Path {
&self.db_path
}
}