feat: add UE4SS DLL runtime extension
This commit is contained in:
@@ -239,6 +239,7 @@ export interface RuntimeLifecycleProfileResponse {
|
||||
actionRefs?: Record<string, string>;
|
||||
transportKeys?: string[];
|
||||
clientManagerRef?: string;
|
||||
dllExtensionRefs?: string[];
|
||||
platforms?: string[];
|
||||
}
|
||||
|
||||
@@ -302,6 +303,23 @@ export interface RuntimeClientManagerProfileResponse {
|
||||
updatePolicy?: { strategy: string; requireApproval: boolean; healthConfirmationSeconds: number; retainPrevious: boolean };
|
||||
}
|
||||
|
||||
export interface RuntimeDLLExtensionProfileResponse {
|
||||
key: string;
|
||||
displayName: string;
|
||||
kind: "ue4ss-dll";
|
||||
activation: "server-start";
|
||||
version: string;
|
||||
releaseState: "ready" | "unpublished";
|
||||
releaseHost?: string;
|
||||
releaseFilename?: string;
|
||||
checksum?: string;
|
||||
sizeBytes?: number;
|
||||
scumExecutableChecksum?: string;
|
||||
ue4ssAbi?: string;
|
||||
supportedTargets: Array<{ os: string; arch: string }>;
|
||||
updateOnStart: boolean;
|
||||
}
|
||||
|
||||
export interface GamePluginRuntimeProfilesResponse {
|
||||
discovery?: RuntimeDiscoveryProbeResponse[];
|
||||
lifecycleProfiles?: RuntimeLifecycleProfileResponse[];
|
||||
@@ -311,6 +329,7 @@ export interface GamePluginRuntimeProfilesResponse {
|
||||
logEvents?: RuntimeLogEventResponse[];
|
||||
transportProfiles?: RuntimeTransportProfileResponse[];
|
||||
clientManagers?: RuntimeClientManagerProfileResponse[];
|
||||
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
|
||||
}
|
||||
|
||||
export interface GamePluginResponse {
|
||||
@@ -365,6 +384,7 @@ export interface MarketplacePluginResponse {
|
||||
aiPurposes: string[];
|
||||
productionLifecycle: PluginProductionLifecycleDeclaration;
|
||||
validationViolations?: string[];
|
||||
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
|
||||
gameClientBridge?: GameClientBridgeManifestResponse;
|
||||
status: GamePluginStatus;
|
||||
source: string;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { GamePluginRuntimeProfilesResponse, RuntimeDLLExtensionProfileResponse } from "../api/types";
|
||||
|
||||
interface RuntimeDLLExtensionsPanelProps {
|
||||
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function RuntimeDLLExtensionsPanel({ runtimeProfiles, embedded = false }: RuntimeDLLExtensionsPanelProps) {
|
||||
const extensions = runtimeProfiles?.dllExtensions ?? [];
|
||||
if (extensions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const content = <RuntimeDLLExtensionsContent extensions={extensions} />;
|
||||
return embedded ? <section className="console-module" aria-label="UE4SS DLL extensions">{content}</section> : <article className="console-panel" aria-label="UE4SS DLL extensions">{content}</article>;
|
||||
}
|
||||
|
||||
function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExtensionProfileResponse[] }) {
|
||||
return (
|
||||
<>
|
||||
<div className="panel-header">
|
||||
<h2>UE4SS DLL 扩展</h2>
|
||||
<span className="page-status">声明式、固定版本、启动前同步</span>
|
||||
</div>
|
||||
<p className="page-status">Run 不直接执行 DLL;已安装的 UE4SS 会在 SCUM 正常启动时加载它。</p>
|
||||
<div className="console-record-list">
|
||||
{extensions.map((extension) => (
|
||||
<article key={extension.key} className="console-record">
|
||||
<div className="console-record-head">
|
||||
<strong>{extension.displayName}</strong>
|
||||
<span className={extension.releaseState === "ready" ? "status-pill status-active" : "status-pill status-disabled"}>{extension.releaseState === "ready" ? "已固定发布" : "待发布"}</span>
|
||||
</div>
|
||||
<div className="console-record-meta">
|
||||
<span>{extension.key}</span>
|
||||
<span>版本 {extension.version}</span>
|
||||
</div>
|
||||
<dl className="detail-list">
|
||||
<div>
|
||||
<dt>下载声明</dt>
|
||||
<dd>{releaseLocation(extension)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>DLL 校验</dt>
|
||||
<dd>{extension.checksum || "待发布后固定"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>SCUM 兼容</dt>
|
||||
<dd>{extension.scumExecutableChecksum || "待发布后固定"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>UE4SS ABI</dt>
|
||||
<dd>{extension.ue4ssAbi || "待发布后固定"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="tag-list" aria-label={`${extension.displayName} activation policy`}>
|
||||
<span>启动前自动校验和更新</span>
|
||||
<span>运行:SCUM 服务器受监管启动</span>
|
||||
<span>由 UE4SS 正常加载</span>
|
||||
<span>{supportedTargets(extension)}</span>
|
||||
<span>Linux 启动前拒绝</span>
|
||||
</div>
|
||||
{extension.releaseState === "unpublished" && <p className="page-status">发布者尚未提供完整哈希、大小与兼容性固定值;此声明可查看,但不会派发给 Run。</p>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function releaseLocation(extension: RuntimeDLLExtensionProfileResponse): string {
|
||||
return [extension.releaseHost, extension.releaseFilename].filter((value): value is string => Boolean(value)).join(" / ") || "待发布";
|
||||
}
|
||||
|
||||
function supportedTargets(extension: RuntimeDLLExtensionProfileResponse): string {
|
||||
return extension.supportedTargets.map((target) => `${target.os} ${target.arch}`).join(", ") || "Windows amd64";
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user