import { App, Button, Input, Select } from "antd"; import { Switch } from "@/components/ui/base/switch"; import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { CloudUpload, PlugZap, RefreshCw, Search, Trash2, UsersRound } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { PaginationBar } from "@/components/layout/workspace-page"; import "@/lib/plugins/builtin"; import { EAGLE_PLUGIN_ID } from "@/lib/plugins/builtin/eagle"; import { RUNNINGHUB_PLUGIN_ID } from "@/lib/plugins/builtin/workflows"; import { isOfficialApplicationPluginId } from "@/lib/plugins/official-applications"; import { listRegisteredPlugins } from "@/lib/plugins/plugin-registry"; import type { PluginManifest, PluginManifestV2 } from "@/lib/plugins/plugin-types"; import { fetchAdminPlugins, setPluginPlatformAvailability, uninstallPlugin, uploadPlugin, type AdminPluginState, type BackendPlugin, type PluginManagement } from "@/services/api/plugins"; import { UploadPluginModal } from "@/pages/plugins/plugin-documentation-modals"; import { AdminPageFrame } from "../components/admin-shell"; import { AdminDataTable, AdminStatusBadge, AdminTableEmpty } from "../components/admin-ui"; type AdminPluginItem = { manifest: PluginManifest | PluginManifestV2; source: string; management: PluginManagement; status?: string; error?: string; }; export default function AdminPluginsPage() { const { message, modal } = App.useApp(); const [plugins, setPlugins] = useState([]); const [states, setStates] = useState>({}); const [loading, setLoading] = useState(true); const [savingId, setSavingId] = useState(""); const [uploadOpen, setUploadOpen] = useState(false); const [search, setSearch] = useState(""); const [kind, setKind] = useState<"all" | "application" | "protocol" | "payment" | "uploaded">("all"); const [availability, setAvailability] = useState<"all" | "available" | "unavailable">("all"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const reload = async () => { setLoading(true); try { const result = await fetchAdminPlugins(); setPlugins(result.plugins); setStates(result.states); } catch (error) { message.error(error instanceof Error ? error.message : "读取插件管理数据失败"); } finally { setLoading(false); } }; useEffect(() => { void reload(); }, []); const items = useMemo(() => mergePlugins(plugins), [plugins]); const filtered = useMemo(() => { const keyword = search.trim().toLocaleLowerCase(); return items.filter((item) => { if (kind === "uploaded" && item.management.origin !== "uploaded") return false; if (kind !== "all" && kind !== "uploaded" && item.management.kind !== kind) return false; const available = states[item.manifest.id]?.platformAvailable ?? item.status === "enabled"; if (availability === "available" && !available) return false; if (availability === "unavailable" && available) return false; if (!keyword) return true; return [item.manifest.name, item.manifest.id, item.manifest.description, item.manifest.author].filter(Boolean).join(" ").toLocaleLowerCase().includes(keyword); }); }, [availability, items, kind, search, states]); useEffect(() => { setPage((current) => Math.min(current, Math.max(1, Math.ceil(filtered.length / pageSize)))); }, [filtered.length, pageSize]); const changeAvailability = async (item: AdminPluginItem, available: boolean) => { setSavingId(item.manifest.id); try { const state = await setPluginPlatformAvailability(item.manifest.id, available); setStates((current) => ({ ...current, [item.manifest.id]: state })); message.success(`${item.manifest.name}${available ? "已开放" : "已停用"}`); } catch (error) { message.error(error instanceof Error ? error.message : "更新插件可用状态失败"); } finally { setSavingId(""); } }; const upload = async (file: File) => { try { await uploadPlugin(file); setUploadOpen(false); message.success("自定义插件已安装"); await reload(); } catch (error) { message.error(error instanceof Error ? error.message : "安装插件失败"); } }; const remove = (item: AdminPluginItem) => { modal.confirm({ title: `卸载 ${item.manifest.name}?`, content: "插件包、平台状态和所有用户的启用记录都会删除。此操作不可撤销。", okText: "确认卸载", okButtonProps: { danger: true }, cancelText: "取消", onOk: async () => { try { await uninstallPlugin(item.manifest.id); message.success("自定义插件已卸载"); await reload(); } catch (error) { message.error(error instanceof Error ? error.message : "卸载插件失败"); throw error; } }, }); }; const applicationCount = items.filter((item) => item.management.kind === "application").length; const protocolCount = items.filter((item) => item.management.kind === "protocol").length; const paymentCount = items.filter((item) => item.management.kind === "payment").length; const unavailableCount = items.filter((item) => !(states[item.manifest.id]?.platformAvailable ?? item.status === "enabled")).length; const hasFilters = Boolean(search.trim() || kind !== "all" || availability !== "all"); const paginated = filtered.slice((page - 1) * pageSize, page * pageSize); const columns: ColumnsType = [ { title: "插件", key: "plugin", width: 410, render: (_, item) => (
{item.manifest.name} v{item.manifest.version}
{item.manifest.id} {item.error || item.manifest.description || "未提供插件说明"}
), }, { title: "类型", key: "type", width: 160, align: "center", render: (_, item) => (
{!item.manifest.trusted ? (
) : null}
), }, { title: "作用范围", key: "scope", width: 180, align: "center", render: (_, item) => item.management.activationScope === "user" ? (
用户自主启用
) : (
全局生效
管理员统一控制
), }, { title: "平台状态", key: "status", width: 170, align: "center", render: (_, item) => { const available = states[item.manifest.id]?.platformAvailable ?? item.status === "enabled"; return (
void changeAvailability(item, checked)} />
); }, }, { title: "卸载", key: "actions", width: 72, align: "center", render: (_, item) => item.management.origin === "uploaded" ? ( } >
} value={search} aria-label="搜索插件" placeholder="搜索插件名称、ID 或作者" onChange={(event) => { setSearch(event.target.value); setPage(1); }} /> } toolbarFilters={ <> { setAvailability(value); setPage(1); }} options={[ { value: "all", label: "全部状态" }, { value: "available", label: "平台开放" }, { value: "unavailable", label: "平台停用" }, ]} /> } toolbarActive={hasFilters} onReset={() => { setSearch(""); setKind("all"); setAvailability("all"); setPage(1); }} skeletonColumns={5} table={{ className: "app-data-table", size: "small", sticky: true, rowKey: (item) => item.manifest.id, loading, columns, dataSource: paginated, pagination: false, scroll: { x: 1120 } }} empty={} footer={ { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} /> } /> setUploadOpen(false)} onUpload={(file) => void upload(file)} /> ); } function OverviewItem({ label, value, tone = "default" }: { label: string; value: number; tone?: "default" | "warning" }) { return (
{value}
{label}
); } function PluginBrandIcon({ pluginId }: { pluginId: string }) { if (pluginId === "official-payment-wechat-native") { return ( ); } if (pluginId === "official-payment-alipay-page") { return ( ); } return ( ); } function mergePlugins(remote: BackendPlugin[]): AdminPluginItem[] { const byId = new Map(); for (const plugin of listRegisteredPlugins()) { const application = isOfficialApplicationPluginId(plugin.manifest.id); byId.set(plugin.manifest.id, { manifest: plugin.manifest, source: plugin.source || "bundled", management: { origin: "official", kind: application ? "application" : "protocol", activationScope: application ? "user" : "system", configurationScope: application ? (plugin.manifest.id === EAGLE_PLUGIN_ID || plugin.manifest.id === RUNNINGHUB_PLUGIN_ID ? "user" : "none") : "system", }, }); } for (const plugin of remote) byId.set(plugin.manifest.id, plugin); return [...byId.values()].sort((left, right) => managementOrder(left.management) - managementOrder(right.management) || left.manifest.name.localeCompare(right.manifest.name, "zh-CN")); } function managementOrder(value: PluginManagement) { if (value.kind === "application") return 0; if (value.kind === "payment") return 1; if (value.origin === "official") return 2; return 3; } function managementLabel(value: PluginManagement) { if (value.origin === "uploaded") return "自定义插件"; if (value.kind === "application") return "官方应用"; return value.kind === "payment" ? "系统支付协议" : "系统协议"; }