mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
84f9341207
Health check previously classified failures purely by HTTP status code, which meant deprecated/invalid models showed up as a generic "Not found (404)" error pointing users to check the Base URL — misleading when the URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after it was retired). Backend: add detect_error_category() that inspects 4xx response bodies for model-not-found indicators (model_not_found, does not exist, invalid model, not_found_error, etc.) and returns a "modelNotFound" category. Thread the resolved test model through build_stream_check_result so the failed result carries it in model_used. Add StreamCheckResult .error_category field (serde-skipped when None). Frontend: useStreamCheck branches on errorCategory === "modelNotFound" before the HTTP-status fallback and renders a toast.error with the model name and a description pointing to Model Test Config. Add i18n keys (modelNotFound / modelNotFoundHint) for zh/en/ja. Tests: unit-test detect_error_category against real OpenAI/Anthropic error shapes, 5xx false-positive avoidance, and plain 401 auth errors. https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
127 lines
4.3 KiB
TypeScript
127 lines
4.3 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";
|
|
import { useResetCircuitBreaker } from "@/lib/query/failover";
|
|
|
|
export function useStreamCheck(appId: AppId) {
|
|
const { t } = useTranslation();
|
|
const [checkingIds, setCheckingIds] = useState<Set<string>>(new Set());
|
|
const resetCircuitBreaker = useResetCircuitBreaker();
|
|
|
|
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.operational", {
|
|
providerName: providerName,
|
|
responseTimeMs: result.responseTimeMs,
|
|
defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
|
|
}),
|
|
{ closeButton: true },
|
|
);
|
|
|
|
// 测试通过后重置熔断器状态
|
|
resetCircuitBreaker.mutate({ providerId, appType: appId });
|
|
} else if (result.status === "degraded") {
|
|
toast.warning(
|
|
t("streamCheck.degraded", {
|
|
providerName: providerName,
|
|
responseTimeMs: result.responseTimeMs,
|
|
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 {
|
|
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;
|
|
} 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, resetCircuitBreaker],
|
|
);
|
|
|
|
const isChecking = useCallback(
|
|
(providerId: string) => checkingIds.has(providerId),
|
|
[checkingIds],
|
|
);
|
|
|
|
return { checkProvider, isChecking };
|
|
}
|