import { useEffect, useMemo, useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Plus, Settings, ArrowLeft, Bot, Book, Wrench, Server, RefreshCw, } from "lucide-react"; import type { Provider } from "@/types"; import type { EnvConflict } from "@/types/env"; import { useProvidersQuery } from "@/lib/query"; import { providersApi, settingsApi, type AppId, type ProviderSwitchEvent, } from "@/lib/api"; import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env"; import { useProviderActions } from "@/hooks/useProviderActions"; import { extractErrorMessage } from "@/utils/errorUtils"; import { AppSwitcher } from "@/components/AppSwitcher"; import { ProviderList } from "@/components/providers/ProviderList"; import { AddProviderDialog } from "@/components/providers/AddProviderDialog"; import { EditProviderDialog } from "@/components/providers/EditProviderDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { SettingsPage } from "@/components/settings/SettingsPage"; import { UpdateBadge } from "@/components/UpdateBadge"; import { EnvWarningBanner } from "@/components/env/EnvWarningBanner"; import UsageScriptModal from "@/components/UsageScriptModal"; import UnifiedMcpPanel from "@/components/mcp/UnifiedMcpPanel"; import PromptPanel from "@/components/prompts/PromptPanel"; import { SkillsPage } from "@/components/skills/SkillsPage"; import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog"; import { AgentsPanel } from "@/components/agents/AgentsPanel"; import { Button } from "@/components/ui/button"; type View = "providers" | "settings" | "prompts" | "skills" | "mcp" | "agents"; function App() { const { t } = useTranslation(); const [activeApp, setActiveApp] = useState("claude"); const [currentView, setCurrentView] = useState("providers"); const [isAddOpen, setIsAddOpen] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [usageProvider, setUsageProvider] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const [envConflicts, setEnvConflicts] = useState([]); const [showEnvBanner, setShowEnvBanner] = useState(false); const promptPanelRef = useRef(null); const mcpPanelRef = useRef(null); const skillsPageRef = useRef(null); const addActionButtonClass = "bg-orange-500 hover:bg-orange-600 dark:bg-orange-500 dark:hover:bg-orange-600 text-white shadow-lg shadow-orange-500/30 dark:shadow-orange-500/40 rounded-full w-8 h-8"; const { data, isLoading, refetch } = useProvidersQuery(activeApp); const providers = useMemo(() => data?.providers ?? {}, [data]); const currentProviderId = data?.currentProviderId ?? ""; const isClaudeApp = activeApp === "claude"; // 🎯 使用 useProviderActions Hook 统一管理所有 Provider 操作 const { addProvider, updateProvider, switchProvider, deleteProvider, saveUsageScript, } = useProviderActions(activeApp); // 监听来自托盘菜单的切换事件 useEffect(() => { let unsubscribe: (() => void) | undefined; const setupListener = async () => { try { unsubscribe = await providersApi.onSwitched( async (event: ProviderSwitchEvent) => { if (event.appType === activeApp) { await refetch(); } }, ); } catch (error) { console.error("[App] Failed to subscribe provider switch event", error); } }; setupListener(); return () => { unsubscribe?.(); }; }, [activeApp, refetch]); // 应用启动时检测所有应用的环境变量冲突 useEffect(() => { const checkEnvOnStartup = async () => { try { const allConflicts = await checkAllEnvConflicts(); const flatConflicts = Object.values(allConflicts).flat(); if (flatConflicts.length > 0) { setEnvConflicts(flatConflicts); const dismissed = sessionStorage.getItem("env_banner_dismissed"); if (!dismissed) { setShowEnvBanner(true); } } } catch (error) { console.error( "[App] Failed to check environment conflicts on startup:", error, ); } }; checkEnvOnStartup(); }, []); // 切换应用时检测当前应用的环境变量冲突 useEffect(() => { const checkEnvOnSwitch = async () => { try { const conflicts = await checkEnvConflicts(activeApp); if (conflicts.length > 0) { // 合并新检测到的冲突 setEnvConflicts((prev) => { const existingKeys = new Set( prev.map((c) => `${c.varName}:${c.sourcePath}`), ); const newConflicts = conflicts.filter( (c) => !existingKeys.has(`${c.varName}:${c.sourcePath}`), ); return [...prev, ...newConflicts]; }); const dismissed = sessionStorage.getItem("env_banner_dismissed"); if (!dismissed) { setShowEnvBanner(true); } } } catch (error) { console.error( "[App] Failed to check environment conflicts on app switch:", error, ); } }; checkEnvOnSwitch(); }, [activeApp]); // 打开网站链接 const handleOpenWebsite = async (url: string) => { try { await settingsApi.openExternal(url); } catch (error) { const detail = extractErrorMessage(error) || t("notifications.openLinkFailed", { defaultValue: "链接打开失败", }); toast.error(detail); } }; // 编辑供应商 const handleEditProvider = async (provider: Provider) => { await updateProvider(provider); setEditingProvider(null); }; // 确认删除供应商 const handleConfirmDelete = async () => { if (!confirmDelete) return; await deleteProvider(confirmDelete.id); setConfirmDelete(null); }; // 复制供应商 const handleDuplicateProvider = async (provider: Provider) => { // 1️⃣ 计算新的 sortIndex:如果原供应商有 sortIndex,则复制它 const newSortIndex = provider.sortIndex !== undefined ? provider.sortIndex + 1 : undefined; const duplicatedProvider: Omit = { name: `${provider.name} copy`, settingsConfig: JSON.parse(JSON.stringify(provider.settingsConfig)), // 深拷贝 websiteUrl: provider.websiteUrl, category: provider.category, sortIndex: newSortIndex, // 复制原 sortIndex + 1 meta: provider.meta ? JSON.parse(JSON.stringify(provider.meta)) : undefined, // 深拷贝 }; // 2️⃣ 如果原供应商有 sortIndex,需要将后续所有供应商的 sortIndex +1 if (provider.sortIndex !== undefined) { const updates = Object.values(providers) .filter( (p) => p.sortIndex !== undefined && p.sortIndex >= newSortIndex! && p.id !== provider.id, ) .map((p) => ({ id: p.id, sortIndex: p.sortIndex! + 1, })); // 先更新现有供应商的 sortIndex,为新供应商腾出位置 if (updates.length > 0) { try { await providersApi.updateSortOrder(updates, activeApp); } catch (error) { console.error("[App] Failed to update sort order", error); toast.error( t("provider.sortUpdateFailed", { defaultValue: "排序更新失败", }), ); return; // 如果排序更新失败,不继续添加 } } } // 3️⃣ 添加复制的供应商 await addProvider(duplicatedProvider); }; // 导入配置成功后刷新 const handleImportSuccess = async () => { await refetch(); try { await providersApi.updateTrayMenu(); } catch (error) { console.error("[App] Failed to refresh tray menu", error); } }; const renderContent = () => { switch (currentView) { case "settings": return ( setCurrentView("providers")} onImportSuccess={handleImportSuccess} /> ); case "prompts": return ( setCurrentView("providers")} appId={activeApp} /> ); case "skills": return ( setCurrentView("providers")} /> ); case "mcp": return ( setCurrentView("providers")} /> ); case "agents": return setCurrentView("providers")} />; default: return (
setIsAddOpen(true)} />
); } }; return (
{/* 全局拖拽区域(顶部 4px),避免上边框无法拖动 */}
{/* 环境变量警告横幅 */} {showEnvBanner && envConflicts.length > 0 && ( { setShowEnvBanner(false); sessionStorage.setItem("env_banner_dismissed", "true"); }} onDeleted={async () => { // 删除后重新检测 try { const allConflicts = await checkAllEnvConflicts(); const flatConflicts = Object.values(allConflicts).flat(); setEnvConflicts(flatConflicts); if (flatConflicts.length === 0) { setShowEnvBanner(false); } } catch (error) { console.error( "[App] Failed to re-check conflicts after deletion:", error, ); } }} /> )}
{currentView !== "providers" ? (

{currentView === "settings" && t("settings.title")} {currentView === "prompts" && t("prompts.title", { appName: t(`apps.${activeApp}`) })} {currentView === "skills" && t("skills.title")} {currentView === "mcp" && t("mcp.unifiedPanel.title")} {currentView === "agents" && "Agents"}

) : ( <>
CC Switch
setCurrentView("settings")} /> )}
{currentView === "prompts" && ( )} {currentView === "mcp" && ( )} {currentView === "skills" && ( <> )} {currentView === "providers" && ( <>
{isClaudeApp && ( )} {isClaudeApp && ( )}
)}
{renderContent()}
{ if (!open) { setEditingProvider(null); } }} onSubmit={handleEditProvider} appId={activeApp} /> {usageProvider && ( setUsageProvider(null)} onSave={(script) => { void saveUsageScript(usageProvider, script); }} /> )} void handleConfirmDelete()} onCancel={() => setConfirmDelete(null)} />
); } export default App;