import { AlertTriangle, CakeSlice, Candy, Search, Sparkles, Trash2 } from "lucide-react"; import { type CSSProperties, 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, runtimeKeyResetStages, runtimeLogStages, runtimeRunBuildStages, runtimeUpdateStages, useRuntimeTaskController } from "../components/RuntimeTaskProgress"; import { ConfirmDialog, ManagementDialog, UsageMeter } from "../components/OperationControls"; import { ServerDeploymentWorkflow } from "../components/ServerDeploymentWorkflow"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { canDeleteServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, 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, serverDeleteConfirmation } from "../schemas/serverManagement"; import { isPlatformAdmin } from "../contracts/workspace"; import { downloadArtifactReference, safeArtifactFilename } from "../utils/artifactTransfer"; import { cx } from "../utils/classes"; type ListState = "loading" | "ready" | "error"; interface RunTargetSelectionState { instance: ServerInstanceResponse; targetOs: string; targetArch: string; } const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [ { id: "all", label: "全部" }, { id: "online", label: "在线" }, { id: "offline", label: "离线" }, { id: "attention", label: "需关注" } ]; const serverListRefreshMs = 5000; const serverMetricFreshMs = 30000; 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 [editDeployment, setEditDeployment] = useState<{ instance: ServerInstanceResponse; deployment: import("../api/types").ServerDeploymentResponse } | null>(null); const runtimeTask = useRuntimeTaskController(); const [runtimeTaskActions, setRuntimeTaskActions] = useState([]); const [deleteConfirmation, setDeleteConfirmation] = useState | null>(null); const [deletePassword, setDeletePassword] = useState(""); const [deleteBusy, setDeleteBusy] = useState(false); const [runTargetSelection, setRunTargetSelection] = useState(null); const refreshList = useCallback(async (showLoading = true) => { if (showLoading) 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); if (showLoading) 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 || "" }; }); if (showLoading) { setListState("ready"); setListError(""); } } catch (error) { if (showLoading) 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]); useEffect(() => { const timer = window.setInterval(() => { void refreshList(false); void refreshMetrics(); }, serverListRefreshMs); return () => window.clearInterval(timer); }, [refreshList, refreshMetrics]); 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"); async function handleCreate(nextForm: ServerCreateFormState) { const operationId = operations.begin({ intent: "创建服务器", targetKind: "server", targetId: "platform", requester: session.displayName }); try { const result = await platformApiClient.createServerWorkflow(serverCreateRequestFromForm(nextForm)); operations.succeed(operationId, result.job.id ? `已创建实例 ${result.instance.id},安装任务 ${result.job.id} 已派发` : `已创建服务器 ${result.instance.id};部署方式和启动项已随创建向导保存,请生成并启动专属 Run。`, result.job.id ? result.job : undefined); setForm(defaultServerCreateForm(plugins, endpoints)); setShowCreate(false); await refresh(); onNavigate("serverDetail", { serverId: result.instance.id, routeKey: "run-builder" }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "创建失败", operationId); } } async function openEditDeployment(instance: ServerInstanceResponse) { try { const deployment = await platformApiClient.getServerDeployment(instance.id); const plugin = plugins.find((item) => item.id === instance.pluginId); setForm({ ...defaultServerCreateForm(plugins, endpoints), name: instance.name, pluginId: instance.pluginId, runEndpointId: instance.runEndpointId, profileKey: deployment.profileKey ?? plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", createInputs: deployment.createInputs ?? pluginCreateInputDefaults(plugin), deploymentMode: deployment.mode ?? "guided-install", shell: deployment.shell ?? "" }); setEditDeployment({ instance, deployment }); } catch (error) { const operationId = operations.begin({ intent: "读取部署设置", targetKind: "server", targetId: instance.id, requester: session.displayName }); operations.fail(operationId, error instanceof Error ? error.message : "部署设置加载失败"); } } async function handleUpdateDeployment(nextForm: ServerCreateFormState) { if (!editDeployment) return; const { instance } = editDeployment; const operationId = operations.begin({ intent: "更新部署定义", targetKind: "server", targetId: instance.id, requester: session.displayName }); try { await platformApiClient.updateServerDeployment(instance.id, { runEndpointId: nextForm.runEndpointId || undefined, mode: nextForm.deploymentMode, profileKey: nextForm.profileKey || undefined, createInputs: nextForm.createInputs, serverRoot: nextForm.serverRoot.trim() || undefined, workingDirectory: nextForm.workingDirectory.trim() || undefined, installCommand: nextForm.installCommand.trim() || undefined, startCommand: nextForm.startCommand.trim() || undefined, stopCommand: nextForm.stopCommand.trim() || undefined, statusCommand: nextForm.statusCommand.trim() || undefined, shell: nextForm.shell || undefined }); operations.succeed(operationId, "部署设置已保存;路径和命令保持受保护状态。"); setEditDeployment(null); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "部署设置保存失败"); } } async function handleDeleteServer() { if (!deleteConfirmation) { return; } setDeleteBusy(true); const operationId = operations.begin({ intent: "删除服务器", targetKind: "server", targetId: deleteConfirmation.serverInstanceId, requester: session.displayName }); try { await platformApiClient.deleteServerInstance(deleteConfirmation.serverInstanceId, { password: deletePassword }); operations.succeed(operationId, `服务器已删除:${deleteConfirmation.serverInstanceId}`); setDeleteConfirmation(null); setDeletePassword(""); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "服务器删除失败", operationId); } finally { setDeleteBusy(false); setDeletePassword(""); } } function openRunTargetSelection(instance: ServerInstanceResponse) { const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId); setRuntimeTaskActions([]); setRunTargetSelection({ instance, targetOs: defaults.runOs, targetArch: "amd64" }); } async function handleRunTargetSubmit(event: FormEvent) { event.preventDefault(); if (!runTargetSelection) { return; } const { instance, targetOs, targetArch } = runTargetSelection; setRunTargetSelection(null); await generateRunForTarget(instance, targetOs, targetArch); } async function generateRunForTarget(instance: ServerInstanceResponse, targetOs: string, targetArch: string) { const intent = quickRuntimeActionLabel("generate-run"); const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:generate-run`, requester: session.displayName }); setRuntimeTaskActions([]); try { const distribution = await runtimeTask.runTrackedTask({ title: intent, description: `${instance.name}(${instance.id})选择生成平台 ${targetOs}/${targetArch},正在拉取 run 更新并构建可下载产物。`, stages: runtimeRunBuildStages, start: async () => { await requireQuickRuntimeActionAvailable(instance.id, "generate-run"); const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, targetOs, targetArch)); return { value: distribution, jobId: distribution.buildJobId }; }, poll: (jobId) => platformApiClient.getJob(jobId) }); const artifact = { artifactId: distribution.artifactId, checksum: distribution.checksum }; const message = `run ${distribution.targetOs}/${distribution.targetArch} 已构建完成,artifact ${distribution.artifactId}`; operations.succeed(operationId, message); runtimeTask.succeedTask(message); setRuntimeTaskActions([ { label: "下载 run", kind: "primary", onClick: () => void downloadGeneratedRun(instance, artifact) }, { label: "更新 run", disabled: !serverIsOnline(instance.state), title: serverIsOnline(instance.state) ? "更新 run" : "run 未运行,无法在线更新", onClick: () => void pushGeneratedRunUpdate(instance, artifact) } ]); await refresh(); } catch (error) { const message = error instanceof Error ? error.message : "运行操作失败"; operations.fail(operationId, message, operationId); runtimeTask.failTask(message); } } async function handleQuickRuntimeAction(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction) { if (action === "generate-run") { openRunTargetSelection(instance); return; } if (action === "reset-run-key") { const confirmed = typeof window === "undefined" || window.confirm("重置 run 密钥会立刻断开已部署 run,并要求重新生成新的 run 可执行文件。继续?"); if (!confirmed) { return; } } 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 generatedClientProfile: string | null = null; try { await requireQuickRuntimeActionAvailable(instance.id, action); let message: string; 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 === "reset-run-key") { const key = await platformApiClient.resetRunKey(instance.id); return `run 密钥已重置到第 ${key.generation} 代,旧 run 会话已失效,请重新生成并部署 run`; } 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}`; } const job = await platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, defaults.logSourceKey)); return `历史日志回填任务已排队,job ${job.id}`; } }); } operations.succeed(operationId, message); runtimeTask.succeedTask(message); 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 === "创建服务器"); const runTargetPlugin = runTargetSelection ? plugins.find((plugin) => plugin.id === runTargetSelection.instance.pluginId) : undefined; const runTargetOsOptions = runPlatformOptions(runTargetPlugin, runTargetSelection?.targetOs ?? "linux"); const runTargetArchOptions = ["amd64", "arm64"]; return (

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

服务器管理

{metricsError && (
服务器指标不可用:{metricsError}
)} {latestCreate && (
)} setShowCreate(false)} onSubmit={handleCreate} /> platformApiClient.revealServerDeployment(editDeployment?.instance.id ?? "")} onClose={() => setEditDeployment(null)} onSubmit={handleUpdateDeployment} /> setRunTargetSelection(null)} >
void handleRunTargetSubmit(event)} aria-label="选择生成平台">
{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 })} onEdit={() => void openEditDeployment(card.instance)} onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)} onDelete={() => { setDeletePassword(""); setDeleteConfirmation(serverDeleteConfirmation(card.instance)); }} /> ))}
)} { setDeleteConfirmation(null); setDeletePassword(""); }} onConfirm={() => void handleDeleteServer()} >
); } type ServerQuickRuntimeAction = | "generate-run" | "download-run" | "push-run-update" | "reset-run-key" | "generate-client-manager" | "dependencies-check" | "dependencies-install" | "historical-logs"; async function requireQuickRuntimeActionAvailable(serverInstanceId: string, action: ServerQuickRuntimeAction): Promise { const runtimeActions = await platformApiClient.getServerRuntimeActions(serverInstanceId); const runtimeAction = runtimeActions.actions.find((candidate) => candidate.key === action); if (!runtimeAction) { throw new Error("平台未返回该运行操作"); } if (!runtimeAction.available) { throw new Error(`该操作不可用:${runtimeAction.reason || "平台暂未开放该操作"}`); } } function serverDeleteDisabledReason(session: PageComponentProps["session"], instance: ServerInstanceResponse): string { if (!isPlatformAdmin(session) && instance.ownerUserId !== session.id) { return "仅创建人或平台管理员可删除"; } if (!canDeleteServer(instance.state)) { return "运行中、安装中或已删除的服务器不能直接删除"; } return ""; } interface ServerCardProps { card: ServerCardView; metricsPending: boolean; metricsUnavailable: boolean; canManage: boolean; deleteDisabledReason: string; onOpen: () => void; onEdit: () => void; onQuickAction: (action: ServerQuickRuntimeAction) => void; onDelete: () => void; } function ServerCard({ card, metricsPending, metricsUnavailable, canManage, deleteDisabledReason, onOpen, onEdit, onQuickAction, onDelete }: ServerCardProps) { const { instance, metrics, pendingJobs, failedJobs = 0 } = card; const online = serverIsOnline(instance.state); const canDelete = deleteDisabledReason === ""; const canOpenActions = canManage || canDelete; const metricsWaiting = metrics?.source === "run-metrics-pending"; const metricsStale = isMetricsStale(metrics); 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 = 288; 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) => { if (!canManage) { return; } 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 && 指标不可用} {!metricsUnavailable && metricsWaiting && 等待指标上报} {!metricsUnavailable && metricsStale && 指标过期} {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", "historical-logs"] } ]; function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string { switch (action) { case "generate-run": return "生成 run"; case "download-run": return "下载 run"; case "push-run-update": return "更新 run"; case "reset-run-key": return "重置 run 密钥"; case "generate-client-manager": return "生成客户端"; case "dependencies-check": return "依赖检查"; case "dependencies-install": return "依赖安装"; case "historical-logs": return "历史日志"; } } function quickRuntimeStages(action: ServerQuickRuntimeAction) { if (action === "generate-run") { return runtimeRunBuildStages; } if (action === "generate-client-manager") { return runtimeBuildStages; } if (action === "download-run") { return runtimeDownloadStages; } if (action === "push-run-update") { return runtimeUpdateStages; } if (action === "reset-run-key") { return runtimeKeyResetStages; } 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; } if (action === "reset-run-key") { 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 ? "scum-server-events" : "latest-log" }; } function runPlatformOptions(plugin: GamePluginResponse | undefined, fallback: string): string[] { const options = new Set(); const add = (value: string | undefined) => { const normalized = value?.trim().toLowerCase(); if (normalized) { options.add(normalized); } }; add(fallback); plugin?.supportedOs?.forEach(add); plugin?.runtimeProfiles?.lifecycleProfiles?.forEach((profile) => profile.platforms?.forEach(add)); ["linux", "windows", "darwin"].forEach(add); return [...options]; } function runPlatformLabel(platform: string): string { switch (platform) { case "linux": return "Linux"; case "windows": return "Windows"; case "darwin": return "macOS"; default: return platform; } } function formatStat(value: number | undefined, pending: boolean, format: (value: number) => string): string { if (typeof value === "number" && Number.isFinite(value)) { return format(value); } return pending ? "…" : "--"; } function isMetricsStale(metrics?: ServerMetricsResponse): boolean { if (!metrics || metrics.source === "run-metrics-pending") return false; const collectedAt = new Date(metrics.collectedAt).getTime(); return Number.isFinite(collectedAt) && Date.now() - collectedAt > serverMetricFreshMs; } 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"; }