refactor(backend): complete phase 1 - full AppError migration (100%)

Finalized the backend error handling refactoring by migrating all remaining
modules to use AppError, eliminating all temporary error conversions.

## Changes

### Fully Migrated Modules

- **mcp.rs** (129 lines changed)
  - Migrated 13 functions from Result<T, String> to Result<T, AppError>
  - Added AppError::McpValidation for domain-specific validation errors
  - Functions: validate_server_spec, validate_mcp_entry, upsert_in_config_for,
    delete_in_config_for, set_enabled_and_sync_for, sync_enabled_to_claude,
    import_from_claude, import_from_codex, sync_enabled_to_codex
  - Removed all temporary error conversions

- **usage_script.rs** (143 lines changed)
  - Migrated 4 functions: execute_usage_script, send_http_request,
    validate_result, validate_single_usage
  - Used AppError::Message for JS runtime errors
  - Used AppError::InvalidInput for script validation errors
  - Improved error construction with ok_or_else (lazy evaluation)

- **lib.rs** (47 lines changed)
  - Migrated create_tray_menu() and switch_provider_internal()
  - Simplified PoisonError handling with AppError::from
  - Added error logging in update_tray_menu()
  - Improved error handling in menu update logic

- **migration.rs** (10 lines changed)
  - Migrated migrate_copies_into_config()
  - Used AppError::io() helper for file operations

- **speedtest.rs** (8 lines changed)
  - Migrated build_client() and test_endpoints()
  - Used AppError::Message for HTTP client errors

- **app_store.rs** (14 lines changed)
  - Migrated set_app_config_dir_to_store() and migrate_app_config_dir_from_settings()
  - Used AppError::Message for Tauri Store errors
  - Used AppError::io() for file system operations

### Fixed Previous Temporary Solutions

- **import_export.rs** (2 lines changed)
  - Removed AppError::Message wrapper for mcp::sync_enabled_to_codex
  - Now directly calls the AppError-returning function (no conversion needed)

- **commands.rs** (6 lines changed)
  - Updated query_provider_usage() and test_api_endpoints()
  - Explicit .to_string() conversion for Tauri command interface

## New Error Types

- **AppError::McpValidation**: Domain-specific error for MCP configuration validation
  - Separates MCP validation errors from generic Config errors
  - Follows domain-driven design principles

## Statistics

- Files changed: 8
- Lines changed: +237/-122 (net +115)
- Compilation:  Success (7.13s, 0 warnings)
- Tests:  4/4 passed

## Benefits

- **100% Migration**: All modules now use AppError consistently
- **Domain Errors**: Added McpValidation for better error categorization
- **No Temporary Solutions**: Eliminated all AppError::Message conversions for internal calls
- **Performance**: Used ok_or_else for lazy error construction
- **Maintainability**: Removed ~60 instances of .map_err(|e| format!("...", e))
- **Debugging**: Added error logging in critical paths (tray menu updates)

## Phase 1 Complete

Total impact across 3 commits:
- 25 files changed
- +671/-302 lines (net +369)
- 100% of codebase migrated from Result<T, String> to Result<T, AppError>
- 0 compilation warnings
- All tests passing

Ready for Phase 2: Splitting commands.rs by domain.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jason
2025-10-27 20:36:08 +08:00
parent 1cc0e4bc8d
commit 4aa9512e36
8 changed files with 237 additions and 122 deletions

View File

@@ -4,13 +4,15 @@ use serde_json::Value;
use std::collections::HashMap;
use std::time::Duration;
use crate::error::AppError;
/// 执行用量查询脚本
pub async fn execute_usage_script(
script_code: &str,
api_key: &str,
base_url: &str,
timeout_secs: u64,
) -> Result<Value, String> {
) -> Result<Value, AppError> {
// 1. 替换变量
let replaced = script_code
.replace("{{apiKey}}", api_key)
@@ -18,75 +20,80 @@ pub async fn execute_usage_script(
// 2. 在独立作用域中提取 request 配置(确保 Runtime/Context 在 await 前释放)
let request_config = {
let runtime = Runtime::new().map_err(|e| format!("创建 JS 运行时失败: {}", e))?;
let context = Context::full(&runtime).map_err(|e| format!("创建 JS 上下文失败: {}", e))?;
let runtime = Runtime::new()
.map_err(|e| AppError::Message(format!("创建 JS 运行时失败: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::Message(format!("创建 JS 上下文失败: {}", e)))?;
context.with(|ctx| {
// 执行用户代码,获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| format!("解析配置失败: {}", e))?;
.map_err(|e| AppError::Message(format!("解析配置失败: {}", e)))?;
// 提取 request 配置
let request: rquickjs::Object = config
.get("request")
.map_err(|e| format!("缺少 request 配置: {}", e))?;
.map_err(|e| AppError::Message(format!("缺少 request 配置: {}", e)))?;
// 将 request 转换为 JSON 字符串
let request_json: String = ctx
.json_stringify(request)
.map_err(|e| format!("序列化 request 失败: {}", e))?
.ok_or("序列化返回 None")?
.map_err(|e| AppError::Message(format!("序列化 request 失败: {}", e)))?
.ok_or_else(|| AppError::Message("序列化返回 None".into()))?
.get()
.map_err(|e| format!("获取字符串失败: {}", e))?;
.map_err(|e| AppError::Message(format!("获取字符串失败: {}", e)))?;
Ok::<_, String>(request_json)
Ok::<_, AppError>(request_json)
})?
}; // Runtime 和 Context 在这里被 drop
// 3. 解析 request 配置
let request: RequestConfig = serde_json::from_str(&request_config)
.map_err(|e| format!("request 配置格式错误: {}", e))?;
.map_err(|e| AppError::Message(format!("request 配置格式错误: {}", e)))?;
// 4. 发送 HTTP 请求
let response_data = send_http_request(&request, timeout_secs).await?;
// 5. 在独立作用域中执行 extractor确保 Runtime/Context 在函数结束前释放)
let result: Value = {
let runtime = Runtime::new().map_err(|e| format!("创建 JS 运行时失败: {}", e))?;
let context = Context::full(&runtime).map_err(|e| format!("创建 JS 上下文失败: {}", e))?;
let runtime = Runtime::new()
.map_err(|e| AppError::Message(format!("创建 JS 运行时失败: {}", e)))?;
let context = Context::full(&runtime)
.map_err(|e| AppError::Message(format!("创建 JS 上下文失败: {}", e)))?;
context.with(|ctx| {
// 重新 eval 获取配置对象
let config: rquickjs::Object = ctx
.eval(replaced.clone())
.map_err(|e| format!("重新解析配置失败: {}", e))?;
.map_err(|e| AppError::Message(format!("重新解析配置失败: {}", e)))?;
// 提取 extractor 函数
let extractor: Function = config
.get("extractor")
.map_err(|e| format!("缺少 extractor 函数: {}", e))?;
.map_err(|e| AppError::Message(format!("缺少 extractor 函数: {}", e)))?;
// 将响应数据转换为 JS 值
let response_js: rquickjs::Value = ctx
.json_parse(response_data.as_str())
.map_err(|e| format!("解析响应 JSON 失败: {}", e))?;
.map_err(|e| AppError::Message(format!("解析响应 JSON 失败: {}", e)))?;
// 调用 extractor(response)
let result_js: rquickjs::Value = extractor
.call((response_js,))
.map_err(|e| format!("执行 extractor 失败: {}", e))?;
.map_err(|e| AppError::Message(format!("执行 extractor 失败: {}", e)))?;
// 转换为 JSON 字符串
let result_json: String = ctx
.json_stringify(result_js)
.map_err(|e| format!("序列化结果失败: {}", e))?
.ok_or("序列化返回 None")?
.map_err(|e| AppError::Message(format!("序列化结果失败: {}", e)))?
.ok_or_else(|| AppError::Message("序列化返回 None".into()))?
.get()
.map_err(|e| format!("获取字符串失败: {}", e))?;
.map_err(|e| AppError::Message(format!("获取字符串失败: {}", e)))?;
// 解析为 serde_json::Value
serde_json::from_str(&result_json).map_err(|e| format!("JSON 解析失败: {}", e))
serde_json::from_str(&result_json)
.map_err(|e| AppError::Message(format!("JSON 解析失败: {}", e)))
})?
}; // Runtime 和 Context 在这里被 drop
@@ -108,11 +115,11 @@ struct RequestConfig {
}
/// 发送 HTTP 请求
async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<String, String> {
async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<String, AppError> {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(|e| format!("创建客户端失败: {}", e))?;
.map_err(|e| AppError::Message(format!("创建客户端失败: {}", e)))?;
let method = config
.method
@@ -135,13 +142,13 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
let resp = req
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
.map_err(|e| AppError::Message(format!("请求失败: {}", e)))?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| format!("读取响应失败: {}", e))?;
.map_err(|e| AppError::Message(format!("读取响应失败: {}", e)))?;
if !status.is_success() {
let preview = if text.len() > 200 {
@@ -149,22 +156,24 @@ async fn send_http_request(config: &RequestConfig, timeout_secs: u64) -> Result<
} else {
text.clone()
};
return Err(format!("HTTP {} : {}", status, preview));
return Err(AppError::Message(format!("HTTP {} : {}", status, preview)));
}
Ok(text)
}
/// 验证脚本返回值(支持单对象或数组)
fn validate_result(result: &Value) -> Result<(), String> {
fn validate_result(result: &Value) -> Result<(), AppError> {
// 如果是数组,验证每个元素
if let Some(arr) = result.as_array() {
if arr.is_empty() {
return Err("脚本返回的数组不能为空".to_string());
return Err(AppError::InvalidInput("脚本返回的数组不能为空".into()));
}
for (idx, item) in arr.iter().enumerate() {
validate_single_usage(item)
.map_err(|e| format!("数组索引[{}]验证失败: {}", idx, e))?;
.map_err(|e| {
AppError::InvalidInput(format!("数组索引[{}]验证失败: {}", idx, e))
})?;
}
return Ok(());
}
@@ -174,33 +183,75 @@ fn validate_result(result: &Value) -> Result<(), String> {
}
/// 验证单个用量数据对象
fn validate_single_usage(result: &Value) -> Result<(), String> {
let obj = result.as_object().ok_or("脚本必须返回对象或对象数组")?;
fn validate_single_usage(result: &Value) -> Result<(), AppError> {
let obj = result.as_object().ok_or_else(|| {
AppError::InvalidInput("脚本必须返回对象或对象数组".into())
})?;
// 所有字段均为可选,只进行类型检查
if obj.contains_key("isValid") && !result["isValid"].is_null() && !result["isValid"].is_boolean() {
return Err("isValid 必须是布尔值或 null".to_string());
if obj.contains_key("isValid")
&& !result["isValid"].is_null()
&& !result["isValid"].is_boolean()
{
return Err(AppError::InvalidInput(
"isValid 必须是布尔值或 null".into(),
));
}
if obj.contains_key("invalidMessage") && !result["invalidMessage"].is_null() && !result["invalidMessage"].is_string() {
return Err("invalidMessage 必须是字符串或 null".to_string());
if obj.contains_key("invalidMessage")
&& !result["invalidMessage"].is_null()
&& !result["invalidMessage"].is_string()
{
return Err(AppError::InvalidInput(
"invalidMessage 必须是字符串或 null".into(),
));
}
if obj.contains_key("remaining") && !result["remaining"].is_null() && !result["remaining"].is_number() {
return Err("remaining 必须是数字或 null".to_string());
if obj.contains_key("remaining")
&& !result["remaining"].is_null()
&& !result["remaining"].is_number()
{
return Err(AppError::InvalidInput(
"remaining 必须是数字或 null".into(),
));
}
if obj.contains_key("unit") && !result["unit"].is_null() && !result["unit"].is_string() {
return Err("unit 必须是字符串或 null".to_string());
if obj.contains_key("unit")
&& !result["unit"].is_null()
&& !result["unit"].is_string()
{
return Err(AppError::InvalidInput(
"unit 必须是字符串或 null".into(),
));
}
if obj.contains_key("total") && !result["total"].is_null() && !result["total"].is_number() {
return Err("total 必须是数字或 null".to_string());
if obj.contains_key("total")
&& !result["total"].is_null()
&& !result["total"].is_number()
{
return Err(AppError::InvalidInput(
"total 必须是数字或 null".into(),
));
}
if obj.contains_key("used") && !result["used"].is_null() && !result["used"].is_number() {
return Err("used 必须是数字或 null".to_string());
if obj.contains_key("used")
&& !result["used"].is_null()
&& !result["used"].is_number()
{
return Err(AppError::InvalidInput(
"used 必须是数字或 null".into(),
));
}
if obj.contains_key("planName") && !result["planName"].is_null() && !result["planName"].is_string() {
return Err("planName 必须是字符串或 null".to_string());
if obj.contains_key("planName")
&& !result["planName"].is_null()
&& !result["planName"].is_string()
{
return Err(AppError::InvalidInput(
"planName 必须是字符串或 null".into(),
));
}
if obj.contains_key("extra") && !result["extra"].is_null() && !result["extra"].is_string() {
return Err("extra 必须是字符串或 null".to_string());
if obj.contains_key("extra")
&& !result["extra"].is_null()
&& !result["extra"].is_string()
{
return Err(AppError::InvalidInput(
"extra 必须是字符串或 null".into(),
));
}
Ok(())