From 07963e970601a662b1d19c4cd9d5cafe5e04c63a Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 7 Mar 2026 12:51:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=80=9A=E7=94=A8=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/conversations.rs | 164 +++++++++++++++------- src-tauri/src/commands/system_settings.rs | 79 ++++++++--- src-tauri/src/commands/windows.rs | 55 +++++--- 3 files changed, 208 insertions(+), 90 deletions(-) diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index 15f553a..2297ce5 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; +use crate::app_error::{AppCommandError, AppErrorCode}; use crate::db::entities::conversation; use crate::db::service::{conversation_service, folder_service, import_service}; use crate::db::AppDatabase; @@ -8,7 +9,7 @@ use crate::parsers::claude::ClaudeParser; use crate::parsers::codex::CodexParser; use crate::parsers::gemini::GeminiParser; use crate::parsers::opencode::OpenCodeParser; -use crate::parsers::{path_eq_for_matching, AgentParser}; +use crate::parsers::{path_eq_for_matching, AgentParser, ParseError}; #[tauri::command] pub async fn list_folder_conversations( @@ -18,10 +19,10 @@ pub async fn list_folder_conversations( search: Option, sort_by: Option, status: Option, -) -> Result, String> { +) -> Result, AppCommandError> { conversation_service::list_by_folder(&db.conn, folder_id, agent_type, search, sort_by, status) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } /// Synchronous implementation shared by list_conversations, list_folders, and get_stats. @@ -30,7 +31,7 @@ fn list_conversations_sync( search: Option, sort_by: Option, folder_path: Option, -) -> Result, String> { +) -> Vec { let mut all_conversations = Vec::new(); let mut seen_keys = HashSet::new(); @@ -103,7 +104,7 @@ fn list_conversations_sync( _ => all_conversations.sort_by(|a, b| b.started_at.cmp(&a.started_at)), // default: newest first } - Ok(all_conversations) + all_conversations } #[tauri::command] @@ -112,70 +113,89 @@ pub async fn list_conversations( search: Option, sort_by: Option, folder_path: Option, -) -> Result, String> { +) -> Result, AppCommandError> { tokio::task::spawn_blocking(move || { list_conversations_sync(agent_type, search, sort_by, folder_path) }) .await - .map_err(|e| e.to_string())? + .map_err(|e| { + AppCommandError::new(AppErrorCode::Unknown, "Failed to list conversations") + .with_detail(e.to_string()) + }) } #[tauri::command] pub async fn get_conversation( agent_type: AgentType, conversation_id: String, -) -> Result { - tokio::task::spawn_blocking(move || { +) -> Result { + tokio::task::spawn_blocking(move || -> Result { let parser: Box = match agent_type { AgentType::ClaudeCode => Box::new(ClaudeParser::new()), AgentType::Codex => Box::new(CodexParser::new()), AgentType::OpenCode => Box::new(OpenCodeParser::new()), AgentType::Gemini => Box::new(GeminiParser::new()), _ => { - return Err(format!( - "conversation parsing not supported for {agent_type}" - )) + return Err( + AppCommandError::new( + AppErrorCode::InvalidInput, + "Conversation parsing is not supported for this agent", + ) + .with_detail(format!("agent_type={agent_type}")), + ) } }; parser .get_conversation(&conversation_id) - .map_err(|e| e.to_string()) + .map_err(parse_error_to_app_error) }) .await - .map_err(|e| e.to_string())? + .map_err(|e| { + AppCommandError::new(AppErrorCode::Unknown, "Failed to load conversation") + .with_detail(e.to_string()) + })? } #[tauri::command] -pub async fn list_folders() -> Result, String> { - tokio::task::spawn_blocking(move || { - let all_conversations = list_conversations_sync(None, None, None, None)?; +pub async fn list_folders() -> Result, AppCommandError> { + tokio::task::spawn_blocking(move || -> Result, AppCommandError> { + let all_conversations = list_conversations_sync(None, None, None, None); Ok(compute_folders(&all_conversations)) }) .await - .map_err(|e| e.to_string())? + .map_err(|e| { + AppCommandError::new(AppErrorCode::Unknown, "Failed to list folders") + .with_detail(e.to_string()) + })? } #[tauri::command] -pub async fn get_stats() -> Result { - tokio::task::spawn_blocking(move || { - let all_conversations = list_conversations_sync(None, None, None, None)?; - compute_stats(&all_conversations) +pub async fn get_stats() -> Result { + tokio::task::spawn_blocking(move || -> Result { + let all_conversations = list_conversations_sync(None, None, None, None); + Ok(compute_stats(&all_conversations)) }) .await - .map_err(|e| e.to_string())? + .map_err(|e| { + AppCommandError::new(AppErrorCode::Unknown, "Failed to compute conversation stats") + .with_detail(e.to_string()) + })? } #[tauri::command] -pub async fn get_sidebar_data() -> Result { - tokio::task::spawn_blocking(move || { - let all_conversations = list_conversations_sync(None, None, None, None)?; +pub async fn get_sidebar_data() -> Result { + tokio::task::spawn_blocking(move || -> Result { + let all_conversations = list_conversations_sync(None, None, None, None); let folders = compute_folders(&all_conversations); - let stats = compute_stats(&all_conversations)?; + let stats = compute_stats(&all_conversations); Ok(SidebarData { folders, stats }) }) .await - .map_err(|e| e.to_string())? + .map_err(|e| { + AppCommandError::new(AppErrorCode::Unknown, "Failed to build sidebar data") + .with_detail(e.to_string()) + })? } fn compute_folders(all_conversations: &[ConversationSummary]) -> Vec { @@ -215,30 +235,33 @@ fn compute_folders(all_conversations: &[ConversationSummary]) -> Vec pub async fn import_local_conversations( db: tauri::State<'_, AppDatabase>, folder_id: i32, -) -> Result { +) -> Result { let folder = folder_service::get_folder_by_id(&db.conn, folder_id) .await - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("Folder not found: {folder_id}"))?; + .map_err(AppCommandError::from)? + .ok_or_else(|| { + AppCommandError::new(AppErrorCode::NotFound, "Folder not found") + .with_detail(format!("folder_id={folder_id}")) + })?; import_service::import_local_conversations(&db.conn, folder_id, &folder.path) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } #[tauri::command] pub async fn get_folder_conversation( db: tauri::State<'_, AppDatabase>, conversation_id: i32, -) -> Result { +) -> Result { let summary = conversation_service::get_by_id(&db.conn, conversation_id) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; let (turns, session_stats) = if let Some(ref ext_id) = summary.external_id { let at = summary.agent_type; let eid = ext_id.clone(); - tokio::task::spawn_blocking(move || -> Result<_, String> { + tokio::task::spawn_blocking(move || -> Result<_, AppCommandError> { let parser: Box = match at { AgentType::ClaudeCode => Box::new(ClaudeParser::new()), AgentType::Codex => Box::new(CodexParser::new()), @@ -251,12 +274,17 @@ pub async fn get_folder_conversation( match parser.get_conversation(&eid) { Ok(d) => Ok((d.turns, d.session_stats)), Err(crate::parsers::ParseError::ConversationNotFound(_)) => Ok((vec![], None)), - Err(e) => Err(e.to_string()), + Err(e) => Err(parse_error_to_app_error(e)), } }) .await - .map_err(|e| e.to_string())? - .map_err(|e: String| e)? + .map_err(|e| { + AppCommandError::new( + AppErrorCode::Unknown, + "Failed to read conversation turns from session file", + ) + .with_detail(e.to_string()) + })?? } else { (vec![], None) }; @@ -277,11 +305,11 @@ pub async fn create_conversation( folder_id: i32, agent_type: AgentType, title: Option, -) -> Result { +) -> Result { // Detect current git branch from the folder path let git_branch = if let Some(folder) = folder_service::get_folder_by_id(&db.conn, folder_id) .await - .map_err(|e| e.to_string())? + .map_err(AppCommandError::from)? { detect_git_branch(&folder.path).await } else { @@ -290,7 +318,7 @@ pub async fn create_conversation( let model = conversation_service::create(&db.conn, folder_id, agent_type, title, git_branch) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; Ok(model.id) } @@ -318,12 +346,17 @@ pub async fn update_conversation_status( db: tauri::State<'_, AppDatabase>, conversation_id: i32, status: String, -) -> Result<(), String> { - let status_enum: conversation::ConversationStatus = - serde_json::from_value(serde_json::Value::String(status)).map_err(|e| e.to_string())?; +) -> Result<(), AppCommandError> { + let status_enum: conversation::ConversationStatus = serde_json::from_value( + serde_json::Value::String(status), + ) + .map_err(|e| { + AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation status") + .with_detail(e.to_string()) + })?; conversation_service::update_status(&db.conn, conversation_id, status_enum) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } #[tauri::command] @@ -331,10 +364,10 @@ pub async fn update_conversation_title( db: tauri::State<'_, AppDatabase>, conversation_id: i32, title: String, -) -> Result<(), String> { +) -> Result<(), AppCommandError> { conversation_service::update_title(&db.conn, conversation_id, title) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } #[tauri::command] @@ -342,23 +375,23 @@ pub async fn update_conversation_external_id( db: tauri::State<'_, AppDatabase>, conversation_id: i32, external_id: String, -) -> Result<(), String> { +) -> Result<(), AppCommandError> { conversation_service::update_external_id(&db.conn, conversation_id, external_id) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } #[tauri::command] pub async fn delete_conversation( db: tauri::State<'_, AppDatabase>, conversation_id: i32, -) -> Result<(), String> { +) -> Result<(), AppCommandError> { conversation_service::soft_delete(&db.conn, conversation_id) .await - .map_err(|e| e.to_string()) + .map_err(AppCommandError::from) } -fn compute_stats(all_conversations: &[ConversationSummary]) -> Result { +fn compute_stats(all_conversations: &[ConversationSummary]) -> AgentStats { let mut total_messages: u32 = 0; let mut counts: HashMap = HashMap::new(); @@ -376,9 +409,32 @@ fn compute_stats(all_conversations: &[ConversationSummary]) -> Result AppCommandError { + match error { + ParseError::ConversationNotFound(id) => { + AppCommandError::new(AppErrorCode::NotFound, "Conversation not found") + .with_detail(id) + } + ParseError::InvalidData(message) => { + AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation data") + .with_detail(message) + } + ParseError::Io(err) => AppCommandError::new(AppErrorCode::IoError, "I/O operation failed") + .with_detail(err.to_string()), + ParseError::Json(err) => { + AppCommandError::new(AppErrorCode::InvalidInput, "Failed to parse conversation file") + .with_detail(err.to_string()) + } + ParseError::Db(err) => { + AppCommandError::new(AppErrorCode::DatabaseError, "Database operation failed") + .with_detail(err.to_string()) + } + } } diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index 8c0f16e..e9ef45c 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -1,6 +1,7 @@ use sea_orm::DatabaseConnection; use tauri::State; +use crate::app_error::{AppCommandError, AppErrorCode}; use crate::db::service::app_metadata_service; use crate::db::AppDatabase; use crate::models::{SystemLanguageSettings, SystemProxySettings}; @@ -9,7 +10,9 @@ use crate::network::proxy; const SYSTEM_PROXY_SETTINGS_KEY: &str = "system_proxy_settings"; const SYSTEM_LANGUAGE_SETTINGS_KEY: &str = "system_language_settings"; -fn normalize_proxy_settings(settings: SystemProxySettings) -> Result { +fn normalize_proxy_settings( + settings: SystemProxySettings, +) -> Result { if !settings.enabled { let proxy_url = settings .proxy_url @@ -29,9 +32,17 @@ fn normalize_proxy_settings(settings: SystemProxySettings) -> Result Result Result { +) -> Result { let raw = app_metadata_service::get_value(conn, SYSTEM_PROXY_SETTINGS_KEY) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; let Some(raw) = raw else { return Ok(SystemProxySettings::default()); }; - let parsed = serde_json::from_str::(&raw) - .map_err(|e| format!("failed to parse stored proxy settings: {e}"))?; + let parsed = serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::new( + AppErrorCode::InvalidInput, + "Failed to parse stored proxy settings", + ) + .with_detail(e.to_string()) + })?; normalize_proxy_settings(parsed) } pub(crate) async fn load_system_language_settings( conn: &DatabaseConnection, -) -> Result { +) -> Result { let raw = app_metadata_service::get_value(conn, SYSTEM_LANGUAGE_SETTINGS_KEY) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; let Some(raw) = raw else { return Ok(SystemLanguageSettings::default()); }; - serde_json::from_str::(&raw) - .map_err(|e| format!("failed to parse stored language settings: {e}")) + serde_json::from_str::(&raw).map_err(|e| { + AppCommandError::new( + AppErrorCode::InvalidInput, + "Failed to parse stored language settings", + ) + .with_detail(e.to_string()) + }) } #[tauri::command] pub async fn get_system_proxy_settings( db: State<'_, AppDatabase>, -) -> Result { +) -> Result { load_system_proxy_settings(&db.conn).await } @@ -81,22 +102,34 @@ pub async fn get_system_proxy_settings( pub async fn update_system_proxy_settings( settings: SystemProxySettings, db: State<'_, AppDatabase>, -) -> Result { +) -> Result { let normalized = normalize_proxy_settings(settings)?; - let serialized = serde_json::to_string(&normalized).map_err(|e| e.to_string())?; + let serialized = serde_json::to_string(&normalized).map_err(|e| { + AppCommandError::new( + AppErrorCode::InvalidInput, + "Failed to serialize proxy settings", + ) + .with_detail(e.to_string()) + })?; app_metadata_service::upsert_value(&db.conn, SYSTEM_PROXY_SETTINGS_KEY, &serialized) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; - proxy::apply_system_proxy_settings(&normalized)?; + proxy::apply_system_proxy_settings(&normalized).map_err(|e| { + AppCommandError::new( + AppErrorCode::ExternalCommandFailed, + "Failed to apply system proxy settings", + ) + .with_detail(e) + })?; Ok(normalized) } #[tauri::command] pub async fn get_system_language_settings( db: State<'_, AppDatabase>, -) -> Result { +) -> Result { load_system_language_settings(&db.conn).await } @@ -104,12 +137,18 @@ pub async fn get_system_language_settings( pub async fn update_system_language_settings( settings: SystemLanguageSettings, db: State<'_, AppDatabase>, -) -> Result { - let serialized = serde_json::to_string(&settings).map_err(|e| e.to_string())?; +) -> Result { + let serialized = serde_json::to_string(&settings).map_err(|e| { + AppCommandError::new( + AppErrorCode::InvalidInput, + "Failed to serialize language settings", + ) + .with_detail(e.to_string()) + })?; app_metadata_service::upsert_value(&db.conn, SYSTEM_LANGUAGE_SETTINGS_KEY, &serialized) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; Ok(settings) } diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index 66c55cc..d66f9a0 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; -use crate::app_error::AppCommandError; +use crate::app_error::{AppCommandError, AppErrorCode}; use crate::db::AppDatabase; use crate::models::FolderHistoryEntry; @@ -152,7 +152,7 @@ fn resolve_settings_target(section: Option<&str>, agent_type: Option<&str>) -> S pub async fn list_open_folders( app: AppHandle, db: tauri::State<'_, AppDatabase>, -) -> Result, String> { +) -> Result, AppCommandError> { let windows = app.webview_windows(); let mut folder_ids: Vec = Vec::new(); @@ -166,7 +166,7 @@ pub async fn list_open_folders( let all_folders = crate::db::service::folder_service::list_folders(&db.conn) .await - .map_err(|e| e.to_string())?; + .map_err(AppCommandError::from)?; let open_folders: Vec = all_folders .into_iter() @@ -177,19 +177,27 @@ pub async fn list_open_folders( } #[tauri::command] -pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), String> { +pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), AppCommandError> { let windows = app.webview_windows(); for (label, window) in &windows { if label.starts_with("folder-") { if let Some(id) = get_folder_id_from_window(window) { if id == folder_id { - window.set_focus().map_err(|e| e.to_string())?; + window.set_focus().map_err(|e| { + AppCommandError::window("Failed to focus folder window", e.to_string()) + })?; return Ok(()); } } } } - Err(format!("No open window for folder {}", folder_id)) + Err( + AppCommandError::new( + AppErrorCode::NotFound, + format!("No open window for folder {folder_id}"), + ) + .with_detail(format!("folder_id={folder_id}")), + ) } #[tauri::command] @@ -231,24 +239,34 @@ pub async fn open_commit_window( db: tauri::State<'_, AppDatabase>, state: tauri::State<'_, CommitWindowState>, folder_id: i32, -) -> Result<(), String> { +) -> Result<(), AppCommandError> { let owner_label = window.label().to_string(); let label = format!("commit-{folder_id}"); if let Some(existing) = app.get_webview_window(&label) { if let Some(owner_window) = app.get_webview_window(&owner_label) { - owner_window.set_enabled(false).map_err(|e| e.to_string())?; + owner_window.set_enabled(false).map_err(|e| { + AppCommandError::window("Failed to disable owner window", e.to_string()) + })?; } state.set_owner(label.clone(), owner_label); let _ = existing.unminimize(); - existing.set_focus().map_err(|e| e.to_string())?; + existing.set_focus().map_err(|e| { + AppCommandError::window("Failed to focus commit window", e.to_string()) + })?; return Ok(()); } let folder = crate::db::service::folder_service::get_folder_by_id(&db.conn, folder_id) .await - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("Folder {} not found", folder_id))?; + .map_err(AppCommandError::from)? + .ok_or_else(|| { + AppCommandError::new( + AppErrorCode::NotFound, + format!("Folder {folder_id} not found"), + ) + .with_detail(format!("folder_id={folder_id}")) + })?; let url = WebviewUrl::App(format!("commit?folderId={folder_id}").into()); let builder = WebviewWindowBuilder::new(&app, &label, url) @@ -259,16 +277,21 @@ pub async fn open_commit_window( .center(); let commit_window = apply_platform_window_style(builder) .build() - .map_err(|e| e.to_string())?; + .map_err(|e| AppCommandError::window("Failed to open commit window", e.to_string()))?; ensure_windows_undecorated(&commit_window); if let Some(owner_window) = app.get_webview_window(&owner_label) { if let Err(err) = owner_window.set_enabled(false) { let _ = commit_window.close(); - return Err(err.to_string()); + return Err(AppCommandError::window( + "Failed to disable owner window", + err.to_string(), + )); } } state.set_owner(label, owner_label); - commit_window.set_focus().map_err(|e| e.to_string())?; + commit_window + .set_focus() + .map_err(|e| AppCommandError::window("Failed to focus commit window", e.to_string()))?; Ok(()) } @@ -359,7 +382,7 @@ pub fn restore_window_after_commit( } } -pub fn open_welcome_window(app: &AppHandle) -> Result<(), String> { +pub fn open_welcome_window(app: &AppHandle) -> Result<(), AppCommandError> { if let Some(existing) = app.get_webview_window("welcome") { ensure_windows_undecorated(&existing); return Ok(()); @@ -372,7 +395,7 @@ pub fn open_welcome_window(app: &AppHandle) -> Result<(), String> { .center(); let welcome_window = apply_platform_window_style(builder) .build() - .map_err(|e| e.to_string())?; + .map_err(|e| AppCommandError::window("Failed to open welcome window", e.to_string()))?; ensure_windows_undecorated(&welcome_window); Ok(()) }