import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useQueryClient } from "@tanstack/react-query"; import { Plus, Settings } from "lucide-react"; import type { Provider, UsageScript } from "@/types"; import { useProvidersQuery, useAddProviderMutation, useUpdateProviderMutation, useDeleteProviderMutation, useSwitchProviderMutation, } from "@/lib/query"; import { providersApi, settingsApi, type AppType, type ProviderSwitchEvent, } from "@/lib/api"; import { extractErrorMessage } from "@/utils/errorUtils"; import { AppSwitcher } from "@/components/AppSwitcher"; import { ModeToggle } from "@/components/mode-toggle"; import { ProviderList } from "@/components/providers/ProviderList"; import { AddProviderDialog } from "@/components/providers/AddProviderDialog"; import { EditProviderDialog } from "@/components/providers/EditProviderDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { SettingsDialog } from "@/components/settings/SettingsDialog"; import { UpdateBadge } from "@/components/UpdateBadge"; import UsageScriptModal from "@/components/UsageScriptModal"; import McpPanel from "@/components/mcp/McpPanel"; import { Button } from "@/components/ui/button"; function App() { const { t } = useTranslation(); const queryClient = useQueryClient(); const [activeApp, setActiveApp] = useState("claude"); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isAddOpen, setIsAddOpen] = useState(false); const [isMcpOpen, setIsMcpOpen] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [usageProvider, setUsageProvider] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const { data, isLoading, refetch } = useProvidersQuery(activeApp); const providers = useMemo(() => data?.providers ?? {}, [data]); const currentProviderId = data?.currentProviderId ?? ""; const addProviderMutation = useAddProviderMutation(activeApp); const updateProviderMutation = useUpdateProviderMutation(activeApp); const deleteProviderMutation = useDeleteProviderMutation(activeApp); const switchProviderMutation = useSwitchProviderMutation(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]); const handleNotify = useCallback( (message: string, type: "success" | "error", duration?: number) => { const options = duration ? { duration } : undefined; if (type === "error") { toast.error(message, options); } else { toast.success(message, options); } }, [], ); const handleOpenWebsite = useCallback( async (url: string) => { try { await settingsApi.openExternal(url); } catch (error) { const detail = extractErrorMessage(error) || t("notifications.openLinkFailed", { defaultValue: "链接打开失败", }); toast.error(detail); } }, [t], ); const handleAddProvider = useCallback( async (provider: Omit) => { await addProviderMutation.mutateAsync(provider); }, [addProviderMutation], ); const handleEditProvider = useCallback( async (provider: Provider) => { try { await updateProviderMutation.mutateAsync(provider); await providersApi.updateTrayMenu(); setEditingProvider(null); } catch { // 错误提示由 mutation 统一处理 } }, [updateProviderMutation], ); const handleSyncClaudePlugin = useCallback( async (provider: Provider) => { if (activeApp !== "claude") return; try { const settings = await settingsApi.get(); if (!settings?.enableClaudePluginIntegration) { return; } const isOfficial = provider.category === "official"; await settingsApi.applyClaudePluginConfig({ official: isOfficial }); toast.success( isOfficial ? t("notifications.appliedToClaudePlugin", { defaultValue: "已同步为官方配置", }) : t("notifications.removedFromClaudePlugin", { defaultValue: "已移除 Claude 插件配置", }), { duration: 2200 }, ); } catch (error) { const detail = extractErrorMessage(error) || t("notifications.syncClaudePluginFailed", { defaultValue: "同步 Claude 插件失败", }); toast.error(detail, { duration: 4200 }); } }, [activeApp, t], ); const handleSwitchProvider = useCallback( async (provider: Provider) => { try { await switchProviderMutation.mutateAsync(provider.id); await handleSyncClaudePlugin(provider); } catch { // 错误提示由 mutation 与同步函数处理 } }, [switchProviderMutation, handleSyncClaudePlugin], ); const handleRequestDelete = useCallback((provider: Provider) => { setConfirmDelete(provider); }, []); const handleConfirmDelete = useCallback(async () => { if (!confirmDelete) return; try { await deleteProviderMutation.mutateAsync(confirmDelete.id); } finally { setConfirmDelete(null); } }, [confirmDelete, deleteProviderMutation]); const handleImportSuccess = useCallback(async () => { await refetch(); try { await providersApi.updateTrayMenu(); } catch (error) { console.error("[App] Failed to refresh tray menu", error); } }, [refetch]); const handleSaveUsageScript = useCallback( async (provider: Provider, script: UsageScript) => { try { const updatedProvider: Provider = { ...provider, meta: { ...provider.meta, usage_script: script, }, }; await providersApi.update(updatedProvider, activeApp); await queryClient.invalidateQueries({ queryKey: ["providers", activeApp], }); toast.success( t("provider.usageSaved", { defaultValue: "用量查询配置已保存", }), ); } catch (error) { const detail = extractErrorMessage(error) || t("provider.usageSaveFailed", { defaultValue: "用量查询配置保存失败", }); toast.error(detail); } }, [activeApp, queryClient, t], ); return (
CC Switch setIsSettingsOpen(true)} />
setIsAddOpen(true)} />
{ if (!open) { setEditingProvider(null); } }} onSubmit={handleEditProvider} appType={activeApp} /> {usageProvider && ( setUsageProvider(null)} onSave={(script) => { void handleSaveUsageScript(usageProvider, script); }} onNotify={handleNotify} /> )} void handleConfirmDelete()} onCancel={() => setConfirmDelete(null)} />
); } export default App;