refactor(scum): remove orphaned host panels

This commit is contained in:
npc0-hue
2026-07-29 16:26:19 +08:00
parent 9fafd17915
commit 03339fb3e8
18 changed files with 31 additions and 1185 deletions
@@ -1,70 +0,0 @@
import { describe, expect, it } from "vitest";
import type { GamePluginResponse } from "../api/types";
import { normalizeScumRouteKey, resolveScumOperationsPageContract } from "./scumOperations";
const plugin = {
id: "game.scum",
pages: [{
key: "files-config",
title: "文件与配置",
path: "/files-config",
permissions: ["server.game-client.read", "server.game-client.command", "server.remote.access", "unknown.permission"],
bridgeActions: ["server.instances.read", "logs.query", "remote.access.request", "unknown.action"]
}],
gameClientBridge: {
commands: [
{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.json", timeoutSeconds: 30, maxPayloadBytes: 4096 },
{ type: "maintenance.prepare", title: "Prepare maintenance", permission: "server.game-client.maintenance", approvalLevel: "platform-admin", payloadSchemaRef: "schemas/bridge/maintenance.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }
],
snapshots: [
{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 },
{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.json", keepForSeconds: 3600, maxRecords: 24 }
],
queryTemplates: [
{ key: "scum.player.search", title: "Search player", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/player-search.parameters.json", resultSchemaRef: "schemas/bridge/player-search.result.json", maxRows: 50, timeoutSeconds: 10 }
],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: [{ pageKey: "files-config", commandTypes: ["announcement.send"], snapshotTypes: ["companion.health"], queryTemplateKeys: ["scum.player.search"] }]
},
runtimeProfiles: {
logSources: [{ key: "scum-chat-events", kind: "file.tail", streamKey: "scum.chat", retentionDays: 30 }],
logEvents: [{ key: "scum-chat", title: "SCUM chat", sourceKey: "scum-chat-events", eventType: "scum.chat", permission: "server.logs.read", schemaRef: "schemas/log-events/chat.json", retentionDays: 30, severity: "info" }]
},
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] }
} satisfies Pick<GamePluginResponse, "id" | "pages" | "gameClientBridge" | "runtimeProfiles" | "productionLifecycle">;
describe("SCUM operations page contract", () => {
it("projects only declarations owned by the plugin operations page", () => {
const resolution = resolveScumOperationsPageContract(plugin, "server-1");
expect(resolution).toMatchObject({
available: true,
contract: {
pluginId: "game.scum",
routeKey: "files-config",
serverInstanceId: "server-1",
permissions: ["server.game-client.read", "server.game-client.command", "server.remote.access"],
bridgeActions: ["server.instances.read", "logs.query", "remote.access.request"],
commands: [{ type: "announcement.send" }],
snapshots: [{ type: "companion.health" }],
queryTemplates: [{ key: "scum.player.search", engine: "sqlite" }],
logEvents: [{ eventType: "scum.chat" }],
productionLifecycle: { dependencyPolicy: "required" }
}
});
expect(JSON.stringify(resolution)).not.toMatch(/sqlText|hostPath|sessionToken|componentKey|credential|socket/i);
});
it("reports declaration and server-context availability without inventing fallback semantics", () => {
expect(resolveScumOperationsPageContract({ ...plugin, id: "game.other" }, "server-1")).toMatchObject({ available: false });
expect(resolveScumOperationsPageContract(plugin, "")).toMatchObject({ available: false, reason: "缺少服务器实例上下文。" });
expect(resolveScumOperationsPageContract({ ...plugin, gameClientBridge: undefined }, "server-1")).toMatchObject({ available: false });
});
it("migrates only legacy SCUM page keys to the files-and-config workbench", () => {
expect(normalizeScumRouteKey("game.scum", "overview")).toBe("files-config");
expect(normalizeScumRouteKey("game.scum", "logs")).toBe("files-config");
expect(normalizeScumRouteKey("game.other", "logs")).toBe("logs");
});
});
-146
View File
@@ -1,146 +0,0 @@
import type {
GameClientBridgeCommandDeclarationResponse,
GameClientBridgeQueryTemplateDeclarationResponse,
GameClientBridgeSnapshotDeclarationResponse,
GamePluginResponse,
PluginProductionLifecycleDeclaration,
RuntimeLogEventResponse,
RuntimeLogSourceResponse
} from "../api/types";
import {
isPluginBridgeAction,
isPluginPermission,
type PluginBridgeAction,
type PluginPermission
} from "./pluginBridge";
export const scumOperationsPluginId = "game.scum";
export const scumOperationsRouteKey = "files-config";
export interface ScumOperationsPageContract {
pluginId: typeof scumOperationsPluginId;
routeKey: typeof scumOperationsRouteKey;
serverInstanceId: string;
title: string;
permissions: PluginPermission[];
bridgeActions: PluginBridgeAction[];
commands: GameClientBridgeCommandDeclarationResponse[];
snapshots: GameClientBridgeSnapshotDeclarationResponse[];
queryTemplates: GameClientBridgeQueryTemplateDeclarationResponse[];
logSources: RuntimeLogSourceResponse[];
logEvents: RuntimeLogEventResponse[];
productionLifecycle: PluginProductionLifecycleDeclaration;
}
export interface ScumCompanionHealthView {
status: "online" | "degraded" | "offline" | "unknown";
version?: string;
observedAt?: string;
latencyMs?: number;
capabilities: string[];
}
export interface ScumSessionView {
sessionId: string;
playerName: string;
startedAt?: string;
}
export interface ScumPlayerView {
playerId: string;
playerName: string;
status: string;
squadId?: string;
pingMs?: number;
lastSeenAt?: string;
}
export interface ScumSquadView {
squadId: string;
name: string;
memberCount: number;
leaderPlayerId?: string;
lastActiveAt?: string;
}
export interface ScumVehicleView {
vehicleId: string;
vehicleType: string;
status: string;
ownerPlayerId?: string;
squadId?: string;
fuelPercent?: number;
healthPercent?: number;
lastSeenAt?: string;
}
export interface ScumFlagView {
flagId: string;
status: string;
ownerPlayerId?: string;
squadId?: string;
radiusMeters?: number;
lastUpdatedAt?: string;
}
export interface ScumSnapshotCollection<T> {
observedAt?: string;
total: number;
items: T[];
}
export interface ScumOperationsSnapshotView {
health?: ScumCompanionHealthView;
sessions: ScumSnapshotCollection<ScumSessionView>;
players: ScumSnapshotCollection<ScumPlayerView>;
squads: ScumSnapshotCollection<ScumSquadView>;
vehicles: ScumSnapshotCollection<ScumVehicleView>;
flags: ScumSnapshotCollection<ScumFlagView>;
}
export type ScumOperationsPageResolution =
| { available: true; contract: ScumOperationsPageContract }
| { available: false; reason: string };
type ScumPluginProjection = Pick<GamePluginResponse, "id" | "pages" | "gameClientBridge" | "runtimeProfiles" | "productionLifecycle" | "fileWorkspace">;
export function normalizeScumRouteKey(pluginId: string, routeKey: string): string { return pluginId === scumOperationsPluginId && ["overview", "operations", "config", "logs"].includes(routeKey) ? scumOperationsRouteKey : routeKey; }
export function resolveScumOperationsPageContract(plugin: ScumPluginProjection, serverInstanceId: string): ScumOperationsPageResolution {
if (plugin.id !== scumOperationsPluginId) {
return { available: false, reason: "该路由仅承载 game.scum 插件声明的运维页。" };
}
if (!serverInstanceId.trim()) {
return { available: false, reason: "缺少服务器实例上下文。" };
}
const page = plugin.pages.find((candidate) => candidate.key === scumOperationsRouteKey);
if (!page) {
return { available: false, reason: "SCUM 插件未声明 operations 页面。" };
}
const manifest = plugin.gameClientBridge;
const bridgePage = manifest?.pages?.find((candidate) => candidate.pageKey === scumOperationsRouteKey);
if (!manifest || !bridgePage) {
return { available: false, reason: "SCUM 插件未声明 operations 的 Game Client Bridge 契约。" };
}
const commandTypes = new Set(bridgePage.commandTypes ?? []);
const snapshotTypes = new Set(bridgePage.snapshotTypes ?? []);
const queryTemplateKeys = new Set(bridgePage.queryTemplateKeys ?? []);
return {
available: true,
contract: {
pluginId: scumOperationsPluginId,
routeKey: scumOperationsRouteKey,
serverInstanceId,
title: page.title,
permissions: page.permissions.filter(isPluginPermission),
bridgeActions: (page.bridgeActions ?? []).filter(isPluginBridgeAction),
commands: manifest.commands.filter((command) => commandTypes.has(command.type)),
snapshots: manifest.snapshots.filter((snapshot) => snapshotTypes.has(snapshot.type)),
queryTemplates: (manifest.queryTemplates ?? []).filter((template) => queryTemplateKeys.has(template.key)),
logSources: [...(plugin.runtimeProfiles?.logSources ?? [])],
logEvents: [...(plugin.runtimeProfiles?.logEvents ?? [])],
productionLifecycle: plugin.productionLifecycle
}
};
}