mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
4aa9512e36
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>
109 lines
3.4 KiB
Rust
109 lines
3.4 KiB
Rust
use futures::future::join_all;
|
|
use reqwest::{Client, Url};
|
|
use serde::Serialize;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use crate::error::AppError;
|
|
|
|
const DEFAULT_TIMEOUT_SECS: u64 = 8;
|
|
const MAX_TIMEOUT_SECS: u64 = 30;
|
|
const MIN_TIMEOUT_SECS: u64 = 2;
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct EndpointLatency {
|
|
pub url: String,
|
|
pub latency: Option<u128>,
|
|
pub status: Option<u16>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
fn build_client(timeout_secs: u64) -> Result<Client, AppError> {
|
|
Client::builder()
|
|
.timeout(Duration::from_secs(timeout_secs))
|
|
.redirect(reqwest::redirect::Policy::limited(5))
|
|
.user_agent("cc-switch-speedtest/1.0")
|
|
.build()
|
|
.map_err(|e| AppError::Message(format!("创建 HTTP 客户端失败: {e}")))
|
|
}
|
|
|
|
fn sanitize_timeout(timeout_secs: Option<u64>) -> u64 {
|
|
let secs = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
|
secs.clamp(MIN_TIMEOUT_SECS, MAX_TIMEOUT_SECS)
|
|
}
|
|
|
|
pub async fn test_endpoints(
|
|
urls: Vec<String>,
|
|
timeout_secs: Option<u64>,
|
|
) -> Result<Vec<EndpointLatency>, AppError> {
|
|
if urls.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let timeout = sanitize_timeout(timeout_secs);
|
|
let client = build_client(timeout)?;
|
|
|
|
let tasks = urls.into_iter().map(|raw_url| {
|
|
let client = client.clone();
|
|
async move {
|
|
let trimmed = raw_url.trim().to_string();
|
|
if trimmed.is_empty() {
|
|
return EndpointLatency {
|
|
url: raw_url,
|
|
latency: None,
|
|
status: None,
|
|
error: Some("URL 不能为空".to_string()),
|
|
};
|
|
}
|
|
|
|
let parsed_url = match Url::parse(&trimmed) {
|
|
Ok(url) => url,
|
|
Err(err) => {
|
|
return EndpointLatency {
|
|
url: trimmed,
|
|
latency: None,
|
|
status: None,
|
|
error: Some(format!("URL 无效: {err}")),
|
|
};
|
|
}
|
|
};
|
|
|
|
// 先进行一次“热身”请求,忽略其结果,仅用于复用连接/绕过首包惩罚
|
|
let _ = client.get(parsed_url.clone()).send().await;
|
|
|
|
// 第二次请求开始计时,并将其作为结果返回
|
|
let start = Instant::now();
|
|
match client.get(parsed_url).send().await {
|
|
Ok(resp) => {
|
|
let latency = start.elapsed().as_millis();
|
|
EndpointLatency {
|
|
url: trimmed,
|
|
latency: Some(latency),
|
|
status: Some(resp.status().as_u16()),
|
|
error: None,
|
|
}
|
|
}
|
|
Err(err) => {
|
|
let status = err.status().map(|s| s.as_u16());
|
|
let error_message = if err.is_timeout() {
|
|
"请求超时".to_string()
|
|
} else if err.is_connect() {
|
|
"连接失败".to_string()
|
|
} else {
|
|
err.to_string()
|
|
};
|
|
|
|
EndpointLatency {
|
|
url: trimmed,
|
|
latency: None,
|
|
status,
|
|
error: Some(error_message),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let results = join_all(tasks).await;
|
|
Ok(results)
|
|
}
|