Skip to main content

cc_switch_lib/database/
migration.rs

1//! JSON → SQLite 数据迁移
2//!
3//! 将旧版 config.json (MultiAppConfig) 数据迁移到 SQLite 数据库。
4
5use super::{lock_conn, to_json_string, Database};
6use crate::app_config::MultiAppConfig;
7use crate::error::AppError;
8use rusqlite::{params, Connection};
9
10impl Database {
11    /// 从 MultiAppConfig 迁移数据到数据库
12    pub fn migrate_from_json(&self, config: &MultiAppConfig) -> Result<(), AppError> {
13        let mut conn = lock_conn!(self.conn);
14        let tx = conn
15            .transaction()
16            .map_err(|e| AppError::Database(e.to_string()))?;
17
18        Self::migrate_from_json_tx(&tx, config)?;
19
20        tx.commit()
21            .map_err(|e| AppError::Database(format!("Commit migration failed: {e}")))?;
22        Ok(())
23    }
24
25    /// 运行迁移的 dry-run 模式(在内存数据库中验证,不写入磁盘)
26    ///
27    /// 用于部署前验证迁移逻辑是否正确。
28    pub fn migrate_from_json_dry_run(config: &MultiAppConfig) -> Result<(), AppError> {
29        let mut conn =
30            Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
31        Self::create_tables_on_conn(&conn)?;
32        Self::apply_schema_migrations_on_conn(&conn)?;
33
34        let tx = conn
35            .transaction()
36            .map_err(|e| AppError::Database(e.to_string()))?;
37        Self::migrate_from_json_tx(&tx, config)?;
38
39        // 显式 drop transaction 而不提交(内存数据库会被丢弃)
40        drop(tx);
41        Ok(())
42    }
43
44    /// 在事务中执行迁移
45    fn migrate_from_json_tx(
46        tx: &rusqlite::Transaction<'_>,
47        config: &MultiAppConfig,
48    ) -> Result<(), AppError> {
49        // 1. 迁移 Providers
50        Self::migrate_providers(tx, config)?;
51
52        // 2. 迁移 MCP Servers
53        Self::migrate_mcp_servers(tx, config)?;
54
55        // 3. 迁移 Prompts
56        Self::migrate_prompts(tx, config)?;
57
58        // 4. 迁移 Skills
59        Self::migrate_skills(tx, config)?;
60
61        // 5. 迁移 Common Config
62        Self::migrate_common_config(tx, config)?;
63
64        Ok(())
65    }
66
67    /// 迁移供应商数据
68    fn migrate_providers(
69        tx: &rusqlite::Transaction<'_>,
70        config: &MultiAppConfig,
71    ) -> Result<(), AppError> {
72        for (app_key, manager) in &config.apps {
73            let app_type = app_key;
74            let current_id = &manager.current;
75
76            for (id, provider) in &manager.providers {
77                let is_current = if id == current_id { 1 } else { 0 };
78
79                // 处理 meta 和 endpoints
80                let mut meta_clone = provider.meta.clone().unwrap_or_default();
81                let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
82
83                tx.execute(
84                    "INSERT OR REPLACE INTO providers (
85                        id, app_type, name, settings_config, website_url, category,
86                        created_at, sort_index, notes, icon, icon_color, meta, is_current
87                    ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
88                    params![
89                        id,
90                        app_type,
91                        provider.name,
92                        to_json_string(&provider.settings_config)?,
93                        provider.website_url,
94                        provider.category,
95                        provider.created_at,
96                        provider.sort_index,
97                        provider.notes,
98                        provider.icon,
99                        provider.icon_color,
100                        to_json_string(&meta_clone)?,
101                        is_current,
102                    ],
103                )
104                .map_err(|e| AppError::Database(format!("Migrate provider failed: {e}")))?;
105
106                // 迁移 Endpoints
107                for (url, endpoint) in endpoints {
108                    tx.execute(
109                        "INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
110                         VALUES (?1, ?2, ?3, ?4)",
111                        params![id, app_type, url, endpoint.added_at],
112                    )
113                    .map_err(|e| AppError::Database(format!("Migrate endpoint failed: {e}")))?;
114                }
115            }
116        }
117        Ok(())
118    }
119
120    /// 迁移 MCP 服务器数据
121    fn migrate_mcp_servers(
122        tx: &rusqlite::Transaction<'_>,
123        config: &MultiAppConfig,
124    ) -> Result<(), AppError> {
125        if let Some(servers) = &config.mcp.servers {
126            for (id, server) in servers {
127                tx.execute(
128                    "INSERT OR REPLACE INTO mcp_servers (
129                        id, name, server_config, description, homepage, docs, tags,
130                        enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_openclaw, enabled_hermes
131                    ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
132                    params![
133                        id,
134                        server.name,
135                        to_json_string(&server.server)?,
136                        server.description,
137                        server.homepage,
138                        server.docs,
139                        to_json_string(&server.tags)?,
140                        server.apps.claude,
141                        server.apps.codex,
142                        server.apps.gemini,
143                        server.apps.opencode,
144                        server.apps.openclaw,
145                        server.apps.hermes,
146                    ],
147                )
148                .map_err(|e| AppError::Database(format!("Migrate mcp server failed: {e}")))?;
149            }
150        }
151        Ok(())
152    }
153
154    /// 迁移提示词数据
155    fn migrate_prompts(
156        tx: &rusqlite::Transaction<'_>,
157        config: &MultiAppConfig,
158    ) -> Result<(), AppError> {
159        let migrate_app_prompts = |prompts_map: &std::collections::HashMap<
160            String,
161            crate::prompt::Prompt,
162        >,
163                                   app_type: &str|
164         -> Result<(), AppError> {
165            for (id, prompt) in prompts_map {
166                tx.execute(
167                        "INSERT OR REPLACE INTO prompts (
168                            id, app_type, name, content, description, enabled, created_at, updated_at
169                        ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
170                        params![
171                            id,
172                            app_type,
173                            prompt.name,
174                            prompt.content,
175                            prompt.description,
176                            prompt.enabled,
177                            prompt.created_at,
178                            prompt.updated_at,
179                        ],
180                    )
181                    .map_err(|e| AppError::Database(format!("Migrate prompt failed: {e}")))?;
182            }
183            Ok(())
184        };
185
186        migrate_app_prompts(&config.prompts.claude.prompts, "claude")?;
187        migrate_app_prompts(&config.prompts.codex.prompts, "codex")?;
188        migrate_app_prompts(&config.prompts.gemini.prompts, "gemini")?;
189
190        Ok(())
191    }
192
193    /// 迁移 Skills 数据
194    fn migrate_skills(
195        tx: &rusqlite::Transaction<'_>,
196        config: &MultiAppConfig,
197    ) -> Result<(), AppError> {
198        // v3.10.0+:Skills 的 SSOT 已迁移到文件系统(~/.cc-switch-tui/skills/)+ 数据库统一结构。
199        //
200        // 旧版 config.json 里的 `skills.skills` 仅记录“安装状态”,但不包含完整元数据,
201        // 且无法保证 SSOT 目录中一定存在对应的 skill 文件。
202        //
203        // 因此这里不再直接把旧的安装状态写入新 skills 表,避免产生“数据库显示已安装但文件缺失”的不一致。
204        // 迁移后可通过:
205        // - 前端「导入已有」(扫描各应用的 skills 目录并复制到 SSOT)
206        // - 或后续启动时的自动扫描逻辑
207        // 来重建已安装技能记录。
208
209        for repo in &config.skills.repos {
210            tx.execute(
211                "INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
212                params![repo.owner, repo.name, repo.branch, repo.enabled],
213            ).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?;
214        }
215
216        Ok(())
217    }
218
219    /// 迁移通用配置片段
220    fn migrate_common_config(
221        tx: &rusqlite::Transaction<'_>,
222        config: &MultiAppConfig,
223    ) -> Result<(), AppError> {
224        if let Some(snippet) = &config.common_config_snippets.claude {
225            tx.execute(
226                "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
227                params!["common_config_claude", snippet],
228            )
229            .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
230        }
231        if let Some(snippet) = &config.common_config_snippets.codex {
232            tx.execute(
233                "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
234                params!["common_config_codex", snippet],
235            )
236            .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
237        }
238        if let Some(snippet) = &config.common_config_snippets.gemini {
239            tx.execute(
240                "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
241                params!["common_config_gemini", snippet],
242            )
243            .map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
244        }
245
246        Ok(())
247    }
248}