Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+245 -2
View File
@@ -66,11 +66,13 @@ type Core interface {
StopServerInstanceForSession(string, domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error)
GetServerInstance(string) (domain.ServerInstance, error)
GetServerInstanceForSession(string, string) (domain.ServerInstance, error)
UpdateServerInstanceForSession(string, string, domain.ServerInstanceUpdate) (domain.ServerInstance, error)
ListServerInstances(domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
ListServerInstancesForSession(string, domain.ServerInstanceFilter) ([]domain.ServerInstance, error)
ListServerAdministratorCandidates(string, string) ([]domain.User, error)
AddServerAdministrator(string, string, string) (domain.ServerInstance, error)
RemoveServerAdministrator(string, string, string) (domain.ServerInstance, error)
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
GetServerConfigForSession(string, string) (domain.ServerConfig, error)
@@ -93,6 +95,16 @@ type Core interface {
GetArtifactForSession(string, string) (domain.Artifact, error)
OpenArtifactDownloadForSession(string, domain.ArtifactDownloadReferenceRequest) (domain.ArtifactDownloadReference, error)
ReadArtifactContentForSession(string, domain.ArtifactContentRequest) (domain.ArtifactContent, error)
GetServerRuntimeActionsForSession(string, string) (domain.ServerRuntimeActions, error)
GenerateRunDistributionForSession(string, domain.RunDistributionGenerateRequest) (domain.RunDistribution, error)
GenerateClientManagerDistributionForSession(string, domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error)
OpenLatestRunDistributionDownloadForSession(string, string) (domain.ArtifactDownloadReference, error)
OpenLatestClientManagerDistributionDownloadForSession(string, string, string) (domain.ArtifactDownloadReference, error)
ResetComponentKeyForSession(string, domain.ComponentKeyResetRequest) (domain.EncryptedComponentKey, error)
AuthenticateComponent(domain.ComponentAuthenticationRequest) (domain.ComponentAuthenticationResult, error)
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
QueueDependencyJobForSession(string, domain.DependencyJobRequest) (domain.Job, error)
QueueLogBackfillForSession(string, domain.LogBackfillRequest) (domain.Job, error)
OpenArtifactTransfer(domain.ArtifactTransferOpen) (domain.ArtifactTransferOpenResult, error)
UploadArtifactChunk(domain.ArtifactChunkUpload) (domain.ArtifactChunkUploadResult, error)
QueryArtifactTransferStatus(domain.ArtifactTransferStatusQuery) (domain.ArtifactTransferStatusResult, error)
@@ -121,7 +133,10 @@ type CoreService struct {
logStore LogBodyStore
artifactMu sync.Mutex
artifactTransfers map[string]domain.ArtifactTransferSession
artifactPayloads map[string][]byte
artifactTransferSeq uint64
auditMu sync.Mutex
auditSeq uint64
aiProviderClient AIProviderClient
}
@@ -151,6 +166,7 @@ func newCoreServiceWithLogStore(store repo.Store, logStore LogBodyStore, now fun
jobLeases: map[string]domain.RunJobLease{},
logStore: logStore,
artifactTransfers: map[string]domain.ArtifactTransferSession{},
artifactPayloads: map[string][]byte{},
aiProviderClient: MockAIProviderClient{},
}
}
@@ -523,6 +539,7 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
Pages: manifest.Pages,
Tags: manifest.Tags,
AIPurposes: manifest.AI.Purposes,
RemoteAccess: manifest.RemoteAccess,
Status: domain.GamePluginStatusInstalled,
}
}
@@ -604,6 +621,16 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
base = svc.executeBridgeLogsQuery(base, instance, request.Payload)
case domain.PluginBridgeActionFilesRequest:
base = svc.executeBridgeFileRequest(sessionID, base, request)
case domain.PluginBridgeActionRemoteAccessRequest:
base = svc.executeBridgeRemoteAccessRequest(base, plugin, instance, request.Payload)
case domain.PluginBridgeActionRunDistribution:
base = svc.executeBridgeRunDistribution(sessionID, base, request)
case domain.PluginBridgeActionDependenciesRequest:
base = svc.executeBridgeDependenciesRequest(base, plugin, instance, request.Payload)
case domain.PluginBridgeActionLogsBackfillRequest:
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
case domain.PluginBridgeActionClientManager:
base = svc.executeBridgeClientManager(sessionID, base, request)
case domain.PluginBridgeActionArtifactsOpen:
base = svc.executeBridgeArtifactOpen(sessionID, base, request)
case domain.PluginBridgeActionAIInvoke:
@@ -815,6 +842,146 @@ func (svc *CoreService) executeBridgeFileRequest(sessionID string, base domain.P
return base
}
func (svc *CoreService) executeBridgeRemoteAccessRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
capability := strings.TrimSpace(payload["capability"])
if capability == "" {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "capability is required"}
return base
}
if !containsString(plugin.RequiredRunCapabilities, capability) {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "requested remote capability is not declared by plugin"}
return base
}
job := domain.Job{
ID: jobIDFromParts("job-remote", base.RequestID, capability),
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: capability,
TargetKey: payload["targetKey"],
InputRef: payload["inputRef"],
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
Progress: domain.JobProgress{Percent: 0, Message: "remote access job queued"},
}
created, err := svc.CreateJob(job)
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "queued"
base.Result = map[string]string{
"jobId": created.ID,
"state": string(created.State),
"capability": created.Capability,
"targetKey": created.TargetKey,
"serverInstanceId": created.ServerInstanceID,
}
return base
}
func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
distribution, err := svc.GenerateRunDistributionForSession(sessionID, domain.RunDistributionGenerateRequest{
ServerInstanceID: request.ServerInstanceID,
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "linux"),
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "ok"
base.Result = map[string]string{
"distributionId": distribution.ID,
"artifactId": distribution.ArtifactID,
"checksum": distribution.Checksum,
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
"secretRef": distribution.SecretRef,
"status": string(distribution.Status),
}
return base
}
func (svc *CoreService) executeBridgeClientManager(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
distribution, err := svc.GenerateClientManagerDistributionForSession(sessionID, domain.ClientManagerBuildRequest{
ServerInstanceID: request.ServerInstanceID,
ProfileKey: request.Payload["profileKey"],
TargetOS: defaultBridgeValue(request.Payload["targetOs"], "windows"),
TargetArch: defaultBridgeValue(request.Payload["targetArch"], "amd64"),
RepositoryURL: request.Payload["repositoryUrl"],
SourceRevision: request.Payload["sourceRevision"],
IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID),
})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "ok"
base.Result = map[string]string{
"distributionId": distribution.ID,
"buildJobId": distribution.BuildJobID,
"artifactId": distribution.ArtifactID,
"checksum": distribution.Checksum,
"keyGeneration": strconv.Itoa(distribution.KeyGeneration),
"secretRef": distribution.SecretRef,
"status": string(distribution.Status),
}
return base
}
func (svc *CoreService) executeBridgeDependenciesRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
action := defaultBridgeValue(payload["action"], "check")
capability := domain.JobCapabilityDependenciesCheck
message := "dependency check queued"
if action == "install" {
capability = domain.JobCapabilityDependenciesInstall
message = "dependency install queued"
}
if !containsString(plugin.RequiredRunCapabilities, capability) {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "dependency capability is not declared by plugin"}
return base
}
job, err := svc.CreateJob(domain.Job{
ID: jobIDFromParts("job-dependencies", base.RequestID, capability),
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: capability,
TargetKey: defaultBridgeValue(payload["probeKey"], "dependencies/default"),
InputRef: payload["inputRef"],
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
Progress: domain.JobProgress{Percent: 0, Message: message},
})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "queued"
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "targetKey": job.TargetKey}
return base
}
func (svc *CoreService) executeBridgeLogsBackfillRequest(base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
if !containsString(plugin.RequiredRunCapabilities, domain.JobCapabilityLogsBackfill) {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "capability_denied", Message: "log backfill capability is not declared by plugin"}
return base
}
job, err := svc.CreateJob(domain.Job{
ID: jobIDFromParts("job-logs-backfill", base.RequestID, payload["sourceKey"]),
ServerInstanceID: instance.ID,
RunEndpointID: instance.RunEndpointID,
Capability: domain.JobCapabilityLogsBackfill,
TargetKey: defaultBridgeValue(payload["sourceKey"], "logs/default"),
InputRef: payload["checkpointRef"],
IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID),
Progress: domain.JobProgress{Percent: 0, Message: "historical log backfill queued"},
})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = "queued"
base.Result = map[string]string{"jobId": job.ID, "state": string(job.State), "capability": job.Capability, "sourceKey": job.TargetKey}
return base
}
func bridgeExecutionError(base domain.PluginBridgeExecuteResponse, err error) domain.PluginBridgeExecuteResponse {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "execution_failed", Message: safeBridgeReason(err.Error())}
@@ -855,6 +1022,11 @@ func pluginPermissionsFromManifest(permissions []string) domain.PluginPermission
aggregate.Jobs = true
case "server.artifacts.read", "server.artifacts.write":
aggregate.Artifacts = true
case "server.remote.access":
aggregate.RemoteAccess = true
case "server.run.distribution", "server.dependencies.manage", "server.client-manager.manage":
aggregate.Jobs = true
aggregate.Artifacts = true
}
}
return aggregate
@@ -962,6 +1134,7 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
Pages: plugin.Pages,
Tags: plugin.Tags,
AIPurposes: plugin.AIPurposes,
RemoteAccess: plugin.RemoteAccess,
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: "platform-registry",
@@ -1077,6 +1250,37 @@ func (svc *CoreService) GetServerInstanceForSession(sessionID string, id string)
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) UpdateServerInstanceForSession(sessionID string, id string, update domain.ServerInstanceUpdate) (domain.ServerInstance, error) {
if err := validator.ValidateServerInstanceUpdate(update); err != nil {
return domain.ServerInstance{}, err
}
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerInstance{}, err
}
instance, err := svc.store.ServerInstances().Get(id)
if err != nil {
return domain.ServerInstance{}, err
}
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
return domain.ServerInstance{}, ErrForbidden
}
if instance.State == domain.ServerInstanceStateDeleted {
return domain.ServerInstance{}, validationError("deleted server instances cannot be edited")
}
if update.Name != nil {
instance.Name = *update.Name
}
instance.UpdatedAt = svc.now()
if err := validator.ValidateStoredServerInstance(instance); err != nil {
return domain.ServerInstance{}, err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return domain.ServerInstance{}, err
}
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) ListServerInstances(filter domain.ServerInstanceFilter) ([]domain.ServerInstance, error) {
return svc.store.ServerInstances().List(filter)
}
@@ -1487,6 +1691,35 @@ func (svc *CoreService) RemoveServerAdministrator(sessionID string, serverInstan
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) ArchiveServerInstanceForSession(sessionID string, serverInstanceID string) (domain.ServerInstance, error) {
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerInstance{}, err
}
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
if err != nil {
return domain.ServerInstance{}, err
}
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
return domain.ServerInstance{}, ErrForbidden
}
if instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateInstalling {
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before archive")
}
if instance.State == domain.ServerInstanceStateDeleted {
return domain.CopyServerInstance(instance), nil
}
instance.State = domain.ServerInstanceStateDeleted
instance.UpdatedAt = svc.now()
if err := validator.ValidateStoredServerInstance(instance); err != nil {
return domain.ServerInstance{}, err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return domain.ServerInstance{}, err
}
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
if job.State == "" {
job.State = domain.JobStateQueued
@@ -1522,7 +1755,11 @@ func (svc *CoreService) CreateJob(job domain.Job) (domain.Job, error) {
if err != nil {
return domain.Job{}, fmt.Errorf("get server instance dependency: %w", err)
}
if err := validateJobServerTarget(job, instance); err != nil {
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
return domain.Job{}, fmt.Errorf("get server plugin dependency: %w", err)
}
if err := validateJobServerTarget(job, instance, plugin); err != nil {
return domain.Job{}, err
}
}
@@ -1632,13 +1869,19 @@ func validateRunnableEndpoint(endpoint domain.RunEndpoint, capability string) er
return nil
}
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance) error {
func validateJobServerTarget(job domain.Job, instance domain.ServerInstance, plugin domain.GamePlugin) error {
if instance.State == domain.ServerInstanceStateDeleted {
return validationError("server instance must not be deleted")
}
if instance.RunEndpointID != job.RunEndpointID {
return validationError("job runEndpointId must match server instance")
}
if plugin.ID != instance.PluginID {
return validationError("job plugin must match server instance")
}
if !containsString(plugin.RequiredRunCapabilities, job.Capability) {
return validationError("plugin missing required capability: " + job.Capability)
}
return nil
}