Restore interactive server terminal drawer

This commit is contained in:
npc0-hue
2026-08-10 23:43:17 +08:00
parent 1207ba6c5b
commit 0154f42485
7 changed files with 410 additions and 83 deletions
+11 -7
View File
@@ -10,6 +10,7 @@ import { ServersPage } from "./ServersPage";
import { UsersPage } from "./UsersPage";
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
import serverManagementTerminalSource from "../components/ServerManagementTerminalDrawer.tsx?raw";
import serverCreateSchemaSource from "../schemas/serverManagement.ts?raw";
import serversPageSource from "./ServersPage.tsx?raw";
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
@@ -213,23 +214,26 @@ describe("first-party console pages", () => {
}
});
it("removes raw log and command panels while preserving the safe terminal entry", () => {
it("removes page-level raw panels while preserving the historical terminal drawer", () => {
expect(serversPageSource).not.toContain("ServerLiveLogDrawer");
expect(serversPageSource).not.toContain("ServerManagementTerminalDrawer");
expect(serversPageSource).not.toContain("live-logs");
expect(serversPageSource).not.toContain("historical-logs");
expect(serversPageSource).not.toContain("requestLogBackfill");
expect(serversPageSource).not.toContain("管理终端");
expect(serverDetailPageSource).not.toContain("ServerManagementTerminalDrawer");
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
expect(serverDetailPageSource).not.toContain("SourceRCONCommandPanel");
expect(serverDetailPageSource).not.toContain("ServerLiveLogDrawer");
expect(serverDetailPageSource).not.toContain('<span>实时日志</span>');
expect(serverDetailPageSource).not.toContain("openServerLogEvents");
expect(serverDetailPageSource).not.toContain("previewServerConfigDiff");
expect(serverDetailPageSource).not.toContain("approveServerConfigWrite");
expect(serverDetailPageSource).not.toContain("操作历史");
expect(serverDetailPageSource).toContain("<span>打开终端</span>");
expect(serverDetailPageSource).toContain("RemoteTerminalEntryDialog");
expect(serverManagementTerminalSource).toContain("openServerLogEvents");
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
expect(serverManagementTerminalSource).toContain("terminalQuickCommandCatalog");
expect(serverManagementTerminalSource).not.toContain("password");
expect(serverManagementTerminalSource).not.toContain("direct socket");
expect(serverDetailPageSource).toContain("runtimeObservationFreshness");
expect(serverDetailPageSource).toContain("Run 未验证");
expect(serverDetailPageSource).toContain("PluginPageSection");
@@ -322,7 +326,7 @@ describe("first-party console pages", () => {
expect(html).not.toContain("概览");
expect(html).toContain("管理");
expect(html).not.toContain("日志");
expect(html).not.toContain("管理终端");
expect(serverDetailPageSource).toContain("打开终端");
expect(html).not.toContain("配置");
expect(html).not.toContain("运行操作");
expect(html).not.toContain("插件控制");
@@ -335,9 +339,9 @@ describe("first-party console pages", () => {
expect(serverDetailPageSource).toContain("plugin:${page.key}");
expect(serverDetailPageSource).toContain("section === \"llm\"");
expect(serverDetailPageSource).toContain("打开终端");
expect(serverDetailPageSource).not.toContain("terminalQuickCommandCatalog");
expect(serverManagementTerminalSource).toContain("terminalQuickCommandCatalog");
expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest");
expect(serverDetailPageSource).not.toContain("queueGameClientBridgeCommand");
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
expect(serverDetailPageSource).not.toContain("commandHistory");
});
+7 -5
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { configDiffViewFromPreview } from "./ServerDetailPage";
import serverManagementTerminalSource from "../components/ServerManagementTerminalDrawer.tsx?raw";
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
import type { ServerConfigDiffPreviewResponse } from "../api/types";
@@ -79,7 +80,7 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain("pushRunUpdate");
expect(serverDetailPageSource).not.toContain("generateClientManager");
expect(serverDetailPageSource).not.toContain("ClientManagerLifecyclePanel");
expect(serverDetailPageSource).not.toContain("openServerLogEvents");
expect(serverManagementTerminalSource).toContain("openServerLogEvents");
expect(serverDetailPageSource).not.toContain("authKey");
expect(serverDetailPageSource).not.toContain("password=");
expect(serverDetailPageSource).not.toContain("unix://");
@@ -95,13 +96,14 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain("RuntimeDLLExtensionsPanel");
});
it("does not expose raw SCUM RCON command panels", () => {
it("uses the historical terminal drawer without restoring separate raw command panels", () => {
expect(serverDetailPageSource).not.toContain("SourceRCONCommandPanel");
expect(serverDetailPageSource).not.toContain("queueGameClientBridgeCommand");
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest");
expect(serverDetailPageSource).not.toContain("管理终端");
expect(serverDetailPageSource).toContain("<span>打开终端</span>");
expect(serverDetailPageSource).toContain("RemoteTerminalEntryDialog");
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
expect(serverManagementTerminalSource).toContain("terminal-output-topbar");
expect(serverManagementTerminalSource).toContain("terminalQuickCommandCatalog");
expect(serverDetailPageSource).toContain("PluginPageHostPage");
});
+4 -70
View File
@@ -6,7 +6,6 @@ import type {
ConfigDiffLineResponse,
GamePluginResponse,
JobResponse,
RemoteAdapterDeclarationResponse,
ServerInstanceResponse,
ServerMemberResponse,
ServerMetricsResponse,
@@ -14,7 +13,8 @@ import type {
ServerConfigDiffPreviewResponse,
RunEndpointResponse
} from "../api/types";
import { ConfirmDialog, ManagementDialog, UsageMeter } from "../components/OperationControls";
import { ConfirmDialog, UsageMeter } from "../components/OperationControls";
import { ServerManagementTerminalDrawer } from "../components/ServerManagementTerminalDrawer";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
@@ -51,7 +51,6 @@ export function ServerDetailPage(props: PageComponentProps) {
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
const [confirmBusy, setConfirmBusy] = useState(false);
const [terminalOpen, setTerminalOpen] = useState(false);
const [terminalAdapters, setTerminalAdapters] = useState<LoadState<RemoteAdapterDeclarationResponse[]>>({ status: "loading" });
const refresh = useCallback(async () => {
if (!serverId) {
@@ -160,15 +159,6 @@ export function ServerDetailPage(props: PageComponentProps) {
});
}
function openTerminalEntry(current: ServerInstanceResponse) {
setTerminalOpen(true);
setTerminalAdapters({ status: "loading" });
void platformApiClient
.listRemoteAdapters(current.id)
.then((response) => setTerminalAdapters({ status: "ready", data: response.items }))
.catch((error) => setTerminalAdapters({ status: "error", reason: error instanceof Error ? error.message : "终端能力加载失败" }));
}
if (!serverId) {
return (
<EmptyState title="未选择服务器" description="请从服务器列表进入详情页。" actionLabel="返回服务器列表" onAction={() => onNavigate("servers")} />
@@ -226,7 +216,7 @@ export function ServerDetailPage(props: PageComponentProps) {
className="icon-command"
disabled={!canManageServers}
title={canManageServers ? "打开终端" : "当前账号没有管理权限"}
onClick={() => openTerminalEntry(instance.data)}
onClick={() => setTerminalOpen(true)}
>
<Terminal size={15} />
<span></span>
@@ -291,6 +281,7 @@ export function ServerDetailPage(props: PageComponentProps) {
)}
{section === "manage" && <ServerAdministratorsSection instance={instance.data} session={session} onChanged={(next) => setInstance({ status: "ready", data: next })} />}
{section === "llm" && <LlmSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
<ServerManagementTerminalDrawer open={terminalOpen} serverId={instance.data.id} serverName={instance.data.name} pluginId={instance.data.pluginId} canManage={canManageServers} onClose={() => setTerminalOpen(false)} />
</>
)}
@@ -313,67 +304,10 @@ export function ServerDetailPage(props: PageComponentProps) {
});
}}
/>
{instance.status === "ready" && (
<RemoteTerminalEntryDialog
open={terminalOpen}
instance={instance.data}
adapters={terminalAdapters}
onRefresh={() => openTerminalEntry(instance.data)}
onClose={() => setTerminalOpen(false)}
/>
)}
</section>
);
}
interface RemoteTerminalEntryDialogProps {
open: boolean;
instance: ServerInstanceResponse;
adapters: LoadState<RemoteAdapterDeclarationResponse[]>;
onRefresh: () => void;
onClose: () => void;
}
function RemoteTerminalEntryDialog({ open, instance, adapters, onRefresh, onClose }: RemoteTerminalEntryDialogProps) {
const terminalReadyAdapters = adapters.status === "ready" ? adapters.data.filter(isTerminalReadyAdapter) : [];
return (
<ManagementDialog
open={open}
title="打开终端"
description={`服务器 ${instance.name} 的快捷入口已保留;这里仅使用 Run 声明的安全远程能力,不向浏览器暴露主机连接细节。`}
wide
onClose={onClose}
>
<div className="console-row-list">
<div className="console-row"><span></span><strong>{instance.id}</strong></div>
<div className="console-row"><span>Run </span><strong>{instance.runEndpointId || "未绑定"}</strong></div>
<div className="console-row"><span></span><strong>{terminalReadyAdapters.length > 0 ? "等待 Run 会话实现" : "尚未声明交互会话"}</strong></div>
</div>
<p className="section-copy"> Run </p>
<div className="action-strip" style={{ marginTop: 12 }}>
<button type="button" className="icon-command" onClick={onRefresh}><Sparkles size={14} /><span></span></button>
</div>
{adapters.status === "loading" && <LoadingState label="正在读取 Run 远程能力…" compact />}
{adapters.status === "error" && <ErrorState title="终端能力不可用" reason={adapters.reason} diagnosticId={`remote-terminal:${instance.id}`} onRetry={onRefresh} compact />}
{adapters.status === "ready" && adapters.data.length === 0 && <EmptyState title="暂无远程适配能力" description="Run 还没有为这个服务器上报可用的安全远程入口。" />}
{adapters.status === "ready" && adapters.data.length > 0 && (
<div className="console-row-list" style={{ marginTop: 12 }}>
{adapters.data.map((adapter) => (
<div key={adapter.key} className="console-row">
<span>{adapter.key} · {adapter.kind}</span>
<strong>{adapter.capabilities.join(" / ")}</strong>
</div>
))}
</div>
)}
</ManagementDialog>
);
}
function isTerminalReadyAdapter(adapter: RemoteAdapterDeclarationResponse): boolean {
return adapter.kind === "run-process" || adapter.capabilities.some((capability) => capability === "remote.run.process.start" || capability === "remote.run.process.stop" || capability === "remote.run.program.command");
}
function serverDetailSectionEntries(plugin?: GamePluginResponse): Array<{ id: ServerDetailSection; label: string }> {
const pluginPages = (plugin?.pages ?? []).map((page) => ({ id: `plugin:${page.key}` as ServerDetailSection, label: page.title }));
return [...pluginPages, ...serverDetailSections];