Remove pre-1.0 bridge governance scaffolding

This commit is contained in:
npc0-hue
2026-08-21 09:46:25 +08:00
parent da6c8d607e
commit b5a366e30d
42 changed files with 520 additions and 1066 deletions
@@ -565,8 +565,7 @@ async function preparePluginOperations(headers, server, plugin) {
serverInstanceId: server.id,
operation: "install",
targetVersion: plugin.version,
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`,
confirmed: false
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`
},
headers
);
+2 -2
View File
@@ -35,7 +35,7 @@ const plugin: GamePluginResponse = {
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
tags: ["example"],
aiPurposes: ["logs.diagnose"],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
runtimeProfiles: { lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install", "process.start", "process.stop"] }] },
status: "installed"
};
@@ -58,7 +58,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
tags: ["example"],
aiPurposes: ["logs.diagnose"],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
status: "installed",
source: "platform-registry"
};
+3 -6
View File
@@ -30,7 +30,6 @@ const pendingCommand = {
commandType: "scum.diagnostic.ping",
priority: 20,
state: "pending",
approvalState: "pending",
requesterId: "user-1",
expiresAt: later,
createdAt: now,
@@ -40,7 +39,6 @@ const pendingCommand = {
const completedCommand = {
...pendingCommand,
state: "succeeded",
approvalState: "approved",
resultSummary: "diagnostic completed",
result: {
status: "succeeded",
@@ -80,7 +78,6 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
type: "scum.diagnostic.ping",
title: "Diagnostic ping",
permission: "server.game-client.command",
approvalLevel: "operator",
payloadSchemaRef: "schemas/bridge/commands/diagnostic-ping.request.json",
resultSchemaRef: "schemas/bridge/commands/diagnostic-ping.result.json",
timeoutSeconds: 30,
@@ -126,8 +123,8 @@ const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pic
describe("PlatformApiClient Game Client Bridge operator API", () => {
afterEach(() => vi.unstubAllGlobals());
it("types plugin and marketplace manifest declarations with approval metadata", () => {
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ approvalLevel: "operator" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
it("types plugin and marketplace manifest declarations", () => {
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ type: "scum.diagnostic.ping" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
});
@@ -157,7 +154,7 @@ describe("PlatformApiClient Game Client Bridge operator API", () => {
await expect(client.getGameClientBridgeStatus("server-1")).resolves.toMatchObject({ available: false, profiles: [{ profileKey: "scum-client" }] });
await expect(client.listGameClientBridgeCommands("server-1", { profileKey: "scum-client", state: "pending", commandType: "scum.diagnostic.ping" })).resolves.toMatchObject({ count: 1 });
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending", approvalState: "pending" });
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending" });
await expect(client.getGameClientBridgeCommand("server-1", pendingCommand.id)).resolves.toMatchObject({ result: { status: "succeeded", payload: { delivered: true } } });
await expect(client.cancelGameClientBridgeCommand("server-1", pendingCommand.id, { reason: "maintenance window changed" })).resolves.toMatchObject({ state: "cancelled" });
await expect(client.listGameClientBridgeSnapshots("server-1", { profileKey: "scum-client", type: "scum.players", streamKey: "current", observedAfter: now, limit: 20 })).resolves.toMatchObject({ count: 1, items: [{ sequence: 7 }] });
+2 -2
View File
@@ -14,7 +14,7 @@ describe("PlatformApiClient plugin operations", () => {
const client = new PlatformApiClient("/api/v1", () => "session-token");
await client.listPluginLifecycles({ pluginId: "game.scum" });
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
await client.listAIConfigDiffs({ state: "pending" });
await client.approveAIConfigDiff("diff-1", "approve-1");
@@ -26,6 +26,6 @@ describe("PlatformApiClient plugin operations", () => {
]);
const serialized = JSON.stringify(calls);
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
});
});
+1 -6
View File
@@ -14,8 +14,6 @@ export type RunUpdatePhase = "queued" | "downloading" | "staged" | "restart-requ
export type ServerLifecycleAction = "create" | "start" | "stop" | "status";
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired" | "unknown";
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
export type GameClientBridgeResultStatus = "succeeded" | "failed" | "cancelled" | "unknown";
export type GameClientBridgeJsonValue = string | number | boolean | null | GameClientBridgeJsonValue[] | GameClientBridgeJsonObject;
@@ -27,7 +25,6 @@ export interface GameClientBridgeCommandDeclarationResponse {
type: string;
title: string;
permission: string;
approvalLevel: GameClientBridgeApprovalLevel;
payloadSchemaRef: string;
resultSchemaRef?: string;
timeoutSeconds: number;
@@ -141,7 +138,6 @@ export interface GameClientBridgeCommandResponse {
commandType: string;
priority: number;
state: GameClientBridgeCommandState;
approvalState: GameClientBridgeApprovalState;
requesterId?: string;
resultSummary?: string;
result?: GameClientBridgeCommandResultResponse;
@@ -1589,7 +1585,6 @@ export interface AIConfigRecommendationResponse {
export interface PluginProductionLifecycleDeclaration {
operations: PluginLifecycleOperation[];
dependencyPolicy: "required" | "optional";
approvalRequired: Array<"disable" | "rollback" | "retire">;
}
export type PluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
@@ -1611,7 +1606,7 @@ export interface PluginLifecycleInstallationResponse {
updatedAt: string;
}
export interface PluginLifecycleListResponse { items: PluginLifecycleInstallationResponse[]; count: number; }
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; confirmed: boolean; }
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; }
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; }
export interface AIConfigDiffPreviewResponse {
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { platformApiClient } from "../api/client";
import type { PluginLifecycleInstallationResponse, PluginLifecycleOperation, ServerInstanceResponse } from "../api/types";
import { ConfirmDialog } from "./OperationControls";
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
interface PluginLifecycleWorkbenchProps {
@@ -22,7 +21,6 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
const [targetVersion, setTargetVersion] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [confirming, setConfirming] = useState(false);
const [busy, setBusy] = useState(false);
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
@@ -60,16 +58,13 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
serverInstanceId: selectedServerId,
operation,
targetVersion: targetVersion.trim() || undefined,
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`,
confirmed: disruptiveOperations.includes(operation)
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`
});
const evidence = [response.job?.id && `任务 ${response.job.id}`].filter(Boolean).join(" · ");
setResult({ status: response.status === "queued" || response.status === "accepted" ? "succeeded" : response.status === "deferred" ? "pending" : "failed", label: `${lifecycleOperationLabel(operation)}${response.status}${evidence ? ` · ${evidence}` : ""}` });
setConfirming(false);
await refresh();
} catch (caught) {
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "插件生命周期操作失败" });
setConfirming(false);
} finally {
setBusy(false);
}
@@ -95,7 +90,7 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
{operations.map((item) => <option key={item} value={item}>{lifecycleOperationLabel(item)}</option>)}
</select>
{(operation === "install" || operation === "upgrade") && <input aria-label="目标版本" placeholder="目标版本" value={targetVersion} disabled={busy} onChange={(event) => setTargetVersion(event.target.value)} />}
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => void submit()}>{busy ? "提交中…" : "执行"}</button>
</div>
{installation ? (
<div className="console-record plugin-lifecycle-state">
@@ -109,14 +104,11 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
) : <p className="console-empty-note"></p>}
</>
)}
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
</div>
);
}
const lifecycleOperations: PluginLifecycleOperation[] = ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"];
const disruptiveOperations: PluginLifecycleOperation[] = ["disable", "rollback", "retire"];
function lifecycleOperationLabel(operation: PluginLifecycleOperation) {
return ({ install: "安装", enable: "启用", disable: "停用", upgrade: "升级", rollback: "回滚", retire: "退役", "dependency-check": "依赖检查" } as Record<PluginLifecycleOperation, string>)[operation];
}
@@ -25,7 +25,7 @@ const plugin: GamePluginResponse = {
pages: [],
tags: [],
aiPurposes: [],
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional", approvalRequired: [] },
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional" },
status: "installed",
runtimeProfiles: {
transportProfiles: [{ key: "rcon", kind: "rcon", targetKey: "rcon.password", capabilities: ["remote.run.rcon.command"] }],
@@ -69,9 +69,9 @@ const plugin: GamePluginResponse = {
}],
tags: ["scum"],
aiPurposes: [],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] },
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
gameClientBridge: {
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
commandRetentionSeconds: 86400,
maxCommands: 1000,
+1 -1
View File
@@ -23,7 +23,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
tags: ["example"],
aiPurposes: ["logs.diagnose"],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
runtimeProfiles: {
dllExtensions: [{
key: "scum-simple-rcon-ue4ss",
@@ -16,7 +16,6 @@ const safeCommand = {
commandType: "scum.player.lookup",
priority: 10,
state: "succeeded",
approvalState: "not_required",
requesterId: "user-1",
result: { status: "succeeded", summary: "player found", payload: { found: true }, completedAt: now },
expiresAt: now,
@@ -45,14 +44,14 @@ const safeSnapshotList = {
};
describe("Game Client Bridge safe projection schema", () => {
it("preserves declarations, approval, result, retention and typed snapshot payloads", () => {
it("preserves declarations, result, retention and typed snapshot payloads", () => {
expect(parseSafeGameClientBridgeStatus({
serverInstanceId: "server-1",
pluginId: "game.scum",
available: true,
profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["scum.player.lookup"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
})).toMatchObject({ available: true, profiles: [{ queryTemplateKeys: ["scum.player.search"] }] });
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ approvalState: "not_required", result: { payload: { found: true } } });
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ result: { payload: { found: true } } });
expect(parseSafeGameClientBridgeSnapshotList(safeSnapshotList)).toMatchObject({ count: 1, items: [{ retention: { maxRecords: 24 } }] });
expect(parseSafeGameClientBridgeSnapshotList({
...safeSnapshotList,
-9
View File
@@ -1,5 +1,4 @@
import type {
GameClientBridgeApprovalState,
GameClientBridgeCancelResponse,
GameClientBridgeCommandCancellationResponse,
GameClientBridgeCommandListResponse,
@@ -16,7 +15,6 @@ import type {
} from "../api/types";
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired", "unknown"]);
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
const forbiddenKeys = new Set([
"apikey",
@@ -84,7 +82,6 @@ export function parseSafeGameClientBridgeCommand(value: unknown): GameClientBrid
commandType: string(record.commandType, "commandType"),
priority: number(record.priority, "priority"),
state: commandState(record.state),
approvalState: approvalState(record.approvalState),
expiresAt: string(record.expiresAt, "expiresAt"),
createdAt: string(record.createdAt, "createdAt"),
updatedAt: string(record.updatedAt, "updatedAt")
@@ -284,12 +281,6 @@ function commandState(value: unknown): GameClientBridgeCommandState {
return parsed;
}
function approvalState(value: unknown): GameClientBridgeApprovalState {
const parsed = string(value, "approvalState") as GameClientBridgeApprovalState;
if (!approvalStates.has(parsed)) throw new Error("Game Client Bridge approval state is invalid");
return parsed;
}
function resultStatus(value: unknown): GameClientBridgeResultStatus {
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
@@ -23,7 +23,7 @@ const plugin: GamePluginResponse = {
pages: [],
tags: [],
aiPurposes: [],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
status: "installed",
runtimeProfiles: {
discovery: [{ key: "root-check", kind: "file.exists", targetKey: "server-root", required: true }],