codex_mobile_bridge/storage/
mod.rs1mod decode;
2mod directories;
3mod events;
4mod management_tasks;
5mod pending_requests;
6mod runtimes;
7mod schema;
8#[cfg(test)]
9mod tests;
10mod threads;
11
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use anyhow::{Context, Result};
16use rusqlite::Connection;
17
18pub const PRIMARY_RUNTIME_ID: &str = "primary";
19
20#[derive(Debug, Clone)]
21pub struct Storage {
22 db_path: PathBuf,
23}
24
25impl Storage {
26 pub fn open(db_path: PathBuf) -> Result<Self> {
27 Self::open_with_transient_reset(db_path, true)
28 }
29
30 pub fn open_existing(db_path: PathBuf) -> Result<Self> {
31 Self::open_with_transient_reset(db_path, false)
32 }
33
34 fn open_with_transient_reset(db_path: PathBuf, reset_transient_state: bool) -> Result<Self> {
35 if let Some(parent) = db_path.parent() {
36 fs::create_dir_all(parent)
37 .with_context(|| format!("创建数据库目录失败: {}", parent.display()))?;
38 }
39
40 let storage = Self { db_path };
41 storage.migrate()?;
42 if reset_transient_state {
43 storage.clear_pending_requests()?;
44 storage.clear_legacy_pending_approvals()?;
45 }
46 Ok(storage)
47 }
48
49 fn clear_legacy_pending_approvals(&self) -> Result<()> {
50 let conn = self.connect()?;
51 conn.execute("DELETE FROM pending_approvals", [])?;
52 Ok(())
53 }
54
55 fn connect(&self) -> Result<Connection> {
56 let conn = Connection::open(&self.db_path)
57 .with_context(|| format!("打开数据库失败: {}", self.db_path.display()))?;
58 conn.execute_batch(
59 "PRAGMA foreign_keys = ON;
60 PRAGMA journal_mode = WAL;",
61 )?;
62 Ok(conn)
63 }
64
65 fn migrate(&self) -> Result<()> {
66 let conn = self.connect()?;
67 conn.execute_batch(
68 "CREATE TABLE IF NOT EXISTS directory_bookmarks (
69 path TEXT PRIMARY KEY,
70 display_name TEXT NOT NULL,
71 created_at_ms INTEGER NOT NULL,
72 updated_at_ms INTEGER NOT NULL
73 );
74
75 CREATE TABLE IF NOT EXISTS directory_history (
76 path TEXT PRIMARY KEY,
77 last_used_at_ms INTEGER NOT NULL,
78 use_count INTEGER NOT NULL
79 );
80
81 CREATE TABLE IF NOT EXISTS runtimes (
82 runtime_id TEXT PRIMARY KEY,
83 display_name TEXT NOT NULL,
84 codex_home TEXT NULL,
85 codex_binary TEXT NOT NULL,
86 is_primary INTEGER NOT NULL,
87 auto_start INTEGER NOT NULL,
88 created_at_ms INTEGER NOT NULL,
89 updated_at_ms INTEGER NOT NULL,
90 raw_json TEXT NOT NULL
91 );
92
93 CREATE TABLE IF NOT EXISTS mobile_sessions (
94 device_id TEXT PRIMARY KEY,
95 last_ack_seq INTEGER NOT NULL,
96 updated_at_ms INTEGER NOT NULL
97 );
98
99 CREATE TABLE IF NOT EXISTS events (
100 seq INTEGER PRIMARY KEY AUTOINCREMENT,
101 event_type TEXT NOT NULL,
102 runtime_id TEXT NULL,
103 thread_id TEXT NULL,
104 payload TEXT NOT NULL,
105 created_at_ms INTEGER NOT NULL
106 );
107
108 CREATE TABLE IF NOT EXISTS pending_approvals (
109 approval_id TEXT PRIMARY KEY,
110 runtime_id TEXT NOT NULL DEFAULT 'primary',
111 thread_id TEXT NOT NULL,
112 turn_id TEXT NOT NULL,
113 item_id TEXT NOT NULL,
114 kind TEXT NOT NULL,
115 reason TEXT NULL,
116 command TEXT NULL,
117 cwd TEXT NULL,
118 grant_root TEXT NULL,
119 available_decisions TEXT NOT NULL,
120 created_at_ms INTEGER NOT NULL,
121 raw_json TEXT NOT NULL
122 );
123
124 CREATE TABLE IF NOT EXISTS pending_server_requests (
125 request_id TEXT PRIMARY KEY,
126 runtime_id TEXT NOT NULL DEFAULT 'primary',
127 request_type TEXT NOT NULL,
128 thread_id TEXT NULL,
129 turn_id TEXT NULL,
130 item_id TEXT NULL,
131 title TEXT NULL,
132 reason TEXT NULL,
133 command TEXT NULL,
134 cwd TEXT NULL,
135 grant_root TEXT NULL,
136 tool_name TEXT NULL,
137 arguments TEXT NULL,
138 questions TEXT NOT NULL,
139 proposed_execpolicy_amendment TEXT NULL,
140 network_approval_context TEXT NULL,
141 schema TEXT NULL,
142 available_decisions TEXT NOT NULL,
143 raw_payload TEXT NOT NULL,
144 created_at_ms INTEGER NOT NULL,
145 raw_json TEXT NOT NULL
146 );
147
148 CREATE TABLE IF NOT EXISTS bridge_management_tasks (
149 task_id TEXT PRIMARY KEY,
150 status TEXT NOT NULL,
151 updated_at_ms INTEGER NOT NULL,
152 raw_json TEXT NOT NULL
153 );",
154 )?;
155
156 schema::ensure_thread_index_schema(&conn)?;
157 schema::migrate_legacy_workspaces(&conn)?;
158 schema::ensure_column(
159 &conn,
160 "thread_index",
161 "runtime_id",
162 "TEXT NOT NULL DEFAULT 'primary'",
163 )?;
164 schema::ensure_column(
165 &conn,
166 "thread_index",
167 "archived",
168 "INTEGER NOT NULL DEFAULT 0",
169 )?;
170 schema::ensure_column(&conn, "events", "runtime_id", "TEXT NULL")?;
171 schema::ensure_column(
172 &conn,
173 "pending_approvals",
174 "runtime_id",
175 "TEXT NOT NULL DEFAULT 'primary'",
176 )?;
177 schema::ensure_column(
178 &conn,
179 "pending_server_requests",
180 "runtime_id",
181 "TEXT NOT NULL DEFAULT 'primary'",
182 )?;
183
184 Ok(())
185 }
186
187 pub fn db_path(&self) -> &Path {
188 &self.db_path
189 }
190}