codex-mobile-bridge 0.3.10

Remote bridge and service manager for codex-mobile.
Documentation
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
    }
}