From 2526dbc58fbd6a3b43010be1cc0d655395de60e0 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 25 Nov 2025 16:07:54 +0800 Subject: [PATCH] feat(migration): enable silent JSON to SQLite migration with toast notification - Remove environment variable feature gate (CC_SWITCH_ENABLE_JSON_DB_MIGRATION) - Automatically migrate config.json to SQLite when db doesn't exist - Archive migrated config.json as config.json.migrated for recovery - Add migration success flag in init_status.rs (one-time consumption) - Add get_migration_result command for frontend to query - Show toast notification on successful migration in App.tsx --- src-tauri/src/commands/misc.rs | 7 ++++ src-tauri/src/init_status.rs | 27 +++++++++++++ src-tauri/src/lib.rs | 72 +++++++++------------------------- src/App.tsx | 19 +++++++++ 4 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index 3a83962bf..dc59acabe 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -51,3 +51,10 @@ pub async fn is_portable_mode() -> Result { pub async fn get_init_error() -> Result, String> { Ok(crate::init_status::get_init_error()) } + +/// 获取 JSON→SQLite 迁移结果(若有)。 +/// 只返回一次 true,之后返回 false,用于前端显示一次性 Toast 通知。 +#[tauri::command] +pub async fn get_migration_result() -> Result { + Ok(crate::init_status::take_migration_success()) +} diff --git a/src-tauri/src/init_status.rs b/src-tauri/src/init_status.rs index ce9966279..86e64a4a9 100644 --- a/src-tauri/src/init_status.rs +++ b/src-tauri/src/init_status.rs @@ -25,6 +25,33 @@ pub fn get_init_error() -> Option { cell().read().ok()?.clone() } +// ============================================================ +// 迁移结果状态 +// ============================================================ + +static MIGRATION_SUCCESS: OnceLock> = OnceLock::new(); + +fn migration_cell() -> &'static RwLock { + MIGRATION_SUCCESS.get_or_init(|| RwLock::new(false)) +} + +pub fn set_migration_success() { + if let Ok(mut guard) = migration_cell().write() { + *guard = true; + } +} + +/// 获取并消费迁移成功状态(只返回一次 true,之后返回 false) +pub fn take_migration_success() -> bool { + if let Ok(mut guard) = migration_cell().write() { + let val = *guard; + *guard = false; + val + } else { + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce0f80676..a7ad529db 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -113,25 +113,6 @@ const TRAY_SECTIONS: [TrayAppSection; 3] = [ }, ]; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum JsonMigrationMode { - Disabled, - DryRun, - Enabled, -} - -/// 解析 JSON→DB 迁移模式:默认关闭,支持 dryrun/模拟演练 -fn json_migration_mode() -> JsonMigrationMode { - match std::env::var("CC_SWITCH_ENABLE_JSON_DB_MIGRATION") { - Ok(val) => match val.trim().to_ascii_lowercase().as_str() { - "1" | "true" | "yes" | "on" => JsonMigrationMode::Enabled, - "dryrun" | "dry-run" | "simulate" | "sim" => JsonMigrationMode::DryRun, - _ => JsonMigrationMode::Disabled, - }, - Err(_) => JsonMigrationMode::Disabled, - } -} - fn append_provider_section<'a>( app: &'a tauri::AppHandle, mut menu_builder: MenuBuilder<'a, tauri::Wry, tauri::AppHandle>, @@ -561,8 +542,7 @@ pub fn run() { let db_path = app_config_dir.join("cc-switch.db"); let json_path = app_config_dir.join("config.json"); - // Check if config.json→SQLite migration needed (feature gated, disabled by default) - let migration_mode = json_migration_mode(); + // 检查是否需要从 config.json 迁移到 SQLite let has_json = json_path.exists(); let has_db = db_path.exists(); @@ -576,42 +556,27 @@ pub fn run() { } }; + // 静默迁移:检测到旧版 config.json 且数据库不存在时自动迁移 if !has_db && has_json { - match migration_mode { - JsonMigrationMode::Disabled => { - log::warn!( - "Detected config.json but migration is disabled by default. \ - Set CC_SWITCH_ENABLE_JSON_DB_MIGRATION=1 to migrate, or =dryrun to validate first." - ); - } - JsonMigrationMode::DryRun => { - log::info!("Running migration dry-run (validation only, no disk writes)"); - match crate::app_config::MultiAppConfig::load() { - Ok(config) => { - if let Err(e) = crate::database::Database::migrate_from_json_dry_run(&config) { - log::error!("Migration dry-run failed: {e}"); - } else { - log::info!("Migration dry-run succeeded (no database written)"); - } + log::info!("检测到旧版配置文件,开始自动迁移到数据库..."); + match crate::app_config::MultiAppConfig::load() { + Ok(config) => { + if let Err(e) = db.migrate_from_json(&config) { + log::error!("配置迁移失败: {e},将从现有配置导入"); + } else { + log::info!("✓ 配置迁移成功"); + // 标记迁移成功,供前端显示 Toast + crate::init_status::set_migration_success(); + // 归档旧配置文件(重命名而非删除,便于用户恢复) + let archive_path = json_path.with_extension("json.migrated"); + if let Err(e) = std::fs::rename(&json_path, &archive_path) { + log::warn!("归档旧配置文件失败: {e}"); + } else { + log::info!("✓ 旧配置已归档为 config.json.migrated"); } - Err(e) => log::error!("Failed to load config.json for dry-run: {e}"), - } - } - JsonMigrationMode::Enabled => { - log::info!("Starting migration from config.json to SQLite (user opt-in)"); - match crate::app_config::MultiAppConfig::load() { - Ok(config) => { - if let Err(e) = db.migrate_from_json(&config) { - log::error!("Migration failed: {e}"); - } else { - log::info!("Migration successful"); - // Optional: Rename config.json to prevent re-migration - // let _ = std::fs::rename(&json_path, json_path.with_extension("json.migrated")); - } - } - Err(e) => log::error!("Failed to load config.json for migration: {e}"), } } + Err(e) => log::error!("加载旧配置文件失败: {e},将从现有配置导入"), } } @@ -822,6 +787,7 @@ pub fn run() { commands::pick_directory, commands::open_external, commands::get_init_error, + commands::get_migration_result, commands::get_app_config_path, commands::open_app_config_folder, commands::get_claude_common_config_snippet, diff --git a/src/App.tsx b/src/App.tsx index 75b92631e..4d70535df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import { invoke } from "@tauri-apps/api/core"; import { Plus, Settings, @@ -123,6 +124,24 @@ function App() { checkEnvOnStartup(); }, []); + // 应用启动时检查是否刚完成了配置迁移 + useEffect(() => { + const checkMigration = async () => { + try { + const migrated = await invoke("get_migration_result"); + if (migrated) { + toast.success( + t("migration.success", { defaultValue: "配置迁移成功" }), + ); + } + } catch (error) { + console.error("[App] Failed to check migration result:", error); + } + }; + + checkMigration(); + }, [t]); + // 切换应用时检测当前应用的环境变量冲突 useEffect(() => { const checkEnvOnSwitch = async () => {