folders.rs多语言处理

This commit is contained in:
xintaofei
2026-03-07 15:59:14 +08:00
parent 6e5219cc10
commit 349845c137
8 changed files with 105 additions and 99 deletions

View File

@@ -136,13 +136,11 @@ pub async fn get_conversation(
AgentType::OpenCode => Box::new(OpenCodeParser::new()), AgentType::OpenCode => Box::new(OpenCodeParser::new()),
AgentType::Gemini => Box::new(GeminiParser::new()), AgentType::Gemini => Box::new(GeminiParser::new()),
_ => { _ => {
return Err( return Err(AppCommandError::new(
AppCommandError::new( AppErrorCode::InvalidInput,
AppErrorCode::InvalidInput, "Conversation parsing is not supported for this agent",
"Conversation parsing is not supported for this agent",
)
.with_detail(format!("agent_type={agent_type}")),
) )
.with_detail(format!("agent_type={agent_type}")))
} }
}; };
@@ -178,8 +176,11 @@ pub async fn get_stats() -> Result<AgentStats, AppCommandError> {
}) })
.await .await
.map_err(|e| { .map_err(|e| {
AppCommandError::new(AppErrorCode::Unknown, "Failed to compute conversation stats") AppCommandError::new(
.with_detail(e.to_string()) AppErrorCode::Unknown,
"Failed to compute conversation stats",
)
.with_detail(e.to_string())
})? })?
} }
@@ -347,13 +348,11 @@ pub async fn update_conversation_status(
conversation_id: i32, conversation_id: i32,
status: String, status: String,
) -> Result<(), AppCommandError> { ) -> Result<(), AppCommandError> {
let status_enum: conversation::ConversationStatus = serde_json::from_value( let status_enum: conversation::ConversationStatus =
serde_json::Value::String(status), serde_json::from_value(serde_json::Value::String(status)).map_err(|e| {
) AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation status")
.map_err(|e| { .with_detail(e.to_string())
AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation status") })?;
.with_detail(e.to_string())
})?;
conversation_service::update_status(&db.conn, conversation_id, status_enum) conversation_service::update_status(&db.conn, conversation_id, status_enum)
.await .await
.map_err(AppCommandError::from) .map_err(AppCommandError::from)
@@ -419,8 +418,7 @@ fn compute_stats(all_conversations: &[ConversationSummary]) -> AgentStats {
fn parse_error_to_app_error(error: ParseError) -> AppCommandError { fn parse_error_to_app_error(error: ParseError) -> AppCommandError {
match error { match error {
ParseError::ConversationNotFound(id) => { ParseError::ConversationNotFound(id) => {
AppCommandError::new(AppErrorCode::NotFound, "Conversation not found") AppCommandError::new(AppErrorCode::NotFound, "Conversation not found").with_detail(id)
.with_detail(id)
} }
ParseError::InvalidData(message) => { ParseError::InvalidData(message) => {
AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation data") AppCommandError::new(AppErrorCode::InvalidInput, "Invalid conversation data")
@@ -428,10 +426,11 @@ fn parse_error_to_app_error(error: ParseError) -> AppCommandError {
} }
ParseError::Io(err) => AppCommandError::new(AppErrorCode::IoError, "I/O operation failed") ParseError::Io(err) => AppCommandError::new(AppErrorCode::IoError, "I/O operation failed")
.with_detail(err.to_string()), .with_detail(err.to_string()),
ParseError::Json(err) => { ParseError::Json(err) => AppCommandError::new(
AppCommandError::new(AppErrorCode::InvalidInput, "Failed to parse conversation file") AppErrorCode::InvalidInput,
.with_detail(err.to_string()) "Failed to parse conversation file",
} )
.with_detail(err.to_string()),
ParseError::Db(err) => { ParseError::Db(err) => {
AppCommandError::new(AppErrorCode::DatabaseError, "Database operation failed") AppCommandError::new(AppErrorCode::DatabaseError, "Database operation failed")
.with_detail(err.to_string()) .with_detail(err.to_string())

View File

@@ -305,7 +305,7 @@ pub async fn set_folder_parent_branch(
db: tauri::State<'_, AppDatabase>, db: tauri::State<'_, AppDatabase>,
path: String, path: String,
parent_branch: Option<String>, parent_branch: Option<String>,
) -> Result<(), String> { ) -> Result<(), AppCommandError> {
// Find folder by path first // Find folder by path first
use crate::db::entities::folder; use crate::db::entities::folder;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
@@ -314,12 +314,15 @@ pub async fn set_folder_parent_branch(
.filter(folder::Column::DeletedAt.is_null()) .filter(folder::Column::DeletedAt.is_null())
.one(&db.conn) .one(&db.conn)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| {
AppCommandError::new(AppErrorCode::DatabaseError, "Failed to query folder")
.with_detail(e.to_string())
})?;
if let Some(folder_model) = row { if let Some(folder_model) = row {
folder_service::set_folder_parent_branch(&db.conn, folder_model.id, parent_branch) folder_service::set_folder_parent_branch(&db.conn, folder_model.id, parent_branch)
.await .await
.map_err(|e| e.to_string())?; .map_err(AppCommandError::from)?;
} }
Ok(()) Ok(())
} }
@@ -606,11 +609,10 @@ pub async fn git_worktree_add(
.await .await
.map_err(AppCommandError::io)?; .map_err(AppCommandError::io)?;
if check.status.success() { if check.status.success() {
return Err(AppCommandError::new( return Err(
AppErrorCode::AlreadyExists, AppCommandError::new(AppErrorCode::AlreadyExists, "Branch already exists")
"Branch already exists", .with_detail(branch_name),
) );
.with_detail(branch_name));
} }
// 校验目录是否已存在 // 校验目录是否已存在
@@ -810,10 +812,10 @@ pub async fn git_diff_with_branch(
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
return Err( return Err(AppCommandError::external_command(
AppCommandError::external_command("git diff failed", stderr) "git diff failed",
.with_detail(format!("branch={target_branch}")), format!("branch={target_branch}; {stderr}"),
); ));
} }
Ok(String::from_utf8_lossy(&output.stdout).to_string()) Ok(String::from_utf8_lossy(&output.stdout).to_string())
@@ -1104,7 +1106,10 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCom
} }
#[tauri::command] #[tauri::command]
pub async fn git_merge(path: String, branch_name: String) -> Result<GitMergeResult, AppCommandError> { pub async fn git_merge(
path: String,
branch_name: String,
) -> Result<GitMergeResult, AppCommandError> {
// Count commits to be merged before performing merge // Count commits to be merged before performing merge
let count_output = crate::process::tokio_command("git") let count_output = crate::process::tokio_command("git")
.args(["rev-list", "--count", &format!("HEAD..{}", branch_name)]) .args(["rev-list", "--count", &format!("HEAD..{}", branch_name)])
@@ -1289,9 +1294,11 @@ fn should_refresh_git_status_for_paths(root_display: &str, changed_paths: &[Stri
.any(|path| !ignored.contains(path.as_str())) .any(|path| !ignored.contains(path.as_str()))
} }
fn canonicalize_watch_root(root: &Path) -> Result<(PathBuf, String), String> { fn canonicalize_watch_root(root: &Path) -> Result<(PathBuf, String), AppCommandError> {
let canonical = std::fs::canonicalize(root) let canonical = std::fs::canonicalize(root).map_err(|e| {
.map_err(|e| format!("Unable to resolve workspace root: {e}"))?; AppCommandError::new(AppErrorCode::NotFound, "Unable to resolve workspace root")
.with_detail(e.to_string())
})?;
let key = normalize_slash_path(&canonical); let key = normalize_slash_path(&canonical);
Ok((canonical, key)) Ok((canonical, key))
} }
@@ -1560,18 +1567,27 @@ fn validate_new_name(new_name: &str) -> Result<&str, String> {
} }
#[tauri::command] #[tauri::command]
pub async fn start_file_tree_watch(app: tauri::AppHandle, root_path: String) -> Result<(), String> { pub async fn start_file_tree_watch(
app: tauri::AppHandle,
root_path: String,
) -> Result<(), AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
if !root.exists() || !root.is_dir() { if !root.exists() || !root.is_dir() {
return Err("Folder does not exist".to_string()); return Err(AppCommandError::new(
AppErrorCode::NotFound,
"Folder does not exist",
));
} }
let (root_canonical, key) = canonicalize_watch_root(&root)?; let (root_canonical, key) = canonicalize_watch_root(&root)?;
{ {
let mut watchers = FILE_WATCHERS let mut watchers = FILE_WATCHERS.lock().map_err(|_| {
.lock() AppCommandError::new(
.map_err(|_| "Failed to lock file watcher registry".to_string())?; AppErrorCode::Unknown,
"Failed to lock file watcher registry",
)
})?;
if let Some(entry) = watchers.get_mut(&key) { if let Some(entry) = watchers.get_mut(&key) {
entry.ref_count += 1; entry.ref_count += 1;
return Ok(()); return Ok(());
@@ -1606,19 +1622,30 @@ pub async fn start_file_tree_watch(app: tauri::AppHandle, root_path: String) ->
} }
}, },
) )
.map_err(|e| format!("Failed to create file watcher: {e}"))?, .map_err(|e| {
AppCommandError::new(AppErrorCode::IoError, "Failed to create file watcher")
.with_detail(e.to_string())
})?,
); );
watcher watcher
.as_mut() .as_mut()
.ok_or_else(|| "Failed to create file watcher".to_string())? .ok_or_else(|| {
AppCommandError::new(AppErrorCode::Unknown, "Failed to create file watcher")
})?
.watch(&root_canonical, RecursiveMode::Recursive) .watch(&root_canonical, RecursiveMode::Recursive)
.map_err(|e| format!("Failed to start file watcher: {e}"))?; .map_err(|e| {
AppCommandError::new(AppErrorCode::IoError, "Failed to start file watcher")
.with_detail(e.to_string())
})?;
let should_cleanup_new_watcher = { let should_cleanup_new_watcher = {
let mut watchers = FILE_WATCHERS let mut watchers = FILE_WATCHERS.lock().map_err(|_| {
.lock() AppCommandError::new(
.map_err(|_| "Failed to lock file watcher registry".to_string())?; AppErrorCode::Unknown,
"Failed to lock file watcher registry",
)
})?;
if let Some(entry) = watchers.get_mut(&key) { if let Some(entry) = watchers.get_mut(&key) {
entry.ref_count += 1; entry.ref_count += 1;
true true
@@ -1628,9 +1655,12 @@ pub async fn start_file_tree_watch(app: tauri::AppHandle, root_path: String) ->
FileWatchEntry { FileWatchEntry {
root_canonical, root_canonical,
root_display: root_path, root_display: root_path,
watcher: watcher watcher: watcher.take().ok_or_else(|| {
.take() AppCommandError::new(
.ok_or_else(|| "Failed to initialize file watcher state".to_string())?, AppErrorCode::Unknown,
"Failed to initialize file watcher state",
)
})?,
worker: worker.take(), worker: worker.take(),
ref_count: 1, ref_count: 1,
}, },
@@ -1652,15 +1682,18 @@ pub async fn start_file_tree_watch(app: tauri::AppHandle, root_path: String) ->
} }
#[tauri::command] #[tauri::command]
pub async fn stop_file_tree_watch(root_path: String) -> Result<(), String> { pub async fn stop_file_tree_watch(root_path: String) -> Result<(), AppCommandError> {
let root = PathBuf::from(&root_path); let root = PathBuf::from(&root_path);
let key = canonicalize_watch_root(&root) let key = canonicalize_watch_root(&root)
.map(|(_, key)| key) .map(|(_, key)| key)
.unwrap_or_else(|_| normalize_slash_path(&root)); .unwrap_or_else(|_| normalize_slash_path(&root));
let mut watchers = FILE_WATCHERS let mut watchers = FILE_WATCHERS.lock().map_err(|_| {
.lock() AppCommandError::new(
.map_err(|_| "Failed to lock file watcher registry".to_string())?; AppErrorCode::Unknown,
"Failed to lock file watcher registry",
)
})?;
let target_key = if watchers.contains_key(&key) { let target_key = if watchers.contains_key(&key) {
Some(key) Some(key)

View File

@@ -191,13 +191,11 @@ pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), A
} }
} }
} }
Err( Err(AppCommandError::new(
AppCommandError::new( AppErrorCode::NotFound,
AppErrorCode::NotFound, format!("No open window for folder {folder_id}"),
format!("No open window for folder {folder_id}"),
)
.with_detail(format!("folder_id={folder_id}")),
) )
.with_detail(format!("folder_id={folder_id}")))
} }
#[tauri::command] #[tauri::command]
@@ -251,9 +249,9 @@ pub async fn open_commit_window(
} }
state.set_owner(label.clone(), owner_label); state.set_owner(label.clone(), owner_label);
let _ = existing.unminimize(); let _ = existing.unminimize();
existing.set_focus().map_err(|e| { existing
AppCommandError::window("Failed to focus commit window", e.to_string()) .set_focus()
})?; .map_err(|e| AppCommandError::window("Failed to focus commit window", e.to_string()))?;
return Ok(()); return Ok(());
} }

View File

@@ -286,10 +286,7 @@ impl ClaudeParser {
} }
fn resolve_claude_config_dir() -> PathBuf { fn resolve_claude_config_dir() -> PathBuf {
resolve_claude_config_dir_from( resolve_claude_config_dir_from(std::env::var_os("CLAUDE_CONFIG_DIR"), dirs::home_dir())
std::env::var_os("CLAUDE_CONFIG_DIR"),
dirs::home_dir(),
)
} }
fn resolve_claude_config_dir_from( fn resolve_claude_config_dir_from(
@@ -974,10 +971,7 @@ mod tests {
#[test] #[test]
fn claude_config_dir_defaults_to_home_dot_claude() { fn claude_config_dir_defaults_to_home_dot_claude() {
let resolved = resolve_claude_config_dir_from( let resolved = resolve_claude_config_dir_from(None, Some(PathBuf::from("/Users/default")));
None,
Some(PathBuf::from("/Users/default")),
);
assert_eq!(resolved, PathBuf::from("/Users/default/.claude")); assert_eq!(resolved, PathBuf::from("/Users/default/.claude"));
} }
} }

View File

@@ -162,10 +162,7 @@ impl CodexParser {
} }
fn resolve_codex_home_dir() -> PathBuf { fn resolve_codex_home_dir() -> PathBuf {
resolve_codex_home_dir_from( resolve_codex_home_dir_from(std::env::var_os("CODEX_HOME"), dirs::home_dir())
std::env::var_os("CODEX_HOME"),
dirs::home_dir(),
)
} }
fn resolve_codex_home_dir_from( fn resolve_codex_home_dir_from(
@@ -1257,10 +1254,7 @@ mod tests {
#[test] #[test]
fn codex_home_defaults_to_home_dot_codex() { fn codex_home_defaults_to_home_dot_codex() {
let resolved = resolve_codex_home_dir_from( let resolved = resolve_codex_home_dir_from(None, Some(PathBuf::from("/Users/default")));
None,
Some(PathBuf::from("/Users/default")),
);
assert_eq!(resolved, PathBuf::from("/Users/default/.codex")); assert_eq!(resolved, PathBuf::from("/Users/default/.codex"));
} }
} }

View File

@@ -466,10 +466,7 @@ impl GeminiParser {
} }
fn resolve_gemini_base_dir() -> PathBuf { fn resolve_gemini_base_dir() -> PathBuf {
resolve_gemini_base_dir_from( resolve_gemini_base_dir_from(std::env::var_os("GEMINI_CLI_HOME"), dirs::home_dir())
std::env::var_os("GEMINI_CLI_HOME"),
dirs::home_dir(),
)
} }
fn resolve_gemini_base_dir_from( fn resolve_gemini_base_dir_from(
@@ -610,8 +607,8 @@ fn group_into_turns(messages: Vec<UnifiedMessage>) -> Vec<MessageTurn> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::GeminiParser;
use super::resolve_gemini_base_dir_from; use super::resolve_gemini_base_dir_from;
use super::GeminiParser;
use crate::parsers::AgentParser; use crate::parsers::AgentParser;
use std::env; use std::env;
use std::fs; use std::fs;
@@ -706,10 +703,7 @@ mod tests {
#[test] #[test]
fn gemini_defaults_to_home_dot_gemini() { fn gemini_defaults_to_home_dot_gemini() {
let resolved = resolve_gemini_base_dir_from( let resolved = resolve_gemini_base_dir_from(None, Some(PathBuf::from("/Users/default")));
None,
Some(PathBuf::from("/Users/default")),
);
assert_eq!(resolved, PathBuf::from("/Users/default/.gemini")); assert_eq!(resolved, PathBuf::from("/Users/default/.gemini"));
} }
} }

View File

@@ -251,8 +251,8 @@ mod tests {
use chrono::Utc; use chrono::Utc;
use super::{ use super::{
infer_context_window_max_tokens, latest_turn_total_usage_tokens, merge_context_window_stats, infer_context_window_max_tokens, latest_turn_total_usage_tokens,
path_eq_for_matching, merge_context_window_stats, path_eq_for_matching,
}; };
use crate::models::{MessageTurn, SessionStats, TurnRole, TurnUsage}; use crate::models::{MessageTurn, SessionStats, TurnRole, TurnUsage};

View File

@@ -634,13 +634,7 @@ mod tests {
#[test] #[test]
fn xdg_data_home_falls_back_to_home_local_share() { fn xdg_data_home_falls_back_to_home_local_share() {
let resolved = resolve_xdg_data_home( let resolved = resolve_xdg_data_home(None, Some(PathBuf::from("/Users/default")));
None, assert_eq!(resolved, Some(PathBuf::from("/Users/default/.local/share")));
Some(PathBuf::from("/Users/default")),
);
assert_eq!(
resolved,
Some(PathBuf::from("/Users/default/.local/share"))
);
} }
} }