Remove legacy runtime deployment paths

This commit is contained in:
npc0-hue
2026-09-04 12:36:21 +08:00
parent 14cbc63e61
commit 3cfb98ed47
39 changed files with 407 additions and 589 deletions
+1 -16
View File
@@ -265,20 +265,6 @@ export interface RuntimeInstallPlanResponse {
steps: Array<{ type: string; targetKey: string; packageManager?: string; packageName?: string; version?: string; downloadRef?: string; checksum?: string }>;
}
export interface RuntimeServerDeploymentProfileResponse {
key: string;
version: string;
supportedTargets: Array<{ os: string; arch: string }>;
steamAppId: string;
executableKey: string;
installRootKey: string;
configKey: string;
configFormat: string;
configMappings: Array<{ fieldKey: string; configKey: string; valueType: string; required?: boolean }>;
discoveryMarkers: Array<{ key: string; kind: string; targetKey: string; expected?: string; required?: boolean }>;
verificationChecks: Array<{ key: string; kind: string; targetKey: string; required?: boolean }>;
}
export interface RuntimeLogSourceResponse {
key: string;
kind: string;
@@ -311,7 +297,7 @@ export interface RuntimeDLLExtensionProfileResponse {
releaseFilename?: string;
checksum?: string;
sizeBytes?: number;
scumExecutableChecksum?: string;
targetExecutableChecksum?: string;
ue4ssAbi?: string;
supportedTargets: Array<{ os: string; arch: string }>;
updateOnStart: boolean;
@@ -322,7 +308,6 @@ export interface GamePluginRuntimeProfilesResponse {
lifecycleProfiles?: RuntimeLifecycleProfileResponse[];
dependencyProbes?: RuntimeDependencyProbeResponse[];
installPlans?: RuntimeInstallPlanResponse[];
serverDeployments?: RuntimeServerDeploymentProfileResponse[];
logSources?: RuntimeLogSourceResponse[];
transportProfiles?: RuntimeTransportProfileResponse[];
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
@@ -21,7 +21,7 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
<h2>UE4SS DLL </h2>
<span className="page-status"></span>
</div>
<p className="page-status">Run DLL UE4SS SCUM </p>
<p className="page-status">Run DLL UE4SS </p>
<div className="console-record-list">
{extensions.map((extension) => (
<article key={extension.key} className="console-record">
@@ -43,8 +43,8 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
<dd>{extension.checksum || "待发布后固定"}</dd>
</div>
<div>
<dt>SCUM </dt>
<dd>{extension.scumExecutableChecksum || "待发布后固定"}</dd>
<dt></dt>
<dd>{extension.targetExecutableChecksum || "待发布后固定"}</dd>
</div>
<div>
<dt>UE4SS ABI</dt>
@@ -53,7 +53,7 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
</dl>
<div className="tag-list" aria-label={`${extension.displayName} activation policy`}>
<span></span>
<span>SCUM </span>
<span></span>
<span> UE4SS </span>
<span>{supportedTargets(extension)}</span>
<span>Linux </span>
@@ -27,7 +27,6 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
const [revealError, setRevealError] = useState("");
const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]);
const pluginFields = selectedPlugin?.createFields ?? [];
const isScum = selectedPlugin?.id === "game.scum";
const workflowSteps = kind === "create"
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
@@ -66,7 +65,6 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
if (step === pluginStep) return Boolean(form.pluginId) && Boolean(form.name.trim());
if (step === modeStep) return Boolean(form.deploymentMode);
if (step === configurationStep) {
if (isScum && form.deploymentMode === "guided-install" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
if (form.deploymentMode === "existing-server" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false;
return form.deploymentMode !== "guided-install" || pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim()));
@@ -111,14 +109,14 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
<div className="workflow-hint-grid"><div className="workflow-hint-card"><strong></strong><span></span></div><div className="workflow-hint-card"><strong></strong><span></span></div><div className="workflow-hint-card"><strong> Run </strong><span>Run </span></div></div>
<div className="form-grid"><label><select name="pluginId" value={form.pluginId} onChange={updateForm} required>{plugins.map((plugin) => <option key={plugin.id} value={plugin.id}>{pluginLabel(plugin, plugin.id)}</option>)}</select></label><label><input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label></div>
</div>}
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy"></p>{isScum && <div className="form-guidance"><strong>SCUM </strong><span>Run </span></div>}<div className="deployment-mode-grid">
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy"></p><div className="form-guidance"><strong></strong><span>Run </span></div><div className="deployment-mode-grid">
<ModeOption active={form.deploymentMode === "guided-install"} title="新建并安装" copy="按插件的推荐方案安装并写入游戏配置。适合绝大多数新服务器。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} />
<ModeOption active={form.deploymentMode === "existing-server"} title="接管已有服务器" copy="预检指定目录并接入已有实例;不会把它当作一次新安装。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} />
<ModeOption active={form.deploymentMode === "custom-command"} title="自定义启动方式" copy="用于非标准启动器或脚本;需由 Run 策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
</div></div>}
{step === configurationStep && <div className="deployment-workflow-body">{kind === "edit" && onReveal && <div className="form-guidance"><strong></strong><span>{revealBusy ? "正在读取已保存的目录和命令…" : "这些值只保留在当前编辑窗口,关闭后会清除。"}</span>{revealError && <><span className="field-help">{revealError}</span><button type="button" className="primary-command" disabled={busy || revealBusy} onClick={() => void revealSavedInputs()}></button></>}</div>}<div className="form-grid">
{kind === "edit" && <label><select name="deploymentMode" value={form.deploymentMode} onChange={updateForm}><option value="guided-install"></option><option value="existing-server"></option><option value="custom-command"></option></select><small className="field-help">Run </small></label>}
{form.deploymentMode === "guided-install" && <label>{isScum ? "(必填)" : "(可选)"}<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" required={isScum && !deployment?.serverRootConfigured} /><small className="field-help">SCUM </small></label>}
{form.deploymentMode === "guided-install" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>}
{form.deploymentMode === "existing-server" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已接管目录" : "完整绝对路径"} autoComplete="off" required={!deployment?.serverRootConfigured} /><small className="field-help">Run </small></label>}
{form.deploymentMode === "custom-command" && <label><input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help"></small></label>}
{form.deploymentMode === "guided-install" && pluginFields.map((field) => (
@@ -129,11 +127,11 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
)}</label>
))}
</div>
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} isScum={isScum} />}
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} isScum={isScum} />}
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary></summary><p className="field-help">Run </p><div className="form-grid"><label><input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label><select name="shell" value={form.shell} onChange={updateForm}><option value=""> argv</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label><input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label><input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label><input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label><input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
</div>}
{step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span></span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span></span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span></span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span></span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span></span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{form.deploymentMode === "guided-install" && <div><span></span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}{isScum && <div><span></span><strong>/</strong></div>}</div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>}
{step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span></span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span></span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span></span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span></span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span></span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{form.deploymentMode === "guided-install" && <div><span></span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}<div><span></span><strong></strong></div></div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>}
<div className="confirm-actions"><button type="button" disabled={busy} onClick={() => step === 0 ? closeWorkflow() : setStep((current) => current - 1)}>{step === 0 ? "取消" : "上一步"}</button>{step < reviewStep ? <button type="submit" className="confirm-primary" disabled={busy || !canContinue()}><CircleDashed size={16} /><span></span></button> : <button type="submit" className="confirm-primary" disabled={busy}><Rocket size={16} /><span>{busy ? "保存中…" : actionLabel}</span></button>}</div>
</form>
</ManagementDialog>;
@@ -141,30 +139,19 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return <button type="button" className={cx("deployment-mode-option", active && "deployment-mode-option-active")} onClick={onClick}><strong>{title}</strong><span>{copy}</span></button>; }
function GuidedInstallPlan({ pluginName, isScum }: { pluginName: string; isScum: boolean }) {
const steps = isScum ? [
{ icon: ScanSearch, title: "预检目录与端口", copy: "确认安装目录可用、Run 环境兼容且端口可用。" },
{ icon: Download, title: "下载 SCUM Server", copy: "通过 SteamCMD 安装 App 3792580 到该目录。" },
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "把本页的名称、端口与人数写入 ServerSettings.ini。" },
{ icon: HeartPulse, title: "启动并健康验证", copy: "检查可执行文件、版本、配置、端口和服务进程。" }
] : [
function GuidedInstallPlan({ pluginName }: { pluginName: string }) {
const steps = [
{ icon: ScanSearch, title: "预检目录与 Run", copy: "确认安装目录、权限、端口与 Run 环境可用。" },
{ icon: Download, title: "安装游戏服务端", copy: "按插件声明的推荐方案安装到该目录。" },
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "将本页填写的游戏参数交给自动部署流程。" },
{ icon: HeartPulse, title: "启动并健康验证", copy: "只有启动与插件要求的验证通过才会显示成功。" }
];
return <section className="guided-install-plan" aria-label="新建并安装执行流程"><div className="guided-install-plan-heading"><div><strong>{pluginName} </strong><span></span></div><small>{isScum ? "全部 4 步通过才算安装成功" : "Run 按插件契约执行"}</small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong></strong>{isScum ? "不会跳过验证就标记成功;失败时不会暴露你的目录、命令或凭据。" : "不会把受保护的路径、命令或凭据回显给浏览器。"}</p></section>;
return <section className="guided-install-plan" aria-label="新建并安装执行流程"><div className="guided-install-plan-heading"><div><strong>{pluginName} </strong><span></span></div><small>Run </small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong></strong></p></section>;
}
function ExistingServerAdoptionPlan({ pluginName, isScum }: { pluginName: string; isScum: boolean }) {
const steps = isScum ? [
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写包含 SCUM 服务端文件、数据与配置的目录,不是 Steam 库或 SteamCMD 目录。" },
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、可执行文件、版本、Steam App 标记和所需端口。" },
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "识别 ServerSettings.ini 与现有参数;接管不会写入或覆盖它们。" },
{ icon: ServerCog, title: "建立生命周期", copy: "Run 自动识别这台实例,后续启动、停止和日志仍走平台通道。" },
{ icon: HeartPulse, title: "健康验证", copy: "确认端口、进程与配置可读后,才标记为接管成功。" }
] : [
function ExistingServerAdoptionPlan({ pluginName }: { pluginName: string }) {
const steps = [
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写已有服务端文件、数据与配置所在的主目录。" },
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、插件识别和端口是否可用。" },
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "读取插件需要的现有状态,不把新建默认值写进服务器。" },
@@ -172,5 +159,5 @@ function ExistingServerAdoptionPlan({ pluginName, isScum }: { pluginName: string
{ icon: HeartPulse, title: "健康验证", copy: "验证通过后才标记为接管成功。" }
];
return <section className="guided-install-plan" aria-label="接管已有服务器执行流程"><div className="guided-install-plan-heading"><div><strong>{pluginName} </strong><span> Run 使</span></div><small></small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol>{isScum ? <p><strong>SCUM SteamCMD</strong> SteamCMD Run SteamCMD <br /><strong></strong> SCUM SteamCMD /</p> : <p><strong></strong></p>}</section>;
return <section className="guided-install-plan" aria-label="接管已有服务器执行流程"><div className="guided-install-plan-heading"><div><strong>{pluginName} </strong><span> Run 使</span></div><small></small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong></strong></p></section>;
}
+1 -1
View File
@@ -24,7 +24,7 @@ Plugin page runs with safe platform context.
- `plugin-lifecycle.request`: declared plugin lifecycle request through Platform.
- `ai.invoke`: platform-mediated AI invocation.
The host intersects manifest-level and page-level permissions/actions before exposing context. The SCUM operations page additionally intersects its command, snapshot, and query-template keys with `gameClientBridge.pages.operations`; it does not synthesize undeclared SCUM semantics.
The host intersects manifest-level and page-level permissions/actions before exposing context. A plugin page additionally intersects its command, snapshot, and query-template keys with the matching `gameClientBridge.pages.*` declaration; it does not synthesize undeclared game semantics.
## Forbidden
+12 -8
View File
@@ -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("运行配置绑定");
+1 -1
View File
@@ -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", () => {
+23 -23
View File
@@ -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 {
+52 -38
View File
@@ -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":
-5
View File
@@ -99,8 +99,3 @@ function copyStreamChunk(value: Uint8Array): ArrayBuffer {
copy.set(value);
return copy.buffer;
}
export function safeArtifactError(error: unknown): string {
const message = error instanceof Error ? error.message : "制品传输失败";
return message.replace(/\/Users\/[^\s]+/g, "[path]").replace(/Bearer\s+[^\s]+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]");
}