Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src-tauri/src/commands/config_commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// 配置管理相关命令

use super::error::{AppError, AppResult};
use serde_json::Value;

use ::duckcoding::services::config::{
Expand Down Expand Up @@ -55,9 +56,10 @@ pub async fn get_external_changes() -> Result<Vec<ExternalConfigChange>, 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)?)
}

/// 将外部修改导入集中仓
Expand All @@ -66,9 +68,10 @@ pub async fn import_native_change(
tool: String,
profile: String,
as_new: bool,
) -> Result<ImportExternalChangeResult, String> {
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<ImportExternalChangeResult> {
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]
Expand Down
77 changes: 77 additions & 0 deletions src-tauri/src/commands/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Commands 层错误处理统一模块
//!
//! ## 使用指南
//!
//! ### 当前状态
//!
//! - Commands 层使用 `Result<T, String>` 作为返回类型(Tauri 要求)
//! - Services 层使用 `anyhow::Result<T>` 作为返回类型
//! - Core 层提供完善的 `AppError` 枚举和 `AppResult<T>` 类型别名
//!
//! ### 最佳实践(渐进式迁移)
//!
//! #### 方案 A:保持现状(推荐新手)
//!
//! ```rust
//! #[tauri::command]
//! pub async fn my_command() -> Result<Data, String> {
//! service::do_something().map_err(|e| e.to_string())
//! }
//! ```
//!
//! **优点**:简单直接,无需修改现有代码
//! **缺点**:错误信息丢失上下文
//!
//! #### 方案 B:使用 AppError(推荐专家)
//!
//! ```rust
//! use super::error::AppResult;
//!
//! #[tauri::command]
//! pub async fn my_command() -> AppResult<Data> {
//! let data = service::do_something()?; // anyhow::Result 自动转换
//! Ok(data)
//! }
//! ```
//!
//! **优点**:保留完整错误链,结构化错误信息
//! **缺点**:需要确保所有错误类型实现 `Into<AppError>`
//!
//! #### 方案 C:混合使用(推荐过渡期)
//!
//! ```rust
//! use super::error::AppError;
//!
//! #[tauri::command]
//! pub async fn my_command(id: String) -> Result<Data, String> {
//! 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};
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
146 changes: 87 additions & 59 deletions src-tauri/src/commands/profile_commands.rs
Original file line number Diff line number Diff line change
@@ -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<RwLock<::duckcoding::services::profile_manager::ProfileManager>>,
}

/// Profile 输入数据(前端传递)
#[derive(Debug, Deserialize)]
Expand All @@ -27,58 +34,63 @@ pub enum ProfileInput {

/// 列出所有 Profile 描述符
#[tauri::command]
pub async fn pm_list_all_profiles() -> Result<Vec<ProfileDescriptor>, 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<Vec<ProfileDescriptor>> {
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<Vec<String>, 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<Vec<String>> {
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<serde_json::Value, String> {
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<serde_json::Value> {
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)
}

/// 获取当前激活的 Profile(返回 JSON 供前端使用)
#[tauri::command]
pub async fn pm_get_active_profile(tool_id: String) -> Result<Option<serde_json::Value>, 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<Option<serde_json::Value>> {
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)
}
Expand All @@ -87,18 +99,22 @@ pub async fn pm_get_active_profile(tool_id: String) -> Result<Option<serde_json:
/// 保存 Profile(创建或更新)
#[tauri::command]
pub async fn pm_save_profile(
state: tauri::State<'_, ProfileManagerState>,
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" => {
Expand All @@ -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" => {
Expand All @@ -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<Option<String>, 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<Option<String>> {
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)?)
}
Loading
Loading