import { useMemo } from "react"; import { GripVertical } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { DraggableAttributes, DraggableSyntheticListeners, } from "@dnd-kit/core"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { cn } from "@/lib/utils"; import { ProviderActions } from "@/components/providers/ProviderActions"; import { ProviderIcon } from "@/components/ProviderIcon"; import UsageFooter from "@/components/UsageFooter"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge"; import { useProviderHealth, useResetCircuitBreaker, useSetProxyTarget, } from "@/lib/query/failover"; import { toast } from "sonner"; interface DragHandleProps { attributes: DraggableAttributes; listeners: DraggableSyntheticListeners; isDragging: boolean; } interface ProviderCardProps { provider: Provider; isCurrent: boolean; appId: AppId; onSwitch: (provider: Provider) => void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; onConfigureUsage: (provider: Provider) => void; onOpenWebsite: (url: string) => void; onDuplicate: (provider: Provider) => void; onTest?: (provider: Provider) => void; isTesting?: boolean; isProxyRunning: boolean; proxyPriority?: number; // 代理目标的实际优先级 (1, 2, 3...) allProviders?: Provider[]; // 所有供应商列表,用于计算开启后的优先级 dragHandleProps?: DragHandleProps; } const extractApiUrl = (provider: Provider, fallbackText: string) => { // 优先级 1: 备注 if (provider.notes?.trim()) { return provider.notes.trim(); } // 优先级 2: 官网地址 if (provider.websiteUrl) { return provider.websiteUrl; } // 优先级 3: 从配置中提取请求地址 const config = provider.settingsConfig; if (config && typeof config === "object") { const envBase = (config as Record)?.env?.ANTHROPIC_BASE_URL || (config as Record)?.env?.GOOGLE_GEMINI_BASE_URL; if (typeof envBase === "string" && envBase.trim()) { return envBase; } const baseUrl = (config as Record)?.config; if (typeof baseUrl === "string" && baseUrl.includes("base_url")) { const match = baseUrl.match(/base_url\s*=\s*['"]([^'"]+)['"]/); if (match?.[1]) { return match[1]; } } } return fallbackText; }; export function ProviderCard({ provider, isCurrent, appId, onSwitch, onEdit, onDelete, onConfigureUsage, onOpenWebsite, onDuplicate, onTest, isTesting, isProxyRunning, proxyPriority, allProviders, dragHandleProps, }: ProviderCardProps) { const { t } = useTranslation(); // 获取供应商健康状态 const { data: health } = useProviderHealth(provider.id, appId); // 设置代理目标 const setProxyTargetMutation = useSetProxyTarget(); // 重置熔断器 const resetCircuitBreaker = useResetCircuitBreaker(); const handleSetProxyTarget = async (enabled: boolean) => { try { await setProxyTargetMutation.mutateAsync({ providerId: provider.id, appType: appId, enabled, }); // 计算实际优先级(开启时) let actualPriority: number | undefined; if (enabled && allProviders) { // 模拟开启后的状态:获取所有将要启用代理的 providers const futureProxyTargets = allProviders.filter((p) => { // 包括:已经是代理目标的 或 当前要开启的这个 if (p.id === provider.id) return true; return p.isProxyTarget; }); // 按 sortIndex 排序 const sortedTargets = futureProxyTargets.sort((a, b) => { const indexA = a.sortIndex ?? Number.MAX_SAFE_INTEGER; const indexB = b.sortIndex ?? Number.MAX_SAFE_INTEGER; return indexA - indexB; }); // 找到当前 provider 的位置 const position = sortedTargets.findIndex((p) => p.id === provider.id); actualPriority = position >= 0 ? position + 1 : undefined; } const message = enabled ? actualPriority ? t("provider.proxyTargetEnabled", { defaultValue: `已启用代理目标(优先级:P${actualPriority})`, }) : t("provider.proxyTargetEnabled", { defaultValue: "已启用代理目标", }) : t("provider.proxyTargetDisabled", { defaultValue: "已禁用代理目标", }); const description = enabled ? t("provider.proxyTargetEnabledDesc", { defaultValue: "下次请求将按优先级自动选择此供应商", }) : t("provider.proxyTargetDisabledDesc", { defaultValue: "后续请求将使用其他可用供应商", }); toast.success(message, { description, duration: 4000, }); } catch (error) { toast.error( t("provider.setProxyTargetFailed", { defaultValue: "操作失败", }) + ": " + String(error), ); } }; const handleResetCircuitBreaker = async () => { try { await resetCircuitBreaker.mutateAsync({ providerId: provider.id, appType: appId, }); toast.success( t("provider.circuitBreakerReset", { defaultValue: "熔断器已重置", }), ); } catch (error) { toast.error( t("provider.circuitBreakerResetFailed", { defaultValue: "重置失败", }) + ": " + String(error), ); } }; const fallbackUrlText = t("provider.notConfigured", { defaultValue: "未配置接口地址", }); const displayUrl = useMemo(() => { return extractApiUrl(provider, fallbackUrlText); }, [provider, fallbackUrlText]); // 判断是否为可点击的 URL(备注不可点击) const isClickableUrl = useMemo(() => { // 如果有备注,则不可点击 if (provider.notes?.trim()) { return false; } // 如果显示的是回退文本,也不可点击 if (displayUrl === fallbackUrlText) { return false; } // 其他情况(官网地址或请求地址)可点击 return true; }, [provider.notes, displayUrl, fallbackUrlText]); const usageEnabled = provider.meta?.usage_script?.enabled ?? false; const handleOpenWebsite = () => { if (!isClickableUrl) { return; } onOpenWebsite(displayUrl); }; return (
{/* 供应商图标 */}

{provider.name}

{/* 健康状态徽章和优先级 */} {isProxyRunning && (
{/* 健康徽章:代理目标启用时始终显示,没有健康数据时默认为正常(0失败) */} {(provider.isProxyTarget || health) && ( )} {/* 优先级:仅在代理目标启用时显示 */} {provider.isProxyTarget && proxyPriority && ( P{proxyPriority} )}
)} {provider.category === "third_party" && provider.meta?.isPartner && ( )} {/* 代理目标开关 - 仅在代理服务运行时显示 */} {isProxyRunning && (
e.stopPropagation()} > { handleSetProxyTarget(checked); }} disabled={setProxyTargetMutation.isPending} className="scale-75 data-[state=checked]:bg-green-500" /> {provider.isProxyTarget && ( )} {!provider.isProxyTarget && ( )}
)}
{displayUrl && ( )}
onSwitch(provider)} onEdit={() => onEdit(provider)} onDuplicate={() => onDuplicate(provider)} onTest={onTest ? () => onTest(provider) : undefined} onConfigureUsage={() => onConfigureUsage(provider)} onDelete={() => onDelete(provider)} onResetCircuitBreaker={ isProxyRunning && provider.isProxyTarget ? handleResetCircuitBreaker : undefined } isProxyTarget={provider.isProxyTarget} consecutiveFailures={health?.consecutive_failures ?? 0} />
); }