mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
4282856683
- proxy_request_logs: add pricing_model column recording the basis actually used at write time (NULL = pre-v11 rows, '' = unpriced error rows) - cost backfill recomputes strictly by the persisted basis; the request_model fallback now only applies to placeholder models, so real-but-unpriced takeover rows stay at zero cost until pricing is added instead of being permanently frozen at the alias's price - backfill_missing_usage_costs_for_model can locate rows by pricing_model - usage_daily_rollups: rebuild with request_model + pricing_model in the primary key so the alias-to-real-model mapping and the pricing basis survive the 30-day prune; legacy rows migrate with '' - rollup_and_prune backfills costs before pruning: prune is irreversible and used to run before the startup backfill, permanently booking then-unpriced rows as zero - get_model_stats groups by the effective pricing model (COALESCE(NULLIF(pricing_model,''), model)) so costs aggregate under the model whose prices produced them; response-mode behavior unchanged
273 lines
9.0 KiB
Rust
273 lines
9.0 KiB
Rust
//! 数据库模块 - SQLite 数据持久化
|
|
//!
|
|
//! 此模块提供应用的核心数据存储功能,包括:
|
|
//! - 供应商配置管理
|
|
//! - MCP 服务器配置
|
|
//! - 提示词管理
|
|
//! - Skills 管理
|
|
//! - 通用设置存储
|
|
//!
|
|
//! ## 架构设计
|
|
//!
|
|
//! ```text
|
|
//! database/
|
|
//! ├── mod.rs - Database 结构体 + 初始化
|
|
//! ├── schema.rs - 表结构定义 + Schema 迁移
|
|
//! ├── backup.rs - SQL 导入导出 + 快照备份
|
|
//! ├── migration.rs - JSON → SQLite 数据迁移
|
|
//! └── dao/ - 数据访问对象
|
|
//! ├── providers.rs
|
|
//! ├── mcp.rs
|
|
//! ├── prompts.rs
|
|
//! ├── skills.rs
|
|
//! └── settings.rs
|
|
//! ```
|
|
|
|
pub(crate) mod backup;
|
|
mod dao;
|
|
mod migration;
|
|
mod schema;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
// DAO 类型导出供外部使用
|
|
pub(crate) use dao::providers_seed::{is_official_seed_id, CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID};
|
|
pub(crate) use dao::proxy::{
|
|
validate_cost_multiplier, validate_pricing_source, PRICING_SOURCE_REQUEST,
|
|
PRICING_SOURCE_RESPONSE,
|
|
};
|
|
pub use dao::FailoverQueueItem;
|
|
|
|
use crate::config::get_app_config_dir;
|
|
use crate::error::AppError;
|
|
use rusqlite::{hooks::Action, Connection};
|
|
use serde::Serialize;
|
|
use std::sync::Mutex;
|
|
|
|
// DAO 方法通过 impl Database 提供,无需额外导出
|
|
|
|
/// 当前 Schema 版本号
|
|
/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
|
|
pub(crate) const SCHEMA_VERSION: i32 = 11;
|
|
|
|
/// 安全地序列化 JSON,避免 unwrap panic
|
|
pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
|
|
serde_json::to_string(value)
|
|
.map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))
|
|
}
|
|
|
|
/// 安全地获取 Mutex 锁,避免 unwrap panic
|
|
macro_rules! lock_conn {
|
|
($mutex:expr) => {
|
|
$mutex
|
|
.lock()
|
|
.map_err(|e| AppError::Database(format!("Mutex lock failed: {}", e)))?
|
|
};
|
|
}
|
|
|
|
// 导出宏供子模块使用
|
|
pub(crate) use lock_conn;
|
|
|
|
/// 数据库连接封装
|
|
///
|
|
/// 使用 Mutex 包装 Connection 以支持在多线程环境(如 Tauri State)中共享。
|
|
/// rusqlite::Connection 本身不是 Sync 的,因此需要这层包装。
|
|
pub struct Database {
|
|
pub(crate) conn: Mutex<Connection>,
|
|
}
|
|
|
|
fn register_db_change_hook(conn: &Connection) {
|
|
conn.update_hook(Some(
|
|
|action: Action, _database: &str, table: &str, _row_id: i64| match action {
|
|
Action::SQLITE_INSERT | Action::SQLITE_UPDATE | Action::SQLITE_DELETE => {
|
|
crate::services::webdav_auto_sync::notify_db_changed(table);
|
|
crate::services::s3_auto_sync::notify_db_changed(table);
|
|
}
|
|
_ => {}
|
|
},
|
|
));
|
|
}
|
|
|
|
impl Database {
|
|
/// 初始化数据库连接并创建表
|
|
///
|
|
/// 数据库文件位于 `~/.cc-switch/cc-switch.db`
|
|
pub fn init() -> Result<Self, AppError> {
|
|
let db_path = get_app_config_dir().join("cc-switch.db");
|
|
let db_exists = db_path.exists();
|
|
|
|
// 确保父目录存在
|
|
if let Some(parent) = db_path.parent() {
|
|
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
|
}
|
|
|
|
let conn = Connection::open(&db_path).map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
// 启用外键约束
|
|
conn.execute("PRAGMA foreign_keys = ON;", [])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
if !db_exists {
|
|
// For a brand-new database, configure incremental auto-vacuum
|
|
// before creating any tables so no rebuild is needed later.
|
|
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
}
|
|
register_db_change_hook(&conn);
|
|
|
|
let db = Self {
|
|
conn: Mutex::new(conn),
|
|
};
|
|
db.create_tables()?;
|
|
|
|
// Pre-migration backup: only when upgrading from an existing database
|
|
{
|
|
let conn = lock_conn!(db.conn);
|
|
let version = Self::get_user_version(&conn)?;
|
|
drop(conn);
|
|
if version > 0 && version < SCHEMA_VERSION {
|
|
log::info!(
|
|
"Creating pre-migration database backup (v{version} → v{SCHEMA_VERSION})"
|
|
);
|
|
if let Err(e) = db.backup_database_file() {
|
|
log::warn!("Pre-migration backup failed, continuing migration: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
db.apply_schema_migrations()?;
|
|
if let Err(e) = db.ensure_incremental_auto_vacuum() {
|
|
log::warn!("Failed to ensure incremental auto-vacuum: {e}");
|
|
}
|
|
db.ensure_model_pricing_seeded()?;
|
|
|
|
// Startup cleanup: prune old logs and reclaim space
|
|
if let Err(e) = db.cleanup_old_stream_check_logs(7) {
|
|
log::warn!("Startup stream_check_logs cleanup failed: {e}");
|
|
}
|
|
if let Err(e) = db.rollup_and_prune(30) {
|
|
log::warn!("Startup rollup_and_prune failed: {e}");
|
|
}
|
|
// Reclaim disk space after cleanup
|
|
{
|
|
let conn = lock_conn!(db.conn);
|
|
if let Err(e) = conn.execute_batch("PRAGMA incremental_vacuum;") {
|
|
log::warn!("Startup incremental vacuum failed: {e}");
|
|
}
|
|
}
|
|
|
|
Ok(db)
|
|
}
|
|
|
|
/// 创建内存数据库(用于测试)
|
|
pub fn memory() -> Result<Self, AppError> {
|
|
let conn = Connection::open_in_memory().map_err(|e| AppError::Database(e.to_string()))?;
|
|
|
|
// 启用外键约束
|
|
conn.execute("PRAGMA foreign_keys = ON;", [])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
register_db_change_hook(&conn);
|
|
|
|
let db = Self {
|
|
conn: Mutex::new(conn),
|
|
};
|
|
db.create_tables()?;
|
|
db.ensure_model_pricing_seeded()?;
|
|
|
|
Ok(db)
|
|
}
|
|
|
|
pub(crate) fn get_auto_vacuum_mode(conn: &Connection) -> Result<i32, AppError> {
|
|
conn.query_row("PRAGMA auto_vacuum;", [], |row| row.get(0))
|
|
.map_err(|e| AppError::Database(format!("读取 auto_vacuum 失败: {e}")))
|
|
}
|
|
|
|
fn has_user_tables(conn: &Connection) -> Result<bool, AppError> {
|
|
let count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.map_err(|e| AppError::Database(format!("读取表数量失败: {e}")))?;
|
|
Ok(count > 0)
|
|
}
|
|
|
|
pub(crate) fn ensure_incremental_auto_vacuum_on_conn(
|
|
conn: &Connection,
|
|
) -> Result<bool, AppError> {
|
|
let mode = Self::get_auto_vacuum_mode(conn)?;
|
|
if mode == 2 {
|
|
return Ok(false);
|
|
}
|
|
|
|
let has_tables = Self::has_user_tables(conn)?;
|
|
conn.execute("PRAGMA auto_vacuum = INCREMENTAL;", [])
|
|
.map_err(|e| AppError::Database(format!("设置 auto_vacuum 失败: {e}")))?;
|
|
|
|
if !has_tables {
|
|
return Ok(false);
|
|
}
|
|
|
|
conn.execute("VACUUM;", [])
|
|
.map_err(|e| AppError::Database(format!("执行 VACUUM 失败: {e}")))?;
|
|
conn.execute("PRAGMA foreign_keys = ON;", [])
|
|
.map_err(|e| AppError::Database(format!("恢复 foreign_keys 失败: {e}")))?;
|
|
Ok(true)
|
|
}
|
|
|
|
pub(crate) fn ensure_incremental_auto_vacuum(&self) -> Result<bool, AppError> {
|
|
let mode = {
|
|
let conn = lock_conn!(self.conn);
|
|
Self::get_auto_vacuum_mode(&conn)?
|
|
};
|
|
if mode == 2 {
|
|
return Ok(false);
|
|
}
|
|
|
|
let has_tables = {
|
|
let conn = lock_conn!(self.conn);
|
|
Self::has_user_tables(&conn)?
|
|
};
|
|
if has_tables {
|
|
log::info!(
|
|
"Detected auto_vacuum={mode}, rebuilding database to enable incremental vacuum"
|
|
);
|
|
self.backup_database_file()?;
|
|
}
|
|
|
|
let rebuilt = {
|
|
let conn = lock_conn!(self.conn);
|
|
Self::ensure_incremental_auto_vacuum_on_conn(&conn)?
|
|
};
|
|
|
|
if rebuilt {
|
|
log::info!("Incremental auto-vacuum enabled after database rebuild");
|
|
} else {
|
|
log::info!("Incremental auto-vacuum configured for new database");
|
|
}
|
|
|
|
Ok(rebuilt)
|
|
}
|
|
|
|
/// 检查 MCP 服务器表是否为空
|
|
pub fn is_mcp_table_empty(&self) -> Result<bool, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0))
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(count == 0)
|
|
}
|
|
|
|
/// 检查提示词表是否为空
|
|
pub fn is_prompts_table_empty(&self) -> Result<bool, AppError> {
|
|
let conn = lock_conn!(self.conn);
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0))
|
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
|
Ok(count == 0)
|
|
}
|
|
}
|