fix server log recovery and runtime status

This commit is contained in:
npc0-hue
2026-08-10 14:19:17 +08:00
parent d1f4dce4f5
commit 2becedbfe1
4 changed files with 35 additions and 190 deletions
+6 -4
View File
@@ -219,9 +219,11 @@ describe("first-party console pages", () => {
expect(serversPageSource).not.toContain("ServerManagementTerminalDrawer");
expect(serversPageSource).not.toContain("live-logs");
expect(serversPageSource).not.toContain("管理终端");
expect(serverDetailPageSource).toContain("ServerLiveLogDrawer");
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
expect(serverDetailPageSource).toContain("实时日志");
expect(serverDetailPageSource).not.toContain("ServerLiveLogDrawer");
expect(serverDetailPageSource).not.toContain('<span>实时日志</span>');
expect(serverDetailPageSource).toContain("runtimeObservationFreshness");
expect(serverDetailPageSource).toContain("Run 未验证");
expect(serverDetailPageSource).toContain("管理终端");
});
@@ -333,11 +335,11 @@ describe("first-party console pages", () => {
expect(serverLiveOperationsSource).toContain("handleCommandKeyDown");
expect(serverLiveOperationsSource).toContain("commandHistory");
expect(serverLiveOperationsSource).toContain("ArrowUp");
expect(serverLiveOperationsSource).toContain("listServerLiveLogs");
expect(serverLiveOperationsSource).not.toContain("listServerLiveLogs");
expect(serverLiveOperationsSource).toContain("openServerLogEvents");
expect(serverLiveOperationsSource).toContain("getGameClientBridgeCommand");
expect(serverLiveOperationsSource).toContain("terminalLineFromBridgeCommand");
expect(serverLiveOperationsSource).toContain("streams.filter((stream) => stream.latestSeq > 0)");
expect(serverLiveOperationsSource).not.toContain("terminalRelevantStreams");
expect(serverLiveOperationsSource).toContain("SSE 实时推送");
expect(serverLiveOperationsSource).toContain("mergeTerminalLines");
expect(serverLiveOperationsSource).toContain("terminalInitialHistoryWindow = 500");
+19 -13
View File
@@ -1,4 +1,4 @@
import { Download, MoonStar, PackageOpen, Pencil, ScrollText, ShieldCheck, Sparkles, Square, Terminal, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
import { Download, MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, Terminal, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
import { type FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
@@ -16,7 +16,8 @@ import type {
ServerMetricsResponse,
ServerDeploymentResponse,
MetricSampleResponse,
RemoteAdapterDeclarationResponse
RemoteAdapterDeclarationResponse,
RunEndpointResponse
} from "../api/types";
import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationControls";
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
@@ -24,8 +25,8 @@ import { SourceRCONCommandPanel } from "../components/SourceRCONCommandPanel";
import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { jobCapabilityLabel } from "../contracts/jobPresentation";
import { canStartServer, canStopServer, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
import { ServerLiveLogDrawer, ServerManagementTerminalDrawer } from "../components/ServerLiveOperations";
import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
import { ServerManagementTerminalDrawer } from "../components/ServerLiveOperations";
import {
serverDetailSections,
serverIsOnline,
@@ -63,8 +64,8 @@ export function ServerDetailPage(props: PageComponentProps) {
const [metricHistory, setMetricHistory] = useState<MetricSampleResponse[]>([]);
const [backups, setBackups] = useState<BackupResponse[]>([]);
const [remoteAdapters, setRemoteAdapters] = useState<RemoteAdapterDeclarationResponse[]>([]);
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
const [liveLogOpen, setLiveLogOpen] = useState(false);
const [terminalOpen, setTerminalOpen] = useState(false);
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
const [confirmBusy, setConfirmBusy] = useState(false);
@@ -76,7 +77,7 @@ export function ServerDetailPage(props: PageComponentProps) {
}
setInstance({ status: "loading" });
try {
const [detail, pluginResponse, jobResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse] = await Promise.all([
const [detail, pluginResponse, jobResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse, endpointResponse] = await Promise.all([
platformApiClient.getServerInstance(serverId),
platformApiClient.listGamePlugins(),
platformApiClient.listJobs(serverId),
@@ -86,7 +87,8 @@ export function ServerDetailPage(props: PageComponentProps) {
.catch((error): LoadState<ServerDeploymentResponse> => ({ 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 }))
platformApiClient.listRemoteAdapters(serverId).catch(() => ({ items: [], count: 0 })),
platformApiClient.listRunEndpoints().catch(() => ({ items: [], count: 0 }))
]);
setInstance({ status: "ready", data: detail });
setPlugins(pluginResponse.items);
@@ -95,6 +97,7 @@ export function ServerDetailPage(props: PageComponentProps) {
setMetricHistory(metricHistoryResponse.items);
setBackups(backupResponse.items);
setRemoteAdapters(adapterResponse.items);
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
const artifactLists = await Promise.all(
jobResponse.items.slice(0, 20).map((job) =>
platformApiClient
@@ -107,6 +110,7 @@ export function ServerDetailPage(props: PageComponentProps) {
} catch (error) {
setInstance({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
setArtifacts([]);
setRunEndpoint(undefined);
setDeployment({ status: "error", reason: "部署定义加载失败" });
setMetricHistory([]);
setBackups([]);
@@ -127,14 +131,16 @@ export function ServerDetailPage(props: PageComponentProps) {
const refreshOperationalState = useCallback(async () => {
if (!serverId) return;
try {
const [detail, jobResponse, metricsResponse] = await Promise.all([
const [detail, jobResponse, metricsResponse, endpointResponse] = await Promise.all([
platformApiClient.getServerInstance(serverId),
platformApiClient.listJobs(serverId),
platformApiClient.listServerMetrics()
platformApiClient.listServerMetrics(),
platformApiClient.listRunEndpoints()
]);
setInstance({ status: "ready", data: detail });
setJobs(jobResponse.items);
setMetrics(metricsResponse.items.find((item) => item.serverInstanceId === serverId) ?? null);
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
} catch {
setMetrics(null);
}
@@ -151,6 +157,8 @@ export function ServerDetailPage(props: PageComponentProps) {
);
const canManageServers = session.capabilities.includes("servers.manage");
const readyPlugin = instance.status === "ready" ? plugins.find((plugin) => plugin.id === instance.data.pluginId) : undefined;
const detailFreshness = instance.status === "ready" ? runtimeObservationFreshness(instance.data, runEndpoint) : "unverified";
const detailStateText = instance.status === "ready" && detailFreshness === "fresh" ? stateLabel(instance.data.state) : instance.status === "ready" ? `最后观测:${stateLabel(instance.data.state)}Run 未验证)` : "未验证";
const detailSections = useMemo(() => serverDetailSectionEntries(readyPlugin), [readyPlugin]);
useEffect(() => {
@@ -243,7 +251,7 @@ export function ServerDetailPage(props: PageComponentProps) {
</span>
</div>
<div className="action-strip">
<span className={cx("status-pill", statusClass(instance.data.state))}>{stateLabel(instance.data.state)}</span>
<span className={cx("status-pill", detailFreshness === "fresh" ? statusClass(instance.data.state) : "status-disabled")}>{detailStateText}</span>
<button
type="button"
className="icon-command"
@@ -262,12 +270,11 @@ export function ServerDetailPage(props: PageComponentProps) {
<Square size={15} />
<span></span>
</button>
<button type="button" className="icon-command" onClick={() => setLiveLogOpen(true)}><ScrollText size={15} /><span></span></button>
<button type="button" className="icon-command" disabled={!canManageServers} title={canManageServers ? "管理终端" : "当前账号没有管理权限"} onClick={() => setTerminalOpen(true)}><Terminal size={15} /><span></span></button>
</div>
</div>
<div className="server-detail-stat-strip">
<HeaderStat label="状态" value={serverIsOnline(instance.data.state) ? "在线" : "离线"} />
<HeaderStat label="状态" value={detailFreshness === "fresh" ? (serverIsOnline(instance.data.state) ? "在线" : "离线") : "未验证"} />
<HeaderStat label="玩家" value={metrics?.playerCount !== undefined ? `${metrics.playerCount}${metrics.maxPlayers ? `/${metrics.maxPlayers}` : ""}` : "--"} />
<HeaderStat label="TPS" value={metrics?.tps !== undefined ? metrics.tps.toFixed(1) : "--"} />
<HeaderStat label="延迟" value={metrics?.latencyMs !== undefined ? `${Math.round(metrics.latencyMs)}ms` : "--"} />
@@ -310,7 +317,6 @@ export function ServerDetailPage(props: PageComponentProps) {
{section === "config" && <ConfigSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
{section === "llm" && <LlmSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
{section === "history" && <HistorySection serverId={serverId} serverOperations={serverOperations} jobs={jobs} artifacts={artifacts} metricHistory={metricHistory} backups={backups} remoteAdapters={remoteAdapters} />}
<ServerLiveLogDrawer open={liveLogOpen} serverId={instance.data.id} serverName={instance.data.name} onClose={() => setLiveLogOpen(false)} />
<ServerManagementTerminalDrawer open={terminalOpen} serverId={instance.data.id} serverName={instance.data.name} pluginId={instance.data.pluginId} canManage={canManageServers} onClose={() => setTerminalOpen(false)} />
</>
)}