Skip to main content

cc_switch_lib/database/
schema.rs

1//! Schema 定义和迁移
2//!
3//! 负责数据库表结构的创建和版本迁移。
4
5use super::{lock_conn, Database, SCHEMA_VERSION};
6use crate::error::AppError;
7use rusqlite::{params, Connection};
8
9impl Database {
10    /// 创建所有数据库表
11    pub(crate) fn create_tables(&self) -> Result<(), AppError> {
12        let conn = lock_conn!(self.conn);
13        Self::create_tables_on_conn(&conn)
14    }
15
16    /// 在指定连接上创建表(供迁移和测试使用)
17    pub(crate) fn create_tables_on_conn(conn: &Connection) -> Result<(), AppError> {
18        // 1. Providers 表
19        conn.execute(
20            "CREATE TABLE IF NOT EXISTS providers (
21                id TEXT NOT NULL,
22                app_type TEXT NOT NULL,
23                name TEXT NOT NULL,
24                settings_config TEXT NOT NULL,
25                website_url TEXT,
26                category TEXT,
27                created_at INTEGER,
28                sort_index INTEGER,
29                notes TEXT,
30                icon TEXT,
31                icon_color TEXT,
32                meta TEXT NOT NULL DEFAULT '{}',
33                is_current BOOLEAN NOT NULL DEFAULT 0,
34                in_failover_queue BOOLEAN NOT NULL DEFAULT 0,
35                PRIMARY KEY (id, app_type)
36            )",
37            [],
38        )
39        .map_err(|e| AppError::Database(e.to_string()))?;
40
41        // 2. Provider Endpoints 表
42        conn.execute(
43            "CREATE TABLE IF NOT EXISTS provider_endpoints (
44                id INTEGER PRIMARY KEY AUTOINCREMENT,
45                provider_id TEXT NOT NULL,
46                app_type TEXT NOT NULL,
47                url TEXT NOT NULL,
48                added_at INTEGER,
49                FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE
50            )",
51            [],
52        )
53        .map_err(|e| AppError::Database(e.to_string()))?;
54
55        // 3. MCP Servers 表
56        conn.execute(
57            "CREATE TABLE IF NOT EXISTS mcp_servers (
58            id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL,
59            description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]',
60            enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0,
61            enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
62            enabled_openclaw BOOLEAN NOT NULL DEFAULT 0,
63            enabled_hermes BOOLEAN NOT NULL DEFAULT 0
64        )",
65            [],
66        )
67        .map_err(|e| AppError::Database(e.to_string()))?;
68
69        // 4. Prompts 表
70        conn.execute("CREATE TABLE IF NOT EXISTS prompts (
71            id TEXT NOT NULL, app_type TEXT NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL,
72            description TEXT, enabled BOOLEAN NOT NULL DEFAULT 1, created_at INTEGER, updated_at INTEGER,
73            PRIMARY KEY (id, app_type)
74        )", []).map_err(|e| AppError::Database(e.to_string()))?;
75
76        // 5. Skills 表(v3.10.0+ 统一结构)
77        conn.execute(
78            "CREATE TABLE IF NOT EXISTS skills (
79            id TEXT PRIMARY KEY,
80            name TEXT NOT NULL,
81            description TEXT,
82            directory TEXT NOT NULL,
83            repo_owner TEXT,
84            repo_name TEXT,
85            repo_branch TEXT DEFAULT 'main',
86            readme_url TEXT,
87            enabled_claude BOOLEAN NOT NULL DEFAULT 0,
88            enabled_codex BOOLEAN NOT NULL DEFAULT 0,
89            enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
90            enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
91            enabled_openclaw BOOLEAN NOT NULL DEFAULT 0,
92            enabled_hermes BOOLEAN NOT NULL DEFAULT 0,
93            installed_at INTEGER NOT NULL DEFAULT 0,
94            content_hash TEXT,
95            updated_at INTEGER NOT NULL DEFAULT 0
96        )",
97            [],
98        )
99        .map_err(|e| AppError::Database(e.to_string()))?;
100
101        // 6. Skill Repos 表
102        conn.execute(
103            "CREATE TABLE IF NOT EXISTS skill_repos (
104            owner TEXT NOT NULL, name TEXT NOT NULL, branch TEXT NOT NULL DEFAULT 'main',
105            enabled BOOLEAN NOT NULL DEFAULT 1, PRIMARY KEY (owner, name)
106        )",
107            [],
108        )
109        .map_err(|e| AppError::Database(e.to_string()))?;
110
111        // 7. Settings 表
112        conn.execute(
113            "CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)",
114            [],
115        )
116        .map_err(|e| AppError::Database(e.to_string()))?;
117
118        // 8. Proxy Config 表(三行结构,app_type 主键)
119        conn.execute("CREATE TABLE IF NOT EXISTS proxy_config (
120            app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
121            proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
122            listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
123            enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
124            max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
125            streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
126            circuit_failure_threshold INTEGER NOT NULL DEFAULT 4, circuit_success_threshold INTEGER NOT NULL DEFAULT 2,
127            circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60, circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.6,
128            circuit_min_requests INTEGER NOT NULL DEFAULT 10,
129            default_cost_multiplier TEXT NOT NULL DEFAULT '1',
130            pricing_model_source TEXT NOT NULL DEFAULT 'response',
131            created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
132        )", []).map_err(|e| AppError::Database(e.to_string()))?;
133
134        // 初始化三行数据(每应用不同默认值)
135        //
136        // 兼容旧数据库:
137        // - 老版本 proxy_config 是单例表(没有 app_type 列),此时不能执行三行 seed insert;
138        // - 旧表会在 apply_schema_migrations() 中迁移为三行结构后再插入。
139        if Self::has_column(conn, "proxy_config", "app_type")? {
140            conn.execute(
141                "INSERT OR IGNORE INTO proxy_config (app_type, max_retries,
142                streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
143                circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
144                circuit_error_rate_threshold, circuit_min_requests)
145                VALUES ('claude', 6, 90, 180, 600, 8, 3, 90, 0.7, 15)",
146                [],
147            )
148            .map_err(|e| AppError::Database(e.to_string()))?;
149            conn.execute(
150                "INSERT OR IGNORE INTO proxy_config (app_type, max_retries,
151                streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
152                circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
153                circuit_error_rate_threshold, circuit_min_requests)
154                VALUES ('codex', 3, 60, 120, 600, 4, 2, 60, 0.6, 10)",
155                [],
156            )
157            .map_err(|e| AppError::Database(e.to_string()))?;
158            conn.execute(
159                "INSERT OR IGNORE INTO proxy_config (app_type, max_retries,
160                streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
161                circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
162                circuit_error_rate_threshold, circuit_min_requests)
163                VALUES ('gemini', 5, 60, 120, 600, 4, 2, 60, 0.6, 10)",
164                [],
165            )
166            .map_err(|e| AppError::Database(e.to_string()))?;
167        }
168
169        // 9. Provider Health 表
170        conn.execute("CREATE TABLE IF NOT EXISTS provider_health (
171            provider_id TEXT NOT NULL, app_type TEXT NOT NULL, is_healthy INTEGER NOT NULL DEFAULT 1,
172            consecutive_failures INTEGER NOT NULL DEFAULT 0, last_success_at TEXT, last_failure_at TEXT,
173            last_error TEXT, updated_at TEXT NOT NULL,
174            PRIMARY KEY (provider_id, app_type),
175            FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE
176        )", []).map_err(|e| AppError::Database(e.to_string()))?;
177
178        // 10. Proxy Request Logs 表
179        conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs (
180            request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL,
181            request_model TEXT,
182            input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
183            cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
184            input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0',
185            cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
186            total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER,
187            duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT,
188            provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0,
189            cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL,
190            data_source TEXT NOT NULL DEFAULT 'proxy'
191        )", []).map_err(|e| AppError::Database(e.to_string()))?;
192
193        conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_provider ON proxy_request_logs(provider_id, app_type)", [])
194            .map_err(|e| AppError::Database(e.to_string()))?;
195        conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON proxy_request_logs(created_at)", [])
196            .map_err(|e| AppError::Database(e.to_string()))?;
197        conn.execute(
198            "CREATE INDEX IF NOT EXISTS idx_request_logs_model ON proxy_request_logs(model)",
199            [],
200        )
201        .map_err(|e| AppError::Database(e.to_string()))?;
202        conn.execute(
203            "CREATE INDEX IF NOT EXISTS idx_request_logs_session ON proxy_request_logs(session_id)",
204            [],
205        )
206        .map_err(|e| AppError::Database(e.to_string()))?;
207        conn.execute(
208            "CREATE INDEX IF NOT EXISTS idx_request_logs_status ON proxy_request_logs(status_code)",
209            [],
210        )
211        .map_err(|e| AppError::Database(e.to_string()))?;
212
213        // 11. Model Pricing 表
214        conn.execute(
215            "CREATE TABLE IF NOT EXISTS model_pricing (
216            model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
217            input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
218            cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
219            cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
220        )",
221            [],
222        )
223        .map_err(|e| AppError::Database(e.to_string()))?;
224
225        // 12. Stream Check Logs 表
226        conn.execute("CREATE TABLE IF NOT EXISTS stream_check_logs (
227            id INTEGER PRIMARY KEY AUTOINCREMENT, provider_id TEXT NOT NULL, provider_name TEXT NOT NULL,
228            app_type TEXT NOT NULL, status TEXT NOT NULL, success INTEGER NOT NULL, message TEXT NOT NULL,
229            response_time_ms INTEGER, http_status INTEGER, model_used TEXT,
230            retry_count INTEGER DEFAULT 0, tested_at INTEGER NOT NULL
231        )", []).map_err(|e| AppError::Database(e.to_string()))?;
232
233        conn.execute(
234            "CREATE INDEX IF NOT EXISTS idx_stream_check_logs_provider
235             ON stream_check_logs(app_type, provider_id, tested_at DESC)",
236            [],
237        )
238        .map_err(|e| AppError::Database(e.to_string()))?;
239
240        // 注意:circuit_breaker_config 已合并到 proxy_config 表中
241
242        // 16. Proxy Live Backup 表 (Live 配置备份)
243        conn.execute(
244            "CREATE TABLE IF NOT EXISTS proxy_live_backup (
245            app_type TEXT PRIMARY KEY, original_config TEXT NOT NULL, backed_up_at TEXT NOT NULL
246        )",
247            [],
248        )
249        .map_err(|e| AppError::Database(e.to_string()))?;
250
251        conn.execute(
252            "CREATE TABLE IF NOT EXISTS usage_daily_rollups (
253                date TEXT NOT NULL,
254                app_type TEXT NOT NULL,
255                provider_id TEXT NOT NULL,
256                model TEXT NOT NULL,
257                request_count INTEGER NOT NULL DEFAULT 0,
258                success_count INTEGER NOT NULL DEFAULT 0,
259                input_tokens INTEGER NOT NULL DEFAULT 0,
260                output_tokens INTEGER NOT NULL DEFAULT 0,
261                cache_read_tokens INTEGER NOT NULL DEFAULT 0,
262                cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
263                total_cost_usd TEXT NOT NULL DEFAULT '0',
264                avg_latency_ms INTEGER NOT NULL DEFAULT 0,
265                PRIMARY KEY (date, app_type, provider_id, model)
266            )",
267            [],
268        )
269        .map_err(|e| AppError::Database(e.to_string()))?;
270
271        conn.execute(
272            "CREATE TABLE IF NOT EXISTS session_log_sync (
273                file_path TEXT PRIMARY KEY,
274                last_modified INTEGER NOT NULL,
275                last_line_offset INTEGER NOT NULL DEFAULT 0,
276                last_synced_at INTEGER NOT NULL
277            )",
278            [],
279        )
280        .map_err(|e| AppError::Database(e.to_string()))?;
281
282        // 尝试添加 live_takeover_active 列到 proxy_config 表
283        let _ = conn.execute(
284            "ALTER TABLE proxy_config ADD COLUMN live_takeover_active INTEGER NOT NULL DEFAULT 0",
285            [],
286        );
287
288        // 尝试添加基础配置列到 proxy_config 表(兼容 v3.9.0-2 升级)
289        let _ = conn.execute(
290            "ALTER TABLE proxy_config ADD COLUMN proxy_enabled INTEGER NOT NULL DEFAULT 0",
291            [],
292        );
293        let _ = conn.execute(
294            "ALTER TABLE proxy_config ADD COLUMN listen_address TEXT NOT NULL DEFAULT '127.0.0.1'",
295            [],
296        );
297        let _ = conn.execute(
298            "ALTER TABLE proxy_config ADD COLUMN listen_port INTEGER NOT NULL DEFAULT 15721",
299            [],
300        );
301        let _ = conn.execute(
302            "ALTER TABLE proxy_config ADD COLUMN enable_logging INTEGER NOT NULL DEFAULT 1",
303            [],
304        );
305
306        // 尝试添加超时配置列到 proxy_config 表
307        let _ = conn.execute(
308            "ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60",
309            [],
310        );
311        let _ = conn.execute(
312            "ALTER TABLE proxy_config ADD COLUMN streaming_idle_timeout INTEGER NOT NULL DEFAULT 120",
313            [],
314        );
315        let _ = conn.execute(
316            "ALTER TABLE proxy_config ADD COLUMN non_streaming_timeout INTEGER NOT NULL DEFAULT 600",
317            [],
318        );
319
320        // 兼容:若旧版 proxy_config 仍为单例结构(无 app_type),则在启动时直接转换为三行结构
321        // 说明:user_version=2 时不会再触发 v1->v2 迁移,但新代码查询依赖 app_type 列。
322        if Self::table_exists(conn, "proxy_config")?
323            && !Self::has_column(conn, "proxy_config", "app_type")?
324        {
325            Self::migrate_proxy_config_to_per_app(conn)?;
326        }
327
328        // 确保 in_failover_queue 列存在(对于已存在的 v2 数据库)
329        Self::add_column_if_missing(
330            conn,
331            "providers",
332            "in_failover_queue",
333            "BOOLEAN NOT NULL DEFAULT 0",
334        )?;
335
336        // 删除旧的 failover_queue 表(如果存在)
337        let _ = conn.execute("DROP INDEX IF EXISTS idx_failover_queue_order", []);
338        let _ = conn.execute("DROP TABLE IF EXISTS failover_queue", []);
339
340        // 为故障转移队列创建索引(基于 providers 表)
341        let _ = conn.execute(
342            "CREATE INDEX IF NOT EXISTS idx_providers_failover
343             ON providers(app_type, in_failover_queue, sort_index)",
344            [],
345        );
346
347        Ok(())
348    }
349
350    /// 应用 Schema 迁移
351    pub(crate) fn apply_schema_migrations(&self) -> Result<(), AppError> {
352        let conn = lock_conn!(self.conn);
353        Self::apply_schema_migrations_on_conn(&conn)
354    }
355
356    /// 在指定连接上应用 Schema 迁移
357    pub(crate) fn apply_schema_migrations_on_conn(conn: &Connection) -> Result<(), AppError> {
358        conn.execute("SAVEPOINT schema_migration;", [])
359            .map_err(|e| AppError::Database(format!("开启迁移 savepoint 失败: {e}")))?;
360
361        let mut version = Self::get_user_version(conn)?;
362
363        if version > SCHEMA_VERSION {
364            conn.execute("ROLLBACK TO schema_migration;", []).ok();
365            conn.execute("RELEASE schema_migration;", []).ok();
366            return Err(Self::future_schema_error(version));
367        }
368
369        let result = (|| {
370            while version < SCHEMA_VERSION {
371                match version {
372                    0 => {
373                        log::info!("检测到 user_version=0,迁移到 1(补齐缺失列并设置版本)");
374                        Self::migrate_v0_to_v1(conn)?;
375                        Self::set_user_version(conn, 1)?;
376                    }
377                    1 => {
378                        log::info!(
379                            "迁移数据库从 v1 到 v2(添加使用统计表和完整字段,重构 skills 表)"
380                        );
381                        Self::migrate_v1_to_v2(conn)?;
382                        Self::set_user_version(conn, 2)?;
383                    }
384                    2 => {
385                        log::info!("迁移数据库从 v2 到 v3(Skills 统一管理架构)");
386                        Self::migrate_v2_to_v3(conn)?;
387                        Self::set_user_version(conn, 3)?;
388                    }
389                    3 => {
390                        log::info!("迁移数据库从 v3 到 v4(OpenCode 支持)");
391                        Self::migrate_v3_to_v4(conn)?;
392                        Self::set_user_version(conn, 4)?;
393                    }
394                    4 => {
395                        log::info!("迁移数据库从 v4 到 v5(计费模式支持)");
396                        Self::migrate_v4_to_v5(conn)?;
397                        Self::set_user_version(conn, 5)?;
398                    }
399                    5 => {
400                        log::info!("迁移数据库从 v5 到 v6(使用量聚合表 + Copilot 模板类型统一)");
401                        Self::migrate_v5_to_v6(conn)?;
402                        Self::set_user_version(conn, 6)?;
403                    }
404                    6 => {
405                        log::info!("迁移数据库从 v6 到 v7(Skills 更新检测支持)");
406                        Self::migrate_v6_to_v7(conn)?;
407                        Self::set_user_version(conn, 7)?;
408                    }
409                    7 => {
410                        log::info!("迁移数据库从 v7 到 v8(会话日志使用追踪 + 修正模型定价)");
411                        Self::migrate_v7_to_v8(conn)?;
412                        Self::set_user_version(conn, 8)?;
413                    }
414                    8 => {
415                        log::info!("迁移数据库从 v8 到 v9(全面补充模型定价)");
416                        Self::migrate_v8_to_v9(conn)?;
417                        Self::set_user_version(conn, 9)?;
418                    }
419                    9 => {
420                        log::info!("迁移数据库从 v9 到 v10(添加 Hermes Agent 支持)");
421                        Self::migrate_v9_to_v10(conn)?;
422                        Self::set_user_version(conn, 10)?;
423                    }
424                    10 => {
425                        log::info!("迁移数据库从 v10 到 v11(添加 OpenClaw Skills 支持)");
426                        Self::migrate_v10_to_v11(conn)?;
427                        Self::set_user_version(conn, 11)?;
428                    }
429                    11 => {
430                        log::info!("迁移数据库从 v11 到 v12(添加 OpenClaw MCP 支持)");
431                        Self::migrate_v11_to_v12(conn)?;
432                        Self::set_user_version(conn, 12)?;
433                    }
434                    _ => {
435                        return Err(AppError::Database(format!(
436                            "未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}"
437                        )));
438                    }
439                }
440                version = Self::get_user_version(conn)?;
441            }
442            Ok(())
443        })();
444
445        match result {
446            Ok(_) => {
447                conn.execute("RELEASE schema_migration;", [])
448                    .map_err(|e| AppError::Database(format!("提交迁移 savepoint 失败: {e}")))?;
449                Ok(())
450            }
451            Err(e) => {
452                conn.execute("ROLLBACK TO schema_migration;", []).ok();
453                conn.execute("RELEASE schema_migration;", []).ok();
454                Err(e)
455            }
456        }
457    }
458
459    /// v0 -> v1 迁移:补齐所有缺失列
460    fn migrate_v0_to_v1(conn: &Connection) -> Result<(), AppError> {
461        // providers 表
462        Self::add_column_if_missing(conn, "providers", "category", "TEXT")?;
463        Self::add_column_if_missing(conn, "providers", "created_at", "INTEGER")?;
464        Self::add_column_if_missing(conn, "providers", "sort_index", "INTEGER")?;
465        Self::add_column_if_missing(conn, "providers", "notes", "TEXT")?;
466        Self::add_column_if_missing(conn, "providers", "icon", "TEXT")?;
467        Self::add_column_if_missing(conn, "providers", "icon_color", "TEXT")?;
468        Self::add_column_if_missing(conn, "providers", "meta", "TEXT NOT NULL DEFAULT '{}'")?;
469        Self::add_column_if_missing(
470            conn,
471            "providers",
472            "is_current",
473            "BOOLEAN NOT NULL DEFAULT 0",
474        )?;
475
476        // provider_endpoints 表
477        Self::add_column_if_missing(conn, "provider_endpoints", "added_at", "INTEGER")?;
478
479        // mcp_servers 表
480        Self::add_column_if_missing(conn, "mcp_servers", "description", "TEXT")?;
481        Self::add_column_if_missing(conn, "mcp_servers", "homepage", "TEXT")?;
482        Self::add_column_if_missing(conn, "mcp_servers", "docs", "TEXT")?;
483        Self::add_column_if_missing(conn, "mcp_servers", "tags", "TEXT NOT NULL DEFAULT '[]'")?;
484        Self::add_column_if_missing(
485            conn,
486            "mcp_servers",
487            "enabled_codex",
488            "BOOLEAN NOT NULL DEFAULT 0",
489        )?;
490        Self::add_column_if_missing(
491            conn,
492            "mcp_servers",
493            "enabled_gemini",
494            "BOOLEAN NOT NULL DEFAULT 0",
495        )?;
496
497        // prompts 表
498        Self::add_column_if_missing(conn, "prompts", "description", "TEXT")?;
499        Self::add_column_if_missing(conn, "prompts", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?;
500        Self::add_column_if_missing(conn, "prompts", "created_at", "INTEGER")?;
501        Self::add_column_if_missing(conn, "prompts", "updated_at", "INTEGER")?;
502
503        // skills 表
504        Self::add_column_if_missing(conn, "skills", "installed_at", "INTEGER NOT NULL DEFAULT 0")?;
505
506        // skill_repos 表
507        Self::add_column_if_missing(
508            conn,
509            "skill_repos",
510            "branch",
511            "TEXT NOT NULL DEFAULT 'main'",
512        )?;
513        Self::add_column_if_missing(conn, "skill_repos", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?;
514        // 注意: skills_path 字段已被移除,因为现在支持全仓库递归扫描
515
516        Ok(())
517    }
518
519    /// v1 -> v2 迁移:添加使用统计表和完整字段,重构 skills 表
520    fn migrate_v1_to_v2(conn: &Connection) -> Result<(), AppError> {
521        // providers 表字段
522        Self::add_column_if_missing(
523            conn,
524            "providers",
525            "cost_multiplier",
526            "TEXT NOT NULL DEFAULT '1.0'",
527        )?;
528        Self::add_column_if_missing(conn, "providers", "limit_daily_usd", "TEXT")?;
529        Self::add_column_if_missing(conn, "providers", "limit_monthly_usd", "TEXT")?;
530        Self::add_column_if_missing(conn, "providers", "provider_type", "TEXT")?;
531        Self::add_column_if_missing(
532            conn,
533            "providers",
534            "in_failover_queue",
535            "BOOLEAN NOT NULL DEFAULT 0",
536        )?;
537
538        // 添加代理超时配置字段
539        if Self::table_exists(conn, "proxy_config")? {
540            // 兼容旧版本缺失的基础字段
541            Self::add_column_if_missing(
542                conn,
543                "proxy_config",
544                "proxy_enabled",
545                "INTEGER NOT NULL DEFAULT 0",
546            )?;
547            Self::add_column_if_missing(
548                conn,
549                "proxy_config",
550                "listen_address",
551                "TEXT NOT NULL DEFAULT '127.0.0.1'",
552            )?;
553            Self::add_column_if_missing(
554                conn,
555                "proxy_config",
556                "listen_port",
557                "INTEGER NOT NULL DEFAULT 15721",
558            )?;
559            Self::add_column_if_missing(
560                conn,
561                "proxy_config",
562                "enable_logging",
563                "INTEGER NOT NULL DEFAULT 1",
564            )?;
565
566            Self::add_column_if_missing(
567                conn,
568                "proxy_config",
569                "streaming_first_byte_timeout",
570                "INTEGER NOT NULL DEFAULT 60",
571            )?;
572            Self::add_column_if_missing(
573                conn,
574                "proxy_config",
575                "streaming_idle_timeout",
576                "INTEGER NOT NULL DEFAULT 120",
577            )?;
578            Self::add_column_if_missing(
579                conn,
580                "proxy_config",
581                "non_streaming_timeout",
582                "INTEGER NOT NULL DEFAULT 600",
583            )?;
584        }
585
586        // 删除旧的 failover_queue 表(如果存在)
587        conn.execute("DROP INDEX IF EXISTS idx_failover_queue_order", [])
588            .map_err(|e| AppError::Database(format!("删除 failover_queue 索引失败: {e}")))?;
589        conn.execute("DROP TABLE IF EXISTS failover_queue", [])
590            .map_err(|e| AppError::Database(format!("删除 failover_queue 表失败: {e}")))?;
591
592        // 创建 failover 索引
593        conn.execute(
594            "CREATE INDEX IF NOT EXISTS idx_providers_failover
595             ON providers(app_type, in_failover_queue, sort_index)",
596            [],
597        )
598        .map_err(|e| AppError::Database(format!("创建 failover 索引失败: {e}")))?;
599
600        // proxy_request_logs 表
601        conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs (
602            request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL,
603            request_model TEXT,
604            input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
605            cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
606            input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0',
607            cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
608            total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER,
609            duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT,
610            provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0,
611            cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL
612        )", [])?;
613
614        // 为已存在的表添加新字段
615        Self::add_column_if_missing(conn, "proxy_request_logs", "provider_type", "TEXT")?;
616        Self::add_column_if_missing(
617            conn,
618            "proxy_request_logs",
619            "is_streaming",
620            "INTEGER NOT NULL DEFAULT 0",
621        )?;
622        Self::add_column_if_missing(
623            conn,
624            "proxy_request_logs",
625            "cost_multiplier",
626            "TEXT NOT NULL DEFAULT '1.0'",
627        )?;
628        Self::add_column_if_missing(conn, "proxy_request_logs", "first_token_ms", "INTEGER")?;
629        Self::add_column_if_missing(conn, "proxy_request_logs", "duration_ms", "INTEGER")?;
630
631        // model_pricing 表
632        conn.execute(
633            "CREATE TABLE IF NOT EXISTS model_pricing (
634            model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
635            input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
636            cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
637            cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
638        )",
639            [],
640        )?;
641
642        // 清空并重新插入模型定价
643        conn.execute("DELETE FROM model_pricing", [])
644            .map_err(|e| AppError::Database(format!("清空模型定价失败: {e}")))?;
645        Self::seed_model_pricing(conn)?;
646
647        // 重构 skills 表(添加 app_type 字段)
648        Self::migrate_skills_table(conn)?;
649
650        // 重构 proxy_config 为三行结构(每应用独立配置)
651        Self::migrate_proxy_config_to_per_app(conn)?;
652
653        Ok(())
654    }
655
656    /// 将 proxy_config 迁移为三行结构(每应用独立配置)
657    fn migrate_proxy_config_to_per_app(conn: &Connection) -> Result<(), AppError> {
658        // 检查是否已经是新表结构(幂等性)
659        if !Self::table_exists(conn, "proxy_config")? {
660            // 表不存在,跳过迁移(新安装)
661            return Ok(());
662        }
663
664        if Self::has_column(conn, "proxy_config", "app_type")? {
665            // 已经是三行结构,跳过迁移
666            log::info!("proxy_config 已经是三行结构,跳过迁移");
667            return Ok(());
668        }
669
670        // 读取旧配置
671        let old_config = conn
672            .query_row(
673                "SELECT listen_address, listen_port, max_retries, enable_logging,
674                    streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout
675             FROM proxy_config WHERE id = 1",
676                [],
677                |row| {
678                    Ok((
679                        row.get::<_, String>(0)?,
680                        row.get::<_, i32>(1)?,
681                        row.get::<_, i32>(2)?,
682                        row.get::<_, i32>(3)?,
683                        row.get::<_, i32>(4).unwrap_or(30),
684                        row.get::<_, i32>(5).unwrap_or(60),
685                        row.get::<_, i32>(6).unwrap_or(300),
686                    ))
687                },
688            )
689            .unwrap_or_else(|_| ("127.0.0.1".to_string(), 5000, 3, 1, 30, 60, 300));
690
691        let old_cb = conn.query_row(
692            "SELECT failure_threshold, success_threshold, timeout_seconds, error_rate_threshold, min_requests
693             FROM circuit_breaker_config WHERE id = 1", [],
694            |row| Ok((row.get::<_, i32>(0)?, row.get::<_, i32>(1)?, row.get::<_, i64>(2)?,
695                      row.get::<_, f64>(3)?, row.get::<_, i32>(4)?))
696        ).unwrap_or((5, 2, 60, 0.5, 10));
697
698        let get_bool = |key: &str| -> bool {
699            conn.query_row("SELECT value FROM settings WHERE key = ?", [key], |r| {
700                r.get::<_, String>(0)
701            })
702            .map(|v| v == "true" || v == "1")
703            .unwrap_or(false)
704        };
705
706        let apps = [
707            (
708                "claude",
709                get_bool("proxy_takeover_claude"),
710                get_bool("auto_failover_enabled_claude"),
711                6,
712                45,
713                90,
714                8,
715                3,
716                90,
717                0.6,
718                15,
719            ),
720            (
721                "codex",
722                get_bool("proxy_takeover_codex"),
723                get_bool("auto_failover_enabled_codex"),
724                3,
725                old_config.4,
726                old_config.5,
727                old_cb.0,
728                old_cb.1,
729                old_cb.2,
730                old_cb.3,
731                old_cb.4,
732            ),
733            (
734                "gemini",
735                get_bool("proxy_takeover_gemini"),
736                get_bool("auto_failover_enabled_gemini"),
737                5,
738                old_config.4,
739                old_config.5,
740                old_cb.0,
741                old_cb.1,
742                old_cb.2,
743                old_cb.3,
744                old_cb.4,
745            ),
746        ];
747
748        // 创建新表
749        conn.execute("DROP TABLE IF EXISTS proxy_config_new", [])?;
750        conn.execute("CREATE TABLE proxy_config_new (
751            app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
752            proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
753            listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
754            enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
755            max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
756            streaming_idle_timeout INTEGER NOT NULL DEFAULT 120, non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
757            circuit_failure_threshold INTEGER NOT NULL DEFAULT 4, circuit_success_threshold INTEGER NOT NULL DEFAULT 2,
758            circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60, circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.6,
759            circuit_min_requests INTEGER NOT NULL DEFAULT 10,
760            default_cost_multiplier TEXT NOT NULL DEFAULT '1',
761            pricing_model_source TEXT NOT NULL DEFAULT 'response',
762            created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
763        )", [])?;
764
765        // 插入三行配置
766        for (app, takeover, failover, retries, fb, idle, cb_f, cb_s, cb_t, cb_r, cb_m) in apps {
767            conn.execute(
768                "INSERT INTO proxy_config_new (app_type, proxy_enabled, listen_address, listen_port, enable_logging,
769                 enabled, auto_failover_enabled, max_retries, streaming_first_byte_timeout, streaming_idle_timeout,
770                 non_streaming_timeout, circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
771                 circuit_error_rate_threshold, circuit_min_requests)
772                 VALUES (?1, 0, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
773                rusqlite::params![app, old_config.0, old_config.1, old_config.3,
774                    if takeover { 1 } else { 0 }, if failover { 1 } else { 0 },
775                    retries, fb, idle, old_config.6, cb_f, cb_s, cb_t, cb_r, cb_m]
776            ).map_err(|e| AppError::Database(format!("插入 {app} 配置失败: {e}")))?;
777        }
778
779        // 替换表并清理
780        conn.execute("DROP TABLE IF EXISTS proxy_config", [])?;
781        conn.execute("ALTER TABLE proxy_config_new RENAME TO proxy_config", [])?;
782        conn.execute("DROP TABLE IF EXISTS circuit_breaker_config", [])?;
783        conn.execute("DELETE FROM settings WHERE key LIKE 'proxy_takeover_%'", [])?;
784        conn.execute(
785            "DELETE FROM settings WHERE key LIKE 'auto_failover_enabled_%'",
786            [],
787        )?;
788
789        log::info!("proxy_config 已迁移为三行结构");
790        Ok(())
791    }
792
793    /// 迁移 skills 表:从单 key 主键改为 (directory, app_type) 复合主键
794    fn migrate_skills_table(conn: &Connection) -> Result<(), AppError> {
795        // v3 结构(统一管理架构)已经是更高版本的 skills 表:
796        // - 主键为 id
797        // - 包含 enabled_claude / enabled_codex / enabled_gemini 等列
798        // 在这种情况下,不应再执行 v1 -> v2 的迁移逻辑,否则会因列不匹配而失败。
799        if Self::has_column(conn, "skills", "enabled_claude")?
800            || Self::has_column(conn, "skills", "id")?
801        {
802            log::info!("skills 表已经是 v3 结构,跳过 v1 -> v2 迁移");
803            return Ok(());
804        }
805
806        // 检查是否已经是新表结构
807        if Self::has_column(conn, "skills", "app_type")? {
808            log::info!("skills 表已经包含 app_type 字段,跳过迁移");
809            return Ok(());
810        }
811
812        log::info!("开始迁移 skills 表...");
813
814        // 1. 重命名旧表
815        conn.execute("ALTER TABLE skills RENAME TO skills_old", [])
816            .map_err(|e| AppError::Database(format!("重命名旧 skills 表失败: {e}")))?;
817
818        // 2. 创建新表
819        conn.execute(
820            "CREATE TABLE skills (
821                directory TEXT NOT NULL,
822                app_type TEXT NOT NULL,
823                installed BOOLEAN NOT NULL DEFAULT 0,
824                installed_at INTEGER NOT NULL DEFAULT 0,
825                PRIMARY KEY (directory, app_type)
826            )",
827            [],
828        )
829        .map_err(|e| AppError::Database(format!("创建新 skills 表失败: {e}")))?;
830
831        // 3. 迁移数据:解析 key 格式(如 "claude:my-skill" 或 "codex:foo")
832        //    旧数据如果没有前缀,默认为 claude
833        let mut stmt = conn
834            .prepare("SELECT key, installed, installed_at FROM skills_old")
835            .map_err(|e| AppError::Database(format!("查询旧 skills 数据失败: {e}")))?;
836
837        let old_skills: Vec<(String, bool, i64)> = stmt
838            .query_map([], |row| {
839                Ok((
840                    row.get::<_, String>(0)?,
841                    row.get::<_, bool>(1)?,
842                    row.get::<_, i64>(2)?,
843                ))
844            })
845            .map_err(|e| AppError::Database(format!("读取旧 skills 数据失败: {e}")))?
846            .collect::<Result<Vec<_>, _>>()
847            .map_err(|e| AppError::Database(format!("解析旧 skills 数据失败: {e}")))?;
848
849        let count = old_skills.len();
850
851        for (key, installed, installed_at) in old_skills {
852            // 解析 key: "app:directory" 或 "directory"(默认 claude)
853            let (app_type, directory) = if let Some(idx) = key.find(':') {
854                let (app, dir) = key.split_at(idx);
855                (app.to_string(), dir[1..].to_string()) // 跳过冒号
856            } else {
857                ("claude".to_string(), key.clone())
858            };
859
860            conn.execute(
861                "INSERT INTO skills (directory, app_type, installed, installed_at) VALUES (?1, ?2, ?3, ?4)",
862                rusqlite::params![directory, app_type, installed, installed_at],
863            )
864            .map_err(|e| {
865                AppError::Database(format!("迁移 skill {key} 到新表失败: {e}"))
866            })?;
867        }
868
869        // 4. 删除旧表
870        conn.execute("DROP TABLE skills_old", [])
871            .map_err(|e| AppError::Database(format!("删除旧 skills 表失败: {e}")))?;
872
873        log::info!("skills 表迁移完成,共迁移 {count} 条记录");
874        Ok(())
875    }
876
877    /// v2 -> v3 迁移:Skills 统一管理架构
878    ///
879    /// 将 skills 表从 (directory, app_type) 复合主键结构迁移到统一的 id 主键结构,
880    /// 支持三应用启用标志(enabled_claude, enabled_codex, enabled_gemini)。
881    ///
882    /// 迁移策略:
883    /// 1. 旧数据库只存储安装记录,真正的 skill 文件在文件系统
884    /// 2. 直接重建新表结构,后续由 SkillService 在首次启动时扫描文件系统重建数据
885    fn migrate_v2_to_v3(conn: &Connection) -> Result<(), AppError> {
886        // 检查是否已经是新结构(通过检查是否有 enabled_claude 列)
887        if Self::has_column(conn, "skills", "enabled_claude")? {
888            log::info!("skills 表已经是 v3 结构,跳过迁移");
889            return Ok(());
890        }
891
892        log::info!("开始迁移 skills 表到 v3 结构(统一管理架构)...");
893
894        // 1. 备份旧数据(用于日志)
895        let old_count: i64 = conn
896            .query_row("SELECT COUNT(*) FROM skills", [], |row| row.get(0))
897            .unwrap_or(0);
898        log::info!("旧 skills 表有 {old_count} 条记录");
899
900        // 标记:需要在启动后从文件系统扫描并重建 Skills 数据
901        // 说明:v3 结构将 Skills 的 SSOT 迁移到 ~/.cc-switch-tui/skills/,
902        // 旧表只存“安装记录”,无法直接无损迁移到新结构,因此改为启动后扫描 app 目录导入。
903        let _ = conn.execute(
904            "INSERT OR REPLACE INTO settings (key, value) VALUES ('skills_ssot_migration_pending', 'true')",
905            [],
906        );
907
908        // 2. 删除旧表
909        conn.execute("DROP TABLE IF EXISTS skills", [])
910            .map_err(|e| AppError::Database(format!("删除旧 skills 表失败: {e}")))?;
911
912        // 3. 创建新表
913        conn.execute(
914            "CREATE TABLE skills (
915                id TEXT PRIMARY KEY,
916                name TEXT NOT NULL,
917                description TEXT,
918                directory TEXT NOT NULL,
919                repo_owner TEXT,
920                repo_name TEXT,
921                repo_branch TEXT DEFAULT 'main',
922                readme_url TEXT,
923                enabled_claude BOOLEAN NOT NULL DEFAULT 0,
924                enabled_codex BOOLEAN NOT NULL DEFAULT 0,
925                enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
926                installed_at INTEGER NOT NULL DEFAULT 0
927            )",
928            [],
929        )
930        .map_err(|e| AppError::Database(format!("创建新 skills 表失败: {e}")))?;
931
932        log::info!(
933            "skills 表已迁移到 v3 结构。\n\
934             注意:旧的安装记录已清除,首次启动时将自动扫描文件系统重建数据。"
935        );
936
937        Ok(())
938    }
939
940    /// v3 -> v4 迁移:添加 OpenCode 支持
941    ///
942    /// 为 mcp_servers 和 skills 表添加 enabled_opencode 列。
943    fn migrate_v3_to_v4(conn: &Connection) -> Result<(), AppError> {
944        // 为 mcp_servers 表添加 enabled_opencode 列
945        Self::add_column_if_missing(
946            conn,
947            "mcp_servers",
948            "enabled_opencode",
949            "BOOLEAN NOT NULL DEFAULT 0",
950        )?;
951
952        // 为 skills 表添加 enabled_opencode 列
953        Self::add_column_if_missing(
954            conn,
955            "skills",
956            "enabled_opencode",
957            "BOOLEAN NOT NULL DEFAULT 0",
958        )?;
959
960        log::info!("v3 -> v4 迁移完成:已添加 OpenCode 支持");
961        Ok(())
962    }
963
964    /// v4 -> v5 迁移:新增计费模式配置与请求模型字段
965    fn migrate_v4_to_v5(conn: &Connection) -> Result<(), AppError> {
966        if Self::table_exists(conn, "proxy_config")? {
967            Self::add_column_if_missing(
968                conn,
969                "proxy_config",
970                "default_cost_multiplier",
971                "TEXT NOT NULL DEFAULT '1'",
972            )?;
973            Self::add_column_if_missing(
974                conn,
975                "proxy_config",
976                "pricing_model_source",
977                "TEXT NOT NULL DEFAULT 'response'",
978            )?;
979        }
980        if Self::table_exists(conn, "proxy_request_logs")? {
981            Self::add_column_if_missing(conn, "proxy_request_logs", "request_model", "TEXT")?;
982        }
983
984        log::info!("v4 -> v5 迁移完成:已添加计费模式与请求模型字段");
985        Ok(())
986    }
987
988    fn migrate_v5_to_v6(conn: &Connection) -> Result<(), AppError> {
989        conn.execute(
990            "CREATE TABLE IF NOT EXISTS usage_daily_rollups (
991                date TEXT NOT NULL,
992                app_type TEXT NOT NULL,
993                provider_id TEXT NOT NULL,
994                model TEXT NOT NULL,
995                request_count INTEGER NOT NULL DEFAULT 0,
996                success_count INTEGER NOT NULL DEFAULT 0,
997                input_tokens INTEGER NOT NULL DEFAULT 0,
998                output_tokens INTEGER NOT NULL DEFAULT 0,
999                cache_read_tokens INTEGER NOT NULL DEFAULT 0,
1000                cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
1001                total_cost_usd TEXT NOT NULL DEFAULT '0',
1002                avg_latency_ms INTEGER NOT NULL DEFAULT 0,
1003                PRIMARY KEY (date, app_type, provider_id, model)
1004            )",
1005            [],
1006        )
1007        .map_err(|e| AppError::Database(format!("创建 usage_daily_rollups 表失败: {e}")))?;
1008
1009        let mut stmt = conn
1010            .prepare("SELECT id, app_type, meta FROM providers")
1011            .map_err(|e| AppError::Database(e.to_string()))?;
1012
1013        let rows = stmt
1014            .query_map([], |row| {
1015                Ok((
1016                    row.get::<_, String>(0)?,
1017                    row.get::<_, String>(1)?,
1018                    row.get::<_, String>(2)?,
1019                ))
1020            })
1021            .map_err(|e| AppError::Database(e.to_string()))?;
1022
1023        let mut updates = Vec::new();
1024        for row in rows {
1025            let (id, app_type, meta_str) = row.map_err(|e| AppError::Database(e.to_string()))?;
1026
1027            if let Ok(mut meta) = serde_json::from_str::<serde_json::Value>(&meta_str) {
1028                let mut updated = false;
1029
1030                if let Some(usage_script) = meta.get_mut("usage_script") {
1031                    if let Some(template_type) = usage_script.get_mut("template_type") {
1032                        if template_type == "copilot" {
1033                            *template_type =
1034                                serde_json::Value::String("github_copilot".to_string());
1035                            updated = true;
1036                        }
1037                    }
1038                }
1039
1040                if updated {
1041                    let new_meta_str = serde_json::to_string(&meta)
1042                        .map_err(|e| AppError::Database(e.to_string()))?;
1043                    updates.push((id, app_type, new_meta_str));
1044                }
1045            }
1046        }
1047
1048        for (id, app_type, new_meta) in updates {
1049            conn.execute(
1050                "UPDATE providers SET meta = ?1 WHERE id = ?2 AND app_type = ?3",
1051                params![new_meta, id, app_type],
1052            )
1053            .map_err(|e| AppError::Database(e.to_string()))?;
1054        }
1055
1056        log::info!("v5 -> v6 迁移完成:已添加使用量日聚合表,统一 copilot 模板类型");
1057        Ok(())
1058    }
1059
1060    fn migrate_v6_to_v7(conn: &Connection) -> Result<(), AppError> {
1061        if Self::table_exists(conn, "skills")? {
1062            Self::add_column_if_missing(conn, "skills", "content_hash", "TEXT")?;
1063            Self::add_column_if_missing(
1064                conn,
1065                "skills",
1066                "updated_at",
1067                "INTEGER NOT NULL DEFAULT 0",
1068            )?;
1069        }
1070
1071        log::info!("v6 -> v7 迁移完成:已添加 content_hash 和 updated_at 列");
1072        Ok(())
1073    }
1074
1075    fn migrate_v7_to_v8(conn: &Connection) -> Result<(), AppError> {
1076        if Self::table_exists(conn, "proxy_request_logs")? {
1077            Self::add_column_if_missing(
1078                conn,
1079                "proxy_request_logs",
1080                "data_source",
1081                "TEXT NOT NULL DEFAULT 'proxy'",
1082            )?;
1083        }
1084
1085        conn.execute(
1086            "CREATE TABLE IF NOT EXISTS session_log_sync (
1087                file_path TEXT PRIMARY KEY,
1088                last_modified INTEGER NOT NULL,
1089                last_line_offset INTEGER NOT NULL DEFAULT 0,
1090                last_synced_at INTEGER NOT NULL
1091            )",
1092            [],
1093        )
1094        .map_err(|e| AppError::Database(format!("创建 session_log_sync 表失败: {e}")))?;
1095
1096        if Self::table_exists(conn, "model_pricing")? {
1097            let pricing_fixes: &[(&str, &str, &str, &str, &str)] = &[
1098                ("deepseek-v3.2", "0.28", "0.42", "0.028", "0"),
1099                ("deepseek-v3.1", "0.55", "1.67", "0.055", "0"),
1100                ("deepseek-v3", "0.28", "1.11", "0.028", "0"),
1101                ("doubao-seed-code", "0.17", "1.11", "0.02", "0"),
1102                ("kimi-k2-thinking", "0.55", "2.20", "0.10", "0"),
1103                ("kimi-k2-0905", "0.55", "2.20", "0.10", "0"),
1104                ("kimi-k2-turbo", "1.11", "8.06", "0.14", "0"),
1105                ("minimax-m2.1", "0.27", "0.95", "0.03", "0"),
1106                ("minimax-m2.1-lightning", "0.27", "2.33", "0.03", "0"),
1107                ("minimax-m2", "0.27", "0.95", "0.03", "0"),
1108                ("glm-4.7", "0.39", "1.75", "0.04", "0"),
1109                ("glm-4.6", "0.28", "1.11", "0.03", "0"),
1110                ("mimo-v2-flash", "0.09", "0.29", "0.009", "0"),
1111            ];
1112
1113            for (model_id, input, output, cache_read, cache_creation) in pricing_fixes {
1114                conn.execute(
1115                    "UPDATE model_pricing SET
1116                        input_cost_per_million = ?2,
1117                        output_cost_per_million = ?3,
1118                        cache_read_cost_per_million = ?4,
1119                        cache_creation_cost_per_million = ?5
1120                     WHERE model_id = ?1",
1121                    params![model_id, input, output, cache_read, cache_creation],
1122                )
1123                .map_err(|e| AppError::Database(format!("更新模型 {model_id} 定价失败: {e}")))?;
1124            }
1125        }
1126
1127        log::info!("v7 -> v8 迁移完成:data_source 列、session_log_sync 表、修正 13 个模型定价");
1128        Ok(())
1129    }
1130
1131    /// v8 → v9: 全面补充模型定价(清空 + 重新 seed)
1132    fn migrate_v8_to_v9(conn: &Connection) -> Result<(), AppError> {
1133        conn.execute(
1134            "CREATE TABLE IF NOT EXISTS model_pricing (
1135                model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
1136                input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
1137                cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
1138                cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
1139            )",
1140            [],
1141        )
1142        .map_err(|e| AppError::Database(format!("创建 model_pricing 表失败: {e}")))?;
1143        conn.execute("DELETE FROM model_pricing", [])
1144            .map_err(|e| AppError::Database(format!("清空模型定价失败: {e}")))?;
1145        Self::seed_model_pricing(conn)?;
1146        log::info!("v8 -> v9 迁移完成:已刷新全部模型定价数据");
1147        Ok(())
1148    }
1149
1150    /// v9 -> v10 迁移:添加 Hermes Agent 支持
1151    fn migrate_v9_to_v10(conn: &Connection) -> Result<(), AppError> {
1152        Self::add_column_if_missing(
1153            conn,
1154            "mcp_servers",
1155            "enabled_hermes",
1156            "BOOLEAN NOT NULL DEFAULT 0",
1157        )?;
1158
1159        if Self::table_exists(conn, "skills")? {
1160            Self::add_column_if_missing(
1161                conn,
1162                "skills",
1163                "enabled_hermes",
1164                "BOOLEAN NOT NULL DEFAULT 0",
1165            )?;
1166        }
1167
1168        log::info!("v9 -> v10 迁移完成:已添加 Hermes Agent 支持");
1169        Ok(())
1170    }
1171
1172    /// v10 -> v11 迁移:添加 OpenClaw Skills 支持
1173    fn migrate_v10_to_v11(conn: &Connection) -> Result<(), AppError> {
1174        if Self::table_exists(conn, "skills")? {
1175            Self::add_column_if_missing(
1176                conn,
1177                "skills",
1178                "enabled_openclaw",
1179                "BOOLEAN NOT NULL DEFAULT 0",
1180            )?;
1181        }
1182
1183        log::info!("v10 -> v11 迁移完成:已添加 OpenClaw Skills 支持");
1184        Ok(())
1185    }
1186
1187    /// v11 -> v12 迁移:添加 OpenClaw MCP 支持
1188    fn migrate_v11_to_v12(conn: &Connection) -> Result<(), AppError> {
1189        if Self::table_exists(conn, "mcp_servers")? {
1190            Self::add_column_if_missing(
1191                conn,
1192                "mcp_servers",
1193                "enabled_openclaw",
1194                "BOOLEAN NOT NULL DEFAULT 0",
1195            )?;
1196        }
1197
1198        log::info!("v11 -> v12 迁移完成:已添加 OpenClaw MCP 支持");
1199        Ok(())
1200    }
1201
1202    /// 插入默认模型定价数据
1203    /// 格式: (model_id, display_name, input, output, cache_read, cache_creation)
1204    /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
1205    fn seed_model_pricing(conn: &Connection) -> Result<(), AppError> {
1206        let pricing_data = [
1207            // Claude 4.7 系列
1208            (
1209                "claude-opus-4-7",
1210                "Claude Opus 4.7",
1211                "5",
1212                "25",
1213                "0.50",
1214                "6.25",
1215            ),
1216            // Claude 4.6 系列
1217            (
1218                "claude-opus-4-6-20260206",
1219                "Claude Opus 4.6",
1220                "5",
1221                "25",
1222                "0.50",
1223                "6.25",
1224            ),
1225            (
1226                "claude-sonnet-4-6-20260217",
1227                "Claude Sonnet 4.6",
1228                "3",
1229                "15",
1230                "0.30",
1231                "3.75",
1232            ),
1233            // Claude 4.5 系列 (Latest Models)
1234            (
1235                "claude-opus-4-5-20251101",
1236                "Claude Opus 4.5",
1237                "5",
1238                "25",
1239                "0.50",
1240                "6.25",
1241            ),
1242            (
1243                "claude-sonnet-4-5-20250929",
1244                "Claude Sonnet 4.5",
1245                "3",
1246                "15",
1247                "0.30",
1248                "3.75",
1249            ),
1250            (
1251                "claude-haiku-4-5-20251001",
1252                "Claude Haiku 4.5",
1253                "1",
1254                "5",
1255                "0.10",
1256                "1.25",
1257            ),
1258            // Claude 4 系列 (Legacy Models)
1259            (
1260                "claude-opus-4-20250514",
1261                "Claude Opus 4",
1262                "15",
1263                "75",
1264                "1.50",
1265                "18.75",
1266            ),
1267            (
1268                "claude-opus-4-1-20250805",
1269                "Claude Opus 4.1",
1270                "15",
1271                "75",
1272                "1.50",
1273                "18.75",
1274            ),
1275            (
1276                "claude-sonnet-4-20250514",
1277                "Claude Sonnet 4",
1278                "3",
1279                "15",
1280                "0.30",
1281                "3.75",
1282            ),
1283            // Claude 3.5 系列
1284            (
1285                "claude-3-5-haiku-20241022",
1286                "Claude 3.5 Haiku",
1287                "0.80",
1288                "4",
1289                "0.08",
1290                "1",
1291            ),
1292            (
1293                "claude-3-5-sonnet-20241022",
1294                "Claude 3.5 Sonnet",
1295                "3",
1296                "15",
1297                "0.30",
1298                "3.75",
1299            ),
1300            // GPT-5.4 系列
1301            ("gpt-5.4", "GPT-5.4", "2.50", "15", "0.25", "0"),
1302            ("gpt-5.4-mini", "GPT-5.4 Mini", "0.75", "4.50", "0.075", "0"),
1303            ("gpt-5.4-nano", "GPT-5.4 Nano", "0.20", "1.25", "0.02", "0"),
1304            // GPT-5.2 系列
1305            ("gpt-5.2", "GPT-5.2", "1.75", "14", "0.175", "0"),
1306            ("gpt-5.2-low", "GPT-5.2", "1.75", "14", "0.175", "0"),
1307            ("gpt-5.2-medium", "GPT-5.2", "1.75", "14", "0.175", "0"),
1308            ("gpt-5.2-high", "GPT-5.2", "1.75", "14", "0.175", "0"),
1309            ("gpt-5.2-xhigh", "GPT-5.2", "1.75", "14", "0.175", "0"),
1310            ("gpt-5.2-codex", "GPT-5.2 Codex", "1.75", "14", "0.175", "0"),
1311            (
1312                "gpt-5.2-codex-low",
1313                "GPT-5.2 Codex",
1314                "1.75",
1315                "14",
1316                "0.175",
1317                "0",
1318            ),
1319            (
1320                "gpt-5.2-codex-medium",
1321                "GPT-5.2 Codex",
1322                "1.75",
1323                "14",
1324                "0.175",
1325                "0",
1326            ),
1327            (
1328                "gpt-5.2-codex-high",
1329                "GPT-5.2 Codex",
1330                "1.75",
1331                "14",
1332                "0.175",
1333                "0",
1334            ),
1335            (
1336                "gpt-5.2-codex-xhigh",
1337                "GPT-5.2 Codex",
1338                "1.75",
1339                "14",
1340                "0.175",
1341                "0",
1342            ),
1343            // GPT-5.3 Codex 系列
1344            ("gpt-5.3-codex", "GPT-5.3 Codex", "1.75", "14", "0.175", "0"),
1345            (
1346                "gpt-5.3-codex-low",
1347                "GPT-5.3 Codex",
1348                "1.75",
1349                "14",
1350                "0.175",
1351                "0",
1352            ),
1353            (
1354                "gpt-5.3-codex-medium",
1355                "GPT-5.3 Codex",
1356                "1.75",
1357                "14",
1358                "0.175",
1359                "0",
1360            ),
1361            (
1362                "gpt-5.3-codex-high",
1363                "GPT-5.3 Codex",
1364                "1.75",
1365                "14",
1366                "0.175",
1367                "0",
1368            ),
1369            (
1370                "gpt-5.3-codex-xhigh",
1371                "GPT-5.3 Codex",
1372                "1.75",
1373                "14",
1374                "0.175",
1375                "0",
1376            ),
1377            // GPT-5.1 系列
1378            ("gpt-5.1", "GPT-5.1", "1.25", "10", "0.125", "0"),
1379            ("gpt-5.1-low", "GPT-5.1", "1.25", "10", "0.125", "0"),
1380            ("gpt-5.1-medium", "GPT-5.1", "1.25", "10", "0.125", "0"),
1381            ("gpt-5.1-high", "GPT-5.1", "1.25", "10", "0.125", "0"),
1382            ("gpt-5.1-minimal", "GPT-5.1", "1.25", "10", "0.125", "0"),
1383            ("gpt-5.1-codex", "GPT-5.1 Codex", "1.25", "10", "0.125", "0"),
1384            (
1385                "gpt-5.1-codex-mini",
1386                "GPT-5.1 Codex",
1387                "1.25",
1388                "10",
1389                "0.125",
1390                "0",
1391            ),
1392            (
1393                "gpt-5.1-codex-max",
1394                "GPT-5.1 Codex",
1395                "1.25",
1396                "10",
1397                "0.125",
1398                "0",
1399            ),
1400            (
1401                "gpt-5.1-codex-max-high",
1402                "GPT-5.1 Codex",
1403                "1.25",
1404                "10",
1405                "0.125",
1406                "0",
1407            ),
1408            (
1409                "gpt-5.1-codex-max-xhigh",
1410                "GPT-5.1 Codex",
1411                "1.25",
1412                "10",
1413                "0.125",
1414                "0",
1415            ),
1416            // GPT-5 系列
1417            ("gpt-5", "GPT-5", "1.25", "10", "0.125", "0"),
1418            ("gpt-5-low", "GPT-5", "1.25", "10", "0.125", "0"),
1419            ("gpt-5-medium", "GPT-5", "1.25", "10", "0.125", "0"),
1420            ("gpt-5-high", "GPT-5", "1.25", "10", "0.125", "0"),
1421            ("gpt-5-minimal", "GPT-5", "1.25", "10", "0.125", "0"),
1422            ("gpt-5-codex", "GPT-5 Codex", "1.25", "10", "0.125", "0"),
1423            ("gpt-5-codex-low", "GPT-5 Codex", "1.25", "10", "0.125", "0"),
1424            (
1425                "gpt-5-codex-medium",
1426                "GPT-5 Codex",
1427                "1.25",
1428                "10",
1429                "0.125",
1430                "0",
1431            ),
1432            (
1433                "gpt-5-codex-high",
1434                "GPT-5 Codex",
1435                "1.25",
1436                "10",
1437                "0.125",
1438                "0",
1439            ),
1440            (
1441                "gpt-5-codex-mini",
1442                "GPT-5 Codex",
1443                "1.25",
1444                "10",
1445                "0.125",
1446                "0",
1447            ),
1448            (
1449                "gpt-5-codex-mini-medium",
1450                "GPT-5 Codex",
1451                "1.25",
1452                "10",
1453                "0.125",
1454                "0",
1455            ),
1456            (
1457                "gpt-5-codex-mini-high",
1458                "GPT-5 Codex",
1459                "1.25",
1460                "10",
1461                "0.125",
1462                "0",
1463            ),
1464            // OpenAI Reasoning 系列
1465            ("o3", "OpenAI o3", "2", "8", "0.50", "0"),
1466            ("o4-mini", "OpenAI o4-mini", "1.10", "4.40", "0.275", "0"),
1467            // GPT-4.1 系列
1468            ("gpt-4.1", "GPT-4.1", "2", "8", "0.50", "0"),
1469            ("gpt-4.1-mini", "GPT-4.1 Mini", "0.40", "1.60", "0.10", "0"),
1470            ("gpt-4.1-nano", "GPT-4.1 Nano", "0.10", "0.40", "0.025", "0"),
1471            // Gemini 3.1 系列
1472            (
1473                "gemini-3.1-pro-preview",
1474                "Gemini 3.1 Pro Preview",
1475                "2",
1476                "12",
1477                "0.20",
1478                "0",
1479            ),
1480            (
1481                "gemini-3.1-flash-lite-preview",
1482                "Gemini 3.1 Flash Lite Preview",
1483                "0.25",
1484                "1.50",
1485                "0.025",
1486                "0",
1487            ),
1488            // Gemini 3 系列
1489            (
1490                "gemini-3-pro-preview",
1491                "Gemini 3 Pro Preview",
1492                "2",
1493                "12",
1494                "0.2",
1495                "0",
1496            ),
1497            (
1498                "gemini-3-flash-preview",
1499                "Gemini 3 Flash Preview",
1500                "0.5",
1501                "3",
1502                "0.05",
1503                "0",
1504            ),
1505            // Gemini 2.5 系列
1506            (
1507                "gemini-2.5-pro",
1508                "Gemini 2.5 Pro",
1509                "1.25",
1510                "10",
1511                "0.125",
1512                "0",
1513            ),
1514            (
1515                "gemini-2.5-flash",
1516                "Gemini 2.5 Flash",
1517                "0.3",
1518                "2.5",
1519                "0.03",
1520                "0",
1521            ),
1522            (
1523                "gemini-2.5-flash-lite",
1524                "Gemini 2.5 Flash Lite",
1525                "0.10",
1526                "0.40",
1527                "0.01",
1528                "0",
1529            ),
1530            // Gemini 2.0 系列
1531            (
1532                "gemini-2.0-flash",
1533                "Gemini 2.0 Flash",
1534                "0.10",
1535                "0.40",
1536                "0.025",
1537                "0",
1538            ),
1539            // StepFun 系列
1540            (
1541                "step-3.5-flash",
1542                "Step 3.5 Flash",
1543                "0.10",
1544                "0.30",
1545                "0.02",
1546                "0",
1547            ),
1548            // ====== 国产模型 (CNY/1M tokens) ======
1549            // Doubao (字节跳动)
1550            (
1551                "doubao-seed-code",
1552                "Doubao Seed Code",
1553                "0.17",
1554                "1.11",
1555                "0.02",
1556                "0",
1557            ),
1558            (
1559                "doubao-seed-2-0-pro",
1560                "Doubao Seed 2.0 Pro",
1561                "0.47",
1562                "2.37",
1563                "0",
1564                "0",
1565            ),
1566            (
1567                "doubao-seed-2-0-code",
1568                "Doubao Seed 2.0 Code",
1569                "0.47",
1570                "2.37",
1571                "0",
1572                "0",
1573            ),
1574            (
1575                "doubao-seed-2-0-lite",
1576                "Doubao Seed 2.0 Lite",
1577                "0.25",
1578                "2",
1579                "0",
1580                "0",
1581            ),
1582            (
1583                "doubao-seed-2-0-mini",
1584                "Doubao Seed 2.0 Mini",
1585                "0.03",
1586                "0.31",
1587                "0",
1588                "0",
1589            ),
1590            // DeepSeek 系列
1591            (
1592                "deepseek-v3.2",
1593                "DeepSeek V3.2",
1594                "0.28",
1595                "0.42",
1596                "0.028",
1597                "0",
1598            ),
1599            (
1600                "deepseek-v3.1",
1601                "DeepSeek V3.1",
1602                "0.55",
1603                "1.67",
1604                "0.055",
1605                "0",
1606            ),
1607            ("deepseek-v3", "DeepSeek V3", "0.28", "1.11", "0.028", "0"),
1608            (
1609                "deepseek-chat",
1610                "DeepSeek Chat",
1611                "0.27",
1612                "1.10",
1613                "0.07",
1614                "0",
1615            ),
1616            (
1617                "deepseek-reasoner",
1618                "DeepSeek Reasoner",
1619                "0.55",
1620                "2.19",
1621                "0.14",
1622                "0",
1623            ),
1624            // Kimi (月之暗面)
1625            (
1626                "kimi-k2-thinking",
1627                "Kimi K2 Thinking",
1628                "0.55",
1629                "2.20",
1630                "0.10",
1631                "0",
1632            ),
1633            ("kimi-k2-0905", "Kimi K2", "0.55", "2.20", "0.10", "0"),
1634            (
1635                "kimi-k2-turbo",
1636                "Kimi K2 Turbo",
1637                "1.11",
1638                "8.06",
1639                "0.14",
1640                "0",
1641            ),
1642            ("kimi-k2.5", "Kimi K2.5", "0.60", "2.50", "0.10", "0"),
1643            ("kimi-k2.6", "Kimi K2.6", "0.95", "4.00", "0.16", "0"),
1644            // MiniMax 系列
1645            ("minimax-m2.1", "MiniMax M2.1", "0.27", "0.95", "0.03", "0"),
1646            (
1647                "minimax-m2.1-lightning",
1648                "MiniMax M2.1 Lightning",
1649                "0.27",
1650                "2.33",
1651                "0.03",
1652                "0",
1653            ),
1654            ("minimax-m2", "MiniMax M2", "0.27", "0.95", "0.03", "0"),
1655            ("minimax-m2.5", "MiniMax M2.5", "0.12", "0.95", "0.03", "0"),
1656            (
1657                "minimax-m2.5-lightning",
1658                "MiniMax M2.5 Lightning",
1659                "0.30",
1660                "2.40",
1661                "0.03",
1662                "0",
1663            ),
1664            (
1665                "minimax-m2.7",
1666                "MiniMax M2.7",
1667                "0.30",
1668                "1.20",
1669                "0.06",
1670                "0.375",
1671            ),
1672            (
1673                "minimax-m2.7-highspeed",
1674                "MiniMax M2.7 Highspeed",
1675                "0.60",
1676                "2.40",
1677                "0.06",
1678                "0.375",
1679            ),
1680            // GLM (智谱)
1681            ("glm-4.7", "GLM-4.7", "0.39", "1.75", "0.04", "0"),
1682            ("glm-4.6", "GLM-4.6", "0.28", "1.11", "0.03", "0"),
1683            ("glm-5", "GLM-5", "0.72", "2.30", "0", "0"),
1684            ("glm-5.1", "GLM-5.1", "0.95", "3.15", "0", "0"),
1685            // Mimo (小米)
1686            (
1687                "mimo-v2-flash",
1688                "Mimo V2 Flash",
1689                "0.09",
1690                "0.29",
1691                "0.009",
1692                "0",
1693            ),
1694            ("mimo-v2-pro", "MiMo V2 Pro", "1", "3", "0", "0"),
1695            // Qwen 系列 (阿里巴巴)
1696            ("qwen3.6-plus", "Qwen3.6 Plus", "0.325", "1.95", "0", "0"),
1697            ("qwen3.5-plus", "Qwen3.5 Plus", "0.26", "1.56", "0", "0"),
1698            ("qwen3-max", "Qwen3 Max", "0.78", "3.90", "0", "0"),
1699            (
1700                "qwen3-235b-a22b",
1701                "Qwen3 235B-A22B",
1702                "0.70",
1703                "8.40",
1704                "0",
1705                "0",
1706            ),
1707            (
1708                "qwen3-coder-plus",
1709                "Qwen3 Coder Plus",
1710                "0.65",
1711                "3.25",
1712                "0",
1713                "0",
1714            ),
1715            (
1716                "qwen3-coder-flash",
1717                "Qwen3 Coder Flash",
1718                "0.195",
1719                "0.975",
1720                "0",
1721                "0",
1722            ),
1723            (
1724                "qwen3-coder-next",
1725                "Qwen3 Coder Next",
1726                "0.12",
1727                "0.75",
1728                "0",
1729                "0",
1730            ),
1731            ("qwq-plus", "QwQ Plus", "0.80", "2.40", "0", "0"),
1732            ("qwq-32b", "QwQ 32B", "0.20", "0.60", "0", "0"),
1733            ("qwen3-32b", "Qwen3 32B", "0.16", "0.64", "0", "0"),
1734            // Grok 系列 (xAI)
1735            (
1736                "grok-4.20-0309-reasoning",
1737                "Grok 4.20 Reasoning",
1738                "2",
1739                "6",
1740                "0.20",
1741                "0",
1742            ),
1743            (
1744                "grok-4.20-0309-non-reasoning",
1745                "Grok 4.20",
1746                "2",
1747                "6",
1748                "0.20",
1749                "0",
1750            ),
1751            (
1752                "grok-4-1-fast-reasoning",
1753                "Grok 4.1 Fast Reasoning",
1754                "0.20",
1755                "0.50",
1756                "0.05",
1757                "0",
1758            ),
1759            (
1760                "grok-4-1-fast-non-reasoning",
1761                "Grok 4.1 Fast",
1762                "0.20",
1763                "0.50",
1764                "0.05",
1765                "0",
1766            ),
1767            ("grok-4", "Grok 4", "3", "15", "0.75", "0"),
1768            (
1769                "grok-code-fast-1",
1770                "Grok Code Fast",
1771                "0.20",
1772                "1.50",
1773                "0.02",
1774                "0",
1775            ),
1776            ("grok-3", "Grok 3", "3", "15", "0.75", "0"),
1777            ("grok-3-mini", "Grok 3 Mini", "0.25", "0.50", "0.075", "0"),
1778            // Mistral 系列
1779            ("codestral-2508", "Codestral", "0.30", "0.90", "0.03", "0"),
1780            (
1781                "devstral-small-1.1",
1782                "Devstral Small 1.1",
1783                "0.07",
1784                "0.28",
1785                "0.01",
1786                "0",
1787            ),
1788            ("devstral-2-2512", "Devstral 2", "0.40", "0.90", "0.04", "0"),
1789            (
1790                "devstral-medium",
1791                "Devstral Medium",
1792                "0.40",
1793                "2",
1794                "0.04",
1795                "0",
1796            ),
1797            (
1798                "mistral-large-3-2512",
1799                "Mistral Large 3",
1800                "0.50",
1801                "1.50",
1802                "0.05",
1803                "0",
1804            ),
1805            (
1806                "mistral-medium-3.1",
1807                "Mistral Medium 3.1",
1808                "0.40",
1809                "2",
1810                "0.04",
1811                "0",
1812            ),
1813            (
1814                "mistral-small-3.2-24b",
1815                "Mistral Small 3.2",
1816                "0.075",
1817                "0.20",
1818                "0.01",
1819                "0",
1820            ),
1821            ("magistral-medium", "Magistral Medium", "2", "5", "0", "0"),
1822            // Cohere 系列
1823            ("command-a", "Cohere Command A", "2.50", "10", "0", "0"),
1824            (
1825                "command-r-plus",
1826                "Cohere Command R+",
1827                "2.50",
1828                "10",
1829                "0",
1830                "0",
1831            ),
1832            ("command-r", "Cohere Command R", "0.15", "0.60", "0", "0"),
1833            // OpenAI 补充
1834            ("o3-pro", "OpenAI o3-pro", "20", "80", "0", "0"),
1835            ("o3-mini", "OpenAI o3-mini", "0.55", "2.20", "0.55", "0"),
1836            ("o1", "OpenAI o1", "15", "60", "7.50", "0"),
1837            ("o1-mini", "OpenAI o1-mini", "0.55", "2.20", "0.55", "0"),
1838            ("codex-mini", "Codex Mini", "0.75", "3", "0.025", "0"),
1839            ("gpt-5-mini", "GPT-5 Mini", "0.25", "2", "0.025", "0"),
1840            ("gpt-5-nano", "GPT-5 Nano", "0.05", "0.40", "0.005", "0"),
1841        ];
1842
1843        let mut stmt = conn
1844            .prepare(
1845                "INSERT OR IGNORE INTO model_pricing (
1846                    model_id, display_name, input_cost_per_million, output_cost_per_million,
1847                    cache_read_cost_per_million, cache_creation_cost_per_million
1848                ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
1849            )
1850            .map_err(|e| AppError::Database(format!("准备模型定价语句失败: {e}")))?;
1851        for (model_id, display_name, input, output, cache_read, cache_creation) in pricing_data {
1852            stmt.execute(rusqlite::params![
1853                model_id,
1854                display_name,
1855                input,
1856                output,
1857                cache_read,
1858                cache_creation
1859            ])
1860            .map_err(|e| AppError::Database(format!("插入模型定价失败: {e}")))?;
1861        }
1862
1863        log::info!("已插入 {} 条默认模型定价数据", pricing_data.len());
1864        Ok(())
1865    }
1866
1867    /// 确保模型定价表具备默认数据
1868    pub fn ensure_model_pricing_seeded(&self) -> Result<(), AppError> {
1869        let conn = lock_conn!(self.conn);
1870        Self::ensure_model_pricing_seeded_on_conn(&conn)
1871    }
1872
1873    fn ensure_model_pricing_seeded_on_conn(conn: &Connection) -> Result<(), AppError> {
1874        // 每次启动都增量补种新模型,已有记录保持不变;
1875        // 强制纠正历史价格则交给版本化迁移负责。
1876        Self::seed_model_pricing(conn)
1877    }
1878
1879    // --- 辅助方法 ---
1880
1881    pub(crate) fn future_schema_error(version: i32) -> AppError {
1882        AppError::Database(format!(
1883            "当前数据库由较新版本的 CC Switch 创建,旧版本无法打开。\n\
1884             数据库版本: {version}\n\
1885             当前应用: v{},最高支持数据库版本: {SCHEMA_VERSION}\n\
1886             请运行 `cc-switch update` 升级到最新版;如果仍然失败,请从 GitHub Releases 安装最新版本。",
1887            env!("CARGO_PKG_VERSION")
1888        ))
1889    }
1890
1891    pub(crate) fn get_user_version(conn: &Connection) -> Result<i32, AppError> {
1892        conn.query_row("PRAGMA user_version;", [], |row| row.get(0))
1893            .map_err(|e| AppError::Database(format!("读取 user_version 失败: {e}")))
1894    }
1895
1896    pub(crate) fn set_user_version(conn: &Connection, version: i32) -> Result<(), AppError> {
1897        if version < 0 {
1898            return Err(AppError::Database("user_version 不能为负数".to_string()));
1899        }
1900        let sql = format!("PRAGMA user_version = {version};");
1901        conn.execute(&sql, [])
1902            .map_err(|e| AppError::Database(format!("写入 user_version 失败: {e}")))?;
1903        Ok(())
1904    }
1905
1906    fn validate_identifier(s: &str, kind: &str) -> Result<(), AppError> {
1907        if s.is_empty() {
1908            return Err(AppError::Database(format!("{kind} 不能为空")));
1909        }
1910        if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
1911            return Err(AppError::Database(format!(
1912                "非法{kind}: {s},仅允许字母、数字和下划线"
1913            )));
1914        }
1915        Ok(())
1916    }
1917
1918    pub(crate) fn table_exists(conn: &Connection, table: &str) -> Result<bool, AppError> {
1919        Self::validate_identifier(table, "表名")?;
1920
1921        let mut stmt = conn
1922            .prepare("SELECT name FROM sqlite_master WHERE type='table'")
1923            .map_err(|e| AppError::Database(format!("读取表名失败: {e}")))?;
1924        let mut rows = stmt
1925            .query([])
1926            .map_err(|e| AppError::Database(format!("查询表名失败: {e}")))?;
1927        while let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
1928            let name: String = row
1929                .get(0)
1930                .map_err(|e| AppError::Database(format!("解析表名失败: {e}")))?;
1931            if name.eq_ignore_ascii_case(table) {
1932                return Ok(true);
1933            }
1934        }
1935        Ok(false)
1936    }
1937
1938    pub(crate) fn has_column(
1939        conn: &Connection,
1940        table: &str,
1941        column: &str,
1942    ) -> Result<bool, AppError> {
1943        Self::validate_identifier(table, "表名")?;
1944        Self::validate_identifier(column, "列名")?;
1945
1946        let sql = format!("PRAGMA table_info(\"{table}\");");
1947        let mut stmt = conn
1948            .prepare(&sql)
1949            .map_err(|e| AppError::Database(format!("读取表结构失败: {e}")))?;
1950        let mut rows = stmt
1951            .query([])
1952            .map_err(|e| AppError::Database(format!("查询表结构失败: {e}")))?;
1953        while let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
1954            let name: String = row
1955                .get(1)
1956                .map_err(|e| AppError::Database(format!("读取列名失败: {e}")))?;
1957            if name.eq_ignore_ascii_case(column) {
1958                return Ok(true);
1959            }
1960        }
1961        Ok(false)
1962    }
1963
1964    fn add_column_if_missing(
1965        conn: &Connection,
1966        table: &str,
1967        column: &str,
1968        definition: &str,
1969    ) -> Result<bool, AppError> {
1970        Self::validate_identifier(table, "表名")?;
1971        Self::validate_identifier(column, "列名")?;
1972
1973        if !Self::table_exists(conn, table)? {
1974            return Err(AppError::Database(format!(
1975                "表 {table} 不存在,无法添加列 {column}"
1976            )));
1977        }
1978        if Self::has_column(conn, table, column)? {
1979            return Ok(false);
1980        }
1981
1982        let sql = format!("ALTER TABLE \"{table}\" ADD COLUMN \"{column}\" {definition};");
1983        conn.execute(&sql, [])
1984            .map_err(|e| AppError::Database(format!("为表 {table} 添加列 {column} 失败: {e}")))?;
1985        log::info!("已为表 {table} 添加缺失列 {column}");
1986        Ok(true)
1987    }
1988}