diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 6343596..2c59677 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -494,8 +494,11 @@ impl MultiAppConfig { // 创建提示词对象 let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|_| { + log::warn!("Failed to get system time, using 0 as timestamp"); + 0 + }); let id = format!("auto-imported-{timestamp}"); let prompt = crate::prompt::Prompt { diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index f1557ed..78fdaef 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -6,9 +6,25 @@ use crate::provider::{Provider, ProviderMeta}; use crate::services::skill::{SkillRepo, SkillState}; use indexmap::IndexMap; use rusqlite::{params, Connection, Result}; +use serde::Serialize; use std::collections::HashMap; use std::sync::Mutex; +/// 安全地序列化 JSON,避免 unwrap panic +fn to_json_string(value: &T) -> Result { + 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 struct Database { // 使用 Mutex 包装 Connection 以支持在多线程环境(如 Tauri State)中共享 // rusqlite::Connection 本身不是 Sync 的 @@ -40,7 +56,7 @@ impl Database { } fn create_tables(&self) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); // 1. Providers 表 conn.execute( @@ -152,7 +168,7 @@ impl Database { /// 从 MultiAppConfig 迁移数据 pub fn migrate_from_json(&self, config: &MultiAppConfig) -> Result<(), AppError> { - let mut conn = self.conn.lock().unwrap(); + let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; @@ -178,7 +194,7 @@ impl Database { id, app_type, provider.name, - serde_json::to_string(&provider.settings_config).unwrap(), + to_json_string(&provider.settings_config)?, provider.website_url, provider.category, provider.created_at, @@ -186,7 +202,7 @@ impl Database { provider.notes, provider.icon, provider.icon_color, - serde_json::to_string(&meta_clone).unwrap(), // 不含 endpoints 的 meta + to_json_string(&meta_clone)?, // 不含 endpoints 的 meta is_current, ], ) @@ -215,11 +231,11 @@ impl Database { params![ id, server.name, - serde_json::to_string(&server.server).unwrap(), + to_json_string(&server.server)?, server.description, server.homepage, server.docs, - serde_json::to_string(&server.tags).unwrap(), + to_json_string(&server.tags)?, server.apps.claude, server.apps.codex, server.apps.gemini, @@ -303,13 +319,42 @@ impl Database { Ok(()) } + /// 检查数据库是否为空(需要首次导入) + /// 通过检查是否有任何 MCP 服务器、提示词、Skills 仓库或供应商来判断 + pub fn is_empty_for_first_import(&self) -> Result { + let conn = lock_conn!(self.conn); + + // 检查是否有 MCP 服务器 + let mcp_count: i64 = conn + .query_row("SELECT COUNT(*) FROM mcp_servers", [], |row| row.get(0)) + .map_err(|e| AppError::Database(e.to_string()))?; + + // 检查是否有提示词 + let prompt_count: i64 = conn + .query_row("SELECT COUNT(*) FROM prompts", [], |row| row.get(0)) + .map_err(|e| AppError::Database(e.to_string()))?; + + // 检查是否有 Skills 仓库 + 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()))?; + + // 如果四者都为 0,说明是空数据库 + Ok(mcp_count == 0 && prompt_count == 0 && skill_repo_count == 0 && provider_count == 0) + } + // --- Providers DAO --- pub fn get_all_providers( &self, app_type: &str, ) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( "SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta FROM providers WHERE app_type = ?1 @@ -396,7 +441,7 @@ impl Database { } pub fn get_current_provider(&self, app_type: &str) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn .prepare("SELECT id FROM providers WHERE app_type = ?1 AND is_current = 1 LIMIT 1") .map_err(|e| AppError::Database(e.to_string()))?; @@ -415,7 +460,7 @@ impl Database { } pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> { - let mut conn = self.conn.lock().unwrap(); + let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; @@ -477,7 +522,7 @@ impl Database { } pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM providers WHERE id = ?1 AND app_type = ?2", params![id, app_type], @@ -487,7 +532,7 @@ impl Database { } pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> { - let mut conn = self.conn.lock().unwrap(); + let mut conn = lock_conn!(self.conn); let tx = conn .transaction() .map_err(|e| AppError::Database(e.to_string()))?; @@ -516,7 +561,7 @@ impl Database { provider_id: &str, url: &str, ) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let added_at = chrono::Utc::now().timestamp_millis(); conn.execute( "INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)", @@ -531,7 +576,7 @@ impl Database { provider_id: &str, url: &str, ) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 AND url = ?3", params![provider_id, app_type, url], @@ -543,7 +588,7 @@ impl Database { // --- MCP Servers DAO --- pub fn get_all_mcp_servers(&self) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( "SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini FROM mcp_servers @@ -595,7 +640,7 @@ impl Database { } pub fn save_mcp_server(&self, server: &McpServer) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "INSERT OR REPLACE INTO mcp_servers ( id, name, server_config, description, homepage, docs, tags, @@ -619,7 +664,7 @@ impl Database { } pub fn delete_mcp_server(&self, id: &str) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute("DELETE FROM mcp_servers WHERE id = ?1", params![id]) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) @@ -628,7 +673,7 @@ impl Database { // --- Prompts DAO --- pub fn get_prompts(&self, app_type: &str) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn .prepare( "SELECT id, name, content, description, enabled, created_at, updated_at @@ -671,7 +716,7 @@ impl Database { } pub fn save_prompt(&self, app_type: &str, prompt: &Prompt) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "INSERT OR REPLACE INTO prompts ( id, app_type, name, content, description, enabled, created_at, updated_at @@ -692,7 +737,7 @@ impl Database { } pub fn delete_prompt(&self, app_type: &str, id: &str) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM prompts WHERE id = ?1 AND app_type = ?2", params![id, app_type], @@ -704,7 +749,7 @@ impl Database { // --- Skills DAO --- pub fn get_skills(&self) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn .prepare("SELECT key, installed, installed_at FROM skills ORDER BY key ASC") .map_err(|e| AppError::Database(e.to_string()))?; @@ -737,7 +782,7 @@ impl Database { } pub fn update_skill_state(&self, key: &str, state: &SkillState) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "INSERT OR REPLACE INTO skills (key, installed, installed_at) VALUES (?1, ?2, ?3)", params![key, state.installed, state.installed_at.timestamp()], @@ -747,7 +792,7 @@ impl Database { } pub fn get_skill_repos(&self) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn .prepare("SELECT owner, name, branch, enabled, skills_path FROM skill_repos ORDER BY owner ASC, name ASC") .map_err(|e| AppError::Database(e.to_string()))?; @@ -772,7 +817,7 @@ impl Database { } pub fn save_skill_repo(&self, repo: &SkillRepo) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)", params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path], @@ -781,7 +826,7 @@ impl Database { } pub fn delete_skill_repo(&self, owner: &str, name: &str) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "DELETE FROM skill_repos WHERE owner = ?1 AND name = ?2", params![owner, name], @@ -790,10 +835,31 @@ impl Database { Ok(()) } + /// 初始化默认的 Skill 仓库(首次启动时调用) + pub fn init_default_skill_repos(&self) -> Result { + // 检查是否已有仓库 + let existing = self.get_skill_repos()?; + if !existing.is_empty() { + return Ok(0); + } + + // 获取默认仓库列表 + let default_store = crate::services::skill::SkillStore::default(); + let mut count = 0; + + for repo in &default_store.repos { + self.save_skill_repo(repo)?; + count += 1; + } + + log::info!("初始化默认 Skill 仓库完成,共 {count} 个"); + Ok(count) + } + // --- Settings DAO --- pub fn get_setting(&self, key: &str) -> Result, AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); let mut stmt = conn .prepare("SELECT value FROM settings WHERE key = ?1") .map_err(|e| AppError::Database(e.to_string()))?; @@ -812,7 +878,7 @@ impl Database { } pub fn set_setting(&self, key: &str, value: &str) -> Result<(), AppError> { - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)", params![key, value], @@ -837,7 +903,7 @@ impl Database { self.set_setting(&key, &value) } else { // Delete if None - let conn = self.conn.lock().unwrap(); + let conn = lock_conn!(self.conn); conn.execute("DELETE FROM settings WHERE key = ?1", params![key]) .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f2d0f8a..db06c3a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -495,24 +495,31 @@ pub fn run() { use objc2::runtime::AnyObject; use objc2_app_kit::NSColor; - let ns_window_ptr = window.ns_window().unwrap(); - let ns_window: Retained = - unsafe { Retained::retain(ns_window_ptr as *mut AnyObject).unwrap() }; + match window.ns_window() { + Ok(ns_window_ptr) => { + if let Some(ns_window) = + unsafe { Retained::retain(ns_window_ptr as *mut AnyObject) } + { + // 使用与主界面 banner 相同的蓝色 #3498db + // #3498db = RGB(52, 152, 219) + let bg_color = unsafe { + NSColor::colorWithRed_green_blue_alpha( + 52.0 / 255.0, // R: 52 + 152.0 / 255.0, // G: 152 + 219.0 / 255.0, // B: 219 + 1.0, // Alpha: 1.0 + ) + }; - // 使用与主界面 banner 相同的蓝色 #3498db - // #3498db = RGB(52, 152, 219) - let bg_color = unsafe { - NSColor::colorWithRed_green_blue_alpha( - 52.0 / 255.0, // R: 52 - 152.0 / 255.0, // G: 152 - 219.0 / 255.0, // B: 219 - 1.0, // Alpha: 1.0 - ) - }; - - unsafe { - use objc2::msg_send; - let _: () = msg_send![&*ns_window, setBackgroundColor: &*bg_color]; + unsafe { + use objc2::msg_send; + let _: () = msg_send![&*ns_window, setBackgroundColor: &*bg_color]; + } + } else { + log::warn!("Failed to retain NSWindow reference"); + } + } + Err(e) => log::warn!("Failed to get NSWindow pointer: {e}"), } } } @@ -565,6 +572,115 @@ 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}"), + } + + // 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 + ); + } + } + } + + // 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"); + } + Ok(_) => log::debug!("○ No Claude MCP servers found to import"), + Err(e) => log::warn!("✗ Failed to import Claude MCP: {e}"), + } + + match crate::services::mcp::McpService::import_from_codex(&app_state) { + Ok(count) if count > 0 => { + log::info!("✓ Imported {count} MCP server(s) from Codex"); + } + Ok(_) => log::debug!("○ No Codex MCP servers found to import"), + Err(e) => log::warn!("✗ Failed to import Codex MCP: {e}"), + } + + match crate::services::mcp::McpService::import_from_gemini(&app_state) { + Ok(count) if count > 0 => { + log::info!("✓ Imported {count} MCP server(s) from Gemini"); + } + 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, + 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"); + } + 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 if let Err(e) = app_store::migrate_app_config_dir_from_settings(app.handle()) { log::warn!("迁移 app_config_dir 失败: {e}"); @@ -622,7 +738,11 @@ pub fn run() { .show_menu_on_left_click(true); // 统一使用应用默认图标;待托盘模板图标就绪后再启用 - tray_builder = tray_builder.icon(app.default_window_icon().unwrap().clone()); + if let Some(icon) = app.default_window_icon() { + tray_builder = tray_builder.icon(icon.clone()); + } else { + log::warn!("Failed to get default window icon for tray"); + } let _tray = tray_builder.build(app)?; // 将同一个实例注入到全局状态,避免重复创建导致的不一致 diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 30289d4..edee1d4 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -348,10 +348,7 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result Result .map_err(|e| AppError::McpValidation(format!("解析 ~/.codex/config.toml 失败: {e}")))?; // 确保新结构存在 - if config.mcp.servers.is_none() { - config.mcp.servers = Some(HashMap::new()); - } - let servers = config.mcp.servers.as_mut().unwrap(); + let servers = config.mcp.servers.get_or_insert_with(HashMap::new); let mut changed_total = 0usize; @@ -724,10 +718,7 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result Option toml_arr.push(s.as_str()), - Value::Number(n) if n.is_i64() => toml_arr.push(n.as_i64().unwrap()), - Value::Number(n) if n.is_f64() => toml_arr.push(n.as_f64().unwrap()), + Value::Number(n) if n.is_i64() => { + if let Some(i) = n.as_i64() { + toml_arr.push(i); + } else { + all_same_type = false; + break; + } + } + Value::Number(n) if n.is_f64() => { + if let Some(f) = n.as_f64() { + toml_arr.push(f); + } else { + all_same_type = false; + break; + } + } Value::Bool(b) => toml_arr.push(*b), _ => { all_same_type = false; diff --git a/src-tauri/src/services/prompt.rs b/src-tauri/src/services/prompt.rs index 6105cf5..dd8c546 100644 --- a/src-tauri/src/services/prompt.rs +++ b/src-tauri/src/services/prompt.rs @@ -7,6 +7,14 @@ use crate::prompt::Prompt; use crate::prompt_files::prompt_file_path; use crate::store::AppState; +/// 安全地获取当前 Unix 时间戳 +fn get_unix_timestamp() -> Result { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .map_err(|e| AppError::Message(format!("Failed to get system time: {e}"))) +} + pub struct PromptService; impl PromptService { @@ -64,10 +72,7 @@ impl PromptService { .find(|(_, p)| p.enabled) .map(|(id, p)| (id.clone(), p)) { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; + let timestamp = get_unix_timestamp()?; enabled_prompt.content = live_content.clone(); enabled_prompt.updated_at = Some(timestamp); log::info!("回填 live 提示词内容到已启用项: {enabled_id}"); @@ -135,10 +140,7 @@ impl PromptService { let content = std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?; - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() as i64; + let timestamp = get_unix_timestamp()?; let id = format!("imported-{timestamp}"); let prompt = Prompt { @@ -167,4 +169,56 @@ impl PromptService { std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?; Ok(Some(content)) } + + /// 首次启动时从现有提示词文件自动导入(如果存在) + /// 返回导入的数量 + pub fn import_from_file_on_first_launch( + state: &AppState, + app: AppType, + ) -> Result { + let file_path = prompt_file_path(&app)?; + + // 检查文件是否存在 + if !file_path.exists() { + return Ok(0); + } + + // 读取文件内容 + let content = match std::fs::read_to_string(&file_path) { + Ok(c) => c, + Err(e) => { + log::warn!("读取提示词文件失败: {file_path:?}, 错误: {e}"); + return Ok(0); + } + }; + + // 检查内容是否为空 + if content.trim().is_empty() { + return Ok(0); + } + + log::info!("发现提示词文件,自动导入: {file_path:?}"); + + // 创建提示词对象 + let timestamp = get_unix_timestamp()?; + let id = format!("auto-imported-{timestamp}"); + let prompt = Prompt { + id: id.clone(), + name: format!( + "Auto-imported Prompt {}", + chrono::Local::now().format("%Y-%m-%d %H:%M") + ), + content, + description: Some("Automatically imported on first launch".to_string()), + enabled: true, // 首次导入时自动启用 + created_at: Some(timestamp), + updated_at: Some(timestamp), + }; + + // 保存到数据库 + state.db.save_prompt(app.as_str(), &prompt)?; + + log::info!("自动导入完成: {}", app.as_str()); + Ok(1) + } } diff --git a/src-tauri/src/services/provider.rs b/src-tauri/src/services/provider.rs index 35677b5..84296c1 100644 --- a/src-tauri/src/services/provider.rs +++ b/src-tauri/src/services/provider.rs @@ -424,9 +424,9 @@ impl ProviderService { /// 归一化 Claude 模型键:读旧键(ANTHROPIC_SMALL_FAST_MODEL),写新键(DEFAULT_*), 并删除旧键 fn normalize_claude_models_in_value(settings: &mut Value) -> bool { let mut changed = false; - let env = match settings.get_mut("env") { - Some(v) if v.is_object() => v.as_object_mut().unwrap(), - _ => return changed, + let env = match settings.get_mut("env").and_then(|v| v.as_object_mut()) { + Some(obj) => obj, + None => return changed, }; let model = env diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index 1af2f47..3a9b05b 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -231,7 +231,12 @@ impl SkillService { // 解析技能元数据 match self.parse_skill_metadata(&skill_md) { Ok(meta) => { - let directory = path.file_name().unwrap().to_string_lossy().to_string(); + // 安全地获取目录名 + let Some(dir_name) = path.file_name() else { + log::warn!("Failed to get directory name from path: {path:?}"); + continue; + }; + let directory = dir_name.to_string_lossy().to_string(); // 构建 README URL(考虑 skillsPath) let readme_path = if let Some(ref skills_path) = repo.skills_path { @@ -305,7 +310,12 @@ impl SkillService { continue; } - let directory = path.file_name().unwrap().to_string_lossy().to_string(); + // 安全地获取目录名 + let Some(dir_name) = path.file_name() else { + log::warn!("Failed to get directory name from path: {path:?}"); + continue; + }; + let directory = dir_name.to_string_lossy().to_string(); // 更新已安装状态 let mut found = false;