feat: 清理openspec
This commit is contained in:
@@ -22,7 +22,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.
|
||||
- `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 never includes a deployment target, Run endpoint, lifecycle profile, or Run identity binding. The generated Run package uses plugin defaults and Platform observes the active Run from its authenticated heartbeat.
|
||||
- `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.
|
||||
@@ -38,7 +38,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
|
||||
- `listMetricHistory`, `listBackups`, and `getBackup` read bounded owner-scoped metric and backup projections. Backup responses contain artifact IDs/checksums and recovery/retention state only; they never include body bytes or storage paths.
|
||||
- `listRemoteAdapters` and `requestRemoteAdapter` use declaration-backed logical target keys and return queued status/result references. The browser never receives adapter credentials, host addresses, sockets, Run tokens, leases, session hashes, or secret refs.
|
||||
- Server management DTOs may include bounded `ownerUserId` and `adminUserIds` metadata, but must not include raw run credentials, host paths, direct socket details, user password hashes, or AI provider keys.
|
||||
- Server creation and detail forms derive profile choices and binding fields from `GamePluginResponse.runtimeProfiles`; they must not hardcode a complete state or game-specific machine paths.
|
||||
- Server creation and detail forms use plugin-declared deployment inputs only; lifecycle profile selection, Run identity binding, and Run registration waits are not operator controls. Platform applies the plugin default and observes the active Run from authenticated heartbeats.
|
||||
- AI provider responses expose `apiKeyConfigured` only. Existing secret refs are never rehydrated into edit forms; a blank update preserves the platform-owned secret reference.
|
||||
|
||||
## Redesign Contract Gaps (redesign-platform-web-interactions)
|
||||
|
||||
@@ -461,7 +461,6 @@ export interface ServerInstanceResponse {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
pluginVersion: string;
|
||||
deploymentTargetId?: string;
|
||||
runEndpointId: string;
|
||||
name: string;
|
||||
ownerUserId?: string;
|
||||
@@ -522,10 +521,7 @@ export type ServerCommandShell = "" | "posix-sh" | "powershell" | "cmd";
|
||||
// This request is write-only for paths and commands. The matching response
|
||||
// intentionally returns configured flags rather than those values.
|
||||
export interface ServerDeploymentRequest {
|
||||
runEndpointId?: string;
|
||||
mode: ServerDeploymentMode;
|
||||
profileKey?: string;
|
||||
runtimeBindings?: Record<string, string>;
|
||||
createInputs?: Record<string, string>;
|
||||
serverRoot?: string;
|
||||
workingDirectory?: string;
|
||||
@@ -539,7 +535,6 @@ export interface ServerDeploymentRequest {
|
||||
export interface ServerDeploymentResponse {
|
||||
serverInstanceId: string;
|
||||
mode?: ServerDeploymentMode;
|
||||
profileKey?: string;
|
||||
createInputs?: Record<string, string>;
|
||||
serverRootConfigured: boolean;
|
||||
workingDirectoryConfigured: boolean;
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("ServerDeploymentWorkflow", () => {
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
const initialForm = defaultServerCreateForm([plugin], []);
|
||||
const initialForm = defaultServerCreateForm([plugin]);
|
||||
let submitted: ReturnType<typeof serverCreateRequestFromForm> | undefined;
|
||||
const onSubmit = vi.fn(async (form: typeof initialForm) => {
|
||||
submitted = serverCreateRequestFromForm(form, 17);
|
||||
@@ -64,7 +64,6 @@ describe("ServerDeploymentWorkflow", () => {
|
||||
open
|
||||
kind="create"
|
||||
plugins={[plugin]}
|
||||
endpoints={[]}
|
||||
initialForm={initialForm}
|
||||
onClose={() => undefined}
|
||||
onSubmit={onSubmit}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CheckCircle2, CircleDashed, Compass, Download, FolderCog, HeartPulse, Rocket, ScanSearch, ServerCog, SlidersHorizontal } from "lucide-react";
|
||||
import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { GamePluginResponse, RunEndpointResponse, ServerDeploymentResponse, ServerDeploymentRevealResponse } from "../api/types";
|
||||
import type { GamePluginResponse, ServerDeploymentResponse, ServerDeploymentRevealResponse } from "../api/types";
|
||||
import { ManagementDialog } from "./OperationControls";
|
||||
import { endpointLabel, pluginCreateInputDefaults, pluginLabel, type ServerCreateFormState } from "../contracts/serverManagement";
|
||||
import { pluginCreateInputDefaults, pluginLabel, type ServerCreateFormState } from "../contracts/serverManagement";
|
||||
import { cx } from "../utils/classes";
|
||||
|
||||
type WorkflowKind = "create" | "edit";
|
||||
@@ -12,7 +12,6 @@ interface ServerDeploymentWorkflowProps {
|
||||
open: boolean;
|
||||
kind: WorkflowKind;
|
||||
plugins: GamePluginResponse[];
|
||||
endpoints: RunEndpointResponse[];
|
||||
initialForm: ServerCreateFormState;
|
||||
deployment?: ServerDeploymentResponse;
|
||||
busy?: boolean;
|
||||
@@ -21,7 +20,7 @@ interface ServerDeploymentWorkflowProps {
|
||||
onSubmit: (form: ServerCreateFormState) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initialForm, deployment, busy = false, onReveal, onClose, onSubmit }: ServerDeploymentWorkflowProps) {
|
||||
export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, deployment, busy = false, onReveal, onClose, onSubmit }: ServerDeploymentWorkflowProps) {
|
||||
const [step, setStep] = useState(0);
|
||||
const [form, setForm] = useState<ServerCreateFormState>(initialForm);
|
||||
const [revealBusy, setRevealBusy] = useState(false);
|
||||
@@ -29,17 +28,12 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
|
||||
const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]);
|
||||
const pluginFields = selectedPlugin?.createFields ?? [];
|
||||
const isScum = selectedPlugin?.id === "game.scum";
|
||||
const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;
|
||||
const selectedTargetID = form.runEndpointId;
|
||||
const workflowSteps = kind === "create"
|
||||
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
||||
: needsTargetSelection
|
||||
? [{ label: "选择运行节点", icon: Compass }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
||||
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
|
||||
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
|
||||
const pluginStep = kind === "create" ? 0 : -1;
|
||||
const targetStep = needsTargetSelection ? 0 : -1;
|
||||
const modeStep = kind === "create" ? 1 : -1;
|
||||
const configurationStep = kind === "create" ? 2 : needsTargetSelection ? 1 : 0;
|
||||
const configurationStep = kind === "create" ? 2 : 0;
|
||||
const reviewStep = workflowSteps.length - 1;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,9 +54,8 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
|
||||
setForm((current) => {
|
||||
if (name === "pluginId") {
|
||||
const plugin = plugins.find((item) => item.id === value);
|
||||
return { ...current, pluginId: value, profileKey: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", bindings: {}, createInputs: pluginCreateInputDefaults(plugin) };
|
||||
return { ...current, pluginId: value, createInputs: pluginCreateInputDefaults(plugin) };
|
||||
}
|
||||
if (name === "profileKey") return { ...current, profileKey: value, bindings: {} };
|
||||
return { ...current, [name]: value };
|
||||
});
|
||||
}
|
||||
@@ -72,7 +65,6 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
|
||||
function canContinue() {
|
||||
if (step === pluginStep) return Boolean(form.pluginId) && Boolean(form.name.trim());
|
||||
if (step === modeStep) return Boolean(form.deploymentMode);
|
||||
if (step === targetStep) return Boolean(form.runEndpointId);
|
||||
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;
|
||||
@@ -112,24 +104,20 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
|
||||
const protectedState = (nextValue: string, configured: boolean) => nextValue.trim() ? "将替换" : configured ? "保持已配置" : "未配置";
|
||||
const actionLabel = kind === "create" ? "创建服务器" : "保存部署设置";
|
||||
|
||||
return <ManagementDialog open={open} title={kind === "create" ? "创建服务器" : "编辑部署"} description={kind === "create" ? "先选择插件类型和服务器名称,再按部署方式填写启动项;不要求选择 Run 节点或部署目标。" : "任何运行状态都可以修改部署设置;这里只保存定义,不会直接重启进程。已保存的受保护路径和命令仅在本窗口内读取,关闭后清除。"} wide onClose={closeWorkflow}>
|
||||
return <ManagementDialog open={open} title={kind === "create" ? "创建服务器" : "编辑部署"} description={kind === "create" ? "先选择插件类型和服务器名称,再按部署方式填写启动项;Run 由心跳自动识别,不提供节点或部署目标选择。" : "任何运行状态都可以修改部署设置;这里只保存定义,不会直接重启进程。已保存的受保护路径和命令仅在本窗口内读取,关闭后清除。"} wide onClose={closeWorkflow}>
|
||||
<form className="provider-form dialog-form server-deployment-workflow" onSubmit={(event) => void submit(event)} aria-label={kind === "create" ? "创建服务器部署向导" : "编辑服务器部署向导"}>
|
||||
<ol className="deployment-workflow-steps" style={{ gridTemplateColumns: `repeat(${workflowSteps.length}, minmax(0, 1fr))` }} aria-label="部署步骤">{workflowSteps.map((item, index) => { const Icon = item.icon; return <li key={item.label} className={cx(index === step && "deployment-workflow-step-active", index < step && "deployment-workflow-step-complete")}><span>{index < step ? <CheckCircle2 size={15} /> : <Icon size={15} />}</span><strong>{index + 1}. {item.label}</strong></li>; })}</ol>
|
||||
{step === pluginStep && <div className="deployment-workflow-body">
|
||||
<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>平台在自有构建器中打包,不需要你先选择部署目标。</span></div></div>
|
||||
<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">
|
||||
<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="用于非标准启动器或脚本;需由节点策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
|
||||
<ModeOption active={form.deploymentMode === "custom-command"} title="自定义启动方式" copy="用于非标准启动器或脚本;需由 Run 策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
|
||||
</div></div>}
|
||||
{step === targetStep && <div className="deployment-workflow-body">
|
||||
<div className="form-guidance"><strong>这个草稿尚未绑定运行节点</strong><span>只需在这里补选一次。已绑定服务器编辑时会直接进入相关配置,不会重复要求选择目标。</span></div>
|
||||
<div className="form-grid"><label>运行节点<select name="runEndpointId" value={selectedTargetID} onChange={updateForm} required><option value="">请选择运行节点</option>{endpoints.map((endpoint) => <option key={endpoint.id} value={endpoint.id}>{endpointLabel(endpoint, endpoint.id)}</option>)}</select></label></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">可在此调整部署方式;不会重复要求选择已绑定的运行节点。</small></label>}
|
||||
{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 === "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>}
|
||||
@@ -143,9 +131,9 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi
|
||||
</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 === "custom-command" && <details className="provider-advanced-settings" open><summary>高级启动设置</summary><p className="field-help">只有自定义启动器需要这些设置。执行目录留空时,节点以服务器目录执行。</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>}
|
||||
{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 && (kind === "create" ? <div className="deployment-workflow-body"><div className="deployment-review"><div><span>插件类型</span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div><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, false)}</strong></div>{form.deploymentMode === "custom-command" && <><div><span>启动命令</span><strong>{protectedState(form.startCommand, false)}</strong></div><div><span>执行目录</span><strong>{protectedState(form.workingDirectory, false)}</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>本次保存创建向导配置</strong><span>保存后生成并启动专属 Run;部署执行会按这里选择的方式和启动项进行。</span></div></div> : <div className="deployment-workflow-body"><div className="deployment-review"><div><span>目标</span><strong>{endpointLabel(endpoints.find((endpoint) => endpoint.id === selectedTargetID), selectedTargetID)}</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>本次只保存部署设置</strong><span>{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>}{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>}
|
||||
<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>;
|
||||
@@ -155,12 +143,12 @@ function ModeOption({ active, title, copy, onClick }: { active: boolean; title:
|
||||
|
||||
function GuidedInstallPlan({ pluginName, isScum }: { pluginName: string; isScum: boolean }) {
|
||||
const steps = isScum ? [
|
||||
{ icon: ScanSearch, title: "预检目录与端口", copy: "确认安装目录可用、节点兼容且端口可绑定。" },
|
||||
{ icon: ScanSearch, title: "预检目录与端口", copy: "确认安装目录可用、Run 环境兼容且端口可用。" },
|
||||
{ icon: Download, title: "下载 SCUM Server", copy: "通过 SteamCMD 安装 App 3792580 到该目录。" },
|
||||
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "把本页的名称、端口与人数写入 ServerSettings.ini。" },
|
||||
{ icon: HeartPulse, title: "启动并健康验证", copy: "检查可执行文件、版本、配置、端口和服务进程。" }
|
||||
] : [
|
||||
{ icon: ScanSearch, title: "预检目录与节点", copy: "确认安装目录、权限、端口与运行节点可用。" },
|
||||
{ icon: ScanSearch, title: "预检目录与 Run", copy: "确认安装目录、权限、端口与 Run 环境可用。" },
|
||||
{ icon: Download, title: "安装游戏服务端", copy: "按插件声明的推荐方案安装到该目录。" },
|
||||
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "将本页填写的游戏参数交给受控部署流程。" },
|
||||
{ icon: HeartPulse, title: "启动并健康验证", copy: "只有启动与插件要求的验证通过才会显示成功。" }
|
||||
@@ -174,15 +162,15 @@ function ExistingServerAdoptionPlan({ pluginName, isScum }: { pluginName: string
|
||||
{ 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: ServerCog, title: "建立受控生命周期", copy: "Run 自动识别这台实例,后续启动、停止和日志仍走受控通道。" },
|
||||
{ icon: HeartPulse, title: "健康验证", copy: "确认端口、进程与配置可读后,才标记为接管成功。" }
|
||||
] : [
|
||||
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写已有服务端文件、数据与配置所在的主目录。" },
|
||||
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、插件识别和端口是否可用。" },
|
||||
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "读取插件需要的现有状态,不把新建默认值写进服务器。" },
|
||||
{ icon: ServerCog, title: "绑定受控生命周期", copy: "后续运行操作由绑定的 Run 通过平台通道执行。" },
|
||||
{ icon: ServerCog, title: "建立受控生命周期", copy: "后续运行操作由自动识别的 Run 通过平台通道执行。" },
|
||||
{ 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>{isScum ? <p><strong>SCUM 与 SteamCMD:</strong>接管只需要服务端根目录,不需要填写 SteamCMD 目录。Run 可能按本机策略检查 SteamCMD 是否可用,但它不是接管输入。<br /><strong>升级:</strong>接管不会升级游戏;当前平台尚未提供 SCUM 服务端的受控升级任务,不能承诺自动升级。升级能力需要单独的 SteamCMD 更新任务与备份/健康验证流程。</p> : <p><strong>不会做:</strong>不会重新安装、覆盖已有游戏配置,或把受保护路径回显给浏览器。</p>}</section>;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,6 @@ export interface ServerCreateFormState {
|
||||
id: string;
|
||||
name: string;
|
||||
pluginId: string;
|
||||
deploymentTargetId: string;
|
||||
runEndpointId: string;
|
||||
profileKey: string;
|
||||
bindings: Record<string, string>;
|
||||
createInputs: Record<string, string>;
|
||||
deploymentMode: ServerDeploymentMode;
|
||||
serverRoot: string;
|
||||
@@ -30,12 +26,6 @@ export interface ServerCreateFormState {
|
||||
shell: "" | "posix-sh" | "powershell" | "cmd";
|
||||
}
|
||||
|
||||
export interface RuntimeBindingField {
|
||||
key: string;
|
||||
required: boolean;
|
||||
sensitive: boolean;
|
||||
}
|
||||
|
||||
export interface ServerWorkflowActionState {
|
||||
label: ServerLifecycleActionLabel;
|
||||
serverInstanceId?: string;
|
||||
@@ -73,10 +63,6 @@ export const emptyServerCreateForm: ServerCreateFormState = {
|
||||
id: "",
|
||||
name: "",
|
||||
pluginId: "",
|
||||
deploymentTargetId: "",
|
||||
runEndpointId: "",
|
||||
profileKey: "",
|
||||
bindings: {},
|
||||
createInputs: {},
|
||||
deploymentMode: "guided-install",
|
||||
serverRoot: "",
|
||||
@@ -108,13 +94,6 @@ export function pluginLabel(plugin: GamePluginResponse | undefined, pluginId: st
|
||||
return plugin.serverDisplayName || plugin.name || plugin.id;
|
||||
}
|
||||
|
||||
export function endpointLabel(endpoint: RunEndpointResponse | undefined, runEndpointId: string): string {
|
||||
if (!endpoint) {
|
||||
return runEndpointId;
|
||||
}
|
||||
return endpoint.displayName || endpoint.id;
|
||||
}
|
||||
|
||||
export function canStartServer(state: ServerInstanceState): boolean {
|
||||
return state === "ready" || state === "stopped" || state === "failed";
|
||||
}
|
||||
@@ -127,13 +106,11 @@ export function isPendingJobState(state: JobResponse["state"]): boolean {
|
||||
return state === "queued" || state === "accepted" || state === "running" || state === "retrying";
|
||||
}
|
||||
|
||||
export function defaultServerCreateForm(plugins: GamePluginResponse[], endpoints: RunEndpointResponse[]): ServerCreateFormState {
|
||||
export function defaultServerCreateForm(plugins: GamePluginResponse[]): ServerCreateFormState {
|
||||
const plugin = plugins[0];
|
||||
return {
|
||||
...emptyServerCreateForm,
|
||||
pluginId: plugin?.id ?? "",
|
||||
profileKey: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "",
|
||||
runEndpointId: "",
|
||||
createInputs: pluginCreateInputDefaults(plugin)
|
||||
};
|
||||
}
|
||||
@@ -142,30 +119,6 @@ export function pluginCreateInputDefaults(plugin: GamePluginResponse | undefined
|
||||
return Object.fromEntries((plugin?.createFields ?? []).map((field) => [field.key, field.defaultValue ?? ""]));
|
||||
}
|
||||
|
||||
export function runtimeBindingFields(plugin: GamePluginResponse | undefined, profileKey: string): RuntimeBindingField[] {
|
||||
const profiles = plugin?.runtimeProfiles;
|
||||
const lifecycle = profiles?.lifecycleProfiles?.find((profile) => profile.key === profileKey);
|
||||
if (!profiles || !lifecycle) return [];
|
||||
const fields = new Map<string, RuntimeBindingField>();
|
||||
const add = (key: string | undefined, required: boolean) => {
|
||||
if (!key) return;
|
||||
const current = fields.get(key);
|
||||
fields.set(key, { key, required: required || current?.required === true, sensitive: runtimeBindingKeyIsSensitive(key) });
|
||||
};
|
||||
profiles.discovery?.forEach((probe) => add(probe.targetKey, probe.required === true));
|
||||
profiles.dependencyProbes?.forEach((probe) => add(probe.targetKey, probe.required === true));
|
||||
profiles.logSources?.forEach((source) => add(source.targetKey, Boolean(source.targetKey)));
|
||||
profiles.installPlans?.forEach((plan) => plan.steps.forEach((step) => add(step.targetKey, false)));
|
||||
profiles.transportProfiles?.filter((transport) => lifecycle.transportKeys?.includes(transport.key)).forEach((transport) => add(transport.targetKey || transport.key, true));
|
||||
add(lifecycle.clientManagerRef, Boolean(lifecycle.clientManagerRef));
|
||||
return [...fields.values()].sort((left, right) => left.key.localeCompare(right.key));
|
||||
}
|
||||
|
||||
export function runtimeBindingKeyIsSensitive(key: string): boolean {
|
||||
const normalized = key.toLowerCase();
|
||||
return ["password", "credential", "secret", "token", "dsn"].some((part) => normalized.includes(part));
|
||||
}
|
||||
|
||||
export function serverMetadataFormFromInstance(instance: ServerInstanceResponse): ServerMetadataFormState {
|
||||
return { name: instance.name };
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverDeploymentWorkflowSource).toContain("基本信息");
|
||||
expect(serverDeploymentWorkflowSource).toContain("部署方式");
|
||||
expect(serverDeploymentWorkflowSource).toContain("相关配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("专属 Run");
|
||||
expect(serverDeploymentWorkflowSource).toContain("自动上报心跳");
|
||||
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
|
||||
expect(serverDeploymentWorkflowSource).toContain("执行目录(可选)");
|
||||
expect(serverDeploymentWorkflowSource).toContain("默认使用服务器目录");
|
||||
@@ -197,8 +197,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
|
||||
expect(serverDeploymentWorkflowSource).toContain("不需要填写 SteamCMD 目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的受控升级任务");
|
||||
expect(serverDeploymentWorkflowSource).toContain("已绑定服务器编辑时会直接进入相关配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("可在此调整部署方式;不会重复要求选择已绑定的运行节点");
|
||||
expect(serverDeploymentWorkflowSource).toContain("Run 会按心跳自动识别服务器");
|
||||
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
|
||||
expect(serverDetailPageSource).not.toContain("运行配置绑定");
|
||||
expect(serverDetailPageSource).not.toContain('type={field.sensitive ? "password" : "text"}');
|
||||
@@ -252,8 +251,10 @@ describe("first-party console pages", () => {
|
||||
expect(serverDeploymentWorkflowSource).toContain("配置启动项");
|
||||
expect(serverDeploymentWorkflowSource).toContain("本次保存创建向导配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain('const modeStep = kind === "create" ? 1 : -1;');
|
||||
expect(serverDeploymentWorkflowSource).toContain('const configurationStep = kind === "create" ? 2 : needsTargetSelection ? 1 : 0;');
|
||||
expect(serverDeploymentWorkflowSource).toContain('const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;');
|
||||
expect(serverDeploymentWorkflowSource).toContain('const configurationStep = kind === "create" ? 2 : 0;');
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("needsTargetSelection");
|
||||
expect(serverDeploymentWorkflowSource).not.toContain('name="runEndpointId"');
|
||||
expect(serverDeploymentWorkflowSource).not.toContain("profileKey");
|
||||
expect(serversPageSource).toContain("serverCreateRequestFromForm(nextForm)");
|
||||
expect(serverCreateSchemaSource).not.toContain("runEndpointId: form.runEndpointId");
|
||||
expect(serverCreateSchemaSource).not.toContain("deploymentTargetId: form.deploymentTargetId");
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("ServerDetailPage config write approval", () => {
|
||||
expect(serverDetailPageSource).not.toContain('capability: "process.stop"');
|
||||
});
|
||||
|
||||
it("leaves guided deployment to the dedicated Run registration workflow", () => {
|
||||
it("leaves guided deployment to the generated Run heartbeat workflow", () => {
|
||||
expect(serverDetailPageSource).not.toContain("canDeployServer(instance.data.state)");
|
||||
expect(serverDetailPageSource).not.toContain("requestDeployment(instance.data)");
|
||||
expect(serverDetailPageSource).not.toContain("platformApiClient.deployServerInstance(current.id");
|
||||
|
||||
@@ -206,7 +206,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
<div>
|
||||
<h1 id="server-detail-title">{instance.data.name}</h1>
|
||||
<span className="provider-id">
|
||||
{instance.data.id} · 插件 {instance.data.pluginId}@{instance.data.pluginVersion} · 节点 {instance.data.runEndpointId}
|
||||
{instance.data.id} · 插件 {instance.data.pluginId}@{instance.data.pluginVersion} · Run 心跳 {runEndpoint ? "已自动附着" : "等待上报"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="action-strip">
|
||||
|
||||
@@ -22,7 +22,6 @@ import type { PageComponentProps } from "../contracts/page";
|
||||
import {
|
||||
canDeleteServer,
|
||||
defaultServerCreateForm,
|
||||
endpointLabel,
|
||||
pluginCreateInputDefaults,
|
||||
runtimeObservationFreshness,
|
||||
type ServerCreateFormState
|
||||
@@ -72,7 +71,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const [metricsError, setMetricsError] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<ServerStatusFilter>("all");
|
||||
const [form, setForm] = useState<ServerCreateFormState>(() => defaultServerCreateForm([], []));
|
||||
const [form, setForm] = useState<ServerCreateFormState>(() => defaultServerCreateForm([]));
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [editDeployment, setEditDeployment] = useState<{ instance: ServerInstanceResponse; deployment: import("../api/types").ServerDeploymentResponse } | null>(null);
|
||||
const runtimeTask = useRuntimeTaskController();
|
||||
@@ -98,15 +97,9 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
setJobs(jobResponse.items);
|
||||
if (showLoading) setForm((current) => {
|
||||
const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0];
|
||||
const profileKey = plugin?.runtimeProfiles?.lifecycleProfiles?.some((profile) => profile.key === current.profileKey)
|
||||
? current.profileKey
|
||||
: plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "";
|
||||
return {
|
||||
...current,
|
||||
pluginId: plugin?.id ?? "",
|
||||
profileKey,
|
||||
bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {},
|
||||
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId) ? current.runEndpointId : ""
|
||||
};
|
||||
});
|
||||
if (showLoading) {
|
||||
@@ -170,8 +163,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const operationId = operations.begin({ intent: "创建服务器", targetKind: "server", targetId: "platform", requester: session.displayName });
|
||||
try {
|
||||
const result = await platformApiClient.createServerWorkflow(serverCreateRequestFromForm(nextForm));
|
||||
operations.succeed(operationId, result.job.id ? `已创建实例 ${result.instance.id},安装任务 ${result.job.id} 已派发` : `已创建服务器 ${result.instance.id};部署方式和启动项已随创建向导保存,请生成并启动专属 Run。`, result.job.id ? result.job : undefined);
|
||||
setForm(defaultServerCreateForm(plugins, endpoints));
|
||||
operations.succeed(operationId, result.job.id ? `已创建实例 ${result.instance.id},安装任务 ${result.job.id} 已派发` : `已创建服务器 ${result.instance.id};部署方式和启动项已随创建向导保存,请生成并启动 Run。`, result.job.id ? result.job : undefined);
|
||||
setForm(defaultServerCreateForm(plugins));
|
||||
setShowCreate(false);
|
||||
await refresh();
|
||||
onNavigate("serverDetail", { serverId: result.instance.id });
|
||||
@@ -184,7 +177,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
try {
|
||||
const deployment = await platformApiClient.getServerDeployment(instance.id);
|
||||
const plugin = plugins.find((item) => item.id === instance.pluginId);
|
||||
setForm({ ...defaultServerCreateForm(plugins, endpoints), name: instance.name, pluginId: instance.pluginId, runEndpointId: instance.runEndpointId, profileKey: deployment.profileKey ?? plugin?.runtimeProfiles?.lifecycleProfiles?.[0]?.key ?? "", createInputs: deployment.createInputs ?? pluginCreateInputDefaults(plugin), deploymentMode: deployment.mode ?? "guided-install", shell: deployment.shell ?? "" });
|
||||
setForm({ ...defaultServerCreateForm(plugins), name: instance.name, pluginId: instance.pluginId, createInputs: deployment.createInputs ?? pluginCreateInputDefaults(plugin), deploymentMode: deployment.mode ?? "guided-install", shell: deployment.shell ?? "" });
|
||||
setEditDeployment({ instance, deployment });
|
||||
} catch (error) {
|
||||
const operationId = operations.begin({ intent: "读取部署设置", targetKind: "server", targetId: instance.id, requester: session.displayName });
|
||||
@@ -197,7 +190,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const { instance } = editDeployment;
|
||||
const operationId = operations.begin({ intent: "更新部署定义", targetKind: "server", targetId: instance.id, requester: session.displayName });
|
||||
try {
|
||||
await platformApiClient.updateServerDeployment(instance.id, { runEndpointId: nextForm.runEndpointId || undefined, mode: nextForm.deploymentMode, profileKey: nextForm.profileKey || undefined, createInputs: nextForm.createInputs, serverRoot: nextForm.serverRoot.trim() || undefined, workingDirectory: nextForm.workingDirectory.trim() || undefined, installCommand: nextForm.installCommand.trim() || undefined, startCommand: nextForm.startCommand.trim() || undefined, stopCommand: nextForm.stopCommand.trim() || undefined, statusCommand: nextForm.statusCommand.trim() || undefined, shell: nextForm.shell || undefined });
|
||||
await platformApiClient.updateServerDeployment(instance.id, { mode: nextForm.deploymentMode, createInputs: nextForm.createInputs, serverRoot: nextForm.serverRoot.trim() || undefined, workingDirectory: nextForm.workingDirectory.trim() || undefined, installCommand: nextForm.installCommand.trim() || undefined, startCommand: nextForm.startCommand.trim() || undefined, stopCommand: nextForm.stopCommand.trim() || undefined, statusCommand: nextForm.statusCommand.trim() || undefined, shell: nextForm.shell || undefined });
|
||||
operations.succeed(operationId, "部署设置已保存;路径和命令保持受保护状态。");
|
||||
setEditDeployment(null);
|
||||
await refresh();
|
||||
@@ -504,8 +497,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ServerDeploymentWorkflow open={showCreate && canManageServers} kind="create" plugins={plugins} endpoints={endpoints} initialForm={form} busy={createPending} onClose={() => setShowCreate(false)} onSubmit={handleCreate} />
|
||||
<ServerDeploymentWorkflow open={editDeployment !== null} kind="edit" plugins={plugins} endpoints={endpoints} initialForm={form} deployment={editDeployment?.deployment} busy={editDeployment ? operations.isPending(editDeployment.instance.id, "更新部署定义") : false} onReveal={() => platformApiClient.revealServerDeployment(editDeployment?.instance.id ?? "")} onClose={() => setEditDeployment(null)} onSubmit={handleUpdateDeployment} />
|
||||
<ServerDeploymentWorkflow open={showCreate && canManageServers} kind="create" plugins={plugins} initialForm={form} busy={createPending} onClose={() => setShowCreate(false)} onSubmit={handleCreate} />
|
||||
<ServerDeploymentWorkflow open={editDeployment !== null} kind="edit" plugins={plugins} initialForm={form} deployment={editDeployment?.deployment} busy={editDeployment ? operations.isPending(editDeployment.instance.id, "更新部署定义") : false} onReveal={() => platformApiClient.revealServerDeployment(editDeployment?.instance.id ?? "")} onClose={() => setEditDeployment(null)} onSubmit={handleUpdateDeployment} />
|
||||
|
||||
<ManagementDialog
|
||||
open={runTargetSelection !== null}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { GamePluginResponse, ServerInstanceResponse } from "../api/types";
|
||||
import { defaultServerCreateForm, runtimeBindingFields } from "../contracts/serverManagement";
|
||||
import { defaultServerCreateForm } from "../contracts/serverManagement";
|
||||
import { minimalServerCreateRequestFromForm, serverCreateRequestFromForm, serverInstanceIdFromName, serverLifecycleCommandRequest } from "./serverManagement";
|
||||
|
||||
const plugin: GamePluginResponse = {
|
||||
@@ -42,27 +42,14 @@ const plugin: GamePluginResponse = {
|
||||
};
|
||||
|
||||
describe("runtime profile server creation contracts", () => {
|
||||
it("derives logical binding fields from the selected profile", () => {
|
||||
expect(runtimeBindingFields(plugin, "local")).toEqual([
|
||||
{ key: "java-runtime", required: false, sensitive: false },
|
||||
{ key: "log-source", required: true, sensitive: false },
|
||||
{ key: "package-source", required: false, sensitive: false },
|
||||
{ key: "rcon.password", required: true, sensitive: true },
|
||||
{ key: "server-root", required: true, sensitive: false }
|
||||
]);
|
||||
expect(runtimeBindingFields(plugin, "local").some((field) => field.key === "ftp.profile")).toBe(false);
|
||||
});
|
||||
|
||||
it("submits deployment inputs without binding a Run or runtime profile", () => {
|
||||
const form = defaultServerCreateForm([plugin], []);
|
||||
expect(form.profileKey).toBe("local");
|
||||
const form = defaultServerCreateForm([plugin]);
|
||||
expect(
|
||||
serverCreateRequestFromForm(
|
||||
{
|
||||
...form,
|
||||
id: " server-1 ",
|
||||
name: " Runtime Server ",
|
||||
bindings: { "server-root": " runtime.server-root ", "rcon.password": " secret://runtime/server-1/rcon ", "java-runtime": " " }
|
||||
},
|
||||
17
|
||||
)
|
||||
@@ -79,13 +66,10 @@ describe("runtime profile server creation contracts", () => {
|
||||
});
|
||||
|
||||
it("maps the create UI to a minimal server request", () => {
|
||||
const form = defaultServerCreateForm([plugin], []);
|
||||
const form = defaultServerCreateForm([plugin]);
|
||||
const request = minimalServerCreateRequestFromForm({
|
||||
...form,
|
||||
name: " Minimal Runtime Server ",
|
||||
deploymentTargetId: "run-builder",
|
||||
runEndpointId: "run-existing",
|
||||
bindings: { "rcon.password": "secret://must-not-submit" },
|
||||
serverRoot: "/srv/must-not-submit"
|
||||
}, 16);
|
||||
|
||||
@@ -98,8 +82,8 @@ describe("runtime profile server creation contracts", () => {
|
||||
});
|
||||
|
||||
it("generates server instance ids from the visible server name", () => {
|
||||
const form = defaultServerCreateForm([plugin], []);
|
||||
const request = serverCreateRequestFromForm({ ...form, name: " Runtime Server ", bindings: {} }, 17);
|
||||
const form = defaultServerCreateForm([plugin]);
|
||||
const request = serverCreateRequestFromForm({ ...form, name: " Runtime Server " }, 17);
|
||||
|
||||
expect(request).toMatchObject({
|
||||
id: "server-runtime-server-17",
|
||||
@@ -119,7 +103,7 @@ describe("runtime profile server creation contracts", () => {
|
||||
});
|
||||
|
||||
it("keeps complete paths and commands in a write-only deployment payload", () => {
|
||||
const form = defaultServerCreateForm([plugin], []);
|
||||
const form = defaultServerCreateForm([plugin]);
|
||||
const request = serverCreateRequestFromForm({ ...form, name: "Venv Server", deploymentMode: "custom-command", serverRoot: "/srv/venv-server", workingDirectory: "/srv/venv-server", startCommand: "/srv/venv-server/.venv/bin/python server.py", shell: "" }, 19);
|
||||
expect("runEndpointId" in request).toBe(false);
|
||||
expect("profileKey" in request).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user