diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 32235daa9..09653f0a5 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -855,6 +855,14 @@ type AiModel = { fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; + healthStatus?: "healthy" | "degraded" | "auto_disabled"; + healthConsecutiveFailures?: number; + healthLastEventType?: string | null; + healthLastError?: string | null; + healthLastFailureAt?: string | null; + healthLastSuccessAt?: string | null; + healthAutoDisabledAt?: string | null; + healthAutoDisabledReason?: string | null; isAgentModel?: boolean; agentCodingModelId?: string; isMultimodal?: boolean; @@ -862,6 +870,26 @@ type AiModel = { imageGenConfig?: ImageGenConfig; // 🆕 图片生成特性配置 }; +type AiModelHealthEvent = { + id: string; + modelId?: string | null; + providerId?: string | null; + modelRuntimeId?: string | null; + modelDisplayName?: string | null; + providerType?: string | null; + providerName?: string | null; + eventType: string; + severity: string; + context?: string | null; + counted: boolean; + consecutiveFailures: number; + autoDisabled: boolean; + elapsedMs?: number | null; + requestId?: string | null; + errorMessage?: string | null; + createdAt: string; +}; + type StorageConfig = { id: string; name: string; @@ -1552,6 +1580,9 @@ export function AdminSettingsClient() { const { data: modelsData, mutate: mutateModels } = useSWR<{ models: AiModel[]; }>("/api/admin/ai-models", fetcher); + const { data: modelHealthData, mutate: mutateModelHealth } = useSWR<{ + events: AiModelHealthEvent[]; + }>("/api/admin/ai-model-health?limit=120", fetcher); const { data: storageData, mutate: mutateStorage } = useSWR<{ configs: StorageConfig[]; }>("/api/admin/storage", fetcher); @@ -1682,6 +1713,26 @@ export function AdminSettingsClient() { }); return map; }, [providersData?.providers]); + const modelHealthEventsByModelId = useMemo(() => { + const map = new Map(); + (modelHealthData?.events || []).forEach((event) => { + const modelId = String(event.modelId || ""); + if (!modelId) return; + const list = map.get(modelId) || []; + if (list.length < 5) { + list.push(event); + } + map.set(modelId, list); + }); + return map; + }, [modelHealthData?.events]); + const recentCriticalModelHealthEvents = useMemo( + () => + (modelHealthData?.events || []) + .filter((event) => event.counted || event.autoDisabled) + .slice(0, 12), + [modelHealthData?.events] + ); // 网站设置状态 const [siteSettings, setSiteSettings] = useState({ @@ -7018,6 +7069,25 @@ export function AdminSettingsClient() { } }; + const handleRestoreModelHealth = async (model: AiModel) => { + try { + const response = await fetch("/api/admin/ai-model-health", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ modelId: model.id }), + }); + const data = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(data?.error || "恢复模型失败"); + } + toast.success(`已恢复 ${model.displayName},失败计数已清零`); + mutateModels(); + mutateModelHealth(); + } catch (error: any) { + toast.error(error?.message || "恢复模型失败"); + } + }; + // --- 存储配置逻辑 --- const resetStorageForm = () => { setStorageForm({ @@ -15519,6 +15589,77 @@ export function AdminSettingsClient() {
+
+
+
+
模型故障日志
+

+ 只记录真实模型故障和严重超时;质检不通过不会触发自动禁用。 +

+
+ +
+
+ {recentCriticalModelHealthEvents.length > 0 ? ( + recentCriticalModelHealthEvents.map((event) => ( +
+
+ + {event.modelDisplayName || event.modelRuntimeId || "未知模型"} + + + {event.eventType} + + {event.autoDisabled && ( + + 已自动禁用 + + )} + + 连续失败 {event.consecutiveFailures} 次 + + {event.elapsedMs ? ( + + 耗时 {Math.round(event.elapsedMs / 1000)} 秒 + + ) : null} + + {formatBeijingTime(event.createdAt, { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} + +
+ {event.errorMessage && ( +
+ {event.errorMessage} +
+ )} +
+ )) + ) : ( +
+ 暂无模型故障日志。 +
+ )} +
+
{modelsData?.models?.map((model) => { const provider = providersData?.providers?.find( (p) => p.id === model.providerId @@ -15535,6 +15676,10 @@ export function AdminSettingsClient() { capabilities: (model as any).capabilities, supportsImageGen: model.supportsImageGen, }); + const healthStatus = model.healthStatus || "healthy"; + const modelHealthEvents = + modelHealthEventsByModelId.get(model.id) || []; + const latestHealthEvent = modelHealthEvents[0]; return (
)} + {healthStatus === "auto_disabled" && ( + + 系统自动禁用 + + )} + {healthStatus === "degraded" && ( + + 健康观察中 + + )}
ID: {model.modelId} | 提供商:{" "} {provider?.name ?? "未知"}
+ {(healthStatus !== "healthy" || latestHealthEvent) && ( +
+
+ + 连续故障:{model.healthConsecutiveFailures || 0} 次 + + {model.healthLastEventType && ( + 类型:{model.healthLastEventType} + )} + {model.healthAutoDisabledAt && ( + + 禁用时间: + {formatBeijingTime(model.healthAutoDisabledAt, { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} + + )} +
+ {(model.healthLastError || latestHealthEvent?.errorMessage) && ( +
+ 最近原因: + {model.healthLastError || latestHealthEvent?.errorMessage} +
+ )} +
+ )}
+ {healthStatus === "auto_disabled" && ( + + )}