feat: auto-deploy guided servers on run registration

This commit is contained in:
npc0-hue
2026-07-28 16:57:09 +08:00
parent e2d0bc0595
commit 7f64765c1c
15 changed files with 191 additions and 55 deletions
+4 -7
View File
@@ -162,13 +162,10 @@ 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" ? "重试部署服务器" : "部署服务器")');
it("leaves guided deployment to the dedicated Run registration workflow", () => {
expect(serverDetailPageSource).not.toContain("canDeployServer(instance.data.state)");
expect(serverDetailPageSource).not.toContain("requestDeployment(instance.data)");
expect(serverDetailPageSource).not.toContain("platformApiClient.deployServerInstance(current.id");
expect(serverDetailPageSource).not.toContain('capability: "process.install"');
});
+2 -32
View File
@@ -1,4 +1,4 @@
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, Rocket, ShieldCheck, Sparkles, Square, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
import { ChevronDown, ChevronRight, Download, MoonStar, PackageOpen, Pencil, 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 { canDeployServer, canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
import { canStartServer, canStopServer, defaultServerCreateForm, endpointLabel, pluginCreateInputDefaults, pluginLabel, runtimeBindingFields, serverMetadataFormFromInstance, type ServerCreateFormState, type ServerMetadataFormState } from "../contracts/serverManagement";
import {
serverDetailSections,
serverIsOnline,
@@ -212,26 +212,6 @@ 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;
@@ -296,16 +276,6 @@ 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 -1
View File
@@ -157,7 +157,7 @@ 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);
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));
setShowCreate(false);
await refresh();