mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
feat(health-check): replace real-LLM probe with HTTP reachability check
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB. Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold. Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry. Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden. Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
This commit is contained in:
+27
-73
@@ -6,12 +6,17 @@ import {
|
||||
type StreamCheckResult,
|
||||
} from "@/lib/api/model-test";
|
||||
import type { AppId } from "@/lib/api";
|
||||
import { useResetCircuitBreaker } from "@/lib/query/failover";
|
||||
|
||||
/**
|
||||
* 供应商连通性检查。
|
||||
*
|
||||
* 只探测 base_url 是否可达(任何 HTTP 响应都算可达),不发真实大模型请求。
|
||||
* 刻意 **不** 重置故障转移熔断器——可达 ≠ 配置正确,一个端口通但鉴权废的供应商
|
||||
* 不应被误判为"健康"而切回线上。熔断器只由真实转发流量驱动(见 proxy/forwarder.rs)。
|
||||
*/
|
||||
export function useStreamCheck(appId: AppId) {
|
||||
const { t } = useTranslation();
|
||||
const [checkingIds, setCheckingIds] = useState<Set<string>>(new Set());
|
||||
const resetCircuitBreaker = useResetCircuitBreaker();
|
||||
|
||||
const checkProvider = useCallback(
|
||||
async (
|
||||
@@ -25,89 +30,38 @@ export function useStreamCheck(appId: AppId) {
|
||||
|
||||
if (result.status === "operational") {
|
||||
toast.success(
|
||||
t("streamCheck.operational", {
|
||||
t("streamCheck.reachable", {
|
||||
providerName: providerName,
|
||||
responseTimeMs: result.responseTimeMs,
|
||||
defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
|
||||
defaultValue: `${providerName} 连通正常 (${result.responseTimeMs}ms)`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
|
||||
// 测试通过后重置熔断器状态
|
||||
resetCircuitBreaker.mutate({ providerId, appType: appId });
|
||||
} else if (result.status === "degraded") {
|
||||
toast.warning(
|
||||
t("streamCheck.degraded", {
|
||||
t("streamCheck.reachableSlow", {
|
||||
providerName: providerName,
|
||||
responseTimeMs: result.responseTimeMs,
|
||||
defaultValue: `${providerName} 响应较慢 (${result.responseTimeMs}ms)`,
|
||||
defaultValue: `${providerName} 连通但较慢 (${result.responseTimeMs}ms)`,
|
||||
}),
|
||||
);
|
||||
|
||||
// 降级状态也重置熔断器,因为至少能通信
|
||||
resetCircuitBreaker.mutate({ providerId, appType: appId });
|
||||
} else if (result.errorCategory === "modelNotFound") {
|
||||
// 专门处理"模型不存在/已下架":指向配置入口,比通用 404 文案更有指导性
|
||||
toast.error(
|
||||
t("streamCheck.modelNotFound", {
|
||||
providerName: providerName,
|
||||
model: result.modelUsed,
|
||||
defaultValue: `${providerName} 测试模型 ${result.modelUsed} 不存在或已下架`,
|
||||
}),
|
||||
{
|
||||
description: t("streamCheck.modelNotFoundHint", {
|
||||
defaultValue: "",
|
||||
}),
|
||||
duration: 10000,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
} else if (result.errorCategory === "quotaExceeded") {
|
||||
toast.warning(
|
||||
t("streamCheck.quotaExceeded", {
|
||||
providerName: providerName,
|
||||
defaultValue: `${providerName} Coding Plan quota has been exceeded`,
|
||||
}),
|
||||
{
|
||||
description: t("streamCheck.quotaExceededHint", {
|
||||
defaultValue: "",
|
||||
}),
|
||||
duration: 10000,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
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}`,
|
||||
// 仅当无法建立连接(DNS / 连接被拒 / TLS / 超时)才会到这里
|
||||
toast.error(
|
||||
t("streamCheck.unreachable", {
|
||||
providerName: providerName,
|
||||
message: result.message,
|
||||
defaultValue: `${providerName} 无法连通: ${result.message}`,
|
||||
}),
|
||||
{
|
||||
description: t("streamCheck.unreachableHint", {
|
||||
defaultValue:
|
||||
"无法建立连接(DNS / 连接 / TLS / 超时)。请检查 base_url 与网络。",
|
||||
}),
|
||||
{ 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 },
|
||||
);
|
||||
}
|
||||
duration: 8000,
|
||||
closeButton: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -128,7 +82,7 @@ export function useStreamCheck(appId: AppId) {
|
||||
});
|
||||
}
|
||||
},
|
||||
[appId, t, resetCircuitBreaker],
|
||||
[appId, t],
|
||||
);
|
||||
|
||||
const isChecking = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user