import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { Pencil, Copy, Activity, Loader2, Trash2, BarChart3, Stethoscope, } from "lucide-react"; import { useTranslation } from "../i18n"; import { ProviderIcon } from "./ProviderIcon"; import { UsageFooter } from "./UsageFooter"; import { useUsageQuery } from "../hooks/useUsageQuery"; import { useHealthCheck, localizeHealthError, probeLine, usageLine, healthDotClass, type HealthResult, } from "../hooks/useHealthCheck"; import type { Agent, Model, Provider } from "../types"; interface ConnectivityResult { ok: boolean; latencyMs: number; statusCode?: number | null; error?: string | null; } interface TestState { status: "testing" | "ok" | "fail"; latency?: number; error?: string; } interface ProviderListProps { providers: Provider[]; defaultModel: string | null; models: Record; onEdit: (name: string) => void; onDelete: (name: string) => void; onDuplicate: (name: string) => void; onAdd: () => void; onSwitchProvider: (name: string) => void; onConfigureUsage: (name: string) => void; agent: Agent; } interface ProviderCardProps { provider: Provider; agent: Agent; defaultModel: string | null; models: Record; /** Inline connectivity-test state for this provider, or undefined when idle. */ ts: TestState | undefined; /** 一键体检结论;undefined = 本轮未出结果(检测中或未检测)。 */ health: HealthResult | undefined; /** 体检请求进行中。 */ healthRunning: boolean; /** 体检至少跑过一轮(决定是否渲染灰色「未检测」点)。 */ healthRan: boolean; onEdit: (name: string) => void; onDelete: (name: string) => void; onDuplicate: (name: string) => void; onSwitchProvider: (name: string) => void; onConfigureUsage: (name: string) => void; onTest: (provider: Provider) => void; } function ProviderCard({ provider, agent, defaultModel, models, ts, health, healthRunning, healthRan, onEdit, onDelete, onDuplicate, onSwitchProvider, onConfigureUsage, onTest, }: ProviderCardProps) { const { t } = useTranslation(); // Usage query state lives once per card; both the compact (card header) and // detail (footer) UsageFooter variants read this same state, so a refresh // from either side updates both. const usage = useUsageQuery( agent, provider.name, provider.usageKinds, provider.usageConfig?.autoQueryIntervalMinutes, provider.usageConfig?.enabled === false, provider.usageConfig?.threshold, provider.usageConfig?.templateType === "newapi" || provider.usageConfig?.templateType === "sub2api" ); const providerModels = Object.values(models).filter( (m) => m.provider === provider.name ); const defaultModelName = defaultModel ? models[defaultModel]?.model || models[defaultModel]?.display_name || defaultModel : null; const isActive = provider.active === true; // 一键体检状态点 tooltip:两项检测各自如实展示——探活可能是「不支持模型 // 列表」(中性),账单可能是「失败: xxx」,都不掩盖。 const healthTip = health ? [ health.probe ? probeLine(health.probe, health.latencyMs, t) : null, health.usage ? usageLine(health.usage, t) : null, ] .filter((s): s is string => s !== null) .join(" · ") : ""; return (
onEdit(provider.name)} >

{provider.name}

{/* 一键体检状态点:绿=正常、红=失败、灰=未判定(未检测 / 检测中) */} {health ? ( ) : healthRunning ? ( ) : healthRan ? ( ) : null} {isActive && ( {t("inUse")} )} {provider.note && ( {provider.note} )}
{provider.official_url || provider.base_url || t("noUrl")} {provider.provider_type} {t("modelCount", { count: providerModels.length })}
{defaultModel && models[defaultModel]?.provider === provider.name && ( {t("defaultModel", { name: defaultModelName ?? "" })} )} {/* inline connectivity test result */} {ts && ts.status !== "testing" && ( 6000 ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 border-orange-300 dark:border-orange-500/30" : "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-green-300 dark:border-green-500/30" : "bg-red-100 dark:bg-red-900/30 text-red-500 dark:text-red-400 border-red-300 dark:border-red-500/30" }`} title={ ts.status === "fail" ? ts.error || t("connectivityFail", { name: provider.name, error: "" }) : ts.latency && ts.latency > 6000 ? t("connectivitySlow", { name: provider.name, latency: ts.latency ?? 0 }) : t("connectivityOk", { name: provider.name, latency: ts.latency ?? 0 }) } > {ts.status === "ok" ? `${ts.latency}ms` : "✕"} )} {/* compact usage summary — sits left of the switch button, mirroring cc-switch's card layout (usage → action buttons) */} {usage.supported && ( )} {agent === "kimi_code" && ( ⓘ )} {/* icon button group */}
); } const iconBtn = "w-8 h-8 flex items-center justify-center rounded border border-border text-content-muted hover:text-content-primary hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none transition-colors"; export function ProviderList({ providers, defaultModel, models, onEdit, onDelete, onDuplicate, onAdd, onSwitchProvider, onConfigureUsage, agent, }: ProviderListProps) { const { t } = useTranslation(); const [testState, setTestState] = useState>({}); const health = useHealthCheck(agent); // managed(OAuth 登录托管)供应商排在最前;其余保持原有顺序(稳定排序)。 const sortedProviders = [...providers].sort( (a, b) => Number(b.managed === true) - Number(a.managed === true) ); const handleTest = async (provider: Provider) => { setTestState((s) => ({ ...s, [provider.name]: { status: "testing" } })); try { const result = await invoke("test_connectivity", { provider }); setTestState((s) => ({ ...s, [provider.name]: { status: result.ok ? "ok" : "fail", latency: result.latencyMs, error: result.error ?? undefined, }, })); } catch (e) { setTestState((s) => ({ ...s, [provider.name]: { status: "fail", error: e instanceof Error ? e.message : String(e), }, })); } // auto-clear the inline result after a few seconds setTimeout(() => { setTestState((s) => { const next = { ...s }; delete next[provider.name]; return next; }); }, 6000); }; return (

{t("providers")}

{/* 体检汇总:检测中 / 请求失败 / x 正常 / y 失败 */} {(health.running || health.ran || health.error) && ( {health.error ? t("healthRequestFailed", { error: localizeHealthError(health.error, t), }) : health.running ? t("healthCheckRunning") : t("healthSummary", { ok: health.okCount, fail: health.failCount, })} )}
{providers.length === 0 ? (
⊘
{t("noProviders")}
) : (
{sortedProviders.map((provider) => ( ))}
)}
); }