feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"version": 1,
"action": "start",
"mode": "supervised",
"executableKey": "/bin/sh",
"arguments": ["-c", "curl | bash"],
"environment": {
"GAME_PASSWORD": "password=secret"
}
}
+14 -2
View File
@@ -17,16 +17,28 @@
{"key": "leaky", "kind": "command.version", "targetKey": "java", "expected": "password=super-secret"}
],
"installPlans": [
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]}
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]},
{"key": "unsafe-download", "title": "Unsafe dependency download", "steps": [{"type": "verified-download", "targetKey": "tool", "downloadRef": "https://127.0.0.1/tool", "checksum": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}
],
"clientManagers": [
{
"key": "unsafe-client",
"version": "1.0.0",
"repository": {"url": "https://github.com/F88888/scum_client.git", "revisionPolicy": "branch", "branch": "main"},
"supportedTargets": [{"os": "windows", "arch": "amd64"}],
"build": {"system": "go", "workspaceRef": "scum_client", "entryRef": "main.go"},
"configTemplates": [{"key": "bad", "templateRef": "/Users/tasia/client.json", "outputRef": "config.json"}],
"outputArtifacts": ["scum_client.exe"]
"outputArtifacts": ["scum_client.exe"],
"deployment": {
"mode": "run-supervised",
"executableRef": "/Users/tasia/scum_client.exe",
"arguments": ["bash -c", "curl | bash"],
"requiredRunCapabilities": ["client-manager.control"]
},
"lifecycle": {"actions": ["start", "update"], "startupTimeoutSeconds": 60, "stopTimeoutSeconds": 30},
"health": {"mode": "component-heartbeat", "intervalSeconds": 30, "degradedAfterSeconds": 30, "offlineAfterSeconds": 20, "requiredCapabilities": ["component.heartbeat"]},
"compatibility": {"minimumVersion": "2.0.0", "maximumVersion": "1.0.0", "allowDowngrade": false},
"updatePolicy": {"strategy": "manual-staged", "requireApproval": true, "healthConfirmationSeconds": 5, "retainPrevious": true}
}
]
},
+73 -2
View File
@@ -15,12 +15,15 @@ import {
createRunDistributionRequest,
hasPluginPermission,
parseArtifactReference,
parseClientManagerLifecycleStatus,
parseBridgeExecutionResponse,
parseAIInvocationResponse,
type GamePluginManifest,
type RuntimeClientManagerProfile,
type PluginLifecycleActionDeclaration,
type PluginBridgeContext
} from "../sdk/index.js";
import { validateManifestFile } from "../scripts/validate-manifest.js";
import { validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js";
describe("plugin manifest validation", () => {
it("accepts the development example manifest", () => {
@@ -54,6 +57,24 @@ describe("plugin manifest validation", () => {
expect(errors.some((error) => error.includes("raw credential or AI/provider key"))).toBe(true);
expect(errors.some((error) => error.includes("raw host path"))).toBe(true);
expect(errors.some((error) => error.includes("arbitrary shell"))).toBe(true);
expect(errors.some((error) => error.includes("not approved for dependency download"))).toBe(true);
expect(errors.some((error) => error.includes("client-manager.deploy is required"))).toBe(true);
expect(errors.some((error) => error.includes("offline threshold"))).toBe(true);
expect(errors.some((error) => error.includes("profile version is below minimumVersion"))).toBe(true);
});
it("validates typed lifecycle declarations and rejects shell/path escapes", () => {
const declaration: PluginLifecycleActionDeclaration = {
version: 1,
action: "start",
mode: "supervised",
executableKey: "bin/game-server",
arguments: ["--foreground"]
};
expect(declaration.action).toBe("start");
const errors = validateLifecycleActionFile("tests/fixtures/unsafe-lifecycle-action.json", "start");
expect(errors.some((error) => error.includes("pattern") || error.includes("arbitrary shell"))).toBe(true);
expect(errors.some((error) => error.includes("raw credential"))).toBe(true);
});
});
@@ -323,6 +344,24 @@ describe("plugin SDK", () => {
});
it("types runtime profile declarations without raw credentials", () => {
const clientManager: RuntimeClientManagerProfile = {
key: "safe-client-manager",
version: "1.2.3",
repository: { url: "https://github.com/example/safe-client.git", revisionPolicy: "pinned", revision: "0123456789abcdef" },
supportedTargets: [{ os: "linux", arch: "amd64" }],
build: { system: "go", entryRef: "cmd/client/main.go" },
outputArtifacts: ["safe-client"],
deployment: {
mode: "run-supervised",
executableRef: "safe-client",
arguments: ["--config", "config.json"],
requiredRunCapabilities: ["client-manager.deploy", "client-manager.control", "client-manager.update", "client-manager.rollback", "client-manager.uninstall"]
},
lifecycle: { actions: ["start", "stop", "restart", "status", "update", "rollback", "uninstall"], startupTimeoutSeconds: 30, stopTimeoutSeconds: 15 },
health: { mode: "component-heartbeat", intervalSeconds: 15, degradedAfterSeconds: 45, offlineAfterSeconds: 120, requiredCapabilities: ["component.register", "component.heartbeat", "component.health"] },
compatibility: { minimumVersion: "1.0.0", allowDowngrade: false },
updatePolicy: { strategy: "manual-staged", requireApproval: true, healthConfirmationSeconds: 60, retainPrevious: true }
};
const manifest: GamePluginManifest = {
id: "game.runtime",
name: "Runtime Fixture",
@@ -335,11 +374,13 @@ describe("plugin SDK", () => {
discovery: [{ key: "java", kind: "command.version", targetKey: "java", required: true }],
dependencyProbes: [{ key: "java-21", kind: "java.version", targetKey: "java", minimumVersion: "21" }],
logSources: [{ key: "console", kind: "process.stdout", streamKey: "console", cursorKind: "sequence" }],
transportProfiles: [{ key: "files", kind: "file", capabilities: ["files.read"] }]
transportProfiles: [{ key: "files", kind: "file", capabilities: ["files.read"] }],
clientManagers: [clientManager]
}
};
expect(manifest.runtimeProfiles?.discovery?.[0].targetKey).toBe("java");
expect(manifest.runtimeProfiles?.clientManagers?.[0].deployment?.requiredRunCapabilities).toContain("client-manager.deploy");
expect(JSON.stringify(manifest)).not.toContain("password=");
});
@@ -360,6 +401,11 @@ describe("plugin SDK", () => {
action: "dependencies.request",
payload: { operation: "check", probeKey: "steamcmd" }
});
expect(createDependencyActionRequest({ requestId: "dep-2", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}`, idempotencyKey: "idem-dep-install" })).toMatchObject({
action: "dependencies.request",
payload: { operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}` }
});
expect(() => createDependencyActionRequest({ requestId: "dep-unsafe", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", idempotencyKey: "idem-dep-unsafe" })).toThrow(/reviewed plan SHA-256 digest/);
expect(createLogBackfillRequest({ requestId: "logs-1", context, sourceKey: "chat-log", limit: 500, idempotencyKey: "idem-logs" })).toMatchObject({
action: "logs.backfill.request",
payload: { sourceKey: "chat-log", limit: "500" }
@@ -369,6 +415,31 @@ describe("plugin SDK", () => {
payload: { operation: "generate", profileKey: "scum-client-manager" }
});
expect(JSON.stringify(createClientManagerRequest({ requestId: "client-2", context, operation: "reset-key", profileKey: "scum-client-manager", idempotencyKey: "idem-reset" }))).not.toContain("secret");
expect(createClientManagerRequest({ requestId: "client-3", context, operation: "deploy", profileKey: "scum-client-manager", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: 2, idempotencyKey: "idem-deploy" })).toMatchObject({
action: "client-manager.request",
payload: { operation: "deploy", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: "2" }
});
expect(parseClientManagerLifecycleStatus({
installationId: "cm-install-1",
profileKey: "scum-client-manager",
status: "online",
phase: "healthy",
targetOS: "windows",
targetArch: "amd64",
version: "1.0.0",
artifactId: "artifact-1",
deploymentGeneration: "2",
health: "healthy",
actions: "stop,restart,update,uninstall"
})).toMatchObject({ installationId: "cm-install-1", deploymentGeneration: 2, actions: ["stop", "restart", "update", "uninstall"] });
expect(parseClientManagerLifecycleStatus({
installationId: "cm-install-1",
profileKey: "scum-client-manager",
status: "online",
deploymentGeneration: "2",
actions: "stop",
healthReason: "Bearer stolen-session"
})).toBeUndefined();
});
});