Remove manual runtime controls from server detail

This commit is contained in:
npc0-hue
2026-08-10 12:23:17 +08:00
parent b789925ae5
commit fbd24b6a44
24 changed files with 272 additions and 1361 deletions
+4 -4
View File
@@ -74,9 +74,9 @@ npm run dev
For Docker, the web console is built with `VITE_PLATFORM_API_BASE_URL=/api/v1` and served by Nginx. Nginx proxies `/api/v1` and `/healthz` to the `platform` compose service, so browser code never needs a direct backend container address.
Current UI behavior is a browser-verifiable API-backed management console with the required first-party page routes. Server management now includes platform-mediated lifecycle, config, administrator, runtime distribution, dependency, and log workflows.
Current UI behavior is a browser-verifiable API-backed management console with the required first-party page routes. Server management now includes platform-mediated lifecycle, config, administrator, runtime distribution, dependency, log workflows, and plugin-declared server detail pages.
Server list and server detail surfaces expose runtime actions through platform APIs:
Server list surfaces expose runtime package actions through platform APIs, while server detail keeps day-to-day operations plugin-native and platform-mediated:
- generate run packages for selected OS/architecture targets.
- download the latest authorized run package.
@@ -95,7 +95,7 @@ Manual UI smoke checklist:
2. Open the local Vite URL.
3. Verify 首页、服务器管理、插件市场、用户管理、AI 提供商管理 render without visible overlap on desktop and mobile widths.
4. In 服务器管理, verify the server card action menu contains runtime actions as a compact anchored dropdown/popover. It must not become a tall vertical button tower, reflow the card, cover server metrics/progress bars/status badges, or turn the whole card into an accidental click target.
5. In a server detail route, verify the overview renders the 运行分发 section, action availability reasons, dependency/log controls, and safe redacted refs only.
5. In a server detail route, verify plugin-declared pages render before built-in sections and that no manual 运行配置绑定, generic 运行操作 tab, or generic 插件控制 tab appears.
6. Switch black mecha and magical-girl themes when UI styling changed; runtime controls must keep the shared translucent console surfaces and avoid nested double frames.
Automated browser acceptance remains available for deeper local debug verification:
@@ -107,6 +107,6 @@ LOCAL_DEBUG_PLATFORM_PORT=18189 LOCAL_DEBUG_WEB_PORT=5183 LOCAL_DEBUG_ROOT=/priv
This command verifies the API-backed local debug console path, first-party route markers, plugin/server operation proof, fallback rejection, and forbidden-fragment scans. It writes evidence under `<LOCAL_DEBUG_ROOT>/browser-acceptance/`.
# Client Manager workspace
Server Detail includes a Client Manager lifecycle workspace backed by the typed Platform installation projection. It shows profile/target, desired-active-previous versions and revisions, artifact/checksum metadata, key/deployment generations, registration/health/last-seen, real job phases/progress, action gating reasons, retry guidance, and confirmed deploy/control/update/rollback/revoke/key-reset/uninstall workflows. The browser polls active jobs and never fabricates later phases.
Client Manager lifecycle remains a typed Platform installation projection, but it is not exposed as a generic server-detail tab. Dedicated package/dependency actions live in server-list runtime actions or plugin-declared pages, and the browser polls active jobs without fabricating later phases.
The UI keeps the black-mecha and magical-girl crystal-moonlight console materials and uses shared panel/command tokens. It renders no raw key, token, secret ref/value, host path, PID, socket, credential, DSN, RCON password, or Run endpoint address. 401/403 responses remain platform auth/capability errors, not local fallback success.
+7 -10
View File
@@ -178,15 +178,12 @@ async function main() {
markers: [
server.name,
`${server.id} · 插件 ${server.pluginId}@${server.pluginVersion} · 节点 ${server.runEndpointId}`,
"运行分发",
"run 包",
"客户端管理器",
"依赖",
"概览",
"启动",
"停止",
"日志",
"管理终端",
"配置",
"插件控制",
"AI 助手",
"操作历史"
]
@@ -206,8 +203,8 @@ async function main() {
}
}
const pluginControls = await clickAndVerify(chrome, "插件控制", ["生产生命周期", "Logs 桥接执行", "server.logs.read", "server.artifacts.read", "读取"]);
evidence.routes.push({ name: "服务器详情 / 插件控制", url: await chrome.url(), ...pluginControls });
const pluginPage = await clickAndVerify(chrome, "概览", ["插件概览", "dev-game-plugin 页面 bundle", "server.instances.read"]);
evidence.routes.push({ name: "服务器详情 / 插件声明页面", url: await chrome.url(), ...pluginPage });
evidence.productionInteractions = {
alert: await verifyAlertInteraction(chrome, authHeaders, productionSeed.alert),
@@ -392,12 +389,12 @@ async function verifyResponsiveThemeWalkthroughs(chrome, routeChecks, server) {
await chrome.evaluate((serverID) => {
window.location.hash = `#/servers/${encodeURIComponent(serverID)}`;
}, server.id);
await chrome.waitForText([server.name, "插件控制"], `${scenario.name} / server detail tabs`);
const pluginControls = await clickAndVerify(chrome, "插件控制", ["生产生命周期", "Logs 桥接执行", "server.logs.read", "server.artifacts.read", "读取"]);
await chrome.waitForText([server.name, "概览"], `${scenario.name} / server detail tabs`);
const pluginControls = await clickAndVerify(chrome, "概览", ["插件概览", "dev-game-plugin 页面 bundle", "server.instances.read"]);
const pluginLayout = await chrome.layoutSnapshot();
assertNoVisibleLayoutIssues(pluginLayout, `${scenario.name} / plugin controls`);
routeEvidence.push({
name: "服务器详情 / 插件控制",
name: "服务器详情 / 插件声明页面",
url: await chrome.url(),
requiredMarkers: pluginControls.requiredMarkers,
fallbackScan: pluginControls.fallbackScan,
+1 -1
View File
@@ -23,7 +23,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
## Server Management Workflows
- `createServerWorkflow` posts `ServerLifecycleCreateRequest` with the create-wizard deployment definition to `/server-instances/workflows/create`, including deployment mode, plugin create inputs, and custom startup fields when provided. It must not include deployment target, run endpoint, runtime profile, or runtime bindings during creation; those are established only after creation through generated Run registration, runtime binding, or deployment update flows.
- `getServerRuntimeBinding` reads `/server-instances/{id}/runtime-binding`; `updateServerRuntimeBinding` patches the selected profile and logical refs. Responses contain only profile metadata, logical key names, configured/secret-backed flags, missing keys, and safe reasons. They never contain stored refs or secret values.
- `getServerRuntimeBinding` reads `/server-instances/{id}/runtime-binding`; `updateServerRuntimeBinding` patches the selected profile and logical refs for internal/advanced logical transports. Server detail must not expose a manual runtime-binding tab or require these fields before normal start/stop when plugin-declared deployment/lifecycle data is sufficient. Responses contain only profile metadata, logical key names, configured/secret-backed flags, missing keys, and safe reasons. They never contain stored refs or secret values.
- `startServerInstance` and `stopServerInstance` post `ServerLifecycleCommandRequest` with the current config version and receive the lifecycle job response.
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
- `getServerConfig`, `previewServerConfigDiff`, and `approveServerConfigWrite` call platform-mediated config routes. ServerDetailPage must preview the platform diff first, keep the explicit confirmation step, and dispatch writes only through the approval API.
+3 -3
View File
@@ -2,7 +2,7 @@
## Shared Visual Contract
All first-party pages inherit the platform_web game-operations style with black-mecha default materials and a selectable magical-girl theme. Page implementations must use shared theme tokens and surface classes so 首页、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护, server details, drawers, dialogs, logs, diffs, plugin controls, and operation history all feel like one console.
All first-party pages inherit the platform_web game-operations style with black-mecha default materials and a selectable magical-girl theme. Page implementations must use shared theme tokens and surface classes so 首页、服务器管理、插件市场、用户管理、AI 提供商管理、系统维护, server details, drawers, dialogs, logs, diffs, plugin-declared pages, and operation history all feel like one console.
- Major surfaces remain transparent jelly/glass panels with visible background desktop, icy rim light, diamond borders, shine sweeps, and candy-color accents.
- Built-in magical desktops and user-uploaded backgrounds render behind readable contrast surfaces.
@@ -18,12 +18,12 @@ Platform-administrator-only first screen. Shows online/offline server counts, ab
Default landing page for server owners and server administrators. Shows searchable/status-filterable server cards with online/offline state, player count, TPS, latency, CPU, memory, and disk usage; pending metric values render as stable placeholders. Provides create server workflow, refresh, and routed server detail pages. Empty states cover no servers, no matching filters, and role-scoped no-access.
- Create uses installed plugin and run endpoint selections, then renders the returned instance and install job through one visible operation lifecycle.
- Create uses the installed plugin plus plugin-declared deployment/startup inputs, then opens the returned instance through one visible operation lifecycle.
- The page must not display raw run credentials, host paths, direct socket details, or AI provider keys.
## 服务器详情
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, confirmed start/stop lifecycle actions, and direct live-log/management-terminal entry points. Sections: 日志 (level/keyword/time/source filters + log detail drawer with diagnostics), 管理终端 (platform-mediated plugin command surface), 运行操作 (run binding, read-only deployment status, distribution, lifecycle, members), 配置 (edit with reviewable diff before any write job), 插件控制 (controls grouped by plugin, scoped to this server instance, confirmation + lifecycle feedback per action), AI 助手 (LLM suggestions produce recommendation/diff; write jobs require explicit diff confirmation; no raw AI keys reach the frontend), 操作历史 (operation/job IDs, status, timestamps, target, requester, error reasons).
Daily operations hub for one server. Status header shows online state, player count, TPS, latency, CPU/memory/disk progress, metric freshness, confirmed start/stop lifecycle actions, and direct live-log/management-terminal entry points. Plugin-declared pages render as first-class server tabs before platform sections, so each game owns its menu surface. Built-in sections: 日志 (level/keyword/time/source filters + log detail drawer with diagnostics), 管理终端 (platform-mediated plugin command surface), 配置 (deployment status, metadata, administrators, and config edit with reviewable diff before any write job), AI 助手 (LLM suggestions produce recommendation/diff; write jobs require explicit diff confirmation; no raw AI keys reach the frontend), 操作历史 (operation/job IDs, status, timestamps, target, requester, error reasons). Generic runtime-binding and generic plugin-control tabs must not be exposed in server detail.
## 插件市场
+1 -20
View File
@@ -144,35 +144,16 @@ export interface PlatformOverviewSignal {
at: string;
}
export type ServerDetailSection = "logs" | "terminal" | "runtime" | "config" | "plugins" | "llm" | "history";
export type ServerDetailSection = "logs" | "terminal" | "config" | "llm" | "history" | `plugin:${string}`;
export const serverDetailSections: Array<{ id: ServerDetailSection; label: string }> = [
{ id: "logs", label: "日志" },
{ id: "terminal", label: "管理终端" },
{ id: "runtime", label: "运行操作" },
{ id: "config", label: "配置" },
{ id: "plugins", label: "插件控制" },
{ id: "llm", label: "AI 助手" },
{ id: "history", label: "操作历史" }
];
export interface PluginControlDescriptor {
key: string;
label: string;
description: string;
capability: string;
lifecycleAction?: "start" | "stop" | "status";
dangerous: boolean;
}
export interface PluginControlGroupView {
pluginId: string;
pluginName: string;
version: string;
status: string;
controls: PluginControlDescriptor[];
}
export interface DiffLine {
kind: "same" | "added" | "removed";
text: string;
+7 -8
View File
@@ -199,9 +199,9 @@ describe("first-party console pages", () => {
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的受控升级任务");
expect(serverDeploymentWorkflowSource).toContain("已绑定服务器编辑时会直接进入相关配置");
expect(serverDeploymentWorkflowSource).toContain("可在此调整部署方式;不会重复要求选择已绑定的运行节点");
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id, routeKey: "run-builder" })');
expect(serverDetailPageSource).toContain("运行配置绑定");
expect(serverDetailPageSource).toContain('type={field.sensitive ? "password" : "text"}');
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
expect(serverDetailPageSource).not.toContain("运行配置绑定");
expect(serverDetailPageSource).not.toContain('type={field.sensitive ? "password" : "text"}');
expect(serverDeploymentWorkflowSource).toContain("显示已保存配置");
expect(serverDeploymentWorkflowSource).toContain("revealSavedInputs");
expect(serversPageSource).toContain("revealServerDeployment");
@@ -286,8 +286,8 @@ describe("first-party console pages", () => {
expect(serversPageSource).toContain("requireQuickRuntimeActionAvailable(instance.id, action)");
expect(serversPageSource).toContain("该操作不可用");
expect(generateRunSource.indexOf("runtimeTask.runTrackedTask")).toBeLessThan(generateRunSource.indexOf('await requireQuickRuntimeActionAvailable(instance.id, "generate-run")'));
expect(serverDetailPageSource).toContain("RuntimeTaskProgressDialog");
expect(serverDetailPageSource).toContain("runtimeRunBuildStages");
expect(serverDetailPageSource).not.toContain("RuntimeTaskProgressDialog");
expect(serverDetailPageSource).not.toContain("runtimeRunBuildStages");
expect(runtimeTaskProgressSource).toContain("runtimeBuildStages");
expect(runtimeTaskProgressSource).toContain("runtimeRunBuildStages");
expect(runtimeTaskProgressSource).toContain("进度展示");
@@ -311,9 +311,9 @@ describe("first-party console pages", () => {
expect(html).not.toContain("概览");
expect(html).toContain("日志");
expect(html).toContain("管理终端");
expect(html).toContain("运行操作");
expect(html).toContain("配置");
expect(html).toContain("插件控制");
expect(html).not.toContain("运行操作");
expect(html).not.toContain("插件控制");
expect(html).toContain("AI 助手");
expect(html).toContain("操作历史");
});
@@ -353,7 +353,6 @@ describe("first-party console pages", () => {
it("uses declared SCUM log source keys for log backfill defaults", () => {
expect(serversPageSource).toContain("scum-server-events");
expect(serverDetailPageSource).toContain("scum-server-events");
expect(serversPageSource).not.toContain("server-log");
expect(serverDetailPageSource).not.toContain("server-log");
});
+27 -71
View File
@@ -2,8 +2,6 @@ import { describe, expect, it } from "vitest";
import { configDiffViewFromPreview } from "./ServerDetailPage";
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
import clientManagerLifecyclePanelSource from "../components/ClientManagerLifecyclePanel.tsx?raw";
import runtimeDLLExtensionsPanelSource from "../components/RuntimeDLLExtensionsPanel.tsx?raw";
import sourceRCONCommandPanelSource from "../components/SourceRCONCommandPanel.tsx?raw";
import artifactTransferSource from "../utils/artifactTransfer.ts?raw";
import type { ServerConfigDiffPreviewResponse } from "../api/types";
@@ -67,39 +65,21 @@ describe("ServerDetailPage config write approval", () => {
it("uses platform-mediated artifact download and bridge references without backend internals", () => {
expect(serverDetailPageSource).toContain("openArtifactDownload");
expect(serverDetailPageSource).toContain("readArtifactContent");
expect(serverDetailPageSource).toContain("parsePluginArtifactReference");
expect(serverDetailPageSource).toContain("浏览器制品传输");
expect(serverDetailPageSource).not.toContain("storage://");
expect(serverDetailPageSource).not.toContain("unix://");
expect(serverDetailPageSource).not.toContain("Bearer ");
});
it("surfaces run distribution and client-manager workflows through platform-mediated APIs", () => {
expect(serverDetailPageSource).toContain('params.routeKey !== "run-builder"');
expect(serverDetailPageSource).toContain('id="run-builder"');
expect(serverDetailPageSource).toContain("scrollIntoView");
expect(serverDetailPageSource).toContain("getServerRuntimeActions");
expect(serverDetailPageSource).toContain("generateRunDistribution");
expect(serverDetailPageSource).toContain("downloadLatestRunDistribution");
expect(serverDetailPageSource).toContain("pushRunUpdate");
expect(serverDetailPageSource).toContain("generateClientManager");
expect(serverDetailPageSource).toContain("resetClientManagerKey");
expect(serverDetailPageSource).toContain("checkDependencies");
expect(serverDetailPageSource).toContain("installDependencies");
it("keeps run distribution and client-manager workflows out of server detail tabs", () => {
expect(serverDetailPageSource).not.toContain('id="run-builder"');
expect(serverDetailPageSource).not.toContain("getServerRuntimeActions");
expect(serverDetailPageSource).not.toContain("generateRunDistribution");
expect(serverDetailPageSource).not.toContain("downloadLatestRunDistribution");
expect(serverDetailPageSource).not.toContain("pushRunUpdate");
expect(serverDetailPageSource).not.toContain("generateClientManager");
expect(serverDetailPageSource).not.toContain("ClientManagerLifecyclePanel");
expect(serverDetailPageSource).toContain("openServerLogEvents");
expect(serverDetailPageSource).toContain("requestLogBackfill");
expect(serverDetailPageSource).toContain("ClientManagerLifecyclePanel");
expect(clientManagerLifecyclePanelSource).toContain("listClientManagerLifecycles");
expect(clientManagerLifecyclePanelSource).toContain("deployClientManager");
expect(clientManagerLifecyclePanelSource).toContain("controlClientManager");
expect(clientManagerLifecyclePanelSource).toContain("updateClientManager");
expect(clientManagerLifecyclePanelSource).toContain("retryClientManagerLifecycle");
expect(clientManagerLifecyclePanelSource).toContain("revokeClientManagerSession");
expect(clientManagerLifecyclePanelSource).toContain("uninstallClientManager");
expect(clientManagerLifecyclePanelSource).toContain("expectedDeploymentGeneration");
expect(clientManagerLifecyclePanelSource).not.toContain("secretRef");
expect(clientManagerLifecyclePanelSource).not.toContain("hostPath");
expect(clientManagerLifecyclePanelSource).not.toContain("process.pid");
expect(serverDetailPageSource).not.toContain("authKey");
expect(serverDetailPageSource).not.toContain("password=");
expect(serverDetailPageSource).not.toContain("unix://");
@@ -108,15 +88,11 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain("sqlite://");
});
it("renders declared UE4SS DLL update policy without local paths or RCON secrets", () => {
expect(serverDetailPageSource).toContain("RuntimeDLLExtensionsPanel");
expect(runtimeDLLExtensionsPanelSource).toContain("启动前自动校验和更新");
expect(runtimeDLLExtensionsPanelSource).toContain("运行SCUM 服务器受监管启动");
expect(runtimeDLLExtensionsPanelSource).toContain("由 UE4SS 正常加载");
expect(runtimeDLLExtensionsPanelSource).toContain("Linux 启动前拒绝");
for (const forbidden of ["dllRef", "targetKey", "rconPort", "password="]) {
expect(runtimeDLLExtensionsPanelSource).not.toContain(forbidden);
}
it("does not expose manual runtime configuration surfaces", () => {
expect(serverDetailPageSource).not.toContain("getServerRuntimeBinding");
expect(serverDetailPageSource).not.toContain("updateServerRuntimeBinding");
expect(serverDetailPageSource).not.toContain("运行配置绑定");
expect(serverDetailPageSource).not.toContain("RuntimeDLLExtensionsPanel");
});
it("adds SCUM announcements and raw commands through protected RCON bridge jobs", () => {
@@ -132,33 +108,15 @@ describe("ServerDetailPage config write approval", () => {
}
});
it("loads the dependency catalog only after runtime actions expose dependency operations", () => {
const runtimeDistributionSectionSource = serverDetailPageSource.split("function RuntimeDistributionSection")[1]?.split("function RuntimeBindingFields")[0] ?? "";
expect(runtimeDistributionSectionSource).toContain('action.key === "dependencies-check" || action.key === "dependencies-install"');
expect(runtimeDistributionSectionSource).toContain("dependencyActions.some((action) => action.available)");
expect(runtimeDistributionSectionSource).toContain("getDependencyCatalog(instance.id)");
});
it("reviews and updates only redacted runtime binding metadata", () => {
const runtimeBindingSectionSource = serverDetailPageSource.split("function RuntimeBindingSection")[1]?.split("function RuntimeDistributionSection")[0] ?? "";
expect(serverDetailPageSource).toContain("getServerRuntimeBinding");
expect(serverDetailPageSource).toContain("updateServerRuntimeBinding");
expect(runtimeBindingSectionSource).toContain("missingKeys");
expect(runtimeBindingSectionSource).toContain('type={field.sensitive ? "password" : "text"}');
for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://", "mysql://", "sqlite://"]) {
expect(runtimeBindingSectionSource).not.toContain(forbidden);
}
});
it("routes plugin lifecycle controls through platform lifecycle APIs instead of generic jobs", () => {
expect(serverDetailPageSource).toContain('action === "install" || action === "restart"');
expect(serverDetailPageSource).toContain('action !== "start" && action !== "stop" && action !== "status"');
expect(serverDetailPageSource).toContain('control.lifecycleAction === "start" || control.lifecycleAction === "stop" || control.lifecycleAction === "status"');
expect(serverDetailPageSource).toContain("platformApiClient.startServerInstance(instance.id");
expect(serverDetailPageSource).toContain("platformApiClient.stopServerInstance(instance.id");
expect(serverDetailPageSource).toContain("platformApiClient.queryServerProcessStatus(instance.id");
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(instance, \"start\")");
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(instance, \"stop\")");
it("removes generic plugin controls from server detail", () => {
expect(serverDetailPageSource).not.toContain("PluginControlsSection");
expect(serverDetailPageSource).not.toContain("PluginBridgeExecutionPanel");
expect(serverDetailPageSource).not.toContain("platformApiClient.createJob({");
expect(serverDetailPageSource).not.toContain("插件控制");
expect(serverDetailPageSource).toContain("platformApiClient.startServerInstance(current.id");
expect(serverDetailPageSource).toContain("platformApiClient.stopServerInstance(current.id");
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(current, \"start\")");
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(current, \"stop\")");
expect(serverDetailPageSource).not.toContain('capability: "process.start"');
expect(serverDetailPageSource).not.toContain('capability: "process.stop"');
});
@@ -170,17 +128,15 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain('capability: "process.install"');
});
it("routes the SCUM logs section to the declared file management workbench", () => {
expect(serverDetailPageSource).toContain("serverDetailSectionLabel(entry, instance.data.pluginId)");
expect(serverDetailPageSource).toContain('"文件管理"');
expect(serverDetailPageSource).toContain("ScumFileManagementSection");
it("routes plugin-declared pages into server detail tabs", () => {
expect(serverDetailPageSource).toContain("serverDetailSectionEntries(readyPlugin)");
expect(serverDetailPageSource).toContain("plugin:${page.key}");
expect(serverDetailPageSource).toContain("PluginPageSection");
expect(serverDetailPageSource).toContain("PluginPageHostPage");
expect(serverDetailPageSource).toContain('routeKey: "files-config"');
expect(serverDetailPageSource).toContain('file.kind === "config"');
expect(serverDetailPageSource).not.toContain("ScumFileManagementSection");
});
it("keeps plugin lifecycle and bridge-visible output on platform-owned logical references", () => {
expect(serverDetailPageSource).toContain("parsePluginArtifactReference(result)");
expect(serverDetailPageSource).toContain("platformApiClient.openArtifactDownload(artifact.id)");
expect(serverDetailPageSource).toContain("downloadArtifactReference(reference");
expect(artifactTransferSource).toContain("readContent(reference.artifactId");
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -176,7 +176,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
setForm(defaultServerCreateForm(plugins, endpoints));
setShowCreate(false);
await refresh();
onNavigate("serverDetail", { serverId: result.instance.id, routeKey: "run-builder" });
onNavigate("serverDetail", { serverId: result.instance.id });
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "创建失败", operationId);
}
+1 -1
View File
@@ -6,7 +6,7 @@ First-party routes must be declared here before page implementation.
- `/`: 平台概览(平台管理员默认落地页).
- `/servers`: 服务器管理(服主/服务器管理员默认落地页).
- `/servers/:serverId`: 服务器详情 route(日常运维工作台:日志、管理终端、运行操作、配置、插件控制、AI 助手、操作历史).
- `/servers/:serverId`: 服务器详情 route(日常运维工作台:插件声明页面、日志、管理终端、配置、AI 助手、操作历史).
- `/plugins`: 插件市场.
- `/users`: 用户管理.
- `/ai-providers`: AI 提供商管理.
+2 -2
View File
@@ -39,8 +39,8 @@ describe("console shell routes", () => {
expect(resolved.route.id).toBe("serverDetail");
expect(resolved.params.serverId).toBe("server-example-1");
expect(hashForPage("serverDetail", { serverId: "server-example-1" })).toBe("#/servers/server-example-1");
expect(hashForPage("serverDetail", { serverId: "server-example-1", routeKey: "run-builder" })).toBe("#/servers/server-example-1?focus=run-builder");
expect(resolveRouteHash("#/servers/server-example-1?focus=run-builder", platformAdmin).params).toEqual({ serverId: "server-example-1", routeKey: "run-builder" });
expect(hashForPage("serverDetail", { serverId: "server-example-1", routeKey: "overview" })).toBe("#/servers/server-example-1?focus=overview");
expect(resolveRouteHash("#/servers/server-example-1?focus=overview", platformAdmin).params).toEqual({ serverId: "server-example-1", routeKey: "overview" });
});
it("round-trips hosted plugin page hashes with server context", () => {