Remove legacy client-manager workflows

This commit is contained in:
npc0-hue
2026-09-03 13:08:08 +08:00
parent bf3c382d15
commit fe09d21a56
56 changed files with 304 additions and 4121 deletions
@@ -1,221 +0,0 @@
import { Activity, Ban, KeyRound, PackageCheck, Play, RefreshCw, RotateCcw, ShieldAlert, Square, Trash2, UploadCloud } from "lucide-react";
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
import type { ClientManagerInstallationResponse, ClientManagerLifecycleOperation } from "../api/types";
import type { CurrentUserView } from "../contracts/workspace";
import type { OperationTracker } from "../stores/operations";
import { cx } from "../utils/classes";
import { ConfirmDialog } from "./OperationControls";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "./StateViews";
type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; items: ClientManagerInstallationResponse[] };
interface ClientManagerLifecyclePanelProps {
serverId: string;
serverName: string;
session: CurrentUserView;
operations: OperationTracker;
}
interface PendingConfirmation {
title: string;
description: string;
danger?: boolean;
execute: () => Promise<void>;
}
export function ClientManagerLifecyclePanel({ serverId, serverName, session, operations }: ClientManagerLifecyclePanelProps) {
const [state, setState] = useState<LoadState>({ status: "loading" });
const [result, setResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
const [confirmation, setConfirmation] = useState<PendingConfirmation | null>(null);
const [confirmBusy, setConfirmBusy] = useState(false);
const refresh = useCallback(async (showLoading = false) => {
if (showLoading) setState({ status: "loading" });
try {
const response = await platformApiClient.listClientManagerLifecycles(serverId);
setState({ status: "ready", items: response.items });
} catch (error) {
setState({ status: "error", reason: safeError(error, "Client Manager 状态加载失败") });
}
}, [serverId]);
useEffect(() => { void refresh(true); }, [refresh]);
const hasActiveJob = state.status === "ready" && state.items.some((item) => item.job && ["queued", "accepted", "running", "retrying"].includes(item.job.state));
useEffect(() => {
if (!hasActiveJob) return undefined;
const timer = window.setInterval(() => void refresh(), 2500);
return () => window.clearInterval(timer);
}, [hasActiveJob, refresh]);
async function runCommand(item: ClientManagerInstallationResponse, intent: string, execute: () => Promise<ClientManagerInstallationResponse>) {
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${serverId}:client-manager:${item.profileKey}`, requester: session.displayName });
setResult({ status: "pending", label: `${intent} 已提交,等待 Platform/Run 返回真实状态` });
try {
const next = await execute();
setState((current) => current.status === "ready" ? { status: "ready", items: current.items.map((entry) => entry.id === next.id ? next : entry) } : current);
const label = next.job ? `${intent} 已排队,job ${next.job.id}` : `${intent} 已完成状态更新`;
operations.succeed(operationId, label);
setResult({ status: "succeeded", label });
await refresh();
} catch (error) {
const reason = safeError(error, `${intent} 失败`);
operations.fail(operationId, reason, operationId);
setResult({ status: "failed", label: reason });
}
}
function confirmCommand(config: PendingConfirmation) {
setConfirmation(config);
}
return (
<article className="console-panel client-manager-lifecycle-panel" aria-label="Client Manager 生命周期">
<div className="panel-header">
<h2><PackageCheck size={17} /> Client Manager </h2>
<div className="action-strip">
{result && <ResultBadge status={result.status} label={result.label} />}
<button type="button" className="icon-command" title="刷新 Client Manager 状态" onClick={() => void refresh()}>
<RefreshCw size={15} /><span></span>
</button>
</div>
</div>
{state.status === "loading" && <LoadingState label="正在读取 Client Manager 部署与组件健康状态…" />}
{state.status === "error" && <ErrorState title="Client Manager 状态不可用" reason={state.reason} diagnosticId={`client-manager:${serverId}`} onRetry={() => void refresh(true)} />}
{state.status === "ready" && state.items.length === 0 && <EmptyState title="尚无 Client Manager 生命周期记录" description="先在运行分发区按插件声明构建 Client Manager;可用 artifact 会在这里进入部署闭环。" />}
{state.status === "ready" && state.items.length > 0 && (
<div className="client-manager-lifecycle-list">
{state.items.map((item) => (
<ClientManagerLifecycleRow
key={item.id}
item={item}
serverName={serverName}
runCommand={runCommand}
confirmCommand={confirmCommand}
/>
))}
</div>
)}
<ConfirmDialog
open={confirmation !== null}
title={confirmation?.title ?? ""}
description={confirmation?.description ?? ""}
confirmLabel="确认执行"
danger={confirmation?.danger}
busy={confirmBusy}
onCancel={() => setConfirmation(null)}
onConfirm={() => {
if (!confirmation) return;
setConfirmBusy(true);
void confirmation.execute().finally(() => {
setConfirmBusy(false);
setConfirmation(null);
});
}}
/>
</article>
);
}
interface ClientManagerLifecycleRowProps {
item: ClientManagerInstallationResponse;
serverName: string;
runCommand: (item: ClientManagerInstallationResponse, intent: string, execute: () => Promise<ClientManagerInstallationResponse>) => Promise<void>;
confirmCommand: (config: PendingConfirmation) => void;
}
function ClientManagerLifecycleRow({ item, serverName, runCommand, confirmCommand }: ClientManagerLifecycleRowProps) {
const actionMap = useMemo(() => new Map(item.actions.map((action) => [action.operation, action])), [item.actions]);
const available = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.available ?? false;
const reason = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.reason ?? "Platform 当前状态不允许此操作";
const distributionId = item.distribution?.id ?? "";
const idempotency = (operation: string) => `client-manager.${operation}:${item.serverInstanceId}:${item.profileKey}:${Date.now()}`;
const control = (operation: "start" | "stop" | "restart" | "status" | "rollback") =>
platformApiClient.controlClientManager(item.serverInstanceId, { profileKey: item.profileKey, operation, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency(operation) });
const deploy = () => runCommand(item, item.requiresRedeploy ? "重新部署 Client Manager" : "部署 Client Manager", () => platformApiClient.deployClientManager(item.serverInstanceId, {
profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("deploy")
}));
const update = () => runCommand(item, "更新 Client Manager", () => platformApiClient.updateClientManager(item.serverInstanceId, {
profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, approved: true, idempotencyKey: idempotency("update")
}));
return (
<section className="client-manager-lifecycle-row" aria-label={`${item.profileKey} lifecycle`}>
<div className="client-manager-lifecycle-head">
<div>
<strong>{item.profileKey}</strong>
<span className="provider-id">{item.targetOs}/{item.targetArch} · deployment generation {item.deploymentGeneration} · key generation {item.keyGeneration}</span>
</div>
<div className="tag-list">
<span className={cx("status-pill", lifecycleTone(item.status))}>{lifecycleLabel(item.status)}</span>
<span className={cx("status-pill", healthTone(item.health))}><Activity size={12} /> {healthLabel(item.health)}</span>
</div>
</div>
<div className="client-manager-version-grid">
<VersionCell label="目标版本" version={item.desiredVersion} revision={item.desiredRevision} artifact={item.desiredArtifactId} />
<VersionCell label="当前版本" version={item.activeVersion} revision={item.activeRevision} artifact={item.activeArtifactId} />
<VersionCell label="回滚版本" version={item.previousVersion} revision={item.previousRevision} artifact={item.previousArtifactId} />
<div className="client-manager-version-cell"><span></span><strong>{item.lastSeenAt ? `最后心跳 ${formatTime(item.lastSeenAt)}` : "等待独立注册"}</strong><small>{item.healthReason || "未收到安全健康原因"}</small></div>
</div>
<div className="client-manager-phase-line">
<span><ShieldAlert size={14} /> {item.phase || "等待生命周期事件"}</span>
{item.lastOperation && <span> {item.lastOperation}</span>}
{item.lastSuccessfulJobId && <span> job {item.lastSuccessfulJobId}</span>}
</div>
{item.job && (
<div className="client-manager-job-progress" aria-label="Client Manager job progress">
<div><span>job {item.job.id} · attempt {item.job.attempt} · {item.job.state}</span><strong>{item.job.progress.percent}%</strong></div>
<progress max={100} value={item.job.progress.percent} />
<small>{item.job.progress.message || "等待 Run 回报真实阶段"}</small>
</div>
)}
{(item.retryable || item.requiresRedeploy || item.status === "failed") && (
<div className="client-manager-recovery">
<ShieldAlert size={16} />
<span>{item.requiresRedeploy ? "组件密钥 generation 已变化:旧 artifact/session 已被围栏。请重新构建当前 generation,再执行重新部署。" : item.retryable ? "Run 保留了可恢复状态,可重试当前 intent;界面不会在 job 成功前推进阶段。" : "检查 job 失败原因后选择重新部署、回滚或卸载。"}</span>
</div>
)}
<div className="client-manager-command-grid">
<LifecycleButton icon={<UploadCloud size={14} />} label={item.activeArtifactId ? "重新部署" : "部署"} disabled={!available("deploy") || !distributionId} reason={!distributionId ? "没有可用 distribution" : reason("deploy")} onClick={deploy} />
<LifecycleButton icon={<Play size={14} />} label="启动" disabled={!available("start")} reason={reason("start")} onClick={() => void runCommand(item, "启动 Client Manager", () => control("start"))} />
<LifecycleButton icon={<Square size={14} />} label="停止" disabled={!available("stop")} reason={reason("stop")} onClick={() => void runCommand(item, "停止 Client Manager", () => control("stop"))} />
<LifecycleButton icon={<RefreshCw size={14} />} label="重启" disabled={!available("restart")} reason={reason("restart")} onClick={() => void runCommand(item, "重启 Client Manager", () => control("restart"))} />
<LifecycleButton icon={<Activity size={14} />} label="检查状态" disabled={!available("status")} reason={reason("status")} onClick={() => void runCommand(item, "检查 Client Manager 状态", () => control("status"))} />
<LifecycleButton icon={<UploadCloud size={14} />} label="更新" disabled={!available("update") || !distributionId} reason={!distributionId ? "没有兼容的可用 distribution" : reason("update")} onClick={() => confirmCommand({ title: "批准 Client Manager 更新", description: `${serverName}${item.profileKey}${item.activeVersion || "未安装"} 更新到 ${item.desiredVersion || "目标版本"}。Run 将 staged activate、健康确认,并在失败时恢复 previous slot。`, execute: update })} />
<LifecycleButton icon={<RotateCcw size={14} />} label="回滚" disabled={!available("rollback")} reason={reason("rollback")} onClick={() => confirmCommand({ title: "回滚 Client Manager", description: `确认将 ${item.profileKey} 回滚到 ${item.previousVersion || "previous slot"}?当前组件 session 将被撤销并需要重新注册。`, danger: true, execute: () => runCommand(item, "回滚 Client Manager", () => control("rollback")) })} />
<LifecycleButton icon={<RefreshCw size={14} />} label="重试" disabled={!item.retryable} reason="当前失败不可重试" onClick={() => void runCommand(item, "重试 Client Manager", () => platformApiClient.retryClientManagerLifecycle(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("retry") }))} />
<LifecycleButton icon={<Ban size={14} />} label="撤销会话" disabled={!item.activeArtifactId || item.status === "uninstalled"} reason="组件尚未安装" onClick={() => confirmCommand({ title: "撤销 Client Manager 会话", description: `撤销 ${item.profileKey} 的独立组件 session。Run session 与 job lease 不受影响,组件必须使用当前 key generation 重新注册。`, danger: true, execute: () => runCommand(item, "撤销 Client Manager 会话", () => platformApiClient.revokeClientManagerSession(item.serverInstanceId, { profileKey: item.profileKey, reason: "operator revoked component session" })) })} />
<LifecycleButton icon={<KeyRound size={14} />} label="重置密钥" disabled={item.status === "uninstalled"} reason="已卸载" onClick={() => confirmCommand({ title: "重置 Client Manager 密钥", description: `重置 ${item.profileKey} 的 component key 会撤销旧 session/artifact generation。必须重新构建并重新部署,不会显示或导出原始密钥。`, danger: true, execute: async () => { await platformApiClient.resetClientManagerKey(item.serverInstanceId, { componentKind: "client-manager", componentKey: item.profileKey }); await runCommand(item, "刷新密钥重置状态", () => platformApiClient.getClientManagerLifecycle(item.serverInstanceId, item.profileKey)); } })} />
<LifecycleButton icon={<Trash2 size={14} />} label="卸载" danger disabled={!available("uninstall")} reason={reason("uninstall")} onClick={() => confirmCommand({ title: "卸载 Client Manager", description: `确认停止并卸载 ${serverName}${item.profileKey}Run 只会清理 Client Manager workspacePlatform 保留 build 与 artifact 记录。`, danger: true, execute: () => runCommand(item, "卸载 Client Manager", () => platformApiClient.uninstallClientManager(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, confirmed: true, idempotencyKey: idempotency("uninstall") })) })} />
</div>
</section>
);
}
function VersionCell({ label, version, revision, artifact }: { label: string; version?: string; revision?: string; artifact?: string }) {
return <div className="client-manager-version-cell"><span>{label}</span><strong>{version || "--"}</strong><small>{revision ? `revision ${shortRef(revision)}` : "revision --"}{artifact ? ` · artifact ${shortRef(artifact)}` : ""}</small></div>;
}
function LifecycleButton({ icon, label, disabled, reason, danger, onClick }: { icon: ReactNode; label: string; disabled: boolean; reason: string; danger?: boolean; onClick: () => void }) {
return <button type="button" className={cx("icon-command", danger && "danger-command")} disabled={disabled} title={disabled ? reason : label} onClick={onClick}>{icon}<span>{label}</span></button>;
}
function lifecycleLabel(status: ClientManagerInstallationResponse["status"]): string {
return ({ requested: "已请求", building: "构建中", available: "可部署", deploying: "部署中", installed: "已安装", registering: "等待注册", online: "在线", degraded: "降级", offline: "离线", updating: "更新中", rolling_back: "回滚中", stopping: "停止中", uninstalled: "已卸载", failed: "失败" })[status];
}
function lifecycleTone(status: ClientManagerInstallationResponse["status"]): string { return ["online", "installed"].includes(status) ? "status-active" : ["failed", "offline", "uninstalled"].includes(status) ? "status-disabled" : "status-pending"; }
function healthLabel(health: ClientManagerInstallationResponse["health"]): string { return ({ unknown: "健康未知", healthy: "健康", degraded: "健康降级", unhealthy: "不健康", offline: "心跳离线" })[health]; }
function healthTone(health: ClientManagerInstallationResponse["health"]): string { return health === "healthy" ? "status-active" : health === "unknown" || health === "degraded" ? "status-pending" : "status-disabled"; }
function shortRef(value: string): string { return value.length > 18 ? `${value.slice(0, 18)}` : value; }
function formatTime(value: string): string { const time = new Date(value); return Number.isNaN(time.getTime()) ? "未知" : time.toLocaleString(); }
function safeError(error: unknown, fallback: string): string { const message = error instanceof Error ? error.message : fallback; return message.replace(/Bearer\s+\S+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]").slice(0, 240); }