import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react"; import { type FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import { platformApiClient } from "../api/client"; import type { ConfigDiffLineResponse, ArtifactDownloadReferenceResponse, ArtifactResponse, GamePluginResponse, JobResponse, LogEntryBody, LogStreamResponse, ServerConfigDiffPreviewResponse, ServerConfigResponse, ServerInstanceResponse, ServerMemberResponse, ServerMetricsResponse } from "../api/types"; import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationControls"; 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, pluginLabel } from "../contracts/serverManagement"; import { serverDetailSections, serverIsOnline, type ConfigDiffView, type LlmSuggestionView, type PluginControlDescriptor, type PluginControlGroupView, type ServerDetailSection } from "../contracts/workspace"; import { serverLifecycleCommandRequest } from "../schemas/serverManagement"; import { buildConfigDiff, diffHasChanges } from "../utils/diff"; import { createPluginBridgeDispatcher, createPluginBridgeHostContext, parsePluginArtifactReference } from "../utils/pluginBridgeHost"; import { cx } from "../utils/classes"; import { stateLabel, statusClass } from "./ServersPage"; type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T }; const fallbackConfig = "# server.properties\nmax-players=20\nmotd=Welcome to the server\npvp=true\nview-distance=8\n"; 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 [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, jobResponse] = await Promise.all([ platformApiClient.getServerInstance(serverId), platformApiClient.listGamePlugins(), platformApiClient.listJobs(serverId) ]); setInstance({ status: "ready", data: detail }); setPlugins(pluginResponse.items); setJobs(jobResponse.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([]); } 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); } } }); } 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" && setInstance({ status: "ready", data: next })} />} {section === "logs" && } {section === "config" && } {section === "plugins" && } {section === "llm" && } {section === "history" && } )} 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 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"); 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}({pending[0].state},{pending[0].progress.percent}%) ) : ( 当前没有进行中的任务。 )} 配置版本:v{instance.configVersion},最近更新 {new Date(instance.updatedAt).toLocaleString()}
); } 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.listLogStreams(); const serverStreams = response.items.filter((stream) => stream.serverInstanceId === serverId); 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 { setConfig({ status: "ready", data: { content: fallbackConfig, source: "local" } }); setDraft(fallbackConfig); } }, [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, 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, 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" && ( {config.data.source === "api" ? `配置版本 v${instance.configVersion}` : "本地示例配置(配置读取接口未提供)"} )}
{writeOperation && (
)} {config.status === "loading" && } {previewError && } {config.status === "ready" && (
void prepareDiff(event)}>