Files
omniai-web/src/features/provider-health/ProviderHealthPage.tsx
T

169 lines
6.0 KiB
TypeScript
Raw Normal View History

2026-06-02 12:38:01 +08:00
import { useCallback, useEffect, useState } from "react";
2026-06-05 17:19:38 +08:00
import "../../styles/pages/provider-health.css";
2026-06-02 12:38:01 +08:00
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { providerHealthClient, type ProviderHealthResponse } from "../../api/providerHealthClient";
import WorkspacePageShell from "../../components/WorkspacePageShell";
import type { WebUserSession } from "../../types";
interface ProviderHealthPageProps {
session: WebUserSession | null;
onOpenLogin: () => void;
}
const STATUS_ICON: Record<string, React.ReactNode> = {
healthy: <CheckCircleOutlined style={{ color: "#10b981" }} />,
arrears: <CloseCircleOutlined style={{ color: "#ef4444" }} />,
denied: <CloseCircleOutlined style={{ color: "#ef4444" }} />,
error: <ExclamationCircleOutlined style={{ color: "#f59e0b" }} />,
timeout: <ExclamationCircleOutlined style={{ color: "#f59e0b" }} />,
no_key: <ExclamationCircleOutlined style={{ color: "#f59e0b" }} />,
unknown: <ExclamationCircleOutlined style={{ color: "#9ca3af" }} />,
};
const STATUS_LABEL: Record<string, string> = {
healthy: "正常",
arrears: "欠费",
denied: "权限拒绝",
error: "异常",
timeout: "超时",
no_key: "无密钥",
unknown: "未知",
};
export default function ProviderHealthPage({ session, onOpenLogin }: ProviderHealthPageProps) {
const isAdmin = session?.user?.role === "admin";
const [data, setData] = useState<ProviderHealthResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await providerHealthClient.getStatus();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (isAdmin) void load();
}, [isAdmin, load]);
if (!session) {
return (
<WorkspacePageShell title="服务商健康" fullWidth className="provider-health-page page-motion">
<section className="provider-health-access">
<p></p>
<button type="button" onClick={onOpenLogin}></button>
</section>
</WorkspacePageShell>
);
}
if (!isAdmin) {
return (
<WorkspacePageShell title="服务商健康" fullWidth className="provider-health-page page-motion">
<section className="provider-health-access">
<p></p>
</section>
</WorkspacePageShell>
);
}
return (
<WorkspacePageShell title="服务商健康" fullWidth className="provider-health-page page-motion">
<div className="provider-health-page__inner">
<section className="provider-health-toolbar">
<div>
<h2></h2>
<p> AI </p>
</div>
<button type="button" disabled={loading} onClick={() => void load()}>
<ReloadOutlined />
</button>
</section>
{error ? <p className="provider-health-error">{error}</p> : null}
{/* Health status cards */}
<div className="provider-health-grid">
{data?.health ? Object.entries(data.health).map(([provider, entry]) => (
<div key={provider} className="provider-health-card">
<div className="provider-health-card__header">
{STATUS_ICON[entry.status] || STATUS_ICON.unknown}
<strong>{provider}</strong>
<span className={`provider-health-card__status provider-health-card__status--${entry.status}`}>
{STATUS_LABEL[entry.status] || entry.status}
</span>
</div>
{entry.lastCheck ? (
<small className="provider-health-card__time">
: {new Date(entry.lastCheck).toLocaleString("zh-CN")}
</small>
) : null}
{entry.lastError ? (
<p className="provider-health-card__error">{entry.lastError}</p>
) : null}
</div>
)) : null}
</div>
{/* Call stats table */}
{data?.callStats?.length ? (
<section className="provider-health-section">
<h3> 1h </h3>
<table className="provider-health-table">
<thead>
<tr><th></th><th></th><th></th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{data.callStats.map((row, i) => (
<tr key={i}>
<td>{row.provider}</td>
<td>{row.model}</td>
<td>{row.status}</td>
<td>{row.count}</td>
<td>{row.avg_ms ? `${Math.round(Number(row.avg_ms))}ms` : "-"}</td>
<td>{row.total_cost ? `¥${Number(row.total_cost).toFixed(2)}` : "-"}</td>
</tr>
))}
</tbody>
</table>
</section>
) : null}
{/* Key pool stats */}
{data?.keyStats?.length ? (
<section className="provider-health-section">
<h3></h3>
<table className="provider-health-table">
<thead>
<tr><th></th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{data.keyStats.map((row, i) => (
<tr key={i}>
<td>{row.provider}</td>
<td>{row.total_keys}</td>
<td>{row.active_keys}</td>
<td>{row.current_load}</td>
</tr>
))}
</tbody>
</table>
</section>
) : null}
</div>
</WorkspacePageShell>
);
2026-06-05 17:19:38 +08:00
}