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