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
+4 -2
View File
@@ -13,7 +13,7 @@ Plugins use the platform bridge for every privileged action.
- `run.distribution.request`: request platform-mediated run package generation, download, key reset, or self-update orchestration.
- `dependencies.request`: request typed dependency checks or approved install plans declared by the plugin runtime profile.
- `logs.backfill.request`: request historical log backfill for a declared log source.
- `client-manager.request`: request generation, download, or key reset for a plugin-declared companion client manager.
- `client-manager.request`: request generation/download/key reset or a typed status, deploy, start, stop, restart, update, rollback, session-revoke, retry, or uninstall operation for a plugin-declared companion client manager.
- `ai.invoke`: request platform-mediated AI assistance.
- `theme.tokens`: read safe platform theme tokens.
@@ -29,7 +29,9 @@ Artifact open requests use `createArtifactOpenRequest` with an artifact ID that
Remote access requests use `createRemoteAccessRequest` with a plugin-declared `remote.*` capability, logical target key, optional scoped `input://` or `artifact://` ref, and idempotency key. The SDK never accepts FTP passwords, rsync endpoints, database DSNs, RCON passwords, run sockets, or raw host paths in these envelopes.
Run distribution, dependency, log backfill, and client-manager requests use `createRunDistributionRequest`, `createDependencyActionRequest`, `createLogBackfillRequest`, and `createClientManagerRequest`. These helpers carry operation names, logical profile keys, target OS/architecture, artifact IDs, cursors, and idempotency keys only; raw run keys and client-manager keys are written only into generated packages by platform services.
Run distribution, dependency, log backfill, and client-manager requests use `createRunDistributionRequest`, `createDependencyActionRequest`, `createLogBackfillRequest`, and `createClientManagerRequest`. Client-manager lifecycle envelopes carry only operation names, logical profile/installation IDs, target OS/architecture, artifact IDs, expected deployment generations, and idempotency keys. `parseClientManagerLifecycleStatus` whitelists safe state, version, health, job, artifact, and action fields. Raw run/client-manager keys, component sessions, secret refs, host paths, PIDs, sockets, credentials, and direct Run endpoint details are never plugin bridge fields.
Client-manager lifecycle requests remain Platform-mediated. A plugin declaration does not grant access by itself: Platform rechecks the installed plugin, server owner/administrator scope, runtime binding, assigned Run endpoint capabilities, current distribution target/revision/key generation, and durable installation state before dispatching a typed job.
## Forbidden Data
+141 -1
View File
@@ -36,6 +36,11 @@ export type RunCapability =
| "remote.run.db.sqlite.query"
| "remote.run.logs.transfer"
| "remote.run.rcon.command"
| "client-manager.deploy"
| "client-manager.control"
| "client-manager.update"
| "client-manager.rollback"
| "client-manager.uninstall"
| "artifacts.read"
| "artifacts.write"
| "ai.invoke";
@@ -145,6 +150,7 @@ export type PluginDependencyActionPayload = Record<string, string> & {
operation: "check" | "install";
probeKey?: string;
planKey?: string;
planDigest?: string;
idempotencyKey: string;
};
@@ -156,11 +162,26 @@ export type PluginLogBackfillPayload = Record<string, string> & {
};
export type PluginClientManagerPayload = Record<string, string> & {
operation: "generate" | "download" | "reset-key";
operation:
| "generate"
| "download"
| "reset-key"
| "status"
| "deploy"
| "start"
| "stop"
| "restart"
| "update"
| "rollback"
| "revoke-session"
| "retry"
| "uninstall";
profileKey: string;
targetOS?: RuntimePlatform;
targetArch?: RuntimeArch;
artifactId?: string;
installationId?: string;
expectedDeploymentGeneration?: string;
idempotencyKey: string;
};
@@ -243,6 +264,7 @@ export interface RuntimeTransportProfile {
export interface RuntimeClientManagerProfile {
key: string;
displayName?: string;
version?: string;
repository: {
url: string;
revisionPolicy: "pinned" | "branch" | "tag";
@@ -258,6 +280,36 @@ export interface RuntimeClientManagerProfile {
};
configTemplates?: Array<{ key: string; templateRef: string; outputRef: string }>;
outputArtifacts: string[];
deployment?: {
mode: "run-supervised";
executableRef: string;
arguments?: string[];
autoStart?: boolean;
requiredRunCapabilities: Array<"client-manager.deploy" | "client-manager.control" | "client-manager.update" | "client-manager.rollback" | "client-manager.uninstall">;
};
lifecycle?: {
actions: Array<"start" | "stop" | "restart" | "status" | "update" | "rollback" | "uninstall">;
startupTimeoutSeconds: number;
stopTimeoutSeconds: number;
};
health?: {
mode: "component-heartbeat" | "process";
intervalSeconds: number;
degradedAfterSeconds: number;
offlineAfterSeconds: number;
requiredCapabilities: Array<"component.register" | "component.heartbeat" | "component.health" | "component.control" | "game-client.bridge" | "logs.stream">;
};
compatibility?: {
minimumVersion?: string;
maximumVersion?: string;
allowDowngrade: boolean;
};
updatePolicy?: {
strategy: "manual-staged";
requireApproval: true;
healthConfirmationSeconds: number;
retainPrevious: true;
};
}
export interface GamePluginRuntimeProfiles {
@@ -283,6 +335,24 @@ export interface PluginArtifactReference {
storageBehavior?: string;
}
export interface PluginClientManagerLifecycleStatus {
installationId: string;
profileKey: string;
status: string;
phase?: string;
targetOS?: RuntimePlatform;
targetArch?: RuntimeArch;
version?: string;
previousVersion?: string;
artifactId?: string;
currentJobId?: string;
deploymentGeneration?: number;
health?: string;
healthReason?: string;
lastSeenAt?: string;
actions: string[];
}
export type PluginBridgeExecutionResponse<TResult extends Record<string, string> = Record<string, string>> = {
requestId: string;
pluginId: string;
@@ -315,6 +385,17 @@ export const pluginBridgeActionPolicies: Record<PluginBridgeAction, PluginBridge
export type PluginLifecycleAction = "install" | "start" | "stop" | "restart" | "status";
export interface PluginLifecycleActionDeclaration {
version: 1;
action: PluginLifecycleAction;
mode: "oneshot" | "supervised" | "control";
executableKey?: string;
arguments?: string[];
environment?: Record<`GAME_${string}` | `SERVER_${string}` | `RUN_${string}`, string>;
timeoutMs?: number;
stopTimeoutMs?: number;
}
export type GamePluginActions = Partial<Record<PluginLifecycleAction, string>> & {
install: string;
start: string;
@@ -507,8 +588,12 @@ export function createDependencyActionRequest(input: {
operation: PluginDependencyActionPayload["operation"];
probeKey?: string;
planKey?: string;
planDigest?: string;
idempotencyKey: string;
}): PluginBridgeExecutionRequest<PluginDependencyActionPayload> {
if (input.operation === "install" && !/^sha256:[a-fA-F0-9]{64}$/.test(input.planDigest ?? "")) {
throw new Error("dependency install requires the reviewed plan SHA-256 digest");
}
return createBridgeExecutionRequest({
requestId: input.requestId,
context: input.context,
@@ -517,6 +602,7 @@ export function createDependencyActionRequest(input: {
operation: input.operation,
probeKey: input.probeKey ?? "",
planKey: input.planKey ?? "",
planDigest: input.planDigest ?? "",
idempotencyKey: input.idempotencyKey
}
});
@@ -551,12 +637,16 @@ export function createClientManagerRequest(input: {
targetOS?: RuntimePlatform;
targetArch?: RuntimeArch;
artifactId?: string;
installationId?: string;
expectedDeploymentGeneration?: number;
idempotencyKey: string;
}): PluginBridgeExecutionRequest<PluginClientManagerPayload> {
const payload: PluginClientManagerPayload = {
operation: input.operation,
profileKey: input.profileKey,
artifactId: input.artifactId ?? "",
installationId: input.installationId ?? "",
expectedDeploymentGeneration: typeof input.expectedDeploymentGeneration === "number" ? String(input.expectedDeploymentGeneration) : "",
idempotencyKey: input.idempotencyKey
};
if (input.targetOS) {
@@ -605,6 +695,52 @@ export function parseArtifactReference(result: Record<string, string> | undefine
return reference;
}
export function parseClientManagerLifecycleStatus(result: Record<string, string> | undefined): PluginClientManagerLifecycleStatus | undefined {
if (!result) {
return undefined;
}
const deploymentGeneration = Number(result.deploymentGeneration ?? "0");
const values = [
result.installationId,
result.profileKey,
result.status,
result.phase,
result.targetOS,
result.targetArch,
result.version,
result.previousVersion,
result.artifactId,
result.currentJobId,
result.health,
result.healthReason,
result.lastSeenAt,
result.actions
];
if (!result.installationId || !result.profileKey || !result.status || !Number.isSafeInteger(deploymentGeneration) || deploymentGeneration < 0) {
return undefined;
}
if (values.some((value) => typeof value === "string" && containsUnsafeReferenceContent(value))) {
return undefined;
}
return {
installationId: result.installationId,
profileKey: result.profileKey,
status: result.status,
phase: result.phase,
targetOS: result.targetOS as RuntimePlatform | undefined,
targetArch: result.targetArch as RuntimeArch | undefined,
version: result.version,
previousVersion: result.previousVersion,
artifactId: result.artifactId,
currentJobId: result.currentJobId,
deploymentGeneration,
health: result.health,
healthReason: result.healthReason,
lastSeenAt: result.lastSeenAt,
actions: (result.actions ?? "").split(",").filter(Boolean)
};
}
export function parseBridgeExecutionResponse<TResult extends Record<string, string>>(response: PluginBridgeExecutionResponse<TResult>): PluginBridgeExecutionResponse<TResult> {
const safeError = response.error ? bridgeError(response.error.code, response.error.message, response.error.details ?? []) : undefined;
return {
@@ -672,6 +808,10 @@ function containsUnsafeReferenceContent(value: string): boolean {
lowered.includes("apikey=") ||
lowered.includes("storage://") ||
lowered.includes("file://") ||
lowered.includes("sessiontoken") ||
lowered.includes("secret://") ||
lowered.includes("hostpath") ||
lowered.includes("processid") ||
lowered.startsWith("sk-")
);
}