From 1207ba6c5b533622ee27e64c3b60613f995cbc42 Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Mon, 10 Aug 2026 21:35:02 +0800 Subject: [PATCH] Restore safe server terminal entry --- platform_web/pages/ConsolePages.test.tsx | 5 +- platform_web/pages/ServerDetailPage.test.tsx | 2 + platform_web/pages/ServerDetailPage.tsx | 83 +++++++++++++++++++- platform_web/routes/routes.md | 2 +- 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/platform_web/pages/ConsolePages.test.tsx b/platform_web/pages/ConsolePages.test.tsx index 67d0a40..a149d17 100644 --- a/platform_web/pages/ConsolePages.test.tsx +++ b/platform_web/pages/ConsolePages.test.tsx @@ -213,7 +213,7 @@ describe("first-party console pages", () => { } }); - it("removes raw log and management terminal entry points from server pages", () => { + it("removes raw log and command panels while preserving the safe terminal entry", () => { expect(serversPageSource).not.toContain("ServerLiveLogDrawer"); expect(serversPageSource).not.toContain("ServerManagementTerminalDrawer"); expect(serversPageSource).not.toContain("live-logs"); @@ -228,6 +228,8 @@ describe("first-party console pages", () => { expect(serverDetailPageSource).not.toContain("previewServerConfigDiff"); expect(serverDetailPageSource).not.toContain("approveServerConfigWrite"); expect(serverDetailPageSource).not.toContain("操作历史"); + expect(serverDetailPageSource).toContain("打开终端"); + expect(serverDetailPageSource).toContain("RemoteTerminalEntryDialog"); expect(serverDetailPageSource).toContain("runtimeObservationFreshness"); expect(serverDetailPageSource).toContain("Run 未验证"); expect(serverDetailPageSource).toContain("PluginPageSection"); @@ -332,6 +334,7 @@ describe("first-party console pages", () => { expect(serverDetailPageSource).toContain("PluginPageHostPage"); expect(serverDetailPageSource).toContain("plugin:${page.key}"); expect(serverDetailPageSource).toContain("section === \"llm\""); + expect(serverDetailPageSource).toContain("打开终端"); expect(serverDetailPageSource).not.toContain("terminalQuickCommandCatalog"); expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest"); expect(serverDetailPageSource).not.toContain("queueGameClientBridgeCommand"); diff --git a/platform_web/pages/ServerDetailPage.test.tsx b/platform_web/pages/ServerDetailPage.test.tsx index b765dbd..be8c907 100644 --- a/platform_web/pages/ServerDetailPage.test.tsx +++ b/platform_web/pages/ServerDetailPage.test.tsx @@ -100,6 +100,8 @@ describe("ServerDetailPage config write approval", () => { expect(serverDetailPageSource).not.toContain("queueGameClientBridgeCommand"); expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest"); expect(serverDetailPageSource).not.toContain("管理终端"); + expect(serverDetailPageSource).toContain("打开终端"); + expect(serverDetailPageSource).toContain("RemoteTerminalEntryDialog"); expect(serverDetailPageSource).toContain("PluginPageHostPage"); }); diff --git a/platform_web/pages/ServerDetailPage.tsx b/platform_web/pages/ServerDetailPage.tsx index 92f1a11..84caf09 100644 --- a/platform_web/pages/ServerDetailPage.tsx +++ b/platform_web/pages/ServerDetailPage.tsx @@ -1,4 +1,4 @@ -import { MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react"; +import { 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"; @@ -6,6 +6,7 @@ import type { ConfigDiffLineResponse, GamePluginResponse, JobResponse, + RemoteAdapterDeclarationResponse, ServerInstanceResponse, ServerMemberResponse, ServerMetricsResponse, @@ -13,7 +14,7 @@ import type { ServerConfigDiffPreviewResponse, RunEndpointResponse } from "../api/types"; -import { ConfirmDialog, UsageMeter } from "../components/OperationControls"; +import { ConfirmDialog, ManagementDialog, UsageMeter } from "../components/OperationControls"; import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews"; import type { PageComponentProps } from "../contracts/page"; import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement"; @@ -49,6 +50,8 @@ export function ServerDetailPage(props: PageComponentProps) { const [deployment, setDeployment] = useState>({ status: "loading" }); const [confirm, setConfirm] = useState Promise }>(null); const [confirmBusy, setConfirmBusy] = useState(false); + const [terminalOpen, setTerminalOpen] = useState(false); + const [terminalAdapters, setTerminalAdapters] = useState>({ status: "loading" }); const refresh = useCallback(async () => { if (!serverId) { @@ -157,6 +160,15 @@ 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 ( onNavigate("servers")} /> @@ -209,6 +221,16 @@ export function ServerDetailPage(props: PageComponentProps) {
{detailStateText} + +
+ {adapters.status === "loading" && } + {adapters.status === "error" && } + {adapters.status === "ready" && adapters.data.length === 0 && } + {adapters.status === "ready" && adapters.data.length > 0 && ( +
+ {adapters.data.map((adapter) => ( +
+ {adapter.key} · {adapter.kind} + {adapter.capabilities.join(" / ")} +
+ ))} +
+ )} + + ); +} + +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]; diff --git a/platform_web/routes/routes.md b/platform_web/routes/routes.md index f297be9..8da6ab2 100644 --- a/platform_web/routes/routes.md +++ b/platform_web/routes/routes.md @@ -6,7 +6,7 @@ First-party routes must be declared here before page implementation. - `/`: 平台概览(平台管理员默认落地页). - `/servers`: 服务器管理(服主/服务器管理员默认落地页). -- `/servers/:serverId`: 服务器详情 route(日常运维工作台:插件声明页面、管理、AI 助手;不暴露 raw 日志、管理终端、raw 配置或操作历史页). +- `/servers/:serverId`: 服务器详情 route(日常运维工作台:插件声明页面、管理、AI 助手、打开终端安全入口;不暴露 raw 日志、旧命令面板、raw 配置或操作历史页). - `/plugins`: 插件市场. - `/users`: 用户管理. - `/ai-providers`: AI 提供商管理.