import { Boxes, Eye, Filter, PlugZap, Search, Sparkles } from "lucide-react"; import { type ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { GamePluginStatus, MarketplacePluginFilterRequest, MarketplacePluginResponse, MarketplacePluginStateAction } from "../api/types"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import { PageFrame } from "../components/PageFrame"; import type { PageComponentProps } from "../contracts/page"; import { pluginCatalog } from "../contracts/shell"; import { cx } from "../utils/classes"; type ListState = "loading" | "ready" | "error"; type StatusFilter = GamePluginStatus | "all"; interface PluginsPageInitialState { listState?: ListState; listError?: string; plugins?: MarketplacePluginResponse[]; selectedId?: string; detail?: MarketplacePluginResponse | null; usingFallback?: boolean; actionResult?: { status: "succeeded" | "failed"; label: string } | null; } interface PluginsPageProps extends Partial { initialState?: PluginsPageInitialState; } const statusFilters: Array<{ id: StatusFilter; label: string }> = [ { id: "all", label: "全部" }, { id: "installed", label: "已安装" }, { id: "disabled", label: "停用" }, { id: "invalid", label: "异常" }, { id: "updating", label: "更新中" } ]; export function PluginsPage({ initialState }: PluginsPageProps = {}) { const [listState, setListState] = useState(initialState?.listState ?? "loading"); const [listError, setListError] = useState(initialState?.listError ?? ""); const [plugins, setPlugins] = useState(initialState?.plugins ?? []); const [selectedId, setSelectedId] = useState(initialState?.selectedId ?? ""); const [detail, setDetail] = useState(initialState?.detail ?? null); const [detailPending, setDetailPending] = useState(false); const [detailError, setDetailError] = useState(""); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [serverType, setServerType] = useState(""); const [capability, setCapability] = useState(""); const [actionPending, setActionPending] = useState(null); const [actionResult, setActionResult] = useState<{ status: "succeeded" | "failed"; label: string } | null>(initialState?.actionResult ?? null); const [usingFallback, setUsingFallback] = useState(initialState?.usingFallback ?? false); const refresh = useCallback(async () => { setListState("loading"); setListError(""); const filter: MarketplacePluginFilterRequest = { keyword: keyword.trim() || undefined, status: statusFilter, serverType: serverType.trim() || undefined, capability: capability.trim() || undefined }; try { const response = await platformApiClient.listMarketplacePlugins(filter); setPlugins(response.items); setSelectedId((current) => (current && response.items.some((plugin) => plugin.id === current) ? current : response.items[0]?.id || "")); setUsingFallback(false); setListState("ready"); } catch (error) { if (import.meta.env.DEV) { const fallback = fallbackMarketplacePlugins(filter); setPlugins(fallback); setSelectedId((current) => (current && fallback.some((plugin) => plugin.id === current) ? current : fallback[0]?.id || "")); setUsingFallback(true); setListState("ready"); setListError(""); return; } setListState("error"); setListError(error instanceof Error ? error.message : "插件市场加载失败"); setPlugins([]); setDetail(null); } }, [capability, keyword, serverType, statusFilter]); useEffect(() => { void refresh(); }, [refresh]); useEffect(() => { if (!selectedId) { setDetail(null); setDetailError(""); return; } const selectedSummary = plugins.find((plugin) => plugin.id === selectedId) ?? null; if (usingFallback) { setDetail(selectedSummary); setDetailError(""); return; } setDetailPending(true); setDetailError(""); platformApiClient .getMarketplacePlugin(selectedId) .then((plugin) => setDetail(plugin)) .catch((error: unknown) => { setDetail(selectedSummary); setDetailError(error instanceof Error ? error.message : "插件详情加载失败"); }) .finally(() => setDetailPending(false)); }, [plugins, selectedId, usingFallback]); const serverTypes = useMemo(() => unique(plugins.map((plugin) => plugin.serverType)), [plugins]); const capabilities = useMemo(() => unique(plugins.flatMap((plugin) => [...plugin.capabilities, ...plugin.bridgeActions])), [plugins]); const installedCount = plugins.filter((plugin) => plugin.status === "installed").length; const bridgeActionCount = plugins.reduce((sum, plugin) => sum + plugin.bridgeActions.length, 0); const invalidCount = plugins.filter((plugin) => plugin.status === "invalid").length; function updateSelect(event: ChangeEvent) { const { name, value } = event.target; if (name === "serverType") { setServerType(value); } else if (name === "capability") { setCapability(value); } } async function changeState(action: MarketplacePluginStateAction) { if (!detail || usingFallback) { return; } setActionPending(action); setActionResult(null); try { const updated = await platformApiClient.setMarketplacePluginState(detail.id, { action }); setDetail(updated); setPlugins((current) => current.map((plugin) => (plugin.id === updated.id ? updated : plugin))); setActionResult({ status: "succeeded", label: `${updated.name} 已${stateActionLabel(action)}` }); } catch (error) { setActionResult({ status: "failed", label: error instanceof Error ? error.message : "状态更新失败" }); } finally { setActionPending(null); } } return (
0 ? "warning" : "success" } ]} />
{usingFallback && } {actionResult && } {listState === "loading" && } {listState === "error" && void refresh()} />} {listState === "ready" && plugins.length === 0 && ( } title="暂无匹配插件" description="调整搜索、状态、服务器类型或能力筛选后再试。" actionLabel="清除筛选" onAction={() => { setKeyword(""); setStatusFilter("all"); setServerType(""); setCapability(""); }} /> )} {plugins.length > 0 && (
{plugins.map((plugin) => (

{plugin.name}

{plugin.id}
{statusLabel(plugin.status)}

{plugin.description || plugin.serverDisplayName || plugin.serverType}

版本
{plugin.version}
服务器类型
{plugin.serverType}
能力
{plugin.capabilities.slice(0, 3).join(", ") || "--"}
桥接
{plugin.bridgeActions.slice(0, 3).join(", ") || "--"}
))}
)} {selectedId && (
{detailPending && } {detailError && } {detail && void changeState(action)} />}
)}
); } interface PluginDetailProps { plugin: MarketplacePluginResponse; actionPending: MarketplacePluginStateAction | null; actionsDisabled: boolean; onAction: (action: MarketplacePluginStateAction) => void; } function PluginDetail({ plugin, actionPending, actionsDisabled, onAction }: PluginDetailProps) { return (

{plugin.name}

{plugin.manifestRef}
{statusLabel(plugin.status)}
能力
{plugin.capabilities.join(", ") || "--"}
桥接动作
{plugin.bridgeActions.join(", ") || "--"}
插件页面
{plugin.pages.map((page) => `${page.title}(${page.key})`).join(", ") || "--"}
校验
{plugin.validationViolations?.length ? plugin.validationViolations.join(", ") : "manifest validated"}
); } function DetailStat({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } function fallbackMarketplacePlugins(filter: MarketplacePluginFilterRequest): MarketplacePluginResponse[] { const keyword = filter.keyword?.trim().toLowerCase() ?? ""; return pluginCatalog .map((plugin) => ({ id: plugin.id, name: plugin.name, description: plugin.validation, version: plugin.version, serverType: plugin.serverType, serverDisplayName: plugin.serverType, supportedOs: ["linux"], manifestRef: `manifest://${plugin.id}/${plugin.version}`, createFormSchemaRef: "schemas/create-form.schema.json", capabilities: ["process.install", "process.start", "logs.read"], declaredPermissions: plugin.permissions, permissions: { ai: plugin.permissions.includes("ai.invoke"), logs: plugin.permissions.includes("server.logs.read"), files: plugin.permissions.some((permission) => permission.includes("files")), jobs: plugin.permissions.includes("server.lifecycle"), artifacts: plugin.permissions.some((permission) => permission.includes("artifacts")) }, lifecycleActions: { install: "actions/install.json", start: "actions/start.json", stop: "actions/stop.json" }, bridgeActions: plugin.bridgeActions, pages: [{ key: "overview", title: "Overview", path: "/overview", permissions: plugin.permissions, bridgeActions: plugin.bridgeActions }], tags: [plugin.serverType], aiPurposes: plugin.permissions.includes("ai.invoke") ? ["logs.diagnose"] : [], validationViolations: plugin.status === "invalid" ? [plugin.validation] : undefined, status: plugin.status, source: "local-development" })) .filter((plugin) => !filter.status || filter.status === "all" || plugin.status === filter.status) .filter((plugin) => !filter.serverType || plugin.serverType === filter.serverType) .filter((plugin) => !filter.capability || plugin.capabilities.includes(filter.capability) || plugin.bridgeActions.includes(filter.capability)) .filter((plugin) => !keyword || [plugin.id, plugin.name, plugin.serverType, ...plugin.tags, ...plugin.capabilities].some((value) => value.toLowerCase().includes(keyword))); } function unique(values: string[]): string[] { return Array.from(new Set(values.filter(Boolean))).sort((left, right) => left.localeCompare(right)); } function statusClass(status: GamePluginStatus): string { if (status === "installed") { return "status-active"; } if (status === "invalid") { return "status-error"; } return "status-disabled"; } function statusLabel(status: string): string { return status === "installed" ? "已安装" : status === "disabled" ? "停用" : status === "invalid" ? "异常" : status === "updating" ? "更新中" : status; } function stateActionLabel(action: MarketplacePluginStateAction): string { return action === "disable" ? "停用" : "启用"; }