Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+16 -53
View File
@@ -3,10 +3,10 @@ import { type ChangeEvent, useCallback, useEffect, useMemo, useState } from "rea
import { platformApiClient } from "../api/client";
import type { GamePluginStatus, MarketplacePluginFilterRequest, MarketplacePluginResponse, MarketplacePluginStateAction } from "../api/types";
import { ManagementDialog } from "../components/OperationControls";
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";
@@ -59,28 +59,28 @@ export function PluginsPage({ initialState }: PluginsPageProps = {}) {
serverType: serverType.trim() || undefined,
capability: capability.trim() || undefined
};
if (initialState?.plugins) {
setListState(initialState.listState ?? "ready");
setListError(initialState.listError ?? "");
setPlugins(initialState.plugins);
setSelectedId(initialState.selectedId ?? "");
setDetail(initialState.detail ?? null);
setUsingFallback(initialState.usingFallback ?? false);
return;
}
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 || ""));
setSelectedId((current) => (current && response.items.some((plugin) => plugin.id === current) ? current : ""));
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]);
}, [capability, initialState, keyword, serverType, statusFilter]);
useEffect(() => {
void refresh();
@@ -248,13 +248,13 @@ export function PluginsPage({ initialState }: PluginsPageProps = {}) {
</section>
)}
{selectedId && (
<section className="drawer-panel plugin-detail-panel" aria-label="plugin marketplace detail">
<ManagementDialog open={selectedId !== ""} title={detail?.name ?? "插件详情"} wide onClose={() => { setSelectedId(""); setDetail(null); setDetailError(""); }}>
<div className="plugin-detail-panel" aria-label="plugin marketplace detail">
{detailPending && <LoadingState label="正在加载插件详情…" compact />}
{detailError && <ErrorState title="插件详情加载失败" reason={detailError} diagnosticId={`plugin-detail:${selectedId}`} compact />}
{detail && <PluginDetail plugin={detail} actionPending={actionPending} actionsDisabled={usingFallback} onAction={(action) => void changeState(action)} />}
</section>
)}
</div>
</ManagementDialog>
</div>
);
}
@@ -326,43 +326,6 @@ function DetailStat({ label, value }: { label: string; value: string }) {
);
}
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));
}