import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react"; import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { ConfigDiffLineResponse, ArtifactDownloadReferenceResponse, ArtifactResponse, BackupResponse, ClientManagerDistributionResponse, DependencyCatalogResponse, GamePluginResponse, JobResponse, LogEntryBody, LogStreamResponse, RunDistributionResponse, RunUpdateJobResponse, ServerConfigDiffPreviewResponse, ServerConfigResponse, ServerInstanceResponse, ServerMemberResponse, ServerMetricsResponse, RuntimeBindingResponse, RunEndpointResponse, ServerDeploymentResponse, ServerRuntimeActionsResponse, MetricSampleResponse, RemoteAdapterDeclarationResponse } from "../api/types"; import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationControls"; import { ServerDeploymentWorkflow } from "../components/ServerDeploymentWorkflow"; import { ClientManagerLifecyclePanel } from "../components/ClientManagerLifecyclePanel"; import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel"; import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench"; import { RuntimeDLLExtensionsPanel } from "../components/RuntimeDLLExtensionsPanel"; import { SourceRCONCommandPanel } from "../components/SourceRCONCommandPanel"; import { RuntimeTaskProgressDialog, runtimeBuildStages, runtimeDependencyStages, runtimeDownloadStages, runtimeLogStages, runtimeRunBuildStages, runtimeUpdateStages, type RuntimeTaskDialogAction, type RuntimeTaskStage, useRuntimeTaskController } from "../components/RuntimeTaskProgress"; import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import type { PluginBridgeAction, PluginBridgeManifestContract } from "../contracts/pluginBridge"; import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement"; import { serverDetailSections, serverIsOnline, isPlatformAdmin, type ConfigDiffView, type LlmSuggestionView, type PluginControlDescriptor, type PluginControlGroupView, type ServerDetailSection } from "../contracts/workspace"; import { clientManagerBuildRequest, dependencyJobRequest, logBackfillRequest, runDistributionGenerateRequest, runUpdateRequest, serverLifecycleCommandRequest, serverMetadataUpdateRequestFromForm } from "../schemas/serverManagement"; import { diffHasChanges } from "../utils/diff"; import { createPluginBridgeDispatcher, createPluginBridgeHostContext, parsePluginArtifactReference } from "../utils/pluginBridgeHost"; import { downloadArtifactReference, safeArtifactError, safeArtifactFilename } from "../utils/artifactTransfer"; import { cx } from "../utils/classes"; import { stateLabel, statusClass } from "./ServersPage"; type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T }; const defaultConfigKey = "server.properties"; export function ServerDetailPage({ session, params, operations, onNavigate }: PageComponentProps) { const serverId = params.serverId ?? ""; const [section, setSection] = useState("overview"); const [instance, setInstance] = useState>({ status: "loading" }); const [metrics, setMetrics] = useState(null); const [plugins, setPlugins] = useState([]); const [jobs, setJobs] = useState([]); const [artifacts, setArtifacts] = useState([]); const [metricHistory, setMetricHistory] = useState([]); const [backups, setBackups] = useState([]); const [remoteAdapters, setRemoteAdapters] = useState([]); const [runtimeActions, setRuntimeActions] = useState>({ status: "loading" }); const [runtimeBinding, setRuntimeBinding] = useState>({ status: "loading" }); const [deployment, setDeployment] = useState>({ status: "loading" }); const [endpoints, setEndpoints] = useState([]); const [showDeploymentEditor, setShowDeploymentEditor] = useState(false); const [confirm, setConfirm] = useState Promise }>(null); const [confirmBusy, setConfirmBusy] = useState(false); const refresh = useCallback(async () => { if (!serverId) { setInstance({ status: "error", reason: "缺少服务器 ID" }); return; } setInstance({ status: "loading" }); try { const [detail, pluginResponse, endpointResponse, jobResponse, runtimeResponse, bindingResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse] = await Promise.all([ platformApiClient.getServerInstance(serverId), platformApiClient.listGamePlugins(), platformApiClient.listRunEndpoints(), platformApiClient.listJobs(serverId), platformApiClient .getServerRuntimeActions(serverId) .then((data): LoadState => ({ status: "ready", data })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "运行分发状态加载失败" })), platformApiClient .getServerRuntimeBinding(serverId) .then((data): LoadState => ({ status: "ready", data })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "运行配置加载失败" })), platformApiClient .getServerDeployment(serverId) .then((data): LoadState => ({ status: "ready", data })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })), platformApiClient.listMetricHistory(serverId).catch(() => ({ items: [], count: 0 })), platformApiClient.listBackups(serverId).catch(() => ({ items: [], count: 0 })), platformApiClient.listRemoteAdapters(serverId).catch(() => ({ items: [], count: 0 })) ]); setInstance({ status: "ready", data: detail }); setPlugins(pluginResponse.items); setEndpoints(endpointResponse.items); setJobs(jobResponse.items); setRuntimeActions(runtimeResponse); setRuntimeBinding(bindingResponse); setDeployment(deploymentResponse); setMetricHistory(metricHistoryResponse.items); setBackups(backupResponse.items); setRemoteAdapters(adapterResponse.items); const artifactLists = await Promise.all( jobResponse.items.slice(0, 20).map((job) => platformApiClient .listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" }) .then((response) => response.items) .catch(() => [] as ArtifactResponse[]) ) ); setArtifacts(uniqueArtifacts(artifactLists.flat())); } catch (error) { setInstance({ status: "error", reason: error instanceof Error ? error.message : "加载失败" }); setArtifacts([]); setRuntimeActions({ status: "error", reason: "运行分发状态加载失败" }); setRuntimeBinding({ status: "error", reason: "运行配置加载失败" }); setDeployment({ status: "error", reason: "部署定义加载失败" }); setMetricHistory([]); setBackups([]); setRemoteAdapters([]); } try { const metricsResponse = await platformApiClient.listServerMetrics(); setMetrics(metricsResponse.items.find((item) => item.serverInstanceId === serverId) ?? null); } catch { setMetrics(null); } }, [serverId]); useEffect(() => { void refresh(); }, [refresh]); const serverOperations = useMemo( () => operations.operations.filter((operation) => operation.targetId === serverId || operation.targetId.startsWith(`${serverId}:`)), [operations.operations, serverId] ); function requestLifecycle(current: ServerInstanceResponse, action: "start" | "stop") { setConfirm({ title: action === "start" ? "启动服务器" : "停止服务器", description: action === "start" ? `确认启动服务器 ${current.name}(${current.id})?` : `停止服务器 ${current.name}(${current.id})会断开所有在线玩家,确认继续?`, danger: action === "stop", run: async () => { const operationId = operations.begin({ intent: action === "start" ? "启动服务器" : "停止服务器", targetKind: "server", targetId: current.id, requester: session.displayName }); try { const result = action === "start" ? await platformApiClient.startServerInstance(current.id, serverLifecycleCommandRequest(current, "start")) : await platformApiClient.stopServerInstance(current.id, serverLifecycleCommandRequest(current, "stop")); operations.succeed(operationId, `任务 ${result.job.id}(${result.job.capability})已派发`, result.job); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "操作失败", operationId); } } }); } async function saveDeploymentWorkflow(form: ServerCreateFormState) { if (instance.status !== "ready") return; const current = instance.data; const operationId = operations.begin({ intent: "更新部署定义", targetKind: "server", targetId: current.id, requester: session.displayName }); try { await platformApiClient.updateServerDeployment(current.id, { runEndpointId: form.runEndpointId || undefined, mode: form.deploymentMode, profileKey: form.profileKey || undefined, createInputs: form.createInputs, serverRoot: form.serverRoot.trim() || undefined, workingDirectory: form.workingDirectory.trim() || undefined, installCommand: form.installCommand.trim() || undefined, startCommand: form.startCommand.trim() || undefined, stopCommand: form.stopCommand.trim() || undefined, statusCommand: form.statusCommand.trim() || undefined, shell: form.shell || undefined }); operations.succeed(operationId, "部署设置已保存;路径和命令保持受保护状态。"); setShowDeploymentEditor(false); await refresh(); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "部署设置保存失败"); } } if (!serverId) { return ( onNavigate("servers")} /> ); } return (
{instance.status === "loading" && } {instance.status === "error" && ( void refresh()} /> )} {instance.status !== "ready" && ( )} {instance.status === "ready" && ( <>

{instance.data.name}

{instance.data.id} · 插件 {instance.data.pluginId}@{instance.data.pluginVersion} · 节点 {instance.data.runEndpointId}
{stateLabel(instance.data.state)}
{section === "overview" && setSection("logs")} />} {section === "overview" && ( plugin.id === instance.data.pluginId)} binding={runtimeBinding} session={session} operations={operations} onChanged={() => void refresh()} /> )} {section === "overview" && setShowDeploymentEditor(true)} />} {section === "overview" && plugin.id === instance.data.pluginId)?.runtimeProfiles} />} {section === "overview" && } {section === "overview" && ( setSection("logs")} onChanged={() => void refresh()} /> )} {section === "overview" && ( )} {section === "overview" && ( setInstance({ status: "ready", data: next })} /> )} {section === "overview" && setInstance({ status: "ready", data: next })} />} {section === "logs" && } {section === "config" && } {section === "plugins" && } {section === "llm" && } {section === "history" && } setShowDeploymentEditor(false)} onSubmit={saveDeploymentWorkflow} /> )} setConfirm(null)} onConfirm={() => { if (!confirm) { return; } setConfirmBusy(true); void confirm.run().finally(() => { setConfirmBusy(false); setConfirm(null); }); }} />
); } function uniqueArtifacts(artifacts: ArtifactResponse[]): ArtifactResponse[] { const byID = new Map(); for (const artifact of artifacts) { byID.set(artifact.id, artifact); } return [...byID.values()]; } interface ServerMetadataSectionProps { instance: ServerInstanceResponse; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; onChanged: (instance: ServerInstanceResponse) => void; } function ServerMetadataSection({ instance, session, operations, onChanged }: ServerMetadataSectionProps) { const [draft, setDraft] = useState(() => serverMetadataFormFromInstance(instance)); const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null); useEffect(() => { setDraft(serverMetadataFormFromInstance(instance)); }, [instance.id, instance.name]); async function saveMetadata(event: FormEvent) { event.preventDefault(); const operationId = operations.begin({ intent: "更新服务器信息", targetKind: "server", targetId: instance.id, requester: session.displayName }); setResult({ status: "pending", label: "正在保存服务器信息" }); try { const updated = await platformApiClient.updateServerInstance(instance.id, serverMetadataUpdateRequestFromForm(draft)); onChanged(updated); operations.succeed(operationId, `服务器信息已更新:${updated.id}`); setResult({ status: "succeeded", label: `已更新 ${updated.name}` }); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "服务器信息更新失败"); setResult({ status: "failed", label: error instanceof Error ? error.message : "服务器信息更新失败" }); } } return (

基本信息

{result && }
void saveMetadata(event)}>
); } interface ServerDeploymentSectionProps { instance: ServerInstanceResponse; deployment: LoadState; onEdit: () => void; } function ServerDeploymentSection({ instance, deployment, onEdit }: ServerDeploymentSectionProps) { if (deployment.status === "loading") return ; if (deployment.status === "error") return ; const view = deployment.data; return

部署定义

{view.mode || "未配置"} · 修订 {view.revision}

服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,不会回显。

服务器目录{view.serverRootConfigured ? "已配置" : "未配置"}
高级执行目录{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}
启动设置{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}
{(instance.state === "draft" || instance.state === "failed") && 保存后可从详情明确发起部署。}
; } function deploymentWorkflowForm(instance: ServerInstanceResponse, deployment: ServerDeploymentResponse | undefined, plugins: GamePluginResponse[], endpoints: RunEndpointResponse[]): ServerCreateFormState { const plugin = plugins.find((item) => item.id === instance.pluginId); return { ...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 ?? "" }; } function deploymentProgressLabel(progress: JobResponse["progress"]): string { switch (progress.phase) { case "queued": return "任务已排队,等待 Run 领取"; case "claimed": return "Run 已领取任务"; case "preflight": return "正在执行本机预检"; case "install": return "正在安装服务器"; case "configure": return "正在写入游戏配置"; case "start": return "正在启动服务器"; case "health": return "正在进行健康检查"; default: return "部署任务已提交"; } } interface ServerAdministratorsSectionProps { instance: ServerInstanceResponse; session: PageComponentProps["session"]; onChanged: (instance: ServerInstanceResponse) => void; } function ServerAdministratorsSection({ instance, session, onChanged }: ServerAdministratorsSectionProps) { const [candidates, setCandidates] = useState>({ status: "loading" }); const [selectedUserId, setSelectedUserId] = useState(""); const [busyUserId, setBusyUserId] = useState(null); const [result, setResult] = useState<{ status: "succeeded" | "failed"; label: string } | null>(null); const isOwner = instance.ownerUserId === session.id; const refreshCandidates = useCallback(async () => { if (!isOwner) { setCandidates({ status: "ready", data: [] }); return; } setCandidates({ status: "loading" }); try { const response = await platformApiClient.listServerAdministratorCandidates(instance.id); setCandidates({ status: "ready", data: response.items }); setSelectedUserId((current) => (response.items.some((user) => user.id === current) ? current : response.items[0]?.id ?? "")); } catch (error) { setCandidates({ status: "error", reason: error instanceof Error ? error.message : "加载候选管理员失败" }); } }, [instance.id, isOwner]); useEffect(() => { void refreshCandidates(); }, [refreshCandidates]); async function addAdministrator(event: FormEvent) { event.preventDefault(); if (!selectedUserId) { return; } setBusyUserId(selectedUserId); setResult(null); try { const next = await platformApiClient.addServerAdministrator(instance.id, { userId: selectedUserId }); onChanged(next); setResult({ status: "succeeded", label: "已添加服务器管理员" }); await refreshCandidates(); } catch (error) { setResult({ status: "failed", label: error instanceof Error ? error.message : "添加管理员失败" }); } finally { setBusyUserId(null); } } async function removeAdministrator(userId: string) { setBusyUserId(userId); setResult(null); try { const next = await platformApiClient.removeServerAdministrator(instance.id, userId); onChanged(next); setResult({ status: "succeeded", label: "已移除服务器管理员" }); await refreshCandidates(); } catch (error) { setResult({ status: "failed", label: error instanceof Error ? error.message : "移除管理员失败" }); } finally { setBusyUserId(null); } } return (

管理成员

{isOwner ? "服主可邀请/移除管理员" : "仅服主可调整成员"}
{result && (
)}
服主:{instance.ownerUserId || "未绑定"} 服务器管理员:{instance.adminUserIds.length > 0 ? instance.adminUserIds.join(" / ") : "暂无"}
{isOwner && ( <>
void addAdministrator(event)} aria-label="邀请服务器管理员">
{candidates.status === "loading" && } {candidates.status === "error" && ( void refreshCandidates()} compact /> )} {instance.adminUserIds.length > 0 && (
{instance.adminUserIds.map((userId) => (
{userId} 服务器管理员
))}
)} )}
); } function HeaderStat({ label, value }: { label: string; value: string }) { return ( {label} {value} ); } interface OverviewSectionProps { instance: ServerInstanceResponse; metrics: ServerMetricsResponse | null; jobs: JobResponse[]; onOpenLogs: () => void; } function OverviewSection({ instance, metrics, jobs, onOpenLogs }: OverviewSectionProps) { const pending = jobs.filter((job) => job.state === "queued" || job.state === "accepted" || job.state === "running" || job.state === "retrying"); const failed = jobs.filter((job) => job.state === "failed"); return (

资源使用

{metrics ? new Date(metrics.collectedAt).toLocaleTimeString() : "暂无指标"}

需要关注

{instance.state === "failed" && ⚠ 服务器处于异常状态,建议查看日志与操作历史。} {failed.length > 0 && ⚠ 最近有 {failed.length} 个任务失败。} {pending.length > 0 ? ( 进行中任务:{pending[0].capability}({deploymentProgressLabel(pending[0].progress)},{pending[0].progress.percent}%) ) : ( 当前没有进行中的任务。 )} 配置版本:v{instance.configVersion},最近更新 {new Date(instance.updatedAt).toLocaleString()}
); } interface RuntimeDistributionSectionProps { instance: ServerInstanceResponse; runtimeActions: LoadState; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; onOpenLogs: () => void; onChanged: () => void; } interface RuntimeBindingSectionProps { instance: ServerInstanceResponse; plugin?: GamePluginResponse; binding: LoadState; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; onChanged: () => void; } function RuntimeBindingSection({ instance, plugin, binding, session, operations, onChanged }: RuntimeBindingSectionProps) { const bindingData = binding.status === "ready" ? binding.data : null; const [profileKey, setProfileKey] = useState(bindingData?.profileKey ?? plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? ""); const [values, setValues] = useState>({}); const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null); const canManage = isPlatformAdmin(session) || instance.ownerUserId === session.id; const activeExistingBinding = bindingData?.configured === true && (instance.state === "installing" || instance.state === "running"); const fields = runtimeBindingFields(plugin, profileKey); useEffect(() => { setProfileKey(bindingData?.profileKey ?? plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? ""); setValues({}); }, [bindingData?.profileKey, bindingData?.updatedAt, plugin?.id]); async function saveBinding(event: FormEvent) { event.preventDefault(); const operationId = operations.begin({ intent: "更新运行配置", targetKind: "server", targetId: `${instance.id}:runtime-binding`, requester: session.displayName }); setResult({ status: "pending", label: "正在保存运行配置" }); try { const updated = await platformApiClient.updateServerRuntimeBinding(instance.id, { profileKey, bindings: Object.fromEntries(Object.entries(values).map(([key, value]) => [key, value.trim()]).filter(([, value]) => value !== "")) }); operations.succeed(operationId, updated.status === "complete" ? "运行配置已就绪" : "运行配置已保存,仍有缺失项"); setResult({ status: "succeeded", label: updated.status === "complete" ? "运行配置已就绪" : `仍缺少:${updated.missingKeys.join("、")}` }); setValues({}); onChanged(); } catch (error) { const reason = error instanceof Error ? error.message : "运行配置保存失败"; operations.fail(operationId, reason, operationId); setResult({ status: "failed", label: reason }); } } return (

运行配置绑定

{result && }
{binding.status === "loading" && } {binding.status === "error" && } {bindingData && ( <>
key.configured).length}/${bindingData.keys.length}`} />
{bindingData.reason &&

{bindingData.reason}

} {bindingData.missingKeys.length > 0 &&

缺少逻辑绑定:{bindingData.missingKeys.join("、")}

} {bindingData.keys.length > 0 && (
{bindingData.keys.map((key) => ( {key.key} · {key.configured ? (key.secret ? "受保护" : "已配置") : "缺失"} ))}
)}
void saveBinding(event)}>
{fields.map((field) => { const existing = bindingData.keys.find((key) => key.key === field.key); return ( ); })}
)}
); } function RuntimeDistributionSection({ instance, runtimeActions, session, operations, onOpenLogs, onChanged }: RuntimeDistributionSectionProps) { const defaults = runtimeDefaultsForPlugin(instance.pluginId); const [targetOs, setTargetOs] = useState(defaults.runOs); const [targetArch, setTargetArch] = useState("amd64"); const [profileKey, setProfileKey] = useState(defaults.clientProfileKey); const [repositoryUrl, setRepositoryUrl] = useState(defaults.repositoryUrl); const [sourceRevision, setSourceRevision] = useState(defaults.sourceRevision); const [probeKey, setProbeKey] = useState(defaults.probeKey); const [installPlanKey, setInstallPlanKey] = useState(defaults.installPlanKey); const [logSourceKey, setLogSourceKey] = useState(defaults.logSourceKey); const [checkpointRef, setCheckpointRef] = useState(""); const [lastRun, setLastRun] = useState(null); const [lastClient, setLastClient] = useState(null); const [lastDownload, setLastDownload] = useState(null); const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null); const [dependencyCatalog, setDependencyCatalog] = useState>({ status: "loading" }); const [runUpdates, setRunUpdates] = useState>({ status: "loading" }); const runtimeTask = useRuntimeTaskController(); const [runtimeTaskActions, setRuntimeTaskActions] = useState([]); const refreshRuntimeProjections = useCallback(async () => { const dependencyActions = runtimeActions.status === "ready" ? runtimeActions.data.actions.filter((action) => action.key === "dependencies-check" || action.key === "dependencies-install") : []; const dependencyActionReason = dependencyActions.find((action) => action.reason)?.reason ?? "依赖操作未开放"; const catalogRequest: Promise> = runtimeActions.status === "ready" && dependencyActions.some((action) => action.available) ? platformApiClient .getDependencyCatalog(instance.id) .then((data): LoadState => ({ status: "ready", data })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "依赖目录加载失败" })) : Promise.resolve( runtimeActions.status === "error" ? { status: "error", reason: runtimeActions.reason } : runtimeActions.status === "ready" ? { status: "error", reason: dependencyActionReason } : { status: "loading" } ); const [catalog, updates] = await Promise.all([ catalogRequest, platformApiClient .listRunUpdates(instance.id) .then((data): LoadState => ({ status: "ready", data: data.items })) .catch((error): LoadState => ({ status: "error", reason: error instanceof Error ? error.message : "Run 更新状态加载失败" })) ]); setDependencyCatalog(catalog); setRunUpdates(updates); }, [instance.id, runtimeActions]); useEffect(() => { void refreshRuntimeProjections(); }, [refreshRuntimeProjections]); useEffect(() => { if (dependencyCatalog.status !== "ready") return; const selectedProbe = dependencyCatalog.data.probes.find((probe) => probe.key === probeKey) ?? dependencyCatalog.data.probes[0]; if (selectedProbe && selectedProbe.key !== probeKey) setProbeKey(selectedProbe.key); const matchingPlan = dependencyCatalog.data.plans.find((plan) => plan.key === installPlanKey) ?? dependencyCatalog.data.plans.find((plan) => plan.key === selectedProbe?.installPlanKey) ?? dependencyCatalog.data.plans[0]; if (matchingPlan && matchingPlan.key !== installPlanKey) setInstallPlanKey(matchingPlan.key); }, [dependencyCatalog, installPlanKey, probeKey]); const selectedDependencyProbe = dependencyCatalog.status === "ready" ? dependencyCatalog.data.probes.find((probe) => probe.key === probeKey) : undefined; const selectedDependencyPlan = dependencyCatalog.status === "ready" ? dependencyCatalog.data.plans.find((plan) => plan.key === installPlanKey) : undefined; const latestRunUpdate = runUpdates.status === "ready" ? runUpdates.data[0] : undefined; const actionByKey = useMemo(() => { if (runtimeActions.status !== "ready") { return new Map(); } return new Map(runtimeActions.data.actions.map((action) => [action.key, { available: action.available, reason: action.reason }])); }, [runtimeActions]); function canUse(key: string): boolean { return actionByKey.get(key)?.available ?? false; } function reasonFor(key: string): string { return actionByKey.get(key)?.reason ?? "平台暂未开放该操作"; } async function runOperation( intent: string, execute: () => Promise, summarize: (value: T) => string, taskOptions?: { description: string; stages: RuntimeTaskStage[]; executeStageIndex?: number; trackedJobId?: (value: T) => string; afterSuccess?: (value: T) => void; } ) { const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:runtime`, requester: session.displayName }); setRuntimeTaskActions([]); setResult({ status: "pending", label: `${intent} 执行中` }); try { const value = taskOptions?.trackedJobId ? await runtimeTask.runTrackedTask({ title: intent, description: taskOptions.description, stages: taskOptions.stages, start: async () => { const value = await execute(); return { value, jobId: taskOptions.trackedJobId?.(value) ?? "" }; }, poll: (jobId) => platformApiClient.getJob(jobId) }) : taskOptions ? await runtimeTask.runTask({ title: intent, description: taskOptions.description, stages: taskOptions.stages, executeStageIndex: taskOptions.executeStageIndex, execute }) : await execute(); const label = summarize(value); operations.succeed(operationId, label); setResult({ status: "succeeded", label }); runtimeTask.succeedTask(label); taskOptions?.afterSuccess?.(value); void refreshRuntimeProjections(); onChanged(); } catch (error) { const reason = error instanceof Error ? error.message : `${intent} 失败`; operations.fail(operationId, reason, operationId); setResult({ status: "failed", label: reason }); runtimeTask.failTask(reason); } } function latestRunArtifact(): { artifactId: string; checksum?: string } | null { if (lastRun) { return { artifactId: lastRun.artifactId, checksum: lastRun.checksum }; } if (lastDownload) { return { artifactId: lastDownload.artifactId, checksum: lastDownload.checksum }; } return null; } async function downloadRunArtifact(artifact: { artifactId: string; checksum?: string }) { setRuntimeTaskActions([]); try { const label = 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); setLastDownload(reference); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return `run 下载已开始,文件 ${safeArtifactFilename(reference.filename)}`; } }); runtimeTask.succeedTask(label); } catch (error) { runtimeTask.failTask(error instanceof Error ? error.message : "run 下载失败"); } } async function pushRunArtifact(artifact: { artifactId: string; checksum?: string }) { setRuntimeTaskActions([]); await runOperation( "更新 run", () => platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, artifact.artifactId, artifact.checksum)), (update) => `run 更新任务已排队,job ${update.jobId ?? update.id}`, { description: `将 artifact ${artifact.artifactId} 推送到 ${instance.runEndpointId},并等待平台 job 确认。`, stages: runtimeUpdateStages, executeStageIndex: 2 } ); } async function downloadClientArtifact(profileKeyForDownload: string) { setRuntimeTaskActions([]); try { const label = await runtimeTask.runTask({ title: "下载客户端", description: `${instance.name} 的客户端管理器已生成,正在创建下载引用。`, stages: runtimeDownloadStages, executeStageIndex: 1, execute: async () => { const reference = await platformApiClient.downloadLatestClientManager(instance.id, { profileKey: profileKeyForDownload }); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return `客户端下载已开始,文件 ${safeArtifactFilename(reference.filename)}`; } }); runtimeTask.succeedTask(label); } catch (error) { runtimeTask.failTask(error instanceof Error ? error.message : "客户端下载失败"); } } return (

运行分发

{runtimeActions.status === "ready" ? ( run {runtimeActions.data.runStatus} ) : runtimeActions.status === "error" ? ( ) : ( 读取中 )}
{result && (
)}
void runOperation( "生成 run", async () => { const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, targetOs, targetArch)); setLastRun(distribution); return distribution; }, (distribution) => `run ${distribution.targetOs}/${distribution.targetArch} 二进制已构建,artifact ${distribution.artifactId},generation ${distribution.keyGeneration}`, { description: `为 ${instance.name} 构建 ${targetOs}/${targetArch} run 包,展示拉取 run 更新、检测构建环境、构建中和构建完成进度。`, stages: runtimeRunBuildStages, trackedJobId: (distribution) => distribution.buildJobId, afterSuccess: (distribution) => { const artifact = { artifactId: distribution.artifactId, checksum: distribution.checksum }; setRuntimeTaskActions([ { label: "下载 run", kind: "primary", onClick: () => void downloadRunArtifact(artifact) }, { label: "更新 run", disabled: !serverIsOnline(instance.state), title: serverIsOnline(instance.state) ? "更新 run" : "run 未运行,无法在线更新", onClick: () => void pushRunArtifact(artifact) } ]); } } ) } /> void runOperation( "下载 run", async () => { const reference = await platformApiClient.downloadLatestRunDistribution(instance.id); setLastDownload(reference); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return reference; }, (reference) => `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`, { description: `为 ${instance.name} 创建最新 run 包下载引用,并展示 artifact 定位进度。`, stages: runtimeDownloadStages, executeStageIndex: 1 } ) } secondaryLabel="更新 run" secondaryDisabled={!canUse("push-run-update") || latestRunArtifact() === null || !serverIsOnline(instance.state)} secondaryReason={!serverIsOnline(instance.state) ? "run 未运行,无法在线更新" : latestRunArtifact() === null ? "请先生成或下载 run 包" : reasonFor("push-run-update")} onSecondary={() => void runOperation( "更新 run", async () => { const artifact = latestRunArtifact(); if (!artifact) { throw new Error("请先生成或下载 run 包"); } return platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, artifact.artifactId, artifact.checksum)); }, (update) => `run 更新任务已排队,job ${update.jobId ?? update.id}`, { description: `将最近 run artifact 推送到 ${instance.runEndpointId},并等待平台 job 确认。`, stages: runtimeUpdateStages, executeStageIndex: 2 } ) } > {latestRunUpdate && (
phase {latestRunUpdate.phase} checksum {shortChecksum(latestRunUpdate.checksum)} release {latestRunUpdate.targetRelease ?? "pending"} rollback {latestRunUpdate.rollback ? "yes" : "no"} {latestRunUpdate.message && audit {latestRunUpdate.message}}
)} {runUpdates.status === "error" && }
void runOperation( "重置 run 密钥", () => platformApiClient.resetRunKey(instance.id), (key) => `run 密钥已重置,generation ${key.generation},fingerprint ${key.fingerprint}` ) } /> void runOperation( "生成客户端管理器", async () => { const distribution = await platformApiClient.generateClientManager( instance.id, clientManagerBuildRequest({ serverInstanceId: instance.id, profileKey, targetOs, targetArch, repositoryUrl, sourceRevision }) ); setLastClient(distribution); return distribution; }, (distribution) => `客户端管理器二进制已构建,artifact ${distribution.artifactId},组件密钥仅由 Platform/Run 受控使用`, { description: `按 ${profileKey} profile 拉取客户端代码、安装环境、编译并生成可下载 artifact。`, stages: runtimeBuildStages, trackedJobId: (distribution) => distribution.buildJobId, afterSuccess: () => { setRuntimeTaskActions([{ label: "下载客户端", kind: "primary", onClick: () => void downloadClientArtifact(profileKey) }]); } } ) } secondaryLabel="下载客户端" secondaryDisabled={!canUse("download-client-manager")} secondaryReason={reasonFor("download-client-manager")} onSecondary={() => void runOperation( "下载客户端管理器", async () => { const reference = await platformApiClient.downloadLatestClientManager(instance.id, { profileKey }); await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit)); return reference; }, (reference) => `客户端下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}` ) } /> void runOperation( "重置客户端密钥", () => platformApiClient.resetClientManagerKey(instance.id, { componentKind: "client-manager", componentKey: profileKey }), (key) => `客户端密钥已重置,generation ${key.generation},fingerprint ${key.fingerprint}` ) } /> void runOperation( "依赖检查", () => platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, probeKey)), (job) => `依赖检查任务已排队,job ${job.id}`, { description: `使用 ${probeKey} probe 检查 ${instance.name} 的运行依赖。`, stages: runtimeDependencyStages, executeStageIndex: 1 } ) } secondaryLabel="依赖安装" secondaryDisabled={!canUse("dependencies-install") || !selectedDependencyPlan || selectedDependencyProbe?.installPlanKey !== selectedDependencyPlan.key} secondaryReason={!selectedDependencyPlan ? "请选择 Platform 返回的审核计划" : selectedDependencyProbe?.installPlanKey !== selectedDependencyPlan.key ? "所选计划不属于当前 probe" : reasonFor("dependencies-install")} onSecondary={() => void runOperation( "依赖安装", () => platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probeKey, installPlanKey, selectedDependencyPlan?.digest ?? "")), (job) => `依赖安装任务已排队,job ${job.id}`, { description: `审批 ${installPlanKey} 的 immutable digest ${shortChecksum(selectedDependencyPlan?.digest ?? "")} 后派发依赖安装任务。`, stages: runtimeDependencyStages, executeStageIndex: 2 } ) } > {selectedDependencyProbe && (
{selectedDependencyProbe.key} · {selectedDependencyProbe.state} required {selectedDependencyProbe.required ? "yes" : "no"} {selectedDependencyProbe.evidence && evidence {selectedDependencyProbe.evidence}} {selectedDependencyPlan && digest {shortChecksum(selectedDependencyPlan.digest)}} {selectedDependencyPlan && steps {selectedDependencyPlan.steps.map((step) => `${step.type}:${step.packageManager ?? step.downloadHost ?? step.targetKey}`).join(" → ")}}
)} {dependencyCatalog.status === "error" && }
void runOperation( "实时日志", async () => { onOpenLogs(); return true; }, () => "已打开实时日志视图", { description: `读取 ${instance.name} 的平台日志源并打开实时日志视图。`, stages: runtimeLogStages, executeStageIndex: 1 } ) } secondaryLabel="历史回填" secondaryDisabled={!canUse("historical-logs")} secondaryReason={reasonFor("historical-logs")} onSecondary={() => void runOperation( "历史日志回填", () => platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, logSourceKey, checkpointRef)), (job) => `历史日志回填任务已排队,job ${job.id}`, { description: `从 ${logSourceKey} 日志源准备历史回填游标并派发后台 job。`, stages: runtimeLogStages, executeStageIndex: 1 } ) } >
); } interface RuntimeActionRowProps { title: string; description: string; disabled: boolean; reason: string; actionLabel: string; danger?: boolean; onAction: () => void; secondaryLabel?: string; secondaryDisabled?: boolean; secondaryReason?: string; onSecondary?: () => void; children?: ReactNode; } function RuntimeActionRow({ title, description, disabled, reason, actionLabel, danger, onAction, secondaryLabel, secondaryDisabled, secondaryReason, onSecondary, children }: RuntimeActionRowProps) { return (
{title}

{description}

{disabled && 不可用:{reason}} {children}
{secondaryLabel && onSecondary && ( )}
); } function runtimeDefaultsForPlugin(pluginId: string) { const isScum = pluginId.toLowerCase().includes("scum"); return { runOs: isScum ? "windows" : "linux", clientProfileKey: isScum ? "scum-client-manager" : "client-manager", repositoryUrl: isScum ? "https://github.com/F88888/scum_client.git" : "https://github.com/example/client-manager.git", sourceRevision: "main", probeKey: isScum ? "steamcmd" : "java-21", installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux", logSourceKey: isScum ? "server-log" : "latest-log" }; } function shortChecksum(value: string): string { if (!value) return "unavailable"; return value.length > 22 ? `${value.slice(0, 22)}…` : value; } function runUpdatePhaseLabel(phase: RunUpdateJobResponse["phase"]): string { switch (phase) { case "queued": return "等待下载"; case "downloading": return "分块下载与校验"; case "staged": return "已安全暂存"; case "restart-requested": return "等待重启激活"; case "activating": return "激活与健康确认"; case "succeeded": return "更新成功"; case "rolled-back": return "已回滚"; case "failed": return "更新失败"; } } interface LogsSectionProps { serverId: string; } interface LogFilterState { level: string; keyword: string; source: string; sinceMinutes: string; } function LogsSection({ serverId }: LogsSectionProps) { const [streams, setStreams] = useState>({ status: "loading" }); const [entries, setEntries] = useState>([]); const [filter, setFilter] = useState({ level: "all", keyword: "", source: "all", sinceMinutes: "all" }); const [selected, setSelected] = useState<(LogEntryBody & { source: string }) | null>(null); const refresh = useCallback(async () => { setStreams({ status: "loading" }); try { const response = await platformApiClient.listServerLiveLogs(serverId); const serverStreams = response.items; setStreams({ status: "ready", data: serverStreams }); const collected: Array = []; for (const stream of serverStreams) { try { const cursor = await platformApiClient.queryLogStream({ logStreamId: stream.id, afterSeq: 0, limit: 200 }); collected.push(...cursor.entries.map((entry) => ({ ...entry, source: stream.source || stream.streamKey }))); } catch { // one unreadable stream should not blank the rest } } collected.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); setEntries(collected); } catch (error) { setStreams({ status: "error", reason: error instanceof Error ? error.message : "加载失败" }); } }, [serverId]); useEffect(() => { void refresh(); }, [refresh]); const sources = useMemo(() => [...new Set(entries.map((entry) => entry.source))], [entries]); const visible = useMemo(() => { const keyword = filter.keyword.trim().toLowerCase(); const sinceMs = filter.sinceMinutes === "all" ? null : Date.now() - Number(filter.sinceMinutes) * 60_000; return entries.filter((entry) => { if (filter.level !== "all" && (entry.level ?? "info").toLowerCase() !== filter.level) { return false; } if (filter.source !== "all" && entry.source !== filter.source) { return false; } if (keyword && !entry.line.toLowerCase().includes(keyword)) { return false; } if (sinceMs !== null && new Date(entry.timestamp).getTime() < sinceMs) { return false; } return true; }); }, [entries, filter]); return (

日志

setFilter((current) => ({ ...current, keyword: event.target.value }))} />
{streams.status === "loading" && } {streams.status === "error" && void refresh()} compact />} {streams.status === "ready" && entries.length === 0 && ( void refresh()} /> )} {streams.status === "ready" && entries.length > 0 && visible.length === 0 && ( )} {visible.length > 0 && (
{visible.map((entry) => ( ))}
)} {selected && (
setSelected(null)}>
event.stopPropagation()}>

日志详情

时间
{new Date(selected.timestamp).toLocaleString()}
级别
{(selected.level ?? "info").toUpperCase()}
来源
{selected.source}
序号
{selected.seq}
内容
{selected.line}
{selected.fields && Object.keys(selected.fields).length > 0 && (
字段
{Object.entries(selected.fields) .map(([key, value]) => `${key}=${value}`) .join(" ")}
)}
)}
); } function levelClass(level?: string): string { const normalized = (level ?? "info").toLowerCase(); if (normalized === "error" || normalized === "fatal") { return "log-level-error"; } if (normalized === "warn" || normalized === "warning") { return "log-level-warn"; } return "log-level-info"; } interface ConfigSectionProps { serverId: string; instance: ServerInstanceResponse; session: PageComponentProps["session"]; operations: PageComponentProps["operations"]; } function ConfigSection({ serverId, instance, session, operations }: ConfigSectionProps) { const [config, setConfig] = useState>({ status: "loading" }); const [draft, setDraft] = useState(""); const [diff, setDiff] = useState(null); const [previewBusy, setPreviewBusy] = useState(false); const [previewError, setPreviewError] = useState(null); const refresh = useCallback(async () => { setConfig({ status: "loading" }); try { const response: ServerConfigResponse = await platformApiClient.getServerConfig(serverId); setConfig({ status: "ready", data: { content: response.content, source: "api" } }); setDraft(response.content); } catch (error) { setConfig({ status: "error", reason: error instanceof Error ? error.message : "配置读取接口不可用" }); setDraft(""); } }, [serverId]); useEffect(() => { void refresh(); }, [refresh]); async function prepareDiff(event: FormEvent) { event.preventDefault(); if (config.status !== "ready") { return; } setPreviewBusy(true); setPreviewError(null); try { const preview = await platformApiClient.previewServerConfigDiff(serverId, { expectedConfigVersion: instance.configVersion, expectedChecksum: instance.configChecksum, key: defaultConfigKey, proposedContent: draft }); setDiff(configDiffViewFromPreview(preview)); } catch (error) { setPreviewError(error instanceof Error ? error.message : "配置差异预览失败"); } finally { setPreviewBusy(false); } } async function submitDiff() { if (!diff || config.status !== "ready") { return; } const operationId = operations.begin({ intent: "写入配置", targetKind: "config", targetId: serverId, requester: session.displayName }); try { const dispatch = await platformApiClient.approveServerConfigWrite(serverId, { expectedConfigVersion: diff.configVersion ?? instance.configVersion, expectedChecksum: diff.checksum ?? instance.configChecksum, key: diff.key ?? defaultConfigKey, proposedContent: diff.nextContent, proposedContentInputRef: diff.proposedContentInputRef, idempotencyKey: `web:config.write:${serverId}:${Date.now()}` }); const job = dispatch.job; operations.succeed(operationId, `配置写入任务 ${job.id} 已派发`, job); setDiff(null); } catch (error) { operations.fail(operationId, error instanceof Error ? error.message : "配置写入任务派发失败", operationId); } } const writeOperation = operations.operations.find((operation) => operation.intent === "写入配置" && operation.targetId === serverId); return (

配置

{config.status === "ready" && 配置版本 v{instance.configVersion}{instance.configChecksum ? ` · ${instance.configChecksum.slice(0, 18)}` : ""}}
{writeOperation && (
)} {config.status === "loading" && } {config.status === "error" && void refresh()} compact />} {previewError && } {config.status === "ready" && (
void prepareDiff(event)}>