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