mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat: classify stream check errors with color-coded toasts
Distinguish between "provider rejects probe" (yellow warning) and "genuinely broken" (red error) in health check results. Backend: add AppError::HttpStatus variant to carry structured HTTP status codes, populate http_status on error results, classify codes into short labels (e.g. "Auth rejected (401)"), and truncate overly long response bodies. Frontend: route 401/403/400/429/5xx to toast.warning with localized hints explaining the error may not indicate actual unusability; route 404/402/connection errors to toast.error. Add i18n keys for all three locales (zh/en/ja). Also deduplicate check_once by reusing build_stream_check_result.
This commit is contained in:
@@ -116,15 +116,24 @@ pub async fn stream_check_all_providers(
|
||||
claude_api_format_override,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|e| StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message: e.to_string(),
|
||||
response_time_ms: None,
|
||||
http_status: None,
|
||||
model_used: String::new(),
|
||||
tested_at: chrono::Utc::now().timestamp(),
|
||||
retry_count: 0,
|
||||
.unwrap_or_else(|e| {
|
||||
let (http_status, message) = match &e {
|
||||
crate::error::AppError::HttpStatus { status, .. } => (
|
||||
Some(*status),
|
||||
StreamCheckService::classify_http_status(*status).to_string(),
|
||||
),
|
||||
_ => (None, e.to_string()),
|
||||
};
|
||||
StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message,
|
||||
response_time_ms: None,
|
||||
http_status,
|
||||
model_used: String::new(),
|
||||
tested_at: chrono::Utc::now().timestamp(),
|
||||
retry_count: 0,
|
||||
}
|
||||
});
|
||||
|
||||
let _ = state
|
||||
|
||||
@@ -44,6 +44,8 @@ pub enum AppError {
|
||||
McpValidation(String),
|
||||
#[error("{0}")]
|
||||
Message(String),
|
||||
#[error("HTTP {status}: {body}")]
|
||||
HttpStatus { status: u16, body: String },
|
||||
#[error("{zh} ({en})")]
|
||||
Localized {
|
||||
key: &'static str,
|
||||
|
||||
@@ -271,34 +271,11 @@ impl StreamCheckService {
|
||||
};
|
||||
|
||||
let response_time = start.elapsed().as_millis() as u64;
|
||||
let tested_at = chrono::Utc::now().timestamp();
|
||||
|
||||
match result {
|
||||
Ok((status_code, model)) => {
|
||||
let health_status =
|
||||
Self::determine_status(response_time, config.degraded_threshold_ms);
|
||||
Ok(StreamCheckResult {
|
||||
status: health_status,
|
||||
success: true,
|
||||
message: "Check succeeded".to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: Some(status_code),
|
||||
model_used: model,
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
})
|
||||
}
|
||||
Err(e) => Ok(StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message: e.to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: None,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
}),
|
||||
}
|
||||
Ok(Self::build_stream_check_result(
|
||||
result,
|
||||
response_time,
|
||||
config.degraded_threshold_ms,
|
||||
))
|
||||
}
|
||||
|
||||
/// Claude 流式检查
|
||||
@@ -475,7 +452,7 @@ impl StreamCheckService {
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
// 流式读取:只需首个 chunk
|
||||
@@ -555,7 +532,7 @@ impl StreamCheckService {
|
||||
if i == 0 && status == 404 && urls.len() > 1 {
|
||||
continue;
|
||||
}
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
@@ -630,7 +607,7 @@ impl StreamCheckService {
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
@@ -717,16 +694,25 @@ impl StreamCheckService {
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
},
|
||||
Err(e) => StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message: e.to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: None,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
},
|
||||
Err(e) => {
|
||||
let (http_status, message) = match &e {
|
||||
AppError::HttpStatus { status, .. } => (
|
||||
Some(*status),
|
||||
Self::classify_http_status(*status).to_string(),
|
||||
),
|
||||
_ => (None, e.to_string()),
|
||||
};
|
||||
StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message,
|
||||
response_time_ms: Some(response_time),
|
||||
http_status,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1124,6 +1110,39 @@ impl StreamCheckService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造 HTTP 状态码错误,截断过长的响应体
|
||||
fn http_status_error(status: u16, body: String) -> AppError {
|
||||
let body = if body.len() > 200 {
|
||||
// 安全截断:找到 200 字节内最近的 char 边界
|
||||
let mut end = 200;
|
||||
while end > 0 && !body.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
format!("{}…", &body[..end])
|
||||
} else {
|
||||
body
|
||||
};
|
||||
AppError::HttpStatus { status, body }
|
||||
}
|
||||
|
||||
/// 将 HTTP 状态码映射为简短的分类标签
|
||||
pub(crate) fn classify_http_status(status: u16) -> &'static str {
|
||||
match status {
|
||||
400 => "Bad request (400)",
|
||||
401 => "Auth rejected (401)",
|
||||
402 => "Payment required (402)",
|
||||
403 => "Access denied (403)",
|
||||
404 => "Not found (404)",
|
||||
429 => "Rate limited (429)",
|
||||
500 => "Internal server error (500)",
|
||||
502 => "Bad gateway (502)",
|
||||
503 => "Service unavailable (503)",
|
||||
504 => "Gateway timeout (504)",
|
||||
s if (500..600).contains(&s) => "Server error",
|
||||
_ => "HTTP error",
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_test_model(
|
||||
app_type: &AppType,
|
||||
provider: &Provider,
|
||||
|
||||
Reference in New Issue
Block a user