feat: 完整游戏运维功能
This commit is contained in:
@@ -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
@@ -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-")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user