import { CakeSlice, Candy, Search, Sparkles } from "lucide-react"; import { type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types"; import { UsageMeter } from "../components/OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { defaultServerCreateForm, endpointLabel, pendingJobsForServer, pluginLabel, type ServerCreateFormState } from "../contracts/serverManagement"; import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace"; import { clientManagerBuildRequest, dependencyJobRequest, logBackfillRequest, runDistributionGenerateRequest, runUpdateRequest, serverCreateRequestFromForm } from "../schemas/serverManagement"; import { isPlatformAdmin } from "../contracts/workspace"; import { cx } from "../utils/classes"; type ListState = "loading" | "ready" | "error"; const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [ { id: "all", label: "全部" }, { id: "online", label: "在线" }, { id: "offline", label: "离线" }, { id: "attention", label: "需关注" } ]; export function ServersPage({ session, operations, onNavigate }: PageComponentProps) { const [listState, setListState] = useState("loading"); const [listError, setListError] = useState(""); const [plugins, setPlugins] = useState([]); const [endpoints, setEndpoints] = useState([]); const [instances, setInstances] = useState([]); const [jobs, setJobs] = useState([]); const [metrics, setMetrics] = useState>(new Map()); const [metricsPending, setMetricsPending] = useState(true); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [form, setForm] = useState(() => defaultServerCreateForm([], [])); const [showCreate, setShowCreate] = useState(false); const refresh = useCallback(async () => { setListState("loading"); try { const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([ platformApiClient.listGamePlugins(), platformApiClient.listRunEndpoints(), platformApiClient.listServerInstances(), platformApiClient.listJobs() ]); setPlugins(pluginResponse.items); setEndpoints(endpointResponse.items); setInstances(instanceResponse.items); setJobs(jobResponse.items); setForm((current) => ({ ...current, pluginId: pluginResponse.items.some((plugin) => plugin.id === current.pluginId) ? current.pluginId : pluginResponse.items[0]?.id || "", runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId) ? current.runEndpointId : endpointResponse.items[0]?.id || "" })); setListState("ready"); setListError(""); } catch (error) { setListState("error"); setListError(error instanceof Error ? error.message : "加载失败"); } setMetricsPending(true); try { const metricsResponse = await platformApiClient.listServerMetrics(); setMetrics(new Map(metricsResponse.items.map((item) => [item.serverInstanceId, item]))); } catch { setMetrics(new Map()); } finally { setMetricsPending(false); } }, []); useEffect(() => { void refresh(); }, [refresh]); const cards = useMemo( () => instances.map((instance) => ({ instance, metrics: metrics.get(instance.id), pendingJobs: pendingJobsForServer(jobs, instance.id).length })), [instances, jobs, metrics] ); const visibleCards = useMemo(() => filterServerCards(cards, keyword, statusFilter), [cards, keyword, statusFilter]); const createPending = operations.isPending("platform", "创建服务器"); function updateForm(event: ChangeEvent) { const { name, value } = event.target; setForm((current) => ({ ...current, [name]: value })); } async function handleCreate(event: FormEvent) { event.preventDefault(); const operationId = operations.begin({ intent: "创建服务器", targetKind: "server", targetId: "platform", requester: session.displayName }); try { const result = await platformApiClient.createServerWorkflow(serverCreateRequestFromForm(form)); operations.succeed(operationId, `已创建实例 ${result.instance.id},安装任务 ${result.job.id} 已派发`, result.job); setForm(defaultServerCreateForm(plugins, endpoints)); setShowCreate(false); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "创建失败", operationId); } } async function handleQuickRuntimeAction(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction) { const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId); const intent = quickRuntimeActionLabel(action); const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName }); try { let message = "运行操作已提交"; if (action === "generate-run") { const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, defaults.runOs, "amd64")); message = `run 包已生成,artifact ${distribution.artifactId}`; } else if (action === "download-run") { const reference = await platformApiClient.downloadLatestRunDistribution(instance.id); message = `run 下载引用已创建,artifact ${reference.artifactId}`; } else if (action === "push-run-update") { const reference = await platformApiClient.downloadLatestRunDistribution(instance.id); const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum)); message = `run 更新任务已排队,job ${update.jobId ?? update.id}`; } else if (action === "generate-client-manager") { const distribution = await platformApiClient.generateClientManager( instance.id, clientManagerBuildRequest({ serverInstanceId: instance.id, profileKey: defaults.clientProfileKey, targetOs: defaults.clientOs, targetArch: "amd64", repositoryUrl: defaults.repositoryUrl, sourceRevision: "main" }) ); message = `客户端管理器已生成,artifact ${distribution.artifactId}`; } else if (action === "dependencies-check") { const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey)); message = `依赖检查任务已排队,job ${job.id}`; } else if (action === "dependencies-install") { const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey, defaults.installPlanKey)); message = `依赖安装任务已排队,job ${job.id}`; } else if (action === "live-logs") { onNavigate("serverDetail", { serverId: instance.id }); message = "已打开服务器详情,可切换到日志页查看实时日志"; } else if (action === "historical-logs") { const job = await platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, defaults.logSourceKey)); message = `历史日志回填任务已排队,job ${job.id}`; } operations.succeed(operationId, message); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "运行操作失败", operationId); } } const latestCreate = operations.operations.find((operation) => operation.intent === "创建服务器"); return (

{isPlatformAdmin(session) ? "平台管理员" : "我的服务器"}

服务器管理

{latestCreate && (
)} {showCreate && (
void handleCreate(event)} aria-label="创建服务器">

创建服务器

)}
{listState === "loading" && } {listState === "error" && void refresh()} />} {listState === "ready" && cards.length === 0 && ( } title="暂无可管理的服务器" description={ isPlatformAdmin(session) ? "平台还没有服务器实例。点击上方“创建服务器”开始,或检查运行节点状态。" : "当前账号名下没有可管理的服务器。如果这不符合预期,请联系平台管理员为你分配服务器,或点击刷新重试。" } actionLabel="刷新" onAction={() => void refresh()} /> )} {listState === "ready" && cards.length > 0 && visibleCards.length === 0 && ( { setKeyword(""); setStatusFilter("all"); }} /> )} {visibleCards.length > 0 && (
{visibleCards.map((card) => ( onNavigate("serverDetail", { serverId: card.instance.id })} onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)} /> ))}
)}
); } type ServerQuickRuntimeAction = | "generate-run" | "download-run" | "push-run-update" | "generate-client-manager" | "dependencies-check" | "dependencies-install" | "live-logs" | "historical-logs"; interface ServerCardProps { card: ServerCardView; metricsPending: boolean; onOpen: () => void; onQuickAction: (action: ServerQuickRuntimeAction) => void; } function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardProps) { const { instance, metrics, pendingJobs } = card; const online = serverIsOnline(instance.state); return (
{instance.name} {instance.id} {stateLabel(instance.state)}
玩家 {formatStat(metrics?.playerCount, metricsPending, (value) => (metrics?.maxPlayers ? `${value}/${metrics.maxPlayers}` : `${value}`))} TPS {formatStat(metrics?.tps, metricsPending, (value) => value.toFixed(1))} 延迟 {formatStat(metrics?.latencyMs, metricsPending, (value) => `${Math.round(value)}ms`)} 任务 {pendingJobs > 0 ? `${pendingJobs} 进行中` : online ? "空闲" : "--"}
运行操作
{serverQuickActions.map((action) => ( ))}
); } const serverQuickActions: ServerQuickRuntimeAction[] = [ "generate-run", "download-run", "push-run-update", "generate-client-manager", "dependencies-check", "dependencies-install", "live-logs", "historical-logs" ]; function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string { switch (action) { case "generate-run": return "生成 run"; case "download-run": return "下载 run"; case "push-run-update": return "推送更新"; case "generate-client-manager": return "生成客户端"; case "dependencies-check": return "依赖检查"; case "dependencies-install": return "依赖安装"; case "live-logs": return "实时日志"; case "historical-logs": return "历史日志"; } } function quickRuntimeDefaultsForPlugin(pluginId: string) { const isScum = pluginId.toLowerCase().includes("scum"); return { runOs: isScum ? "windows" : "linux", clientOs: "windows", clientProfileKey: isScum ? "scum-client-manager" : "client-manager", repositoryUrl: isScum ? "https://github.com/F88888/scum_client.git" : "https://github.com/example/client-manager.git", probeKey: isScum ? "steamcmd" : "java-21", installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux", logSourceKey: isScum ? "server-log" : "latest-log" }; } function formatStat(value: number | undefined, pending: boolean, format: (value: number) => string): string { if (typeof value === "number" && Number.isFinite(value)) { return format(value); } return pending ? "…" : "--"; } export function stateLabel(state: ServerInstanceResponse["state"]): string { switch (state) { case "installing": return "安装中"; case "ready": return "就绪"; case "running": return "运行中"; case "stopped": return "已停止"; case "failed": return "异常"; case "draft": return "草稿"; case "deleted": return "已删除"; } } export function statusClass(state: ServerInstanceResponse["state"]): string { if (state === "running" || state === "ready") { return "status-active"; } if (state === "failed" || state === "deleted") { return "status-error"; } return "status-disabled"; }