refactor(backend): phase 1 - unified error handling with thiserror

Introduce AppError enum to replace Result<T, String> pattern across
the codebase, improving error context preservation and type safety.

## Changes

### Core Infrastructure
- Add src/error.rs with AppError enum using thiserror
- Add thiserror dependency to Cargo.toml
- Implement helper functions: io(), json(), toml() for ergonomic error creation
- Implement From<PoisonError> for automatic lock error conversion
- Implement From<AppError> for String to maintain Tauri command compatibility

### Module Migrations (60% complete)
- config.rs: Full migration to AppError
  - read_json_file, write_json_file, atomic_write
  - archive_file, copy_file, delete_file
- claude_mcp.rs: Full migration to AppError
  - get_mcp_status, read_mcp_json, upsert_mcp_server
  - delete_mcp_server, validate_command_in_path
  - set_mcp_servers_map
- codex_config.rs: Full migration to AppError
  - write_codex_live_atomic with rollback support
  - read_and_validate_codex_config_text
  - validate_config_toml
- app_config.rs: Partial migration
  - MultiAppConfig::load, MultiAppConfig::save
- store.rs: Partial migration
  - AppState::save now returns Result<(), AppError>
- commands.rs: Minimal changes
  - Use .map_err(Into::into) for compatibility
- mcp.rs: Minimal changes
  - sync_enabled_to_claude uses Into::into conversion

### Documentation
- Add docs/BACKEND_REFACTOR_PLAN.md with detailed refactoring roadmap

## Benefits
- Type-safe error handling with preserved error chains
- Better error messages with file paths and context
- Reduced boilerplate code (118 Result<T, String> instances to migrate)
- Automatic error conversion for seamless integration

## Testing
- All existing tests pass (4/4)
- Compilation successful with no warnings
- Build time: 0.61s (no performance regression)

## Remaining Work
- claude_plugin.rs (7 functions)
- migration.rs, import_export.rs
- Add unit tests for error.rs
- Complete commands.rs migration after dependent modules

Co-authored-by: Claude <claude@anthropic.com>
This commit is contained in:
Jason
2025-10-27 16:29:11 +08:00
parent bfab1d0ccb
commit c01e495eea
12 changed files with 352 additions and 114 deletions

View File

@@ -1,9 +1,10 @@
use serde::{Deserialize, Serialize};
// unused import removed
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::error::AppError;
/// 获取 Claude Code 配置目录路径
pub fn get_claude_config_dir() -> PathBuf {
if let Some(custom) = crate::settings::get_claude_override_dir() {
@@ -106,14 +107,14 @@ fn ensure_unique_path(dest: PathBuf) -> PathBuf {
}
/// 将现有文件归档到 `~/.cc-switch/archive/<ts>/<category>/` 下,返回归档路径
pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result<Option<PathBuf>, String> {
pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result<Option<PathBuf>, AppError> {
if !src.exists() {
return Ok(None);
}
let mut dest_dir = get_archive_root();
dest_dir.push(ts.to_string());
dest_dir.push(category);
fs::create_dir_all(&dest_dir).map_err(|e| format!("创建归档目录失败: {}", e))?;
fs::create_dir_all(&dest_dir).map_err(|e| AppError::io(&dest_dir, e))?;
let file_name = src
.file_name()
@@ -147,52 +148,53 @@ pub fn get_provider_config_path(provider_id: &str, provider_name: Option<&str>)
}
/// 读取 JSON 配置文件
pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, String> {
pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, AppError> {
if !path.exists() {
return Err(format!("文件不存在: {}", path.display()));
return Err(AppError::Config(format!(
"文件不存在: {}",
path.display()
)));
}
let content =
fs::read_to_string(path).map_err(|e| format!("读取文件失败: {}: {}", path.display(), e))?;
let content = fs::read_to_string(path).map_err(|e| AppError::io(path, e))?;
serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {}: {}", path.display(), e))
serde_json::from_str(&content).map_err(|e| AppError::json(path, e))
}
/// 写入 JSON 配置文件
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String> {
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), AppError> {
// 确保目录存在
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let json =
serde_json::to_string_pretty(data).map_err(|e| format!("序列化 JSON 失败: {}", e))?;
let json = serde_json::to_string_pretty(data)
.map_err(|e| AppError::JsonSerialize { source: e })?;
atomic_write(path, json.as_bytes())
}
/// 原子写入文本文件(用于 TOML/纯文本)
pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> {
pub fn write_text_file(path: &Path, data: &str) -> Result<(), AppError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
atomic_write(path, data.as_bytes())
}
/// 原子写入:写入临时文件后 rename 替换,避免半写状态
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let parent = path.parent().ok_or_else(|| "无效的路径".to_string())?;
let parent = path
.parent()
.ok_or_else(|| AppError::Config("无效的路径".to_string()))?;
let mut tmp = parent.to_path_buf();
let file_name = path
.file_name()
.ok_or_else(|| "无效的文件名".to_string())?
.ok_or_else(|| AppError::Config("无效的文件名".to_string()))?
.to_string_lossy()
.to_string();
let ts = std::time::SystemTime::now()
@@ -202,12 +204,10 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
tmp.push(format!("{}.tmp.{}", file_name, ts));
{
let mut f = fs::File::create(&tmp)
.map_err(|e| format!("创建临时文件失败: {}: {}", tmp.display(), e))?;
f.write_all(data)
.map_err(|e| format!("写入临时文件失败: {}: {}", tmp.display(), e))?;
f.flush()
.map_err(|e| format!("刷新临时文件失败: {}: {}", tmp.display(), e))?;
let mut f =
fs::File::create(&tmp).map_err(|e| AppError::io(&tmp, e))?;
f.write_all(data).map_err(|e| AppError::io(&tmp, e))?;
f.flush().map_err(|e| AppError::io(&tmp, e))?;
}
#[cfg(unix)]
@@ -225,25 +225,25 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
if path.exists() {
let _ = fs::remove_file(path);
}
fs::rename(&tmp, path).map_err(|e| {
format!(
"原子替换失败: {} -> {}: {}",
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
context: format!(
"原子替换失败: {} -> {}",
tmp.display(),
path.display(),
e
)
path.display()
),
source: e,
})?;
}
#[cfg(not(windows))]
{
fs::rename(&tmp, path).map_err(|e| {
format!(
"原子替换失败: {} -> {}: {}",
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
context: format!(
"原子替换失败: {} -> {}",
tmp.display(),
path.display(),
e
)
path.display()
),
source: e,
})?;
}
Ok(())
@@ -285,15 +285,22 @@ mod tests {
}
/// 复制文件
pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> {
fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?;
pub fn copy_file(from: &Path, to: &Path) -> Result<(), AppError> {
fs::copy(from, to).map_err(|e| AppError::IoContext {
context: format!(
"复制文件失败 ({} -> {})",
from.display(),
to.display()
),
source: e,
})?;
Ok(())
}
/// 删除文件
pub fn delete_file(path: &Path) -> Result<(), String> {
pub fn delete_file(path: &Path) -> Result<(), AppError> {
if path.exists() {
fs::remove_file(path).map_err(|e| format!("删除文件失败: {}", e))?;
fs::remove_file(path).map_err(|e| AppError::io(path, e))?;
}
Ok(())
}