package service import ( "errors" "strings" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) type AIProviderClient interface { Invoke(provider domain.AIProvider, request domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error) } type MockAIProviderClient struct{} func (MockAIProviderClient) Invoke(provider domain.AIProvider, request domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error) { model := request.Model if model == "" { model = provider.DefaultModel } if model == "" && len(provider.Models) > 0 { model = provider.Models[0] } recommendation := "Mock AI recommendation for " + request.Purpose + ": review the proposed change before dispatch." result := domain.AIProviderInvocationResult{ Recommendation: recommendation, Usage: domain.AIInvocationUsage{ ProviderID: provider.ID, Model: model, InputTokens: boundedTokenEstimate(request.Prompt + request.CurrentConfig), OutputTokens: boundedTokenEstimate(recommendation), Mocked: true, }, } if request.Purpose == "config.suggest" || request.Purpose == "config.generate" { result.SuggestedConfig = buildSuggestedConfig(request.CurrentConfig, request.Prompt) } return result, nil } func (svc *CoreService) InvokeAIForSession(sessionID string, request domain.AIInvocationRequest) (domain.AIInvocationResponse, error) { request = domain.CopyAIInvocationRequest(request) if err := validator.ValidateAIInvocationRequest(request); err != nil { return domain.AIInvocationResponse{}, err } if _, err := svc.GetCurrentUser(sessionID); err != nil { return domain.AIInvocationResponse{}, err } if request.ServerInstanceID != "" { instance, err := svc.GetServerInstanceForSession(sessionID, request.ServerInstanceID) if err != nil { return domain.AIInvocationResponse{}, err } if request.PluginID != "" && instance.PluginID != request.PluginID { return safeAIDenial(request, "plugin scope does not match server instance"), nil } } if request.PluginID != "" { plugin, err := svc.store.GamePlugins().Get(request.PluginID) if err != nil { return domain.AIInvocationResponse{}, err } authorization, err := validator.AuthorizePluginBridgeAction(plugin, domain.PluginBridgeAuthorizeRequest{ PluginID: request.PluginID, RouteKey: request.RouteKey, ServerInstanceID: request.ServerInstanceID, Action: domain.PluginBridgeActionAIInvoke, AIPurpose: request.Purpose, }) if err != nil { return domain.AIInvocationResponse{}, err } if !authorization.Allowed { return safeAIDenial(request, authorization.Reason), nil } } provider, err := svc.selectAIProvider(request.ProviderID) if err != nil { return domain.AIInvocationResponse{}, err } result, err := svc.aiProviderClient.Invoke(provider, request) if err != nil { return domain.CopyAIInvocationResponse(domain.AIInvocationResponse{ RequestID: request.RequestID, Purpose: request.Purpose, ProviderID: provider.ID, Model: safeModel(request.Model, provider), Status: "error", Usage: domain.AIInvocationUsage{ProviderID: provider.ID, Model: safeModel(request.Model, provider), Mocked: true}, Error: &domain.AIInvocationSafeError{Code: "provider_failed", Message: safeBridgeReason(err.Error())}, }), nil } response := domain.AIInvocationResponse{ RequestID: request.RequestID, Purpose: request.Purpose, ProviderID: provider.ID, Model: result.Usage.Model, Status: "ok", Recommendation: result.Recommendation, Usage: result.Usage, } if result.SuggestedConfig != "" { response.ConfigRecommendation = &domain.AIConfigRecommendation{Key: "server.properties", SuggestedConfig: result.SuggestedConfig, DiffSummary: "review required before config write dispatch"} } if err := validator.ValidateAIInvocationResponse(response); err != nil { return domain.AIInvocationResponse{}, err } return domain.CopyAIInvocationResponse(response), nil } func (svc *CoreService) selectAIProvider(id string) (domain.AIProvider, error) { if id != "" { provider, err := svc.store.AIProviders().Get(id) if err != nil { return domain.AIProvider{}, err } if provider.Status != domain.AIProviderStatusActive { return domain.AIProvider{}, ErrForbidden } return provider, nil } providers, err := svc.store.AIProviders().List(domain.AIProviderFilter{Status: domain.AIProviderStatusActive}) if err != nil { return domain.AIProvider{}, err } if len(providers) == 0 { return domain.AIProvider{}, repo.ErrNotFound } return providers[0], nil } func safeAIDenial(request domain.AIInvocationRequest, reason string) domain.AIInvocationResponse { return domain.CopyAIInvocationResponse(domain.AIInvocationResponse{ RequestID: request.RequestID, Purpose: request.Purpose, Status: "denied", Error: &domain.AIInvocationSafeError{Code: "permission_denied", Message: safeBridgeReason(reason)}, }) } func safeModel(model string, provider domain.AIProvider) string { if strings.TrimSpace(model) != "" { return model } if provider.DefaultModel != "" { return provider.DefaultModel } if len(provider.Models) > 0 { return provider.Models[0] } return "mock-model" } func buildSuggestedConfig(currentConfig string, prompt string) string { base := strings.TrimRight(currentConfig, "\n") if base == "" { base = "# generated server config" } if strings.Contains(strings.ToLower(prompt), "pvp") && !strings.Contains(base, "pvp=") { base += "\npvp=false" } return base + "\n# ai.recommendation=review-required\n" } func boundedTokenEstimate(value string) int { count := len([]rune(value)) / 4 if count < 1 { return 1 } if count > 4096 { return 4096 } return count } type failingAIProviderClient struct{ err error } func (client failingAIProviderClient) Invoke(domain.AIProvider, domain.AIInvocationRequest) (domain.AIProviderInvocationResult, error) { if client.err == nil { return domain.AIProviderInvocationResult{}, errors.New("provider failed") } return domain.AIProviderInvocationResult{}, client.err }