Remove legacy runtime deployment paths
This commit is contained in:
@@ -67,6 +67,8 @@ describe("first-party console pages", () => {
|
||||
|
||||
expect(runPlatformOptions(plugin, "darwin")).toEqual(["windows", "linux"]);
|
||||
expect(runPlatformOptions(undefined, "windows")).toEqual(["windows"]);
|
||||
expect(serversPageSource).not.toContain("quickRuntimeDefaultsForPlugin");
|
||||
expect(serversPageSource).not.toContain("pluginId.toLowerCase().includes");
|
||||
});
|
||||
|
||||
it("renders the platform overview with first-screen health modules", () => {
|
||||
@@ -184,23 +186,25 @@ describe("first-party console pages", () => {
|
||||
expect(serversPageSource).not.toContain('instance.state === "running" || instance.state === "installing"');
|
||||
expect(serverDetailPageSource).not.toContain("<ServerDeploymentWorkflow");
|
||||
expect(serverDetailPageSource).toContain("ServerDeploymentSection");
|
||||
expect(serverDeploymentWorkflowSource).toContain("基本信息");
|
||||
expect(serverDeploymentWorkflowSource).toContain("基本信息");
|
||||
expect(serverDeploymentWorkflowSource).toContain("部署方式");
|
||||
expect(serverDeploymentWorkflowSource).toContain("相关配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("自动上报心跳");
|
||||
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
|
||||
expect(serverDeploymentWorkflowSource).toContain("自动上报心跳");
|
||||
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
|
||||
expect(serverDeploymentWorkflowSource).toContain("执行目录(可选)");
|
||||
expect(serverDeploymentWorkflowSource).toContain("默认使用服务器目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("安装目录{isScum ? \"(必填)\" : \"(可选)\"}");
|
||||
expect(serverDeploymentWorkflowSource).toContain("安装目录(可选)");
|
||||
expect(serverDeploymentWorkflowSource).toContain("新建并安装执行流程");
|
||||
expect(serverDeploymentWorkflowSource).toContain("安装目录”就是游戏服务端、数据和配置将落地的位置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("通过 SteamCMD 安装 App 3792580 到该目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("全部 4 步通过才算安装成功");
|
||||
expect(serverDeploymentWorkflowSource).toContain("插件日志按插件输出原样显示");
|
||||
expect(serverDeploymentWorkflowSource).toContain("按插件声明的推荐方案安装到该目录");
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("SteamCMD 安装 App 3792580");
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("selectedPlugin?.id === \"game.scum\"");
|
||||
expect(serverDeploymentWorkflowSource).toContain("已有服务器目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("不会重装或覆盖现有游戏配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
|
||||
expect(serverDeploymentWorkflowSource).toContain("不需要填写 SteamCMD 目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的自动升级任务");
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("不需要填写 SteamCMD 目录");
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("当前平台尚未提供 SCUM 服务端的自动升级任务");
|
||||
expect(serverDeploymentWorkflowSource).toContain("Run 会按心跳自动识别服务器");
|
||||
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
|
||||
expect(serverDetailPageSource).not.toContain("运行配置绑定");
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("PluginsPage", () => {
|
||||
expect(html).toContain("停用");
|
||||
expect(html).toContain("SCUM Simple RCON UE4SS DLL");
|
||||
expect(html).toContain("启动前自动校验和更新");
|
||||
expect(html).toContain("运行:SCUM 服务器受监管启动");
|
||||
expect(html).toContain("运行:目标服务端受监管启动");
|
||||
expect(html).toContain("Linux 启动前拒绝");
|
||||
expect(html).toContain('role="dialog"');
|
||||
expect(html).not.toContain("billing");
|
||||
|
||||
@@ -138,6 +138,10 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain("tcp://");
|
||||
expect(serverDetailPageSource).not.toContain("mysql://");
|
||||
expect(serverDetailPageSource).not.toContain("sqlite://");
|
||||
expect(serverDetailPageSource).not.toContain("SCUM 部署模板");
|
||||
expect(serverDetailPageSource).not.toContain("isScumTemplate");
|
||||
expect(serverDetailPageSource).toContain("部署验证模板");
|
||||
expect(serverDetailPageSource).toContain("平台普通响应不会回显");
|
||||
});
|
||||
|
||||
it("does not expose manual runtime configuration surfaces", () => {
|
||||
|
||||
@@ -379,35 +379,35 @@ function ServerMetadataSection({ instance, session, operations, onChanged }: Ser
|
||||
}
|
||||
|
||||
interface ServerDeploymentSectionProps {
|
||||
instance: ServerInstanceResponse;
|
||||
deployment: LoadState<ServerDeploymentResponse>;
|
||||
instance: ServerInstanceResponse;
|
||||
deployment: LoadState<ServerDeploymentResponse>;
|
||||
}
|
||||
|
||||
function ServerDeploymentSection({ instance, deployment }: ServerDeploymentSectionProps) {
|
||||
if (deployment.status === "loading") return <LoadingState label="正在加载部署定义…" compact />;
|
||||
if (deployment.status === "error") return <ErrorState title="部署定义不可用" reason={deployment.reason} diagnosticId={`deployment:${instance.id}`} compact />;
|
||||
const view = deployment.data;
|
||||
const projection = view.projection;
|
||||
const isScumTemplate = (instance.pluginId === "game.scum" && (view.mode === "guided-install" || view.mode === "existing-server")) || projection?.templateKey?.startsWith("scum-");
|
||||
return <article className="console-panel" aria-label="server deployment">
|
||||
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> 部署定义</h2><span className="page-status">{view.mode || "未配置"} · 修订 {view.revision}</span></div>
|
||||
<p className="section-copy">服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,不会回显。</p>
|
||||
<div className="console-row-list"><div className="console-row"><span>服务器目录</span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span>高级执行目录</span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span>启动设置</span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span>最近 Run 调度</span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run 执行确认</span><strong>已按 r{view.latestDispatch.deploymentRevision} 确认执行</strong></div>}</div>
|
||||
{isScumTemplate && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>SCUM 部署模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
||||
</article>;
|
||||
if (deployment.status === "loading") return <LoadingState label="正在加载部署定义…" compact />;
|
||||
if (deployment.status === "error") return <ErrorState title="部署定义不可用" reason={deployment.reason} diagnosticId={`deployment:${instance.id}`} compact />;
|
||||
const view = deployment.data;
|
||||
const projection = view.projection;
|
||||
const hasProjection = Boolean(projection?.templateKey || projection?.templateVersion || projection?.preflightState || projection?.discoveryState || projection?.mappingState || projection?.verificationState || projection?.failureCode);
|
||||
return <article className="console-panel" aria-label="server deployment">
|
||||
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> 部署定义</h2><span className="page-status">{view.mode || "未配置"} · 修订 {view.revision}</span></div>
|
||||
<p className="section-copy">服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,平台普通响应不会回显。</p>
|
||||
<div className="console-row-list"><div className="console-row"><span>服务器目录</span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span>高级执行目录</span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span>启动设置</span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span>最近 Run 调度</span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run 执行确认</span><strong>已按 r{view.latestDispatch.deploymentRevision} 确认执行</strong></div>}</div>
|
||||
{hasProjection && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>部署验证模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : projection?.templateKey ?? "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
||||
</article>;
|
||||
}
|
||||
|
||||
function deploymentProjectionLabel(value?: string): string {
|
||||
switch (value) {
|
||||
case "queued": return "排队中";
|
||||
case "running": return "执行中";
|
||||
case "passed": return "已通过";
|
||||
case "applied": return "已写入";
|
||||
case "unchanged": return "未变化";
|
||||
case "failed": return "失败";
|
||||
case "skipped": return "已跳过";
|
||||
default: return "待返回";
|
||||
}
|
||||
switch (value) {
|
||||
case "queued": return "排队中";
|
||||
case "running": return "执行中";
|
||||
case "passed": return "已通过";
|
||||
case "applied": return "已写入";
|
||||
case "unchanged": return "未变化";
|
||||
case "failed": return "失败";
|
||||
case "skipped": return "已跳过";
|
||||
default: return "待返回";
|
||||
}
|
||||
}
|
||||
|
||||
function deploymentProgressLabel(progress: JobResponse["progress"]): string {
|
||||
|
||||
@@ -3,7 +3,16 @@ import { type CSSProperties, type FormEvent, useCallback, useEffect, useMemo, us
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types";
|
||||
import type {
|
||||
DependencyCatalogResponse,
|
||||
DependencyPlanViewResponse,
|
||||
DependencyProbeViewResponse,
|
||||
GamePluginResponse,
|
||||
JobResponse,
|
||||
RunEndpointResponse,
|
||||
ServerInstanceResponse,
|
||||
ServerMetricsResponse
|
||||
} from "../api/types";
|
||||
import {
|
||||
RuntimeTaskProgressDialog,
|
||||
type RuntimeTaskDialogAction,
|
||||
@@ -21,8 +30,8 @@ import type { PageComponentProps } from "../contracts/page";
|
||||
import {
|
||||
canDeleteServer,
|
||||
defaultServerCreateForm,
|
||||
pluginCreateInputDefaults,
|
||||
runtimeObservationFreshness,
|
||||
pluginCreateInputDefaults,
|
||||
runtimeObservationFreshness,
|
||||
type ServerCreateFormState
|
||||
} from "../contracts/serverManagement";
|
||||
import { summarizeServerOperations } from "../contracts/operationsConsole";
|
||||
@@ -223,11 +232,11 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function openRunTargetSelection(instance: ServerInstanceResponse) {
|
||||
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
|
||||
const plugin = plugins.find((item) => item.id === instance.pluginId);
|
||||
const targetOs = runPlatformOptions(plugin, "linux")[0] ?? "linux";
|
||||
setRuntimeTaskActions([]);
|
||||
setRunTargetSelection({ instance, targetOs: defaults.runOs, targetArch: "amd64" });
|
||||
setRunTargetSelection({ instance, targetOs, targetArch: "amd64" });
|
||||
}
|
||||
|
||||
async function handleRunTargetSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
@@ -292,7 +301,6 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
return;
|
||||
}
|
||||
}
|
||||
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
|
||||
const intent = quickRuntimeActionLabel(action);
|
||||
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
|
||||
setRuntimeTaskActions([]);
|
||||
@@ -300,40 +308,40 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
await requireQuickRuntimeActionAvailable(instance.id, action);
|
||||
let message: string;
|
||||
message = await runtimeTask.runTask({
|
||||
title: intent,
|
||||
description: quickRuntimeTaskDescription(instance, action),
|
||||
stages: quickRuntimeStages(action),
|
||||
executeStageIndex: quickRuntimeExecuteStageIndex(action),
|
||||
execute: async () => {
|
||||
if (action === "download-run") {
|
||||
title: intent,
|
||||
description: quickRuntimeTaskDescription(instance, action),
|
||||
stages: quickRuntimeStages(action),
|
||||
executeStageIndex: quickRuntimeExecuteStageIndex(action),
|
||||
execute: async () => {
|
||||
if (action === "download-run") {
|
||||
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
||||
await downloadArtifactReference(reference, (artifactId) => platformApiClient.downloadArtifactContent(artifactId));
|
||||
return `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`;
|
||||
}
|
||||
if (action === "push-run-update") {
|
||||
}
|
||||
if (action === "push-run-update") {
|
||||
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
||||
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
|
||||
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
|
||||
}
|
||||
if (action === "reset-run-key") {
|
||||
}
|
||||
if (action === "reset-run-key") {
|
||||
const key = await platformApiClient.resetRunKey(instance.id);
|
||||
return `run 密钥已重置到第 ${key.generation} 代,旧 run 会话已失效,请重新生成并部署 run`;
|
||||
}
|
||||
if (action === "dependencies-check") {
|
||||
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey));
|
||||
return `依赖检查任务已排队,job ${job.id}`;
|
||||
}
|
||||
if (action === "dependencies-install") {
|
||||
}
|
||||
if (action === "dependencies-check") {
|
||||
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
|
||||
const plan = catalog.plans.find((candidate) => candidate.key === defaults.installPlanKey);
|
||||
const probe = catalog.probes.find((candidate) => candidate.key === defaults.probeKey);
|
||||
if (!plan || probe?.installPlanKey !== plan.key) throw new Error("Platform 未返回与当前 probe 匹配的审核安装计划");
|
||||
const probe = firstDependencyProbe(catalog);
|
||||
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, probe.key));
|
||||
return `依赖检查任务已排队,job ${job.id}`;
|
||||
}
|
||||
if (action === "dependencies-install") {
|
||||
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
|
||||
const { probe, plan } = firstInstallableDependency(catalog);
|
||||
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probe.key, plan.key, plan.digest));
|
||||
return `依赖安装任务已排队,job ${job.id}`;
|
||||
}
|
||||
throw new Error("该运行操作已下线");
|
||||
}
|
||||
});
|
||||
throw new Error("该运行操作已下线");
|
||||
}
|
||||
});
|
||||
operations.succeed(operationId, message);
|
||||
runtimeTask.succeedTask(message);
|
||||
await refresh();
|
||||
@@ -905,15 +913,6 @@ function quickRuntimeTaskDescription(instance: ServerInstanceResponse, action: S
|
||||
return `${instance.name}(${instance.id})${label},通过平台 API 派发并保留可追踪进度。`;
|
||||
}
|
||||
|
||||
function quickRuntimeDefaultsForPlugin(pluginId: string) {
|
||||
const isScum = pluginId.toLowerCase().includes("scum");
|
||||
return {
|
||||
runOs: isScum ? "windows" : "linux",
|
||||
probeKey: isScum ? "steamcmd" : "java-21",
|
||||
installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux"
|
||||
};
|
||||
}
|
||||
|
||||
export function runPlatformOptions(plugin: GamePluginResponse | undefined, fallback: string): string[] {
|
||||
const options = new Set<string>();
|
||||
const add = (value: string | undefined) => {
|
||||
@@ -932,6 +931,21 @@ export function runPlatformOptions(plugin: GamePluginResponse | undefined, fallb
|
||||
return [...options];
|
||||
}
|
||||
|
||||
function firstDependencyProbe(catalog: DependencyCatalogResponse): DependencyProbeViewResponse {
|
||||
const probe = catalog.probes.find((candidate) => candidate.required) ?? catalog.probes[0];
|
||||
if (!probe) throw new Error("插件未声明可检查的运行依赖");
|
||||
return probe;
|
||||
}
|
||||
|
||||
function firstInstallableDependency(catalog: DependencyCatalogResponse): { probe: DependencyProbeViewResponse; plan: DependencyPlanViewResponse } {
|
||||
for (const probe of catalog.probes) {
|
||||
if (!probe.installPlanKey) continue;
|
||||
const plan = catalog.plans.find((candidate) => candidate.key === probe.installPlanKey);
|
||||
if (plan) return { probe, plan };
|
||||
}
|
||||
throw new Error("插件未声明可安装的运行依赖计划");
|
||||
}
|
||||
|
||||
function runPlatformLabel(platform: string): string {
|
||||
switch (platform) {
|
||||
case "linux":
|
||||
|
||||
Reference in New Issue
Block a user