import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { App, Button, Input, Modal, Popconfirm, Switch, Tabs } from "antd"; import { AlertTriangle, Download, Puzzle, RefreshCw, Trash2 } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { installPluginFromUrl, setPluginEnabled, uninstallPlugin, updatePlugin } from "@/lib/canvas/plugin-loader"; import { fetchOfficialPlugins, hasUpgrade, type OfficialPluginEntry } from "@/lib/canvas/plugin-registry"; import { useThemeStore } from "@/stores/use-theme-store"; import { usePluginStore, type InstalledPlugin } from "@/stores/canvas/use-plugin-store"; export function CanvasPluginManagerModal({ open, onClose }: { open: boolean; onClose: () => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const { message } = App.useApp(); const plugins = usePluginStore((state) => state.plugins); const [url, setUrl] = useState(""); const [installing, setInstalling] = useState(false); const [busyId, setBusyId] = useState(null); const [official, setOfficial] = useState([]); const [loadingOfficial, setLoadingOfficial] = useState(false); const [officialError, setOfficialError] = useState(null); const recordById = useMemo(() => new Map(plugins.map((item) => [item.id, item])), [plugins]); const localPlugins = useMemo(() => plugins.filter((item) => item.local), [plugins]); const thirdPartyPlugins = useMemo(() => plugins.filter((item) => !item.local && !item.official), [plugins]); const loadOfficial = useCallback(async () => { setLoadingOfficial(true); setOfficialError(null); try { setOfficial(await fetchOfficialPlugins()); } catch (error) { setOfficialError(error instanceof Error ? error.message : String(error)); } finally { setLoadingOfficial(false); } }, []); // 打开面板时拉取官方清单(仅在尚未加载过时,避免重复请求) useEffect(() => { if (open && official.length === 0 && !loadingOfficial && !officialError) void loadOfficial(); }, [open, official.length, loadingOfficial, officialError, loadOfficial]); const handleInstallUrl = async () => { const target = url.trim(); if (!target) return; setInstalling(true); try { const plugin = await installPluginFromUrl(target); message.success(`已安装插件 ${plugin.name}`); setUrl(""); } catch (error) { message.error(`安装失败:${error instanceof Error ? error.message : String(error)}`); } finally { setInstalling(false); } }; const handleInstallOfficial = async (entry: OfficialPluginEntry) => { setBusyId(entry.id); try { const plugin = await installPluginFromUrl(entry.url, { official: true }); message.success(`已安装 ${plugin.name}`); } catch (error) { message.error(`安装失败:${error instanceof Error ? error.message : String(error)}`); } finally { setBusyId(null); } }; const runOnPlugin = async (record: InstalledPlugin, action: () => Promise, successText: string) => { setBusyId(record.id); try { await action(); message.success(successText); } catch (error) { message.error(`${error instanceof Error ? error.message : String(error)}`); } finally { setBusyId(null); } }; // 已安装插件的操作区:启用开关 +(非本地)更新/卸载 // upgradable=true 时(远程有更高版本),更新按钮高亮为主色以提示升级 const installedControls = (record: InstalledPlugin, upgradable = false) => ( <> runOnPlugin(record, () => setPluginEnabled(record, checked), checked ? "已启用" : "已禁用")} /> {!record.local && ( <> {officialError ? (
加载失败:{officialError}
) : loadingOfficial && official.length === 0 ? ( emptyHint("正在获取官方插件…") ) : official.length === 0 ? ( emptyHint("暂无官方插件") ) : (
{official.map((entry) => { const record = recordById.get(entry.id); // 已安装且远程版本更高 → 显示绿点并高亮升级按钮 const upgradable = Boolean(record && hasUpgrade(record.version, entry.version)); const icon = entry.icon || ; return row( entry.id, upgradable ? withUpgradeDot(icon) : icon, entry.name, // 有升级时标题版本展示为「本地 → 远程」,让用户看清升级到哪个版本 upgradable && record ? `${record.version} → ${entry.version}` : entry.version, entry.description, record ? ( installedControls(record, upgradable) ) : ( ), ); })}
)} ); const localTab =
{localPlugins.map((record) => row(record.id, , record.name, record.version, record.description || record.url, installedControls(record)))}
; const thirdPartyTab = (
setUrl(event.target.value)} onPressEnter={handleInstallUrl} allowClear />
{thirdPartyPlugins.length === 0 ? emptyHint("还没有安装第三方插件") : thirdPartyPlugins.map((record) => row(record.id, , record.name, record.version, record.description || record.url, installedControls(record)))}
); const tabs = [ { key: "official", label: "官方插件", children: officialTab }, ...(localPlugins.length > 0 ? [{ key: "local", label: "本地插件", children: localTab }] : []), { key: "third", label: "第三方插件", children: thirdPartyTab }, ]; return (
插件代码会在当前页面内直接执行,可访问本地数据(包含 AI API Key)。请仅安装你信任来源的插件。
); }