mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +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:
@@ -1,11 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FlaskConical,
|
||||
Coins,
|
||||
} from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, FlaskConical, Coins } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
@@ -47,13 +47,37 @@ export function useStreamCheck(appId: AppId) {
|
||||
// 降级状态也重置熔断器,因为至少能通信
|
||||
resetCircuitBreaker.mutate({ providerId, appType: appId });
|
||||
} else {
|
||||
toast.error(
|
||||
t("streamCheck.failed", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查失败: ${result.message}`,
|
||||
}),
|
||||
);
|
||||
const httpStatus = result.httpStatus;
|
||||
const hintKey = httpStatus
|
||||
? `streamCheck.httpHint.${httpStatus >= 500 ? "5xx" : httpStatus}`
|
||||
: null;
|
||||
const description =
|
||||
(hintKey ? t(hintKey, { defaultValue: "" }) : "") || undefined;
|
||||
|
||||
// 401/403/400 = 检查被拒(供应商可能正常);429/5xx = 临时问题
|
||||
const isProbeRejection =
|
||||
httpStatus != null &&
|
||||
([401, 403, 400, 429].includes(httpStatus) || httpStatus >= 500);
|
||||
|
||||
if (isProbeRejection) {
|
||||
toast.warning(
|
||||
t("streamCheck.rejected", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查被拒: ${result.message}`,
|
||||
}),
|
||||
{ description, duration: 8000, closeButton: true },
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
t("streamCheck.failed", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 检查失败: ${result.message}`,
|
||||
}),
|
||||
{ description, duration: 8000, closeButton: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -2129,7 +2129,17 @@
|
||||
"operational": "{{providerName}} is operational ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} is slow ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} check failed: {{message}}",
|
||||
"error": "{{providerName}} check error: {{error}}"
|
||||
"rejected": "{{providerName}} check rejected: {{message}}",
|
||||
"error": "{{providerName}} check error: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "Provider rejected request format. Health check probe may differ from actual usage.",
|
||||
"401": "API key may be invalid, or provider uses OAuth auth. Check failure doesn't mean it's unusable.",
|
||||
"402": "Account quota or billing issue.",
|
||||
"403": "Provider blocked this request. Some verify client identity; may work fine in actual use.",
|
||||
"404": "Endpoint not found. Check base URL and API path.",
|
||||
"429": "Too many requests. Try again later.",
|
||||
"5xx": "Provider internal error. Likely temporary."
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "Routing Master Switch",
|
||||
|
||||
@@ -2129,7 +2129,17 @@
|
||||
"operational": "{{providerName}} は正常に動作しています ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} の応答が遅いです ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} のチェックに失敗しました: {{message}}",
|
||||
"error": "{{providerName}} のチェックでエラーが発生しました: {{error}}"
|
||||
"rejected": "{{providerName}} のチェックが拒否されました: {{message}}",
|
||||
"error": "{{providerName}} のチェックでエラーが発生しました: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "リクエスト形式が拒否されました。ヘルスチェックの形式は実際の使用と異なる場合があります。",
|
||||
"401": "APIキーが無効か、OAuthなどの認証方式を使用しています。チェック失敗は実際に使えないことを意味しません。",
|
||||
"402": "アカウントの利用枠または請求の問題です。",
|
||||
"403": "プロバイダーがリクエストを拒否しました。実際の使用では正常に動作する場合があります。",
|
||||
"404": "エンドポイントが見つかりません。Base URLとAPIパスを確認してください。",
|
||||
"429": "リクエストが多すぎます。しばらくしてからお試しください。",
|
||||
"5xx": "プロバイダーの内部エラーです。一時的な問題の可能性が高いです。"
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "ルーティング総スイッチ",
|
||||
|
||||
@@ -2130,7 +2130,17 @@
|
||||
"operational": "{{providerName}} 运行正常 ({{responseTimeMs}}ms)",
|
||||
"degraded": "{{providerName}} 响应较慢 ({{responseTimeMs}}ms)",
|
||||
"failed": "{{providerName}} 检查失败: {{message}}",
|
||||
"error": "{{providerName}} 检查出错: {{error}}"
|
||||
"rejected": "{{providerName}} 检查被拒: {{message}}",
|
||||
"error": "{{providerName}} 检查出错: {{error}}",
|
||||
"httpHint": {
|
||||
"400": "供应商拒绝了请求格式。健康检查的探测格式可能与实际使用不同。",
|
||||
"401": "API Key 可能无效,或供应商使用 OAuth 等认证方式。检查失败不代表实际不可用。",
|
||||
"402": "账户配额或计费问题。",
|
||||
"403": "供应商拒绝了此请求。部分供应商会校验客户端身份,检查失败但实际使用可能正常。",
|
||||
"404": "接口地址不存在,请检查 Base URL 和 API 路径。",
|
||||
"429": "请求频率过高,请稍后重试。",
|
||||
"5xx": "供应商内部错误,通常是临时问题。"
|
||||
}
|
||||
},
|
||||
"proxyConfig": {
|
||||
"proxyEnabled": "路由总开关",
|
||||
|
||||
Reference in New Issue
Block a user