feat(health): add stream check core functionality

Add new stream-based health check module to replace model_test:
- Add stream_check command layer with single and batch provider checks
- Add stream_check DAO layer for config and log persistence
- Add stream_check service layer with retry mechanism and health status evaluation
- Add frontend HealthStatusIndicator component
- Add frontend useStreamCheck hook

This provides more comprehensive health checking capabilities.
This commit is contained in:
YoVinchen
2025-12-10 15:48:43 +08:00
parent 6acd6e5090
commit 08647ac3ba
5 changed files with 657 additions and 0 deletions
@@ -0,0 +1,45 @@
import React from "react";
import { cn } from "@/lib/utils";
import type { HealthStatus } from "@/lib/api/model-test";
interface HealthStatusIndicatorProps {
status: HealthStatus;
responseTimeMs?: number;
className?: string;
}
const statusConfig = {
operational: {
color: "bg-emerald-500",
label: "正常",
textColor: "text-emerald-600 dark:text-emerald-400",
},
degraded: {
color: "bg-yellow-500",
label: "降级",
textColor: "text-yellow-600 dark:text-yellow-400",
},
failed: {
color: "bg-red-500",
label: "失败",
textColor: "text-red-600 dark:text-red-400",
},
};
export const HealthStatusIndicator: React.FC<HealthStatusIndicatorProps> = ({
status,
responseTimeMs,
className,
}) => {
const config = statusConfig[status];
return (
<div className={cn("flex items-center gap-2", className)}>
<div className={cn("w-2 h-2 rounded-full", config.color)} />
<span className={cn("text-xs font-medium", config.textColor)}>
{config.label}
{responseTimeMs !== undefined && ` (${responseTimeMs}ms)`}
</span>
</div>
);
};
+77
View File
@@ -0,0 +1,77 @@
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";
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.operational", {
name: providerName,
time: result.responseTimeMs,
defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
}),
);
} else if (result.status === "degraded") {
toast.warning(
t("streamCheck.degraded", {
name: providerName,
time: result.responseTimeMs,
defaultValue: `${providerName} 响应较慢 (${result.responseTimeMs}ms)`,
}),
);
} else {
toast.error(
t("streamCheck.failed", {
name: providerName,
error: result.message,
defaultValue: `${providerName} 检查失败: ${result.message}`,
}),
);
}
return result;
} catch (e) {
toast.error(
t("streamCheck.error", {
name: 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 };
}