fix: expose server deployment trigger

This commit is contained in:
npc0-hue
2026-07-28 15:11:01 +08:00
parent e4ef4024a8
commit c5806cc6b7
10 changed files with 151 additions and 5 deletions
@@ -115,6 +115,10 @@ export function canStopServer(state: ServerInstanceState): boolean {
return state === "running";
}
export function canDeployServer(state: ServerInstanceState): boolean {
return state === "draft" || state === "failed";
}
export function isPendingJobState(state: JobResponse["state"]): boolean {
return state === "queued" || state === "accepted" || state === "running" || state === "retrying";
}
@@ -162,6 +162,16 @@ describe("ServerDetailPage config write approval", () => {
expect(serverDetailPageSource).not.toContain('capability: "process.stop"');
});
it("submits explicit deploy and retry-deploy actions only through the fenced deployment API", () => {
expect(serverDetailPageSource).toContain("canDeployServer(instance.data.state)");
expect(serverDetailPageSource).toContain("requestDeployment(instance.data)");
expect(serverDetailPageSource).toContain("platformApiClient.deployServerInstance(current.id");
expect(serverDetailPageSource).toContain('serverLifecycleCommandRequest(current, "deploy")');
expect(serverDetailPageSource).toContain('instance.data.state === "failed" ? "重试部署" : "部署"');
expect(serverDetailPageSource).toContain('operations.isPending(instance.data.id, instance.data.state === "failed" ? "重试部署服务器" : "部署服务器")');
expect(serverDetailPageSource).not.toContain('capability: "process.install"');
});
it("keeps plugin lifecycle and bridge-visible output on platform-owned logical references", () => {
expect(serverDetailPageSource).toContain("parsePluginArtifactReference(result)");
expect(serverDetailPageSource).toContain("platformApiClient.openArtifactDownload(artifact.id)");
+32 -2
View File
@@ -1,4 +1,4 @@
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, Rocket, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
import { type FormEvent, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
@@ -50,7 +50,7 @@ import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge }
import type { PageComponentProps } from "../contracts/page";
import { jobCapabilityLabel } from "../contracts/jobPresentation";
import type { PluginBridgeAction, PluginBridgeManifestContract } from "../contracts/pluginBridge";
import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
import { canDeployServer, canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
import {
serverDetailSections,
serverIsOnline,
@@ -212,6 +212,26 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
});
}
function requestDeployment(current: ServerInstanceResponse) {
const retry = current.state === "failed";
const intent = retry ? "重试部署服务器" : "部署服务器";
setConfirm({
title: intent,
description: `${retry ? "重新" : ""}部署 ${current.name}${current.id})将向已注册的专属 Run 提交安装任务,确认继续?`,
run: async () => {
const operationId = operations.begin({ intent, targetKind: "server", targetId: current.id, requester: session.displayName });
try {
const result = await platformApiClient.deployServerInstance(current.id, serverLifecycleCommandRequest(current, "deploy"));
operations.succeed(operationId, `部署任务 ${result.job.id}${result.job.capability})已派发`, result.job);
await refresh();
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "部署任务提交失败", operationId);
await refresh();
}
}
});
}
async function saveDeploymentWorkflow(form: ServerCreateFormState) {
if (instance.status !== "ready") return;
const current = instance.data;
@@ -276,6 +296,16 @@ export function ServerDetailPage({ session, params, operations, onNavigate }: Pa
</div>
<div className="action-strip">
<span className={cx("status-pill", statusClass(instance.data.state))}>{stateLabel(instance.data.state)}</span>
{canDeployServer(instance.data.state) && <button
type="button"
className="primary-command"
disabled={deployment.status !== "ready" || !deployment.data.mode || operations.isPending(instance.data.id, instance.data.state === "failed" ? "重试部署服务器" : "部署服务器")}
title={deployment.status !== "ready" || !deployment.data.mode ? "请先配置部署定义" : undefined}
onClick={() => requestDeployment(instance.data)}
>
<Rocket size={15} />
<span>{instance.data.state === "failed" ? "重试部署" : "部署"}</span>
</button>}
<button
type="button"
className="icon-command"
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { GamePluginResponse } from "../api/types";
import { defaultServerCreateForm, runtimeBindingFields } from "../contracts/serverManagement";
import { canDeployServer, defaultServerCreateForm, runtimeBindingFields } from "../contracts/serverManagement";
import { serverCreateRequestFromForm, serverInstanceIdFromName } from "./serverManagement";
const plugin: GamePluginResponse = {
@@ -42,6 +42,13 @@ const plugin: GamePluginResponse = {
};
describe("runtime profile server creation contracts", () => {
it("allows deployment only from draft or failed server states", () => {
expect(canDeployServer("draft")).toBe(true);
expect(canDeployServer("failed")).toBe(true);
expect(canDeployServer("installing")).toBe(false);
expect(canDeployServer("ready")).toBe(false);
});
it("derives logical binding fields from the selected profile", () => {
expect(runtimeBindingFields(plugin, "local")).toEqual([
{ key: "java-runtime", required: false, sensitive: false },
+2 -2
View File
@@ -52,7 +52,7 @@ export function serverInstanceIdFromName(name: string, sequence = Date.now()): s
return normalized ? `server-${normalized}-${suffix}` : `server-${suffix}`;
}
export function serverLifecycleCommandRequest(instance: ServerInstanceResponse, action: "start" | "stop" | "status", sequence = Date.now()): ServerLifecycleCommandRequest {
export function serverLifecycleCommandRequest(instance: ServerInstanceResponse, action: "deploy" | "start" | "stop" | "status", sequence = Date.now()): ServerLifecycleCommandRequest {
return {
expectedConfigVersion: instance.configVersion,
expectedChecksum: instance.configChecksum,
@@ -130,6 +130,6 @@ export function runtimeIdempotencyKey(action: string, serverInstanceId: string,
return `web:${action}:${serverInstanceId}:${sequence}`;
}
export function lifecycleIdempotencyKey(action: "create" | "start" | "stop" | "status", serverInstanceId: string, sequence: number): string {
export function lifecycleIdempotencyKey(action: "create" | "deploy" | "start" | "stop" | "status", serverInstanceId: string, sequence: number): string {
return `web:${action}:${serverInstanceId}:${sequence}`;
}