Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+141 -5
View File
@@ -14,7 +14,14 @@ import {
type ServerCreateFormState
} from "../contracts/serverManagement";
import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace";
import { serverCreateRequestFromForm } from "../schemas/serverManagement";
import {
clientManagerBuildRequest,
dependencyJobRequest,
logBackfillRequest,
runDistributionGenerateRequest,
runUpdateRequest,
serverCreateRequestFromForm
} from "../schemas/serverManagement";
import { isPlatformAdmin } from "../contracts/workspace";
import { cx } from "../utils/classes";
@@ -114,6 +121,56 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
}
}
async function handleQuickRuntimeAction(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction) {
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
const intent = quickRuntimeActionLabel(action);
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
try {
let message = "运行操作已提交";
if (action === "generate-run") {
const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, defaults.runOs, "amd64"));
message = `run 包已生成,artifact ${distribution.artifactId}`;
} else if (action === "download-run") {
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
message = `run 下载引用已创建,artifact ${reference.artifactId}`;
} else if (action === "push-run-update") {
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
message = `run 更新任务已排队,job ${update.jobId ?? update.id}`;
} else if (action === "generate-client-manager") {
const distribution = await platformApiClient.generateClientManager(
instance.id,
clientManagerBuildRequest({
serverInstanceId: instance.id,
profileKey: defaults.clientProfileKey,
targetOs: defaults.clientOs,
targetArch: "amd64",
repositoryUrl: defaults.repositoryUrl,
sourceRevision: "main"
})
);
message = `客户端管理器已生成,artifact ${distribution.artifactId}`;
} else if (action === "dependencies-check") {
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey));
message = `依赖检查任务已排队,job ${job.id}`;
} else if (action === "dependencies-install") {
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey, defaults.installPlanKey));
message = `依赖安装任务已排队,job ${job.id}`;
} else if (action === "live-logs") {
onNavigate("serverDetail", { serverId: instance.id });
message = "已打开服务器详情,可切换到日志页查看实时日志";
} else if (action === "historical-logs") {
const job = await platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, defaults.logSourceKey));
message = `历史日志回填任务已排队,job ${job.id}`;
}
operations.succeed(operationId, message);
await refresh();
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "运行操作失败", operationId);
}
}
const latestCreate = operations.operations.find((operation) => operation.intent === "创建服务器");
return (
@@ -240,7 +297,13 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
{visibleCards.length > 0 && (
<div className="server-card-grid" aria-label="server list">
{visibleCards.map((card) => (
<ServerCard key={card.instance.id} card={card} metricsPending={metricsPending} onOpen={() => onNavigate("serverDetail", { serverId: card.instance.id })} />
<ServerCard
key={card.instance.id}
card={card}
metricsPending={metricsPending}
onOpen={() => onNavigate("serverDetail", { serverId: card.instance.id })}
onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)}
/>
))}
</div>
)}
@@ -248,17 +311,28 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
);
}
type ServerQuickRuntimeAction =
| "generate-run"
| "download-run"
| "push-run-update"
| "generate-client-manager"
| "dependencies-check"
| "dependencies-install"
| "live-logs"
| "historical-logs";
interface ServerCardProps {
card: ServerCardView;
metricsPending: boolean;
onOpen: () => void;
onQuickAction: (action: ServerQuickRuntimeAction) => void;
}
function ServerCard({ card, metricsPending, onOpen }: ServerCardProps) {
function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardProps) {
const { instance, metrics, pendingJobs } = card;
const online = serverIsOnline(instance.state);
return (
<button type="button" className="server-card" onClick={onOpen} aria-label={`打开 ${instance.name} 详情`}>
<article className="server-card" aria-label={`${instance.name} 服务器卡片`}>
<div className="server-card-head">
<span>
<strong>{instance.name}</strong>
@@ -289,10 +363,72 @@ function ServerCard({ card, metricsPending, onOpen }: ServerCardProps) {
<UsageMeter label="内存" percent={metrics?.memoryPercent} />
<UsageMeter label="磁盘" percent={metrics?.diskPercent} />
</div>
</button>
<div className="action-strip" style={{ justifyContent: "space-between" }}>
<button type="button" className="icon-command" onClick={onOpen}>
<Sparkles size={14} />
<span></span>
</button>
<details className="runtime-action-menu">
<summary className="icon-command"></summary>
<div className="action-list">
{serverQuickActions.map((action) => (
<button key={action} type="button" className="theme-upload" onClick={() => onQuickAction(action)}>
<Candy size={13} />
<span>{quickRuntimeActionLabel(action)}</span>
</button>
))}
</div>
</details>
</div>
</article>
);
}
const serverQuickActions: ServerQuickRuntimeAction[] = [
"generate-run",
"download-run",
"push-run-update",
"generate-client-manager",
"dependencies-check",
"dependencies-install",
"live-logs",
"historical-logs"
];
function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string {
switch (action) {
case "generate-run":
return "生成 run";
case "download-run":
return "下载 run";
case "push-run-update":
return "推送更新";
case "generate-client-manager":
return "生成客户端";
case "dependencies-check":
return "依赖检查";
case "dependencies-install":
return "依赖安装";
case "live-logs":
return "实时日志";
case "historical-logs":
return "历史日志";
}
}
function quickRuntimeDefaultsForPlugin(pluginId: string) {
const isScum = pluginId.toLowerCase().includes("scum");
return {
runOs: isScum ? "windows" : "linux",
clientOs: "windows",
clientProfileKey: isScum ? "scum-client-manager" : "client-manager",
repositoryUrl: isScum ? "https://github.com/F88888/scum_client.git" : "https://github.com/example/client-manager.git",
probeKey: isScum ? "steamcmd" : "java-21",
installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux",
logSourceKey: isScum ? "server-log" : "latest-log"
};
}
function formatStat(value: number | undefined, pending: boolean, format: (value: number) => string): string {
if (typeof value === "number" && Number.isFinite(value)) {
return format(value);