功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+205 -6
View File
@@ -23,6 +23,28 @@ var (
ErrForbidden = errors.New("forbidden")
)
type ForbiddenError struct {
Reason string
}
func (err ForbiddenError) Error() string {
if strings.TrimSpace(err.Reason) == "" {
return ErrForbidden.Error()
}
return ErrForbidden.Error() + ": " + err.Reason
}
func (err ForbiddenError) Is(target error) bool {
return target == ErrForbidden
}
func forbiddenError(reason string) error {
if strings.TrimSpace(reason) == "" {
return ErrForbidden
}
return ForbiddenError{Reason: reason}
}
type Core interface {
CreateUser(domain.User) (domain.User, error)
UpdateUser(string, domain.User) (domain.User, error)
@@ -79,6 +101,16 @@ type Core interface {
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
GetProductionCapacityForSession(string) (domain.ProductionCapacitySummary, error)
CheckCapacityAdmissionForSession(string, domain.CapacityAdmissionRequest) (domain.CapacityAdmissionDecision, error)
ListAlertsForSession(string, domain.AlertFilter) ([]domain.AlertRecord, error)
AcknowledgeAlertForSession(string, domain.AlertAcknowledgeRequest) (domain.AlertRecord, error)
ResolveAlertForSession(string, domain.AlertResolveRequest) (domain.AlertRecord, error)
RetryAlertForSession(string, domain.AlertRetryRequest) (domain.AlertRetryResult, error)
ListPluginLifecyclesForSession(string, domain.PluginLifecycleFilter) ([]domain.PluginLifecycleInstallation, error)
RunPluginLifecycleForSession(string, domain.PluginLifecycleRequest) (domain.PluginLifecycleResult, error)
ListAIConfigDiffsForSession(string, domain.AIConfigDiffFilter) ([]domain.AIConfigDiffPreview, error)
ApproveAIConfigDiffForSession(string, domain.AIConfigDiffApprovalRequest) (domain.AIConfigDiffApprovalResult, error)
IngestMetricBatch(domain.MetricBatchIngest) (domain.MetricBatchIngestResult, error)
ListMetricSamplesForSession(string, domain.MetricSampleFilter) ([]domain.MetricSample, error)
CreateBackupForSession(string, domain.BackupRecord) (domain.BackupRecord, error)
@@ -137,6 +169,17 @@ type Core interface {
RegisterClientManager(domain.ClientManagerRegisterRequest) (domain.ClientManagerRegisterResult, error)
AcceptClientManagerHeartbeat(domain.ClientManagerHeartbeat) (domain.ClientManagerHeartbeatResult, error)
ReconcileClientManagerLifecycle() error
QueueGameClientBridgeCommandForSession(string, domain.GameClientBridgeQueueRequest) (domain.GameClientBridgeCommand, error)
ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest) ([]domain.GameClientBridgeCommand, error)
AckGameClientBridgeCommand(domain.GameClientBridgeAckRequest) (domain.GameClientBridgeCommand, error)
CompleteGameClientBridgeCommand(domain.GameClientBridgeResultRequest) (domain.GameClientBridgeCommand, error)
CancelGameClientBridgeCommandForSession(string, domain.GameClientBridgeCancelRequest) (domain.GameClientBridgeCommand, error)
UploadGameClientBridgeSnapshot(domain.GameClientBridgeSnapshotIngestRequest) (domain.GameClientBridgeSnapshot, error)
ReconcileGameClientBridgeCommands() error
GetGameClientBridgeStatusForSession(string, string) (domain.GameClientBridgeStatus, error)
ListGameClientBridgeCommandsForSession(string, domain.GameClientBridgeCommandFilter) ([]domain.GameClientBridgeCommand, error)
GetGameClientBridgeCommandForSession(string, string) (domain.GameClientBridgeCommand, error)
QueryGameClientBridgeSnapshotsForSession(string, domain.GameClientBridgeSnapshotQuery) ([]domain.GameClientBridgeSnapshot, error)
PushRunUpdateForSession(string, domain.RunUpdateRequest) (domain.RunUpdateJob, error)
ListRunUpdateJobsForSession(string, string) ([]domain.RunUpdateJob, error)
GetDependencyCatalogForSession(string, string) (domain.DependencyCatalog, error)
@@ -169,6 +212,8 @@ type CoreService struct {
runSessions map[string]domain.RunControlSession
runSessionSeq uint64
jobMu sync.Mutex
bridgeMu sync.Mutex
bridgeSeq uint64
logStore LogBodyStore
artifactStore ArtifactBodyStore
artifactMu sync.Mutex
@@ -177,6 +222,7 @@ type CoreService struct {
artifactTransferSeq uint64
auditMu sync.Mutex
auditSeq uint64
productionMu sync.Mutex
aiProviderClient AIProviderClient
secretEnvelope SecretEnvelope
}
@@ -237,6 +283,9 @@ func NewCoreServiceWithDurableStores(store repo.Store, logStore LogBodyStore, ar
if err := service.ReconcileClientManagerLifecycle(); err != nil {
return nil, err
}
if err := service.ReconcileGameClientBridgeCommands(); err != nil {
return nil, err
}
return service, nil
}
@@ -538,13 +587,13 @@ func (svc *CoreService) TestAIProvider(id string) (domain.AIProviderTestResult,
result := domain.AIProviderTestResult{
ProviderID: provider.ID,
Mode: "metadata",
Mode: "provider",
Success: true,
Message: "metadata validation passed",
Message: "provider invocation passed",
}
if err := validator.ValidateAIProvider(provider); err != nil {
result.Success = false
result.Message = "metadata validation failed"
result.Message = "provider validation failed"
var validationErr validator.ValidationError
if errors.As(err, &validationErr) {
result.Violations = append(result.Violations, validationErr.Violations...)
@@ -554,9 +603,35 @@ func (svc *CoreService) TestAIProvider(id string) (domain.AIProviderTestResult,
}
if provider.Status != domain.AIProviderStatusActive {
result.Success = false
result.Message = "metadata validation failed"
result.Message = "provider validation failed"
result.Violations = append(result.Violations, "provider must be active")
}
if !result.Success {
return domain.CopyAIProviderTestResult(result), nil
}
_, invokeErr := svc.aiProviderClient.Invoke(provider, domain.AIInvocationRequest{RequestID: "provider-test-" + provider.ID, Purpose: "provider.health", Prompt: "Return a short health acknowledgement.", Model: provider.DefaultModel})
if invokeErr != nil {
result.Success = false
result.Message = "provider invocation failed safely"
result.Violations = []string{"provider invocation failed safely"}
auditID, auditErr := svc.recordAuditEventWithID("platform", "ai.provider.test.failed", "ai-provider", provider.ID, domain.AuditResultFailed, result.Message)
if auditErr != nil {
return domain.AIProviderTestResult{}, auditErr
}
svc.productionMu.Lock()
_, alertErr := svc.upsertAlert(domain.AlertRecord{SourceKind: "ai-provider", SourceID: provider.ID, RuleKey: "ai.provider.failed", Severity: domain.AlertSeverityWarning, Title: "AI provider health check failed", Message: result.Message, Retryable: false, LastAuditEventID: auditID})
svc.productionMu.Unlock()
if alertErr != nil {
return domain.AIProviderTestResult{}, alertErr
}
} else {
svc.productionMu.Lock()
resolveErr := svc.resolveAlertForSource("ai-provider", provider.ID, "ai.provider.failed", "platform", "AI provider health check passed", "")
svc.productionMu.Unlock()
if resolveErr != nil {
return domain.AIProviderTestResult{}, resolveErr
}
}
return domain.CopyAIProviderTestResult(result), nil
}
@@ -584,6 +659,7 @@ func (svc *CoreService) CreateGamePlugin(plugin domain.GamePlugin) (domain.GameP
if plugin.Status == "" {
plugin.Status = domain.GamePluginStatusInstalled
}
plugin.ProductionLifecycle = normalizedProductionLifecycle(plugin.ProductionLifecycle)
if err := validator.ValidateGamePlugin(plugin); err != nil {
return domain.GamePlugin{}, err
}
@@ -621,8 +697,10 @@ func gamePluginFromManifestRegistration(registration domain.GamePluginManifestRe
Pages: manifest.Pages,
Tags: manifest.Tags,
AIPurposes: manifest.AI.Purposes,
ProductionLifecycle: manifest.ProductionLifecycle,
RemoteAccess: manifest.RemoteAccess,
RuntimeProfiles: manifest.RuntimeProfiles,
GameClientBridge: manifest.GameClientBridge,
Status: domain.GamePluginStatusInstalled,
}
}
@@ -714,6 +792,8 @@ func (svc *CoreService) ExecutePluginBridgeAction(sessionID string, request doma
base = svc.executeBridgeLogsBackfillRequest(base, plugin, instance, request.Payload)
case domain.PluginBridgeActionClientManager:
base = svc.executeBridgeClientManager(sessionID, base, request)
case domain.PluginBridgeActionPluginLifecycle:
base = svc.executeBridgePluginLifecycle(sessionID, base, request)
case domain.PluginBridgeActionArtifactsOpen:
base = svc.executeBridgeArtifactOpen(sessionID, base, request)
case domain.PluginBridgeActionAIInvoke:
@@ -775,7 +855,6 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
RouteKey: request.RouteKey,
ServerInstanceID: request.ServerInstanceID,
Purpose: request.AIPurpose,
ProviderID: request.Payload["providerId"],
Model: request.Payload["model"],
Prompt: defaultBridgeValue(request.Payload["prompt"], "Review the current server context and provide a safe recommendation."),
CurrentConfig: request.Payload["currentConfig"],
@@ -795,6 +874,9 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
if response.ConfigRecommendation != nil {
base.Result["suggestedConfig"] = response.ConfigRecommendation.SuggestedConfig
base.Result["diffSummary"] = response.ConfigRecommendation.DiffSummary
base.Result["diffId"] = response.ConfigRecommendation.DiffID
base.Result["key"] = response.ConfigRecommendation.Key
base.Result["expiresAt"] = response.ConfigRecommendation.ExpiresAt
}
if response.Error != nil {
base.Error = &domain.PluginBridgeSafeError{Code: response.Error.Code, Message: response.Error.Message, Details: response.Error.Details}
@@ -802,6 +884,22 @@ func (svc *CoreService) executeBridgeAIInvoke(sessionID string, base domain.Plug
return base
}
func (svc *CoreService) executeBridgePluginLifecycle(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
confirmed, err := strconv.ParseBool(defaultBridgeValue(request.Payload["confirmed"], "false"))
if err != nil {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "confirmed must be true or false"}
return base
}
result, err := svc.RunPluginLifecycleForSession(sessionID, domain.PluginLifecycleRequest{PluginID: request.PluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Payload["operation"]), TargetVersion: request.Payload["targetVersion"], IdempotencyKey: defaultBridgeValue(request.Payload["idempotencyKey"], request.RequestID), Confirmed: confirmed})
if err != nil {
return bridgeExecutionError(base, err)
}
base.Status = result.Status
base.Result = map[string]string{"installationId": result.Installation.ID, "currentState": string(result.Installation.CurrentState), "desiredState": string(result.Installation.DesiredState), "jobId": result.Job.ID, "alertId": result.Installation.AlertID, "auditEventId": result.Installation.AuditEventID, "admissionState": string(result.Decision.State), "admissionReason": result.Decision.Reason}
return base
}
func (svc *CoreService) executeBridgeJobDispatch(sessionID string, base domain.PluginBridgeExecuteResponse, plugin domain.GamePlugin, instance domain.ServerInstance, payload map[string]string) domain.PluginBridgeExecuteResponse {
capability := strings.TrimSpace(payload["capability"])
if capability == "" {
@@ -951,7 +1049,53 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
}
timeoutSeconds, _ := strconv.Atoi(payload["timeoutSeconds"])
maxAttempts, _ := strconv.Atoi(payload["maxAttempts"])
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: declarationKey, TargetKey: payload["targetKey"], Capability: capability, TimeoutSeconds: timeoutSeconds, MaxAttempts: maxAttempts, IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID)})
inputs := map[string]string{}
for key, value := range payload {
if strings.HasPrefix(key, "input.") {
inputs[strings.TrimPrefix(key, "input.")] = value
}
}
if capability == domain.JobCapabilityRemoteRunDBSQLiteQuery {
templateKey := strings.TrimSpace(inputs["templateKey"])
if templateKey == "" {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "input.templateKey is required for sqlite query requests"}
return base
}
template, reason := findBridgeQueryTemplate(plugin, base.RouteKey, templateKey)
if reason != "" {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "query_template_denied", Message: reason}
return base
}
if template.Engine != "sqlite" || template.TransportKey != declarationKey || template.TargetKey != payload["targetKey"] {
base.Status = "denied"
base.Error = &domain.PluginBridgeSafeError{Code: "query_template_denied", Message: "query template transport or target is not approved"}
return base
}
if timeoutSeconds == 0 {
timeoutSeconds = template.TimeoutSeconds
} else if timeoutSeconds > template.TimeoutSeconds {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "query template timeout limit exceeded"}
return base
}
maxRows := template.MaxRows
if requestedRows, ok := inputs["maxRows"]; ok && strings.TrimSpace(requestedRows) != "" {
parsedRows, parseErr := strconv.Atoi(requestedRows)
if parseErr != nil || parsedRows <= 0 {
base.Status = "error"
base.Error = &domain.PluginBridgeSafeError{Code: "validation", Message: "input.maxRows must be a positive integer"}
return base
}
if parsedRows < maxRows {
maxRows = parsedRows
}
}
inputs["templateKey"] = template.Key
inputs["maxRows"] = strconv.Itoa(maxRows)
}
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{ServerInstanceID: instance.ID, DeclarationKey: declarationKey, TargetKey: payload["targetKey"], Capability: capability, TimeoutSeconds: timeoutSeconds, MaxAttempts: maxAttempts, IdempotencyKey: defaultBridgeValue(payload["idempotencyKey"], base.RequestID), InputRef: payload["inputRef"], Inputs: inputs})
if err != nil {
return bridgeExecutionError(base, err)
}
@@ -967,6 +1111,46 @@ func (svc *CoreService) executeBridgeRemoteAccessRequest(sessionID string, base
return base
}
func findBridgeQueryTemplate(plugin domain.GamePlugin, routeKey string, templateKey string) (domain.GameClientBridgeQueryTemplateDeclaration, string) {
pageFound := false
pageAllowsTemplate := false
for _, page := range plugin.GameClientBridge.Pages {
if page.PageKey != routeKey {
continue
}
pageFound = true
if containsString(page.QueryTemplateKeys, templateKey) {
pageAllowsTemplate = true
}
}
if !pageFound || !pageAllowsTemplate {
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template is not declared by the bridge page"
}
var selected domain.GameClientBridgeQueryTemplateDeclaration
for _, template := range plugin.GameClientBridge.QueryTemplates {
if template.Key == templateKey {
selected = template
break
}
}
if selected.Key == "" {
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template is not declared by the plugin"
}
for _, page := range plugin.Pages {
if page.Key != routeKey {
continue
}
if !containsString(page.Permissions, selected.Permission) {
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template permission is not declared by the plugin page"
}
if !containsString(page.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest)) {
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template page does not declare remote access"
}
return selected, ""
}
return domain.GameClientBridgeQueryTemplateDeclaration{}, "query template plugin page is not declared"
}
func (svc *CoreService) executeBridgeRunDistribution(sessionID string, base domain.PluginBridgeExecuteResponse, request domain.PluginBridgeExecuteRequest) domain.PluginBridgeExecuteResponse {
distribution, err := svc.GenerateRunDistributionForSession(sessionID, domain.RunDistributionGenerateRequest{
ServerInstanceID: request.ServerInstanceID,
@@ -1278,14 +1462,29 @@ func marketplacePluginFromGamePlugin(plugin domain.GamePlugin) domain.PluginMark
Pages: plugin.Pages,
Tags: plugin.Tags,
AIPurposes: plugin.AIPurposes,
ProductionLifecycle: plugin.ProductionLifecycle,
RemoteAccess: plugin.RemoteAccess,
RuntimeProfiles: plugin.RuntimeProfiles,
GameClientBridge: plugin.GameClientBridge,
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: "platform-registry",
}
}
func normalizedProductionLifecycle(lifecycle domain.GamePluginProductionLifecycle) domain.GamePluginProductionLifecycle {
if len(lifecycle.Operations) == 0 {
lifecycle.Operations = []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}
}
if lifecycle.DependencyPolicy == "" {
lifecycle.DependencyPolicy = "optional"
}
if len(lifecycle.ApprovalRequired) == 0 {
lifecycle.ApprovalRequired = []string{"disable", "rollback", "retire"}
}
return lifecycle
}
func marketplacePluginMatchesKeyword(plugin domain.PluginMarketplacePlugin, keyword string) bool {
keyword = strings.ToLower(strings.TrimSpace(keyword))
if keyword == "" {