feat: add UE4SS DLL runtime extension

This commit is contained in:
npc0-hue
2026-07-23 09:49:14 +08:00
parent 8e34c8762a
commit 1caf40565c
30 changed files with 1179 additions and 70 deletions
+20
View File
@@ -24,6 +24,20 @@ const marketplacePlugin: MarketplacePluginResponse = {
tags: ["example"],
aiPurposes: ["logs.diagnose"],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
runtimeProfiles: {
dllExtensions: [{
key: "scum-simple-rcon-ue4ss",
displayName: "SCUM Simple RCON UE4SS DLL",
kind: "ue4ss-dll",
activation: "server-start",
version: "0.1.0-unpublished",
releaseState: "unpublished",
releaseHost: "cdn.npc0.com",
releaseFilename: "scum_simple_rcon_ue4s.dll",
supportedTargets: [{ os: "windows", arch: "amd64" }],
updateOnStart: true
}]
},
status: "installed",
source: "platform-registry"
};
@@ -58,6 +72,10 @@ describe("PluginsPage", () => {
expect(html).toContain("安装");
expect(html).toContain("启用");
expect(html).toContain("停用");
expect(html).toContain("SCUM Simple RCON UE4SS DLL");
expect(html).toContain("启动前自动校验和更新");
expect(html).toContain("运行:SCUM 服务器受监管启动");
expect(html).toContain("Linux 启动前拒绝");
expect(html).toContain('role="dialog"');
expect(html).not.toContain("billing");
expect(html).not.toContain("/Users/");
@@ -66,6 +84,7 @@ describe("PluginsPage", () => {
expect(html).not.toContain("sk-");
expect(html).not.toContain("password=");
expect(html).not.toContain("apiKeyRef");
expect(html).not.toContain("ue4ss/Mods/");
});
it("keeps standalone fallback visibly isolated", () => {
@@ -90,6 +109,7 @@ describe("PluginsPage", () => {
expect(pluginsPageSource).toContain("retryDetail");
expect(pluginsPageSource).toContain("当前账号为只读模式");
expect(pluginsPageSource).toContain("平台会返回持久状态");
expect(pluginsPageSource).toContain("UE4SS DLL 声明");
expect(pluginsPageSource).toContain("actionPending !== null");
expect(pluginsPageSource).toContain("operations?.begin");
expect(pluginsPageSource).toContain("operations?.succeed");
+16 -1
View File
@@ -5,6 +5,7 @@ import { platformApiClient } from "../api/client";
import type { GamePluginStatus, MarketplacePluginFilterRequest, MarketplacePluginResponse, MarketplacePluginStateAction } from "../api/types";
import { ConfirmDialog, ManagementDialog } from "../components/OperationControls";
import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench";
import { RuntimeDLLExtensionsPanel } from "../components/RuntimeDLLExtensionsPanel";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import { PageFrame } from "../components/PageFrame";
import type { PageComponentProps } from "../contracts/page";
@@ -294,7 +295,7 @@ export function PluginsPage({ initialState, session, operations }: PluginsPagePr
<ConfirmDialog
open={confirmAction !== null}
title="确认插件状态变更"
description={detail ? `将对 ${detail.name} 执行“${stateActionLabel(confirmAction ?? "disable")}”。平台会返回持久状态,失败时保留当前状态并允许重试。` : "请确认插件状态变更。"}
description={detail ? pluginStateChangeDescription(detail, confirmAction ?? "disable") : "请确认插件状态变更。"}
confirmLabel={stateActionLabel(confirmAction ?? "disable")}
danger={confirmAction === "disable"}
busy={actionPending !== null}
@@ -347,6 +348,7 @@ function PluginDetail({ plugin, actionPending, actionsDisabled, onAction }: Plug
<dd>{plugin.validationViolations?.length ? plugin.validationViolations.join(", ") : "manifest validated"}</dd>
</div>
</dl>
<RuntimeDLLExtensionsPanel runtimeProfiles={plugin.runtimeProfiles} embedded />
<div className="action-strip plugin-detail-actions">
<button type="button" className="primary-command" disabled={actionsDisabled || actionPending !== null || plugin.status === "installed"} onClick={() => onAction("install")} title="安装插件状态">
<PlugZap size={16} />
@@ -397,3 +399,16 @@ function stateActionLabel(action: MarketplacePluginStateAction): string {
}
return action === "disable" ? "停用" : "启用";
}
function pluginStateChangeDescription(plugin: MarketplacePluginResponse, action: MarketplacePluginStateAction): string {
const description = `将对 ${plugin.name} 执行“${stateActionLabel(action)}”。平台会返回持久状态,失败时保留当前状态并允许重试。`;
if (action !== "install" && action !== "enable") {
return description;
}
const extensions = plugin.runtimeProfiles?.dllExtensions ?? [];
if (extensions.length === 0) {
return description;
}
const releases = extensions.map((extension) => [extension.releaseHost, extension.releaseFilename].filter(Boolean).join(" / ") || extension.displayName).join("");
return `${description} UE4SS DLL 声明:Windows amd64 的 SCUM 启动前会校验并按固定发布下载或更新 ${releases}Run 不直接执行 DLL,Linux 会拒绝启动。`;
}
@@ -3,6 +3,7 @@ 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 artifactTransferSource from "../utils/artifactTransfer.ts?raw";
import type { ServerConfigDiffPreviewResponse } from "../api/types";
@@ -103,6 +104,17 @@ 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("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"');
+2
View File
@@ -29,6 +29,7 @@ import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationCont
import { ClientManagerLifecyclePanel } from "../components/ClientManagerLifecyclePanel";
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
import { PluginLifecycleWorkbench } from "../components/PluginLifecycleWorkbench";
import { RuntimeDLLExtensionsPanel } from "../components/RuntimeDLLExtensionsPanel";
import {
RuntimeTaskProgressDialog,
runtimeBuildStages,
@@ -293,6 +294,7 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
onChanged={() => void refresh()}
/>
)}
{section === "overview" && <RuntimeDLLExtensionsPanel runtimeProfiles={plugins.find((plugin) => plugin.id === instance.data.pluginId)?.runtimeProfiles} />}
{section === "overview" && (
<RuntimeDistributionSection
instance={instance.data}