import { AlertTriangle, CakeSlice, Candy, Search, Sparkles } from "lucide-react"; import { type CSSProperties, type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { platformApiClient } from "../api/client"; import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types"; import { RuntimeTaskProgressDialog, type RuntimeTaskDialogAction, runtimeBuildStages, runtimeDependencyStages, runtimeDownloadStages, runtimeLogStages, runtimeUpdateStages, useRuntimeTaskController } from "../components/RuntimeTaskProgress"; import { ManagementDialog, UsageMeter } from "../components/OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { defaultServerCreateForm, endpointLabel, pluginLabel, runtimeBindingFields, type ServerCreateFormState } from "../contracts/serverManagement"; import { summarizeServerOperations } from "../contracts/operationsConsole"; 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 { downloadArtifactReference, safeArtifactFilename } from "../utils/artifactTransfer"; 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 [metricsError, setMetricsError] = useState(""); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [form, setForm] = useState(() => defaultServerCreateForm([], [])); const [showCreate, setShowCreate] = useState(false); const runtimeTask = useRuntimeTaskController(); const [runtimeTaskActions, setRuntimeTaskActions] = useState([]); const refreshList = 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) => { const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0]; const profileKey = plugin?.runtimeProfiles?.lifecycleProfiles?.some((profile) => profile.key === current.profileKey) ? current.profileKey : plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? ""; return { ...current, pluginId: plugin?.id ?? "", profileKey, bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {}, 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 : "加载失败"); } }, []); const refreshMetrics = useCallback(async () => { setMetricsPending(true); try { const metricsResponse = await platformApiClient.listServerMetrics(); setMetrics(new Map(metricsResponse.items.map((item) => [item.serverInstanceId, item]))); setMetricsError(""); } catch (error) { setMetrics(new Map()); setMetricsError(error instanceof Error ? error.message : "服务器指标加载失败"); } finally { setMetricsPending(false); } }, []); const refresh = useCallback(async () => { await Promise.all([refreshList(), refreshMetrics()]); }, [refreshList, refreshMetrics]); useEffect(() => { void refresh(); }, [refresh]); const cards = useMemo( () => summarizeServerOperations(instances, metrics, jobs).map((summary) => ({ instance: summary.instance, metrics: summary.metrics, pendingJobs: summary.activeJobs, activeJobs: summary.activeJobs, failedJobs: summary.failedJobs, latestJob: summary.latestJob })), [instances, jobs, metrics] ); const visibleCards = useMemo(() => filterServerCards(cards, keyword, statusFilter), [cards, keyword, statusFilter]); const createPending = operations.isPending("platform", "创建服务器"); const canManageServers = session.capabilities.includes("servers.manage"); const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId); const createProfileOptions = selectedCreatePlugin?.runtimeProfiles?.lifecycleProfiles ?? []; const createProfileUnavailable = Boolean(selectedCreatePlugin && createProfileOptions.length === 0); const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey); function updateForm(event: ChangeEvent) { const { name, value } = event.target; setForm((current) => { if (name === "pluginId") { const plugin = plugins.find((item) => item.id === value); return { ...current, pluginId: value, profileKey: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", bindings: {} }; } if (name === "profileKey") { return { ...current, profileKey: value, bindings: {} }; } return { ...current, [name]: value }; }); } function updateBinding(key: string, value: string) { setForm((current) => ({ ...current, bindings: { ...current.bindings, [key]: 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 }); setRuntimeTaskActions([]); let generatedRunArtifact: { artifactId: string; checksum?: string } | null = null; let generatedClientProfile: string | null = null; try { let message: string; if (action === "generate-run") { const distribution = await runtimeTask.runTrackedTask({ title: intent, description: quickRuntimeTaskDescription(instance, action), stages: runtimeBuildStages, start: async () => { const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, defaults.runOs, "amd64")); return { value: distribution, jobId: distribution.buildJobId }; }, poll: (jobId) => platformApiClient.getJob(jobId) }); generatedRunArtifact = { artifactId: distribution.artifactId }; message = `run 二进制已构建并上传,artifact ${distribution.artifactId}`; } else if (action === "generate-client-manager") { const distribution = await runtimeTask.runTrackedTask({ title: intent, description: quickRuntimeTaskDescription(instance, action), stages: runtimeBuildStages, start: async () => { const distribution = await platformApiClient.generateClientManager( instance.id, clientManagerBuildRequest({ serverInstanceId: instance.id, profileKey: defaults.clientProfileKey, targetOs: defaults.clientOs, targetArch: "amd64", repositoryUrl: defaults.repositoryUrl, sourceRevision: "main" }) ); return { value: distribution, jobId: distribution.buildJobId }; }, poll: (jobId) => platformApiClient.getJob(jobId) }); generatedClientProfile = defaults.clientProfileKey; message = `客户端二进制已构建并上传,artifact ${distribution.artifactId}`; } else { message = await runtimeTask.runTask({ title: intent, description: quickRuntimeTaskDescription(instance, action), stages: quickRuntimeStages(action), executeStageIndex: quickRuntimeExecuteStageIndex(action), execute: async () => { if (action === "download-run") { const reference = await platformApiClient.downloadLatestRunDistribution(instance.id); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`; } 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)); return `run 更新任务已排队,job ${update.jobId ?? update.id}`; } if (action === "dependencies-check") { const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey)); return `依赖检查任务已排队,job ${job.id}`; } if (action === "dependencies-install") { const catalog = await platformApiClient.getDependencyCatalog(instance.id); const plan = catalog.plans.find((candidate) => candidate.key === defaults.installPlanKey); const probe = catalog.probes.find((candidate) => candidate.key === defaults.probeKey); if (!plan || probe?.installPlanKey !== plan.key) throw new Error("Platform 未返回与当前 probe 匹配的审核安装计划"); const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probe.key, plan.key, plan.digest)); return `依赖安装任务已排队,job ${job.id}`; } if (action === "live-logs") { onNavigate("serverDetail", { serverId: instance.id }); return "已打开服务器详情,可切换到日志页查看实时日志"; } const job = await platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, defaults.logSourceKey)); return `历史日志回填任务已排队,job ${job.id}`; } }); } operations.succeed(operationId, message); runtimeTask.succeedTask(message); if (generatedRunArtifact) { const artifact = generatedRunArtifact; setRuntimeTaskActions([ { label: "下载 run", kind: "primary", onClick: () => void downloadGeneratedRun(instance, artifact) }, { label: "推送更新", onClick: () => void pushGeneratedRunUpdate(instance, artifact) } ]); } else if (generatedClientProfile) { const clientProfile = generatedClientProfile; setRuntimeTaskActions([ { label: "下载客户端", kind: "primary", onClick: () => void downloadGeneratedClient(instance, clientProfile) } ]); } await refresh(); } catch (error) { const message = error instanceof Error ? error.message : "运行操作失败"; operations.fail(operationId, message, operationId); runtimeTask.failTask(message); } } async function downloadGeneratedRun(instance: ServerInstanceResponse, artifact: { artifactId: string; checksum?: string }) { setRuntimeTaskActions([]); try { const message = await runtimeTask.runTask({ title: "下载 run", description: `${instance.name} 的 run 包已生成,正在打开 artifact ${artifact.artifactId}。`, stages: runtimeDownloadStages, executeStageIndex: 1, execute: async () => { const reference = await platformApiClient.openArtifactDownload(artifact.artifactId); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return `run 下载已开始,文件 ${safeArtifactFilename(reference.filename)}`; } }); runtimeTask.succeedTask(message); } catch (error) { runtimeTask.failTask(error instanceof Error ? error.message : "run 下载失败"); } } async function pushGeneratedRunUpdate(instance: ServerInstanceResponse, artifact: { artifactId: string; checksum?: string }) { setRuntimeTaskActions([]); const operationId = operations.begin({ intent: "推送 run 更新", targetKind: "server", targetId: `${instance.id}:push-run-update`, requester: session.displayName }); try { const message = await runtimeTask.runTask({ title: "推送 run 更新", description: `${instance.name} 将使用刚生成的 artifact ${artifact.artifactId} 派发 run 自更新任务。`, stages: runtimeUpdateStages, executeStageIndex: 2, execute: async () => { const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, artifact.artifactId, artifact.checksum)); return `run 更新任务已排队,job ${update.jobId ?? update.id}`; } }); operations.succeed(operationId, message); runtimeTask.succeedTask(message); await refresh(); } catch (error) { const message = error instanceof Error ? error.message : "推送 run 更新失败"; operations.fail(operationId, message, operationId); runtimeTask.failTask(message); } } async function downloadGeneratedClient(instance: ServerInstanceResponse, profileKey: string) { setRuntimeTaskActions([]); try { const message = await runtimeTask.runTask({ title: "下载客户端", description: `${instance.name} 的客户端管理器已生成,正在创建下载引用。`, stages: runtimeDownloadStages, executeStageIndex: 1, execute: async () => { const reference = await platformApiClient.downloadLatestClientManager(instance.id, { profileKey }); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return `客户端下载已开始,文件 ${safeArtifactFilename(reference.filename)}`; } }); runtimeTask.succeedTask(message); } catch (error) { runtimeTask.failTask(error instanceof Error ? error.message : "客户端下载失败"); } } const latestCreate = operations.operations.find((operation) => operation.intent === "创建服务器"); return (

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

服务器管理

{metricsError && (
服务器指标不可用:{metricsError}
)} {latestCreate && (
)} { if (!createPending) setShowCreate(false); }} >
void handleCreate(event)} aria-label="创建服务器">
{createProfileUnavailable && (
当前插件没有声明运行配置,请重新注册完整 manifest 后刷新。
)} {createBindingFields.map((field) => ( ))}
{listState === "loading" && } {listState === "error" && void refreshList()} />} {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; metricsUnavailable: boolean; canManage: boolean; onOpen: () => void; onQuickAction: (action: ServerQuickRuntimeAction) => void; } function ServerCard({ card, metricsPending, metricsUnavailable, canManage, onOpen, onQuickAction }: ServerCardProps) { const { instance, metrics, pendingJobs, failedJobs = 0 } = card; const online = serverIsOnline(instance.state); const menuButtonRef = useRef(null); const menuPanelRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); const [menuStyle, setMenuStyle] = useState({}); const closeMenu = useCallback(() => setMenuOpen(false), []); const openMenu = useCallback(() => { const trigger = menuButtonRef.current; if (!trigger) { setMenuOpen(true); return; } const rect = trigger.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const menuWidth = Math.min(320, Math.max(220, viewportWidth - 24)); const estimatedMenuHeight = 232; const left = Math.min(Math.max(12, rect.right - menuWidth), Math.max(12, viewportWidth - menuWidth - 12)); const belowTop = rect.bottom + 8; const top = belowTop + estimatedMenuHeight <= viewportHeight - 12 ? belowTop : Math.max(12, rect.top - estimatedMenuHeight - 8); setMenuStyle({ left, top, width: menuWidth }); setMenuOpen(true); }, []); const toggleMenu = useCallback(() => { if (menuOpen) { closeMenu(); return; } openMenu(); }, [closeMenu, menuOpen, openMenu]); useEffect(() => { if (!menuOpen) { return undefined; } const handlePointerDown = (event: PointerEvent) => { const target = event.target; if (!(target instanceof Node)) { return; } if (menuButtonRef.current?.contains(target) || menuPanelRef.current?.contains(target)) { return; } closeMenu(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { closeMenu(); menuButtonRef.current?.focus(); } }; document.addEventListener("pointerdown", handlePointerDown, true); document.addEventListener("keydown", handleKeyDown); window.addEventListener("resize", closeMenu); window.addEventListener("scroll", closeMenu, true); return () => { document.removeEventListener("pointerdown", handlePointerDown, true); document.removeEventListener("keydown", handleKeyDown); window.removeEventListener("resize", closeMenu); window.removeEventListener("scroll", closeMenu, true); }; }, [closeMenu, menuOpen]); const chooseQuickAction = (action: ServerQuickRuntimeAction) => { closeMenu(); onQuickAction(action); }; 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`)} 任务 {failedJobs > 0 ? `${failedJobs} 失败` : pendingJobs > 0 ? `${pendingJobs} 进行中` : online ? "空闲" : "--"}
{metricsUnavailable && 指标不可用} {failedJobs > 0 && 存在失败任务,打开详情恢复}
{menuOpen && typeof document !== "undefined" && createPortal(
{serverQuickActionGroups.map((group) => (
{group.label}
{group.actions.map((action) => ( ))}
))}
, document.body )}
); } const serverQuickActionGroups: Array<{ label: string; actions: ServerQuickRuntimeAction[] }> = [ { label: "运行分发", actions: ["generate-run", "download-run", "push-run-update", "generate-client-manager"] }, { label: "诊断维护", actions: ["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 quickRuntimeStages(action: ServerQuickRuntimeAction) { if (action === "generate-run" || action === "generate-client-manager") { return runtimeBuildStages; } if (action === "download-run") { return runtimeDownloadStages; } if (action === "push-run-update") { return runtimeUpdateStages; } if (action === "dependencies-check" || action === "dependencies-install") { return runtimeDependencyStages; } return runtimeLogStages; } function quickRuntimeExecuteStageIndex(action: ServerQuickRuntimeAction): number { if (action === "generate-run" || action === "generate-client-manager") { return 3; } if (action === "push-run-update") { return 2; } return 1; } function quickRuntimeTaskDescription(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction): string { const label = quickRuntimeActionLabel(action); return `${instance.name}(${instance.id})${label},通过平台 API 派发并保留可追踪进度。`; } 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"; }