From 1ee1e9cb2e3af09542c0901144b85a5adaf93c8c Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 28 Nov 2025 12:00:32 +0800 Subject: [PATCH] refactor(startup): improve first-launch import logic with per-table detection - Replace global `is_empty_for_first_import()` with independent checks: - `is_mcp_table_empty()` for MCP server imports - `is_prompts_table_empty()` for prompt imports - Skills and providers already have built-in idempotency checks - Fix misleading logs in provider import: - Change `import_default_config` return type from `Result<()>` to `Result` - Return `true` when actually imported, `false` when skipped - Only log success message when import actually occurred - Add idempotency protection to `import_from_file_on_first_launch` This allows each data type to be independently recovered if deleted, rather than requiring all tables to be empty for any import to trigger. --- src-tauri/src/commands/provider.rs | 8 +- src-tauri/src/database/mod.rs | 27 ++---- src-tauri/src/lib.rs | 123 ++++++++++-------------- src-tauri/src/services/prompt.rs | 6 ++ src-tauri/src/services/provider/live.rs | 9 +- src-tauri/src/services/provider/mod.rs | 4 +- 6 files changed, 79 insertions(+), 98 deletions(-) diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 280d74417..cce5aabd3 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -86,7 +86,7 @@ pub fn switch_provider( .map_err(|e| e.to_string()) } -fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<(), AppError> { +fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result { ProviderService::import_default_config(state, app_type) } @@ -94,7 +94,7 @@ fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result pub fn import_default_config_test_hook( state: &AppState, app_type: AppType, -) -> Result<(), AppError> { +) -> Result { import_default_config_internal(state, app_type) } @@ -102,9 +102,7 @@ pub fn import_default_config_test_hook( #[tauri::command] pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; - import_default_config_internal(&state, app_type) - .map(|_| true) - .map_err(Into::into) + import_default_config_internal(&state, app_type).map_err(Into::into) } /// 查询供应商用量 diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index fdc612006..4602c562c 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -115,28 +115,21 @@ impl Database { Ok(db) } - /// 检查数据库是否为空(需要首次导入) - /// - /// 通过检查是否有任何 MCP 服务器、提示词、Skills 仓库或供应商来判断 - pub fn is_empty_for_first_import(&self) -> Result { + /// 检查 MCP 服务器表是否为空 + pub fn is_mcp_table_empty(&self) -> Result { let conn = lock_conn!(self.conn); - - let mcp_count: i64 = 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) + } - let prompt_count: i64 = conn + /// 检查提示词表是否为空 + pub fn is_prompts_table_empty(&self) -> Result { + 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()))?; - - let skill_repo_count: i64 = conn - .query_row("SELECT COUNT(*) FROM skill_repos", [], |row| row.get(0)) - .map_err(|e| AppError::Database(e.to_string()))?; - - let provider_count: i64 = conn - .query_row("SELECT COUNT(*) FROM providers", [], |row| row.get(0)) - .map_err(|e| AppError::Database(e.to_string()))?; - - Ok(mcp_count == 0 && prompt_count == 0 && skill_repo_count == 0 && provider_count == 0) + Ok(count == 0) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 311879f41..cd8cbc9f8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -612,54 +612,47 @@ pub fn run() { let app_state = AppState::new(db); - // 检查是否需要首次导入(数据库为空) - let need_first_import = app_state - .db - .is_empty_for_first_import() - .unwrap_or_else(|e| { - log::warn!("Failed to check if database is empty: {e}"); - false - }); + // ============================================================ + // 按表独立判断的导入逻辑(各类数据独立检查,互不影响) + // ============================================================ - if need_first_import { - // 数据库为空,尝试从用户现有的配置文件导入数据并初始化默认配置 - log::info!( - "Empty database detected, importing existing configurations and initializing defaults..." - ); - - // 1. 初始化默认 Skills 仓库(3个) - match app_state.db.init_default_skill_repos() { - Ok(count) if count > 0 => { - log::info!("✓ Initialized {count} default skill repositories"); - } - Ok(_) => log::debug!("No default skill repositories to initialize"), - Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"), + // 1. 初始化默认 Skills 仓库(已有内置检查:表非空则跳过) + match app_state.db.init_default_skill_repos() { + Ok(count) if count > 0 => { + log::info!("✓ Initialized {count} default skill repositories"); } + Ok(_) => {} // 表非空,静默跳过 + Err(e) => log::warn!("✗ Failed to initialize default skill repos: {e}"), + } - // 2. 导入供应商配置(从 live 配置文件) - for app in [ - crate::app_config::AppType::Claude, - crate::app_config::AppType::Codex, - crate::app_config::AppType::Gemini, - ] { - match crate::services::provider::ProviderService::import_default_config( - &app_state, - app.clone(), - ) { - Ok(_) => { - log::info!("✓ Imported default provider for {}", app.as_str()); - } - Err(e) => { - log::debug!( - "○ No default provider to import for {}: {}", - app.as_str(), - e - ); - } + // 2. 导入供应商配置(已有内置检查:该应用已有供应商则跳过) + for app in [ + crate::app_config::AppType::Claude, + crate::app_config::AppType::Codex, + crate::app_config::AppType::Gemini, + ] { + match crate::services::provider::ProviderService::import_default_config( + &app_state, + app.clone(), + ) { + Ok(true) => { + log::info!("✓ Imported default provider for {}", app.as_str()); + } + Ok(false) => {} // 已有供应商,静默跳过 + Err(e) => { + log::debug!( + "○ No default provider to import for {}: {}", + app.as_str(), + e + ); } } + } + + // 3. 导入 MCP 服务器配置(表空时触发) + if app_state.db.is_mcp_table_empty().unwrap_or(false) { + log::info!("MCP table empty, importing from live configurations..."); - // 3. 导入 MCP 服务器配置 match crate::services::mcp::McpService::import_from_claude(&app_state) { Ok(count) if count > 0 => { log::info!("✓ Imported {count} MCP server(s) from Claude"); @@ -683,42 +676,28 @@ pub fn run() { Ok(_) => log::debug!("○ No Gemini MCP servers found to import"), Err(e) => log::warn!("✗ Failed to import Gemini MCP: {e}"), } + } - // 4. 导入提示词文件 - match crate::services::prompt::PromptService::import_from_file_on_first_launch( - &app_state, + // 4. 导入提示词文件(表空时触发) + if app_state.db.is_prompts_table_empty().unwrap_or(false) { + log::info!("Prompts table empty, importing from live configurations..."); + + for app in [ crate::app_config::AppType::Claude, - ) { - Ok(count) if count > 0 => { - log::info!("✓ Imported {count} prompt(s) from Claude"); - } - Ok(_) => log::debug!("○ No Claude prompt file found to import"), - Err(e) => log::warn!("✗ Failed to import Claude prompt: {e}"), - } - - match crate::services::prompt::PromptService::import_from_file_on_first_launch( - &app_state, crate::app_config::AppType::Codex, - ) { - Ok(count) if count > 0 => { - log::info!("✓ Imported {count} prompt(s) from Codex"); - } - Ok(_) => log::debug!("○ No Codex prompt file found to import"), - Err(e) => log::warn!("✗ Failed to import Codex prompt: {e}"), - } - - match crate::services::prompt::PromptService::import_from_file_on_first_launch( - &app_state, crate::app_config::AppType::Gemini, - ) { - Ok(count) if count > 0 => { - log::info!("✓ Imported {count} prompt(s) from Gemini"); + ] { + match crate::services::prompt::PromptService::import_from_file_on_first_launch( + &app_state, + app.clone(), + ) { + Ok(count) if count > 0 => { + log::info!("✓ Imported {count} prompt(s) for {}", app.as_str()); + } + Ok(_) => log::debug!("○ No prompt file found for {}", app.as_str()), + Err(e) => log::warn!("✗ Failed to import prompt for {}: {e}", app.as_str()), } - Ok(_) => log::debug!("○ No Gemini prompt file found to import"), - Err(e) => log::warn!("✗ Failed to import Gemini prompt: {e}"), } - - log::info!("First-time import completed"); } // 迁移旧的 app_config_dir 配置到 Store diff --git a/src-tauri/src/services/prompt.rs b/src-tauri/src/services/prompt.rs index dd8c546c2..8b7aeabf8 100644 --- a/src-tauri/src/services/prompt.rs +++ b/src-tauri/src/services/prompt.rs @@ -176,6 +176,12 @@ impl PromptService { state: &AppState, app: AppType, ) -> Result { + // 幂等性保护:该应用已有提示词则跳过 + let existing = state.db.get_prompts(app.as_str())?; + if !existing.is_empty() { + return Ok(0); + } + let file_path = prompt_file_path(&app)?; // 检查文件是否存在 diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index b4db7221b..6389827b9 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -215,11 +215,14 @@ pub fn read_live_settings(app_type: AppType) -> Result { } /// Import default configuration from live files -pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> { +/// +/// Returns `Ok(true)` if a provider was actually imported, +/// `Ok(false)` if skipped (providers already exist for this app). +pub fn import_default_config(state: &AppState, app_type: AppType) -> Result { { let providers = state.db.get_all_providers(app_type.as_str())?; if !providers.is_empty() { - return Ok(()); + return Ok(false); // 已有供应商,跳过 } } @@ -298,7 +301,7 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), .db .set_current_provider(app_type.as_str(), &provider.id)?; - Ok(()) + Ok(true) // 真正导入了 } /// Write Gemini live configuration with authentication handling diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 67ca16e6d..8baa5afb2 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -225,7 +225,9 @@ impl ProviderService { } /// Import default configuration from live files (re-export) - pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> { + /// + /// Returns `Ok(true)` if imported, `Ok(false)` if skipped. + pub fn import_default_config(state: &AppState, app_type: AppType) -> Result { import_default_config(state, app_type) }