import { CSS } from "@dnd-kit/utilities"; import { DndContext, closestCenter } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { useEffect, useMemo, useRef, useState, type CSSProperties, } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Search, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { providersApi } from "@/lib/api/providers"; import { useDragSort } from "@/hooks/useDragSort"; // import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏 import { ProviderCard } from "@/components/providers/ProviderCard"; import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState"; import { useAutoFailoverEnabled, useFailoverQueue, useAddToFailoverQueue, useRemoveFromFailoverQueue, } from "@/lib/query/failover"; import { useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; interface ProviderListProps { providers: Record; currentProviderId: string; appId: AppId; onSwitch: (provider: Provider) => void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; /** OpenCode: remove from live config (not delete from database) */ onRemoveFromConfig?: (provider: Provider) => void; onDuplicate: (provider: Provider) => void; onConfigureUsage?: (provider: Provider) => void; onOpenWebsite: (url: string) => void; onOpenTerminal?: (provider: Provider) => void; onCreate?: () => void; isLoading?: boolean; isProxyRunning?: boolean; // 代理服务运行状态 isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管) activeProviderId?: string; // 代理当前实际使用的供应商 ID(用于故障转移模式下标注绿色边框) } export function ProviderList({ providers, currentProviderId, appId, onSwitch, onEdit, onDelete, onRemoveFromConfig, onDuplicate, onConfigureUsage, onOpenWebsite, onOpenTerminal, onCreate, isLoading = false, isProxyRunning = false, isProxyTakeover = false, activeProviderId, }: ProviderListProps) { const { t } = useTranslation(); const { sortedProviders, sensors, handleDragEnd } = useDragSort( providers, appId, ); // OpenCode: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig const { data: opencodeLiveIds } = useQuery({ queryKey: ["opencodeLiveProviderIds"], queryFn: () => providersApi.getOpenCodeLiveProviderIds(), enabled: appId === "opencode", }); // OpenCode: 判断供应商是否已添加到 opencode.json const isProviderInConfig = useCallback( (providerId: string): boolean => { if (appId !== "opencode") return true; // 非 OpenCode 应用始终返回 true return opencodeLiveIds?.includes(providerId) ?? false; }, [appId, opencodeLiveIds], ); // 流式健康检查 - 功能已隐藏 // const { checkProvider, isChecking } = useStreamCheck(appId); // 故障转移相关 const { data: isAutoFailoverEnabled } = useAutoFailoverEnabled(appId); const { data: failoverQueue } = useFailoverQueue(appId); const addToQueue = useAddToFailoverQueue(); const removeFromQueue = useRemoveFromFailoverQueue(); // 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式 const isFailoverModeActive = isProxyTakeover === true && isAutoFailoverEnabled === true; // 计算供应商在故障转移队列中的优先级(基于 sortIndex 排序) const getFailoverPriority = useCallback( (providerId: string): number | undefined => { if (!isFailoverModeActive || !failoverQueue) return undefined; const index = failoverQueue.findIndex( (item) => item.providerId === providerId, ); return index >= 0 ? index + 1 : undefined; }, [isFailoverModeActive, failoverQueue], ); // 判断供应商是否在故障转移队列中 const isInFailoverQueue = useCallback( (providerId: string): boolean => { if (!isFailoverModeActive || !failoverQueue) return false; return failoverQueue.some((item) => item.providerId === providerId); }, [isFailoverModeActive, failoverQueue], ); // 切换供应商的故障转移队列状态 const handleToggleFailover = useCallback( (providerId: string, enabled: boolean) => { if (enabled) { addToQueue.mutate({ appType: appId, providerId }); } else { removeFromQueue.mutate({ appType: appId, providerId }); } }, [appId, addToQueue, removeFromQueue], ); // handleTest 功能已隐藏 - 供应商请求格式复杂难以统一测试 // const handleTest = (provider: Provider) => { // checkProvider(provider.id, provider.name); // }; const [searchTerm, setSearchTerm] = useState(""); const [isSearchOpen, setIsSearchOpen] = useState(false); const searchInputRef = useRef(null); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const key = event.key.toLowerCase(); if ((event.metaKey || event.ctrlKey) && key === "f") { event.preventDefault(); setIsSearchOpen(true); return; } if (key === "escape") { setIsSearchOpen(false); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); useEffect(() => { if (isSearchOpen) { const frame = requestAnimationFrame(() => { searchInputRef.current?.focus(); searchInputRef.current?.select(); }); return () => cancelAnimationFrame(frame); } }, [isSearchOpen]); const filteredProviders = useMemo(() => { const keyword = searchTerm.trim().toLowerCase(); if (!keyword) return sortedProviders; return sortedProviders.filter((provider) => { const fields = [provider.name, provider.notes, provider.websiteUrl]; return fields.some((field) => field?.toString().toLowerCase().includes(keyword), ); }); }, [searchTerm, sortedProviders]); if (isLoading) { return (
{[0, 1, 2].map((index) => (
))}
); } if (sortedProviders.length === 0) { return ; } const renderProviderList = () => ( provider.id)} strategy={verticalListSortingStrategy} >
{filteredProviders.map((provider) => ( handleToggleFailover(provider.id, enabled) } activeProviderId={activeProviderId} /> ))}
); return (
{isSearchOpen && (
setSearchTerm(event.target.value)} placeholder={t("provider.searchPlaceholder", { defaultValue: "Search name, notes, or URL...", })} aria-label={t("provider.searchAriaLabel", { defaultValue: "Search providers", })} className="pr-16 pl-9" /> {searchTerm && ( )}
{t("provider.searchScopeHint", { defaultValue: "Matches provider name, notes, and URL.", })} {t("provider.searchCloseHint", { defaultValue: "Press Esc to close", })}
)}
{filteredProviders.length === 0 ? (
{t("provider.noSearchResults", { defaultValue: "No providers match your search.", })}
) : ( renderProviderList() )}
); } interface SortableProviderCardProps { provider: Provider; isCurrent: boolean; appId: AppId; isInConfig: boolean; onSwitch: (provider: Provider) => void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; /** OpenCode: remove from live config (not delete from database) */ onRemoveFromConfig?: (provider: Provider) => void; onDuplicate: (provider: Provider) => void; onConfigureUsage?: (provider: Provider) => void; onOpenWebsite: (url: string) => void; onOpenTerminal?: (provider: Provider) => void; onTest?: (provider: Provider) => void; isTesting: boolean; isProxyRunning: boolean; isProxyTakeover: boolean; // 故障转移相关 isAutoFailoverEnabled: boolean; failoverPriority?: number; isInFailoverQueue: boolean; onToggleFailover: (enabled: boolean) => void; activeProviderId?: string; } function SortableProviderCard({ provider, isCurrent, appId, isInConfig, onSwitch, onEdit, onDelete, onRemoveFromConfig, onDuplicate, onConfigureUsage, onOpenWebsite, onOpenTerminal, onTest, isTesting, isProxyRunning, isProxyTakeover, isAutoFailoverEnabled, failoverPriority, isInFailoverQueue, onToggleFailover, activeProviderId, }: SortableProviderCardProps) { const { setNodeRef, attributes, listeners, transform, transition, isDragging, } = useSortable({ id: provider.id }); const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; return (
onConfigureUsage(item) : () => undefined } onOpenWebsite={onOpenWebsite} onOpenTerminal={onOpenTerminal} onTest={onTest} isTesting={isTesting} isProxyRunning={isProxyRunning} isProxyTakeover={isProxyTakeover} dragHandleProps={{ attributes, listeners, isDragging, }} // 故障转移相关 isAutoFailoverEnabled={isAutoFailoverEnabled} failoverPriority={failoverPriority} isInFailoverQueue={isInFailoverQueue} onToggleFailover={onToggleFailover} activeProviderId={activeProviderId} />
); }