Files
CC-Switch/src/hooks/useStreamCheck.ts
T
Jason a5903d8600 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.
2026-06-14 21:21:45 +08:00

95 lines
3.0 KiB
TypeScript

import { useState, useCallback } from "react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import {
streamCheckProvider,
type StreamCheckResult,
} from "@/lib/api/model-test";
import type { AppId } from "@/lib/api";
/**
* 供应商连通性检查。
*
* 只探测 base_url 是否可达(任何 HTTP 响应都算可达),不发真实大模型请求。
* 刻意 **不** 重置故障转移熔断器——可达 ≠ 配置正确,一个端口通但鉴权废的供应商
* 不应被误判为"健康"而切回线上。熔断器只由真实转发流量驱动(见 proxy/forwarder.rs)。
*/
export function useStreamCheck(appId: AppId) {
const { t } = useTranslation();
const [checkingIds, setCheckingIds] = useState<Set<string>>(new Set());
const checkProvider = useCallback(
async (
providerId: string,
providerName: string,
): Promise<StreamCheckResult | null> => {
setCheckingIds((prev) => new Set(prev).add(providerId));
try {
const result = await streamCheckProvider(appId, providerId);
if (result.status === "operational") {
toast.success(
t("streamCheck.reachable", {
providerName: providerName,
responseTimeMs: result.responseTimeMs,
defaultValue: `${providerName} 连通正常 (${result.responseTimeMs}ms)`,
}),
{ closeButton: true },
);
} else if (result.status === "degraded") {
toast.warning(
t("streamCheck.reachableSlow", {
providerName: providerName,
responseTimeMs: result.responseTimeMs,
defaultValue: `${providerName} 连通但较慢 (${result.responseTimeMs}ms)`,
}),
);
} else {
// 仅当无法建立连接(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 与网络。",
}),
duration: 8000,
closeButton: true,
},
);
}
return result;
} catch (e) {
toast.error(
t("streamCheck.error", {
providerName: providerName,
error: String(e),
defaultValue: `${providerName} 检查出错: ${String(e)}`,
}),
);
return null;
} finally {
setCheckingIds((prev) => {
const next = new Set(prev);
next.delete(providerId);
return next;
});
}
},
[appId, t],
);
const isChecking = useCallback(
(providerId: string) => checkingIds.has(providerId),
[checkingIds],
);
return { checkProvider, isChecking };
}