diff --git a/src-tauri/src/commands/config_commands.rs b/src-tauri/src/commands/config_commands.rs index e3670b9..14f1465 100644 --- a/src-tauri/src/commands/config_commands.rs +++ b/src-tauri/src/commands/config_commands.rs @@ -1,5 +1,6 @@ // 配置管理相关命令 +use super::error::{AppError, AppResult}; use serde_json::Value; use ::duckcoding::services::config::{ @@ -55,9 +56,10 @@ pub async fn get_external_changes() -> Result, String> /// 确认外部变更(清除脏标记并刷新 checksum) #[tauri::command] -pub async fn ack_external_change(tool: String) -> Result<(), String> { - let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {tool}"))?; - config::acknowledge_external_change(&tool_obj).map_err(|e| e.to_string()) +pub async fn ack_external_change(tool: String) -> AppResult<()> { + let tool_obj = + Tool::by_id(&tool).ok_or_else(|| AppError::ToolNotFound { tool: tool.clone() })?; + Ok(config::acknowledge_external_change(&tool_obj)?) } /// 将外部修改导入集中仓 @@ -66,9 +68,10 @@ pub async fn import_native_change( tool: String, profile: String, as_new: bool, -) -> Result { - let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("❌ 未知的工具: {tool}"))?; - config::import_external_change(&tool_obj, &profile, as_new).map_err(|e| e.to_string()) +) -> AppResult { + let tool_obj = + Tool::by_id(&tool).ok_or_else(|| AppError::ToolNotFound { tool: tool.clone() })?; + Ok(config::import_external_change(&tool_obj, &profile, as_new)?) } #[tauri::command] diff --git a/src-tauri/src/commands/error.rs b/src-tauri/src/commands/error.rs new file mode 100644 index 0000000..a69f0d1 --- /dev/null +++ b/src-tauri/src/commands/error.rs @@ -0,0 +1,77 @@ +//! Commands 层错误处理统一模块 +//! +//! ## 使用指南 +//! +//! ### 当前状态 +//! +//! - Commands 层使用 `Result` 作为返回类型(Tauri 要求) +//! - Services 层使用 `anyhow::Result` 作为返回类型 +//! - Core 层提供完善的 `AppError` 枚举和 `AppResult` 类型别名 +//! +//! ### 最佳实践(渐进式迁移) +//! +//! #### 方案 A:保持现状(推荐新手) +//! +//! ```rust +//! #[tauri::command] +//! pub async fn my_command() -> Result { +//! service::do_something().map_err(|e| e.to_string()) +//! } +//! ``` +//! +//! **优点**:简单直接,无需修改现有代码 +//! **缺点**:错误信息丢失上下文 +//! +//! #### 方案 B:使用 AppError(推荐专家) +//! +//! ```rust +//! use super::error::AppResult; +//! +//! #[tauri::command] +//! pub async fn my_command() -> AppResult { +//! let data = service::do_something()?; // anyhow::Result 自动转换 +//! Ok(data) +//! } +//! ``` +//! +//! **优点**:保留完整错误链,结构化错误信息 +//! **缺点**:需要确保所有错误类型实现 `Into` +//! +//! #### 方案 C:混合使用(推荐过渡期) +//! +//! ```rust +//! use super::error::AppError; +//! +//! #[tauri::command] +//! pub async fn my_command(id: String) -> Result { +//! let tool = Tool::by_id(&id) +//! .ok_or_else(|| AppError::ToolNotFound { tool: id.clone() })?; +//! +//! service::process(&tool) +//! .map_err(|e| e.to_string()) +//! } +//! ``` +//! +//! **优点**:关键错误使用结构化类型,其他保持简单 +//! **缺点**:代码风格不统一 +//! +//! ### 错误类型速查 +//! +//! - `AppError::ToolNotFound { tool }` - 工具未找到 +//! - `AppError::ConfigNotFound { path }` - 配置文件未找到 +//! - `AppError::ProfileNotFound { profile }` - Profile 未找到 +//! - `AppError::ValidationError { field, reason }` - 验证失败 +//! - `AppError::Custom(String)` - 自定义错误 +//! +//! ### 迁移计划 +//! +//! 1. ✅ 创建 error.rs 模块(本文件) +//! 2. ⏳ 迁移高频命令(config/profile/tool) +//! 3. ⏳ 迁移中频命令(proxy/session) +//! 4. ⏳ 迁移低频命令(其他) +//! +//! ## 导出 +//! +//! 重导出 core::error 中的类型供 commands 层使用 + +pub use ::duckcoding::core::error::{AppError, AppResult}; diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6174304..8f5fca2 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,5 +1,6 @@ pub mod balance_commands; pub mod config_commands; +pub mod error; // 错误处理统一模块 pub mod log_commands; pub mod onboarding; pub mod profile_commands; // Profile 管理命令(v2.0) diff --git a/src-tauri/src/commands/profile_commands.rs b/src-tauri/src/commands/profile_commands.rs index 1fa706a..517f5fb 100644 --- a/src-tauri/src/commands/profile_commands.rs +++ b/src-tauri/src/commands/profile_commands.rs @@ -1,8 +1,15 @@ //! Profile 管理 Tauri 命令(v2.1 - 简化版) -use ::duckcoding::services::profile_manager::{ProfileDescriptor, ProfileManager}; -use anyhow::Result; +use super::error::AppResult; +use ::duckcoding::services::profile_manager::ProfileDescriptor; use serde::Deserialize; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// Profile 管理器 State +pub struct ProfileManagerState { + pub manager: Arc>, +} /// Profile 输入数据(前端传递) #[derive(Debug, Deserialize)] @@ -27,43 +34,46 @@ pub enum ProfileInput { /// 列出所有 Profile 描述符 #[tauri::command] -pub async fn pm_list_all_profiles() -> Result, String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager.list_all_descriptors().map_err(|e| e.to_string()) +pub async fn pm_list_all_profiles( + state: tauri::State<'_, ProfileManagerState>, +) -> AppResult> { + let manager = state.manager.read().await; + Ok(manager.list_all_descriptors()?) } /// 列出指定工具的 Profile 名称 #[tauri::command] -pub async fn pm_list_tool_profiles(tool_id: String) -> Result, String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager.list_profiles(&tool_id).map_err(|e| e.to_string()) +pub async fn pm_list_tool_profiles( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, +) -> AppResult> { + let manager = state.manager.read().await; + Ok(manager.list_profiles(&tool_id)?) } /// 获取指定 Profile(返回 JSON 供前端使用) #[tauri::command] -pub async fn pm_get_profile(tool_id: String, name: String) -> Result { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; +pub async fn pm_get_profile( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, + name: String, +) -> AppResult { + let manager = state.manager.read().await; let value = match tool_id.as_str() { "claude-code" => { - let profile = manager - .get_claude_profile(&name) - .map_err(|e| e.to_string())?; - serde_json::to_value(&profile).map_err(|e| e.to_string())? + let profile = manager.get_claude_profile(&name)?; + serde_json::to_value(&profile)? } "codex" => { - let profile = manager - .get_codex_profile(&name) - .map_err(|e| e.to_string())?; - serde_json::to_value(&profile).map_err(|e| e.to_string())? + let profile = manager.get_codex_profile(&name)?; + serde_json::to_value(&profile)? } "gemini-cli" => { - let profile = manager - .get_gemini_profile(&name) - .map_err(|e| e.to_string())?; - serde_json::to_value(&profile).map_err(|e| e.to_string())? + let profile = manager.get_gemini_profile(&name)?; + serde_json::to_value(&profile)? } - _ => return Err(format!("不支持的工具 ID: {}", tool_id)), + _ => return Err(super::error::AppError::ToolNotFound { tool: tool_id }), }; Ok(value) @@ -71,14 +81,16 @@ pub async fn pm_get_profile(tool_id: String, name: String) -> Result Result, String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - let name = manager - .get_active_profile_name(&tool_id) - .map_err(|e| e.to_string())?; +pub async fn pm_get_active_profile( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, +) -> AppResult> { + let manager = state.manager.read().await; + let name = manager.get_active_profile_name(&tool_id)?; if let Some(profile_name) = name { - pm_get_profile(tool_id, profile_name).await.map(Some) + drop(manager); // 释放读锁 + pm_get_profile(state, tool_id, profile_name).await.map(Some) } else { Ok(None) } @@ -87,18 +99,22 @@ pub async fn pm_get_active_profile(tool_id: String) -> Result, tool_id: String, name: String, input: ProfileInput, -) -> Result<(), String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; +) -> AppResult<()> { + let manager = state.manager.write().await; // 写锁 match tool_id.as_str() { "claude-code" => { if let ProfileInput::Claude { api_key, base_url } = input { - manager.save_claude_profile(&name, api_key, base_url) + Ok(manager.save_claude_profile(&name, api_key, base_url)?) } else { - Err(anyhow::anyhow!("Claude Code 需要 Claude Profile 数据")) + Err(super::error::AppError::ValidationError { + field: "input".to_string(), + reason: "Claude Code 需要 Claude Profile 数据".to_string(), + }) } } "codex" => { @@ -108,9 +124,12 @@ pub async fn pm_save_profile( wire_api, } = input { - manager.save_codex_profile(&name, api_key, base_url, Some(wire_api)) + Ok(manager.save_codex_profile(&name, api_key, base_url, Some(wire_api))?) } else { - Err(anyhow::anyhow!("Codex 需要 Codex Profile 数据")) + Err(super::error::AppError::ValidationError { + field: "input".to_string(), + reason: "Codex 需要 Codex Profile 数据".to_string(), + }) } } "gemini-cli" => { @@ -120,48 +139,57 @@ pub async fn pm_save_profile( model, } = input { - manager.save_gemini_profile(&name, api_key, base_url, model) + Ok(manager.save_gemini_profile(&name, api_key, base_url, model)?) } else { - Err(anyhow::anyhow!("Gemini CLI 需要 Gemini Profile 数据")) + Err(super::error::AppError::ValidationError { + field: "input".to_string(), + reason: "Gemini CLI 需要 Gemini Profile 数据".to_string(), + }) } } - _ => Err(anyhow::anyhow!("不支持的工具 ID: {}", tool_id)), + _ => Err(super::error::AppError::ToolNotFound { tool: tool_id }), } - .map_err(|e| e.to_string()) } /// 删除 Profile #[tauri::command] -pub async fn pm_delete_profile(tool_id: String, name: String) -> Result<(), String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager - .delete_profile(&tool_id, &name) - .map_err(|e| e.to_string()) +pub async fn pm_delete_profile( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, + name: String, +) -> AppResult<()> { + let manager = state.manager.write().await; + Ok(manager.delete_profile(&tool_id, &name)?) } /// 激活 Profile #[tauri::command] -pub async fn pm_activate_profile(tool_id: String, name: String) -> Result<(), String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager - .activate_profile(&tool_id, &name) - .map_err(|e| e.to_string()) +pub async fn pm_activate_profile( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, + name: String, +) -> AppResult<()> { + let manager = state.manager.write().await; + Ok(manager.activate_profile(&tool_id, &name)?) } /// 获取当前激活的 Profile 名称 #[tauri::command] -pub async fn pm_get_active_profile_name(tool_id: String) -> Result, String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager - .get_active_profile_name(&tool_id) - .map_err(|e| e.to_string()) +pub async fn pm_get_active_profile_name( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, +) -> AppResult> { + let manager = state.manager.read().await; + Ok(manager.get_active_profile_name(&tool_id)?) } /// 从原生配置文件捕获 Profile #[tauri::command] -pub async fn pm_capture_from_native(tool_id: String, name: String) -> Result<(), String> { - let manager = ProfileManager::new().map_err(|e| e.to_string())?; - manager - .capture_from_native(&tool_id, &name) - .map_err(|e| e.to_string()) +pub async fn pm_capture_from_native( + state: tauri::State<'_, ProfileManagerState>, + tool_id: String, + name: String, +) -> AppResult<()> { + let manager = state.manager.write().await; + Ok(manager.capture_from_native(&tool_id, &name)?) } diff --git a/src-tauri/src/commands/proxy_commands.rs b/src-tauri/src/commands/proxy_commands.rs index 0f375ae..08915fc 100644 --- a/src-tauri/src/commands/proxy_commands.rs +++ b/src-tauri/src/commands/proxy_commands.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::sync::Arc; use tauri::State; +use crate::commands::profile_commands::ProfileManagerState; use ::duckcoding::services::proxy::ProxyManager; use ::duckcoding::services::proxy_config_manager::ProxyConfigManager; use ::duckcoding::utils::config::read_global_config; @@ -137,10 +138,9 @@ pub async fn test_proxy_request( async fn try_start_proxy_internal( tool_id: &str, manager_state: &ProxyManagerState, + profile_state: &ProfileManagerState, ) -> Result<(String, u16), String> { - use ::duckcoding::services::profile_manager::ProfileManager; - - let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?; + let profile_mgr = profile_state.manager.read().await; let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?; // 读取当前配置 @@ -225,11 +225,10 @@ async fn try_start_proxy_internal( pub async fn start_tool_proxy( tool_id: String, manager_state: State<'_, ProxyManagerState>, + profile_state: State<'_, ProfileManagerState>, ) -> Result { - use ::duckcoding::services::profile_manager::ProfileManager; - // 备份当前状态(用于回滚) - let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?; + let profile_mgr = profile_state.manager.read().await; let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?; let backup_config = proxy_config_mgr @@ -242,7 +241,7 @@ pub async fn start_tool_proxy( .map_err(|e| e.to_string())?; // 执行启动操作 - match try_start_proxy_internal(&tool_id, &manager_state).await { + match try_start_proxy_internal(&tool_id, &manager_state, &profile_state).await { Ok((tool_id, proxy_port)) => Ok(format!( "✅ {} 透明代理已启动\n监听端口: {}\n已切换到代理配置", tool_id, proxy_port @@ -258,9 +257,11 @@ pub async fn start_tool_proxy( tracing::info!("已回滚代理配置"); } - // 回滚 Profile 激活状态 + // 回滚 Profile 激活状态(需要写锁) + drop(profile_mgr); // 释放读锁 + let profile_mgr_write = profile_state.manager.write().await; if let Some(name) = backup_profile { - if let Err(rollback_err) = profile_mgr.activate_profile(&tool_id, &name) { + if let Err(rollback_err) = profile_mgr_write.activate_profile(&tool_id, &name) { tracing::error!("回滚 Profile 失败: {}", rollback_err); } else { tracing::info!("已回滚 Profile 到: {}", name); @@ -277,10 +278,9 @@ pub async fn start_tool_proxy( pub async fn stop_tool_proxy( tool_id: String, manager_state: State<'_, ProxyManagerState>, + profile_state: State<'_, ProfileManagerState>, ) -> Result { - use ::duckcoding::services::profile_manager::ProfileManager; - - let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?; + let profile_mgr = profile_state.manager.write().await; let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?; // 读取代理配置 @@ -373,11 +373,11 @@ pub async fn update_proxy_from_profile( tool_id: String, profile_name: String, manager_state: State<'_, ProxyManagerState>, + profile_state: State<'_, ProfileManagerState>, ) -> Result<(), String> { - use ::duckcoding::services::profile_manager::ProfileManager; use ::duckcoding::services::proxy_config_manager::ProxyConfigManager; - let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?; + let profile_mgr = profile_state.manager.read().await; let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?; // 根据工具类型读取 Profile @@ -448,9 +448,8 @@ pub async fn update_proxy_config( tool_id: String, config: ::duckcoding::models::proxy_config::ToolProxyConfig, manager_state: State<'_, ProxyManagerState>, + profile_state: State<'_, ProfileManagerState>, ) -> Result<(), String> { - use ::duckcoding::services::profile_manager::ProfileManager; - // ========== 运行时保护检查 ========== if manager_state.manager.is_running(&tool_id).await { return Err(format!("{} 代理正在运行,请先停止代理再修改配置", tool_id)); @@ -470,7 +469,7 @@ pub async fn update_proxy_config( && config.real_api_key.is_some() && config.real_base_url.is_some() { - let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?; + let profile_mgr = profile_state.manager.write().await; let proxy_profile_name = format!("dc_proxy_{}", tool_id.replace("-", "_")); let proxy_endpoint = format!("http://127.0.0.1:{}", config.port); diff --git a/src-tauri/src/commands/session_commands.rs b/src-tauri/src/commands/session_commands.rs index 5cf3803..e4c3dc4 100644 --- a/src-tauri/src/commands/session_commands.rs +++ b/src-tauri/src/commands/session_commands.rs @@ -1,5 +1,6 @@ // 会话管理 Tauri 命令 +use crate::commands::error::AppResult; use duckcoding::services::session::{SessionListResponse, SESSION_MANAGER}; /// 获取会话列表 @@ -8,26 +9,20 @@ pub async fn get_session_list( tool_id: String, page: usize, page_size: usize, -) -> Result { - SESSION_MANAGER - .get_session_list(&tool_id, page, page_size) - .map_err(|e| format!("Failed to get session list: {e}")) +) -> AppResult { + Ok(SESSION_MANAGER.get_session_list(&tool_id, page, page_size)?) } /// 删除单个会话 #[tauri::command] -pub async fn delete_session(session_id: String) -> Result<(), String> { - SESSION_MANAGER - .delete_session(&session_id) - .map_err(|e| format!("Failed to delete session: {e}")) +pub async fn delete_session(session_id: String) -> AppResult<()> { + Ok(SESSION_MANAGER.delete_session(&session_id)?) } /// 清空指定工具的所有会话 #[tauri::command] -pub async fn clear_all_sessions(tool_id: String) -> Result<(), String> { - SESSION_MANAGER - .clear_sessions(&tool_id) - .map_err(|e| format!("Failed to clear sessions: {e}")) +pub async fn clear_all_sessions(tool_id: String) -> AppResult<()> { + Ok(SESSION_MANAGER.clear_sessions(&tool_id)?) } /// 更新会话配置 @@ -38,22 +33,18 @@ pub async fn update_session_config( custom_profile_name: Option, url: String, api_key: String, -) -> Result<(), String> { - SESSION_MANAGER - .update_session_config( - &session_id, - &config_name, - custom_profile_name.as_deref(), - &url, - &api_key, - ) - .map_err(|e| format!("Failed to update session config: {e}")) +) -> AppResult<()> { + Ok(SESSION_MANAGER.update_session_config( + &session_id, + &config_name, + custom_profile_name.as_deref(), + &url, + &api_key, + )?) } /// 更新会话备注 #[tauri::command] -pub async fn update_session_note(session_id: String, note: Option) -> Result<(), String> { - SESSION_MANAGER - .update_session_note(&session_id, note.as_deref()) - .map_err(|e| format!("Failed to update session note: {e}")) +pub async fn update_session_note(session_id: String, note: Option) -> AppResult<()> { + Ok(SESSION_MANAGER.update_session_note(&session_id, note.as_deref())?) } diff --git a/src-tauri/src/commands/tool_commands/detection.rs b/src-tauri/src/commands/tool_commands/detection.rs index d766ef2..22d37ca 100644 --- a/src-tauri/src/commands/tool_commands/detection.rs +++ b/src-tauri/src/commands/tool_commands/detection.rs @@ -1,3 +1,4 @@ +use crate::commands::error::{AppError, AppResult}; use crate::commands::tool_management::ToolRegistryState; use crate::commands::types::ToolStatus; use ::duckcoding::utils::{parse_version_string, CommandExecutor, ToolCandidate}; @@ -13,12 +14,9 @@ use ::duckcoding::utils::{parse_version_string, CommandExecutor, ToolCandidate}; pub async fn scan_all_tool_candidates( tool_id: String, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result, String> { +) -> AppResult> { let registry = registry_state.registry.lock().await; - registry - .scan_tool_candidates(&tool_id) - .await - .map_err(|e| e.to_string()) + Ok(registry.scan_tool_candidates(&tool_id).await?) } /// 检测单个工具但不保存(仅用于预览) @@ -32,7 +30,7 @@ pub async fn scan_all_tool_candidates( pub async fn detect_tool_without_save( tool_id: String, _registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { let command_executor = CommandExecutor::new(); // 根据工具ID确定检测命令和名称 @@ -40,7 +38,7 @@ pub async fn detect_tool_without_save( "claude-code" => ("claude", "Claude Code"), "codex" => ("codex", "CodeX"), "gemini-cli" => ("gemini", "Gemini CLI"), - _ => return Err(format!("未知工具ID: {}", tool_id)), + _ => return Err(AppError::ToolNotFound { tool: tool_id }), }; // 检测工具是否存在 @@ -84,10 +82,9 @@ pub async fn detect_single_tool( tool_id: String, force_redetect: Option, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { let registry = registry_state.registry.lock().await; - registry + Ok(registry .detect_single_tool_with_cache(&tool_id, force_redetect.unwrap_or(false)) - .await - .map_err(|e| e.to_string()) + .await?) } diff --git a/src-tauri/src/commands/tool_commands/installation.rs b/src-tauri/src/commands/tool_commands/installation.rs index 1525367..ae0f4de 100644 --- a/src-tauri/src/commands/tool_commands/installation.rs +++ b/src-tauri/src/commands/tool_commands/installation.rs @@ -1,3 +1,4 @@ +use crate::commands::error::{AppError, AppResult}; use crate::commands::tool_management::ToolRegistryState; use crate::commands::types::{InstallResult, ToolStatus}; use ::duckcoding::models::{InstallMethod, Tool}; @@ -46,7 +47,7 @@ pub async fn install_tool( tool: String, method: String, force: Option, -) -> Result { +) -> AppResult { // 应用代理配置(如果已配置) apply_global_proxy().ok(); @@ -56,14 +57,19 @@ pub async fn install_tool( // 获取工具定义 let tool_obj = - Tool::by_id(&tool).ok_or_else(|| "❌ 未知的工具\n\n请联系开发者报告此问题".to_string())?; + Tool::by_id(&tool).ok_or_else(|| AppError::ToolNotFound { tool: tool.clone() })?; // 转换安装方法 let install_method = match method.as_str() { "npm" => InstallMethod::Npm, "brew" => InstallMethod::Brew, "official" => InstallMethod::Official, - _ => return Err(format!("❌ 未知的安装方法: {method}")), + _ => { + return Err(AppError::ValidationError { + field: "method".to_string(), + reason: format!("未知的安装方法: {}", method), + }) + } }; // 使用 InstallerService 安装 @@ -89,7 +95,7 @@ pub async fn install_tool( } Err(e) => { // 安装失败,返回错误信息 - Err(e.to_string()) + Err(e.into()) } } } diff --git a/src-tauri/src/commands/tool_commands/management.rs b/src-tauri/src/commands/tool_commands/management.rs index ca23603..5153bf1 100644 --- a/src-tauri/src/commands/tool_commands/management.rs +++ b/src-tauri/src/commands/tool_commands/management.rs @@ -1,3 +1,4 @@ +use crate::commands::error::{AppError, AppResult}; use crate::commands::tool_management::ToolRegistryState; use crate::commands::types::ToolStatus; use ::duckcoding::models::InstallMethod; @@ -16,20 +17,24 @@ pub async fn add_manual_tool_instance( install_method: String, // "npm" | "brew" | "official" | "other" installer_path: Option, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { // 解析安装方法 let parsed_method = match install_method.as_str() { "npm" => InstallMethod::Npm, "brew" => InstallMethod::Brew, "official" => InstallMethod::Official, "other" => InstallMethod::Other, - _ => return Err(format!("未知的安装方法: {}", install_method)), + _ => { + return Err(AppError::ValidationError { + field: "install_method".to_string(), + reason: format!("未知的安装方法: {}", install_method), + }) + } }; // 委托给 ToolRegistry let registry = registry_state.registry.lock().await; - registry + Ok(registry .add_tool_instance(&tool_id, &path, parsed_method, installer_path) - .await - .map_err(|e| e.to_string()) + .await?) } diff --git a/src-tauri/src/commands/tool_commands/scanner.rs b/src-tauri/src/commands/tool_commands/scanner.rs index 6241cff..b4764b1 100644 --- a/src-tauri/src/commands/tool_commands/scanner.rs +++ b/src-tauri/src/commands/tool_commands/scanner.rs @@ -1,3 +1,4 @@ +use crate::commands::error::AppResult; use ::duckcoding::utils::{scan_installer_paths, InstallerCandidate}; /// 扫描工具路径的安装器 @@ -10,8 +11,6 @@ use ::duckcoding::utils::{scan_installer_paths, InstallerCandidate}; /// /// 返回:安装器候选列表 #[tauri::command] -pub async fn scan_installer_for_tool_path( - tool_path: String, -) -> Result, String> { +pub async fn scan_installer_for_tool_path(tool_path: String) -> AppResult> { Ok(scan_installer_paths(&tool_path)) } diff --git a/src-tauri/src/commands/tool_commands/update.rs b/src-tauri/src/commands/tool_commands/update.rs index 52f04c1..71103d4 100644 --- a/src-tauri/src/commands/tool_commands/update.rs +++ b/src-tauri/src/commands/tool_commands/update.rs @@ -1,3 +1,4 @@ +use crate::commands::error::{AppError, AppResult}; use crate::commands::tool_management::ToolRegistryState; use crate::commands::types::{ToolStatus, UpdateResult}; use ::duckcoding::models::Tool; @@ -6,14 +7,15 @@ use ::duckcoding::services::VersionService; /// 检查工具更新(不执行更新) #[tauri::command] -pub async fn check_update(tool: String) -> Result { +pub async fn check_update(tool: String) -> AppResult { // 应用代理配置(如果已配置) apply_global_proxy().ok(); #[cfg(debug_assertions)] tracing::debug!(tool = %tool, "检查更新(使用VersionService)"); - let tool_obj = Tool::by_id(&tool).ok_or_else(|| format!("未知工具: {tool}"))?; + let tool_obj = + Tool::by_id(&tool).ok_or_else(|| AppError::ToolNotFound { tool: tool.clone() })?; let version_service = VersionService::new(); @@ -55,12 +57,9 @@ pub async fn check_update(tool: String) -> Result { pub async fn check_update_for_instance( instance_id: String, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { let registry = registry_state.registry.lock().await; - registry - .check_update_for_instance(&instance_id) - .await - .map_err(|e| e.to_string()) + Ok(registry.check_update_for_instance(&instance_id).await?) } /// 刷新数据库中所有工具的版本号(使用配置的路径检测) @@ -73,17 +72,14 @@ pub async fn check_update_for_instance( #[tauri::command] pub async fn refresh_all_tool_versions( registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result, String> { +) -> AppResult> { let registry = registry_state.registry.lock().await; - registry - .refresh_all_tool_versions() - .await - .map_err(|e| e.to_string()) + Ok(registry.refresh_all_tool_versions().await?) } /// 批量检查所有工具更新 #[tauri::command] -pub async fn check_all_updates() -> Result, String> { +pub async fn check_all_updates() -> AppResult> { // 应用代理配置(如果已配置) apply_global_proxy().ok(); @@ -124,10 +120,9 @@ pub async fn update_tool_instance( instance_id: String, force: Option, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { let registry = registry_state.registry.lock().await; - registry + Ok(registry .update_instance(&instance_id, force.unwrap_or(false)) - .await - .map_err(|e| e.to_string()) + .await?) } diff --git a/src-tauri/src/commands/tool_commands/validation.rs b/src-tauri/src/commands/tool_commands/validation.rs index 14785c6..68121a4 100644 --- a/src-tauri/src/commands/tool_commands/validation.rs +++ b/src-tauri/src/commands/tool_commands/validation.rs @@ -1,3 +1,4 @@ +use crate::commands::error::AppResult; use crate::commands::tool_management::ToolRegistryState; use crate::commands::types::NodeEnvironment; use ::duckcoding::utils::platform::PlatformInfo; @@ -8,7 +9,7 @@ use std::os::windows::process::CommandExt; /// 检测 Node.js 和 npm 环境 #[tauri::command] -pub async fn check_node_environment() -> Result { +pub async fn check_node_environment() -> AppResult { let enhanced_path = PlatformInfo::current().build_enhanced_path(); let run_command = |cmd: &str| -> Result { #[cfg(target_os = "windows")] @@ -74,10 +75,7 @@ pub async fn validate_tool_path( _tool_id: String, path: String, registry_state: tauri::State<'_, ToolRegistryState>, -) -> Result { +) -> AppResult { let registry = registry_state.registry.lock().await; - registry - .validate_tool_path(&path) - .await - .map_err(|e| e.to_string()) + Ok(registry.validate_tool_path(&path).await?) } diff --git a/src-tauri/src/commands/window_commands.rs b/src-tauri/src/commands/window_commands.rs index 78725f1..1be9bd6 100644 --- a/src-tauri/src/commands/window_commands.rs +++ b/src-tauri/src/commands/window_commands.rs @@ -1,3 +1,4 @@ +use crate::commands::error::{AppError, AppResult}; use ::duckcoding::ui; use tauri::{Manager, WebviewWindow}; @@ -7,7 +8,7 @@ use tauri::{Manager, WebviewWindow}; /// - `window`: WebviewWindow 实例 /// - `action`: 关闭操作类型 ("minimize" 或 "quit") #[tauri::command] -pub fn handle_close_action(window: WebviewWindow, action: String) -> Result<(), String> { +pub fn handle_close_action(window: WebviewWindow, action: String) -> AppResult<()> { match action.as_str() { "minimize" => { // 隐藏到托盘 @@ -18,6 +19,9 @@ pub fn handle_close_action(window: WebviewWindow, action: String) -> Result<(), window.app_handle().exit(0); Ok(()) } - other => Err(format!("未知的关闭操作: {other}")), + other => Err(AppError::ValidationError { + field: "action".to_string(), + reason: format!("未知的关闭操作: {}", other), + }), } } diff --git a/src-tauri/src/core/error.rs b/src-tauri/src/core/error.rs index 0d933a4..bc8ad9c 100644 --- a/src-tauri/src/core/error.rs +++ b/src-tauri/src/core/error.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use thiserror::Error; /// 应用错误类型 @@ -7,6 +8,7 @@ use thiserror::Error; /// - 每个错误类型携带上下文信息 /// - 支持错误链(通过 #[from] 自动转换) /// - 易于扩展(新增枚举变体即可) +/// - 实现 Serialize 以支持 Tauri 命令(source 字段会转为字符串) #[derive(Debug, Error)] pub enum AppError { // ==================== 工具相关错误 ==================== @@ -326,3 +328,264 @@ macro_rules! ensure { } }; } + +// ==================== Serde 序列化实现 ==================== + +/// 自定义序列化实现,将 source 字段转换为字符串 +impl Serialize for AppError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + match self { + // 工具相关错误 + AppError::ToolNotFound { tool } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ToolNotFound")?; + state.serialize_field("tool", tool)?; + state.end() + } + AppError::ToolNotInstalled { tool } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ToolNotInstalled")?; + state.serialize_field("tool", tool)?; + state.end() + } + AppError::ToolAlreadyInstalled { tool, version } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "ToolAlreadyInstalled")?; + state.serialize_field("tool", tool)?; + state.serialize_field("version", version)?; + state.end() + } + AppError::InstallationFailed { tool, reason } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "InstallationFailed")?; + state.serialize_field("tool", tool)?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::VersionCheckFailed { tool, reason } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "VersionCheckFailed")?; + state.serialize_field("tool", tool)?; + state.serialize_field("reason", reason)?; + state.end() + } + + // 配置相关错误 + AppError::ConfigNotFound { path } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ConfigNotFound")?; + state.serialize_field("path", path)?; + state.end() + } + AppError::InvalidConfig { path, reason } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "InvalidConfig")?; + state.serialize_field("path", path)?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::ConfigReadError { path, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "ConfigReadError")?; + state.serialize_field("path", path)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::ConfigWriteError { path, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "ConfigWriteError")?; + state.serialize_field("path", path)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::ProfileNotFound { profile } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ProfileNotFound")?; + state.serialize_field("profile", profile)?; + state.end() + } + AppError::ProfileAlreadyExists { profile } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ProfileAlreadyExists")?; + state.serialize_field("profile", profile)?; + state.end() + } + + // 网络相关错误 + AppError::NetworkError { url, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "NetworkError")?; + state.serialize_field("url", url)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::ProxyConfigError { reason } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "ProxyConfigError")?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::ApiError { + endpoint, + status_code, + body, + } => { + let mut state = serializer.serialize_struct("AppError", 4)?; + state.serialize_field("type", "ApiError")?; + state.serialize_field("endpoint", endpoint)?; + state.serialize_field("status_code", status_code)?; + state.serialize_field("body", body)?; + state.end() + } + AppError::DownloadError { url, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "DownloadError")?; + state.serialize_field("url", url)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + + // 文件系统错误 + AppError::FileNotFound { path } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "FileNotFound")?; + state.serialize_field("path", path)?; + state.end() + } + AppError::DirCreationError { path, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "DirCreationError")?; + state.serialize_field("path", path)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::PermissionDenied { path, operation } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "PermissionDenied")?; + state.serialize_field("path", path)?; + state.serialize_field("operation", operation)?; + state.end() + } + + // 解析错误 + AppError::JsonParseError { context, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "JsonParseError")?; + state.serialize_field("context", context)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::TomlParseError { context, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "TomlParseError")?; + state.serialize_field("context", context)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::TomlSerializeError { context, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "TomlSerializeError")?; + state.serialize_field("context", context)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + + // 业务逻辑错误 + AppError::EnvironmentError { requirement } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "EnvironmentError")?; + state.serialize_field("requirement", requirement)?; + state.end() + } + AppError::ValidationError { field, reason } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "ValidationError")?; + state.serialize_field("field", field)?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::Timeout { + operation, + timeout_secs, + } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "Timeout")?; + state.serialize_field("operation", operation)?; + state.serialize_field("timeout_secs", timeout_secs)?; + state.end() + } + AppError::Unimplemented { feature, platform } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "Unimplemented")?; + state.serialize_field("feature", feature)?; + state.serialize_field("platform", platform)?; + state.end() + } + + // 更新相关错误 + AppError::UpdateCheckFailed { reason } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "UpdateCheckFailed")?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::UpdateDownloadFailed { version, source } => { + let mut state = serializer.serialize_struct("AppError", 3)?; + state.serialize_field("type", "UpdateDownloadFailed")?; + state.serialize_field("version", version)?; + state.serialize_field("error", &source.to_string())?; + state.end() + } + AppError::UpdateInstallFailed { reason } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "UpdateInstallFailed")?; + state.serialize_field("reason", reason)?; + state.end() + } + + // 认证相关错误 + AppError::InvalidApiKey => { + let mut state = serializer.serialize_struct("AppError", 1)?; + state.serialize_field("type", "InvalidApiKey")?; + state.end() + } + AppError::AuthenticationFailed { reason } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "AuthenticationFailed")?; + state.serialize_field("reason", reason)?; + state.end() + } + AppError::Forbidden { resource } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "Forbidden")?; + state.serialize_field("resource", resource)?; + state.end() + } + + // 通用错误 + AppError::Internal { message } => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "Internal")?; + state.serialize_field("message", message)?; + state.end() + } + AppError::Custom(msg) => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "Custom")?; + state.serialize_field("message", msg)?; + state.end() + } + AppError::Other(err) => { + let mut state = serializer.serialize_struct("AppError", 2)?; + state.serialize_field("type", "Other")?; + state.serialize_field("message", &err.to_string())?; + state.end() + } + } + } +} diff --git a/src-tauri/src/core/error_test.rs b/src-tauri/src/core/error_test.rs new file mode 100644 index 0000000..c0e33b4 --- /dev/null +++ b/src-tauri/src/core/error_test.rs @@ -0,0 +1,73 @@ +//! AppError 序列化测试 + +#[cfg(test)] +mod tests { + use crate::core::error::AppError; + use serde_json; + + #[test] + fn test_tool_not_found_serialization() { + let error = AppError::ToolNotFound { + tool: "claude-code".to_string(), + }; + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("ToolNotFound")); + assert!(json.contains("claude-code")); + } + + #[test] + fn test_config_read_error_serialization() { + let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); + let error = AppError::ConfigReadError { + path: "/test/path".to_string(), + source: io_error, + }; + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("ConfigReadError")); + assert!(json.contains("/test/path")); + assert!(json.contains("error")); // source 字段被转换为 error + } + + #[test] + fn test_custom_error_serialization() { + let error = AppError::Custom("测试错误信息".to_string()); + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("Custom")); + assert!(json.contains("测试错误信息")); + } + + #[test] + fn test_validation_error_serialization() { + let error = AppError::ValidationError { + field: "api_key".to_string(), + reason: "不能为空".to_string(), + }; + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("ValidationError")); + assert!(json.contains("api_key")); + assert!(json.contains("不能为空")); + } + + #[test] + fn test_json_parse_error_serialization() { + let json_error = serde_json::from_str::("invalid json").unwrap_err(); + let error = AppError::JsonParseError { + context: "测试上下文".to_string(), + source: json_error, + }; + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("JsonParseError")); + assert!(json.contains("测试上下文")); + assert!(json.contains("error")); // source 字段被转换为 error + } + + #[test] + fn test_profile_not_found_serialization() { + let error = AppError::ProfileNotFound { + profile: "my-profile".to_string(), + }; + let json = serde_json::to_string(&error).unwrap(); + assert!(json.contains("ProfileNotFound")); + assert!(json.contains("my-profile")); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 139bc07..b48ca8f 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -3,6 +3,9 @@ pub mod http; pub mod log_utils; pub mod logger; +#[cfg(test)] +mod error_test; + // 导出核心类型 pub use error::{AppError, AppResult, ErrorContext}; pub use http::{build_http_client, get_global_client}; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9c4ce57..1be6abb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -195,6 +195,10 @@ fn main() { registry: init_ctx.tool_registry, }; + let profile_manager_state = ProfileManagerState { + manager: init_ctx.profile_manager, + }; + // 判断单实例模式 let single_instance_enabled = determine_single_instance_mode(); @@ -209,6 +213,7 @@ fn main() { .manage(watcher_state) .manage(update_service_state) .manage(tool_registry_state) + .manage(profile_manager_state) .setup(|app| { setup_app_hooks(app)?; Ok(()) diff --git a/src-tauri/src/services/config/types.rs b/src-tauri/src/services/config/types.rs index 3a0c451..8dcc4af 100644 --- a/src-tauri/src/services/config/types.rs +++ b/src-tauri/src/services/config/types.rs @@ -23,6 +23,7 @@ pub struct ClaudeSettingsPayload { /// Gemini CLI 环境变量 Payload #[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] pub struct GeminiEnvPayload { pub api_key: String, pub base_url: String, diff --git a/src-tauri/src/services/session/manager.rs b/src-tauri/src/services/session/manager.rs index 29b8650..ba2d43b 100644 --- a/src-tauri/src/services/session/manager.rs +++ b/src-tauri/src/services/session/manager.rs @@ -362,6 +362,9 @@ impl SessionManager { pub fn shutdown_session_manager() { tracing::info!("SessionManager 关闭信号已发送"); CANCELLATION_TOKEN.cancel(); + + // 等待一小段时间让任务完成 + std::thread::sleep(std::time::Duration::from_millis(200)); } #[cfg(test)] diff --git a/src-tauri/src/setup/initialization.rs b/src-tauri/src/setup/initialization.rs index adf7696..1fa0e92 100644 --- a/src-tauri/src/setup/initialization.rs +++ b/src-tauri/src/setup/initialization.rs @@ -12,6 +12,7 @@ use tokio::sync::Mutex as TokioMutex; pub struct InitializationContext { pub proxy_manager: Arc, pub tool_registry: Arc>, + pub profile_manager: Arc>, } /// 初始化日志系统 @@ -120,7 +121,10 @@ async fn run_migrations() -> Result<(), Box> { } /// 自动启动配置的代理 -async fn auto_start_proxies(proxy_manager: &Arc) { +async fn auto_start_proxies( + proxy_manager: &Arc, + _profile_manager: &Arc>, +) { duckcoding::auto_start_proxies(proxy_manager).await; } @@ -142,15 +146,26 @@ pub async fn initialize_app() -> Result, @@ -46,4 +21,4 @@ const Button = React.forwardRef( ); Button.displayName = 'Button'; -export { Button, buttonVariants }; +export { Button }; diff --git a/src/hooks/theme-context.ts b/src/hooks/theme-context.ts new file mode 100644 index 0000000..62c5abb --- /dev/null +++ b/src/hooks/theme-context.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; + +export type Theme = 'light' | 'dark' | 'system'; + +export interface ThemeContextType { + theme: Theme; + actualTheme: 'light' | 'dark'; // 实际应用的主题(考虑系统设置) + setTheme: (theme: Theme) => void; +} + +export const ThemeContext = createContext(undefined); diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx index 0bd151a..32560b3 100644 --- a/src/hooks/useTheme.tsx +++ b/src/hooks/useTheme.tsx @@ -1,14 +1,5 @@ -import { createContext, useContext, useEffect, useState } from 'react'; - -type Theme = 'light' | 'dark' | 'system'; - -interface ThemeContextType { - theme: Theme; - actualTheme: 'light' | 'dark'; // 实际应用的主题(考虑系统设置) - setTheme: (theme: Theme) => void; -} - -const ThemeContext = createContext(undefined); +import { useEffect, useState } from 'react'; +import { Theme, ThemeContext } from './theme-context'; export function ThemeProvider({ children }: { children: React.ReactNode }) { // 从 localStorage 读取主题设置,默认为 system @@ -59,11 +50,3 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { ); } - -export function useTheme() { - const context = useContext(ThemeContext); - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider'); - } - return context; -} diff --git a/src/hooks/useThemeHook.ts b/src/hooks/useThemeHook.ts new file mode 100644 index 0000000..4770663 --- /dev/null +++ b/src/hooks/useThemeHook.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { ThemeContext, ThemeContextType } from './theme-context'; + +export function useTheme(): ThemeContextType { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/src/pages/SettingsPage/hooks/useSettingsForm.ts b/src/pages/SettingsPage/hooks/useSettingsForm.ts index f147f2d..4ed7468 100644 --- a/src/pages/SettingsPage/hooks/useSettingsForm.ts +++ b/src/pages/SettingsPage/hooks/useSettingsForm.ts @@ -113,7 +113,6 @@ export function useSettingsForm({ initialConfig, onConfigChange }: UseSettingsFo proxyUsername, proxyPassword, proxyBypassUrls, - globalConfig, onConfigChange, ]);