package httpapi import ( "encoding/json" "errors" "fmt" "net/http" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) // listRuntimePolicySets godoc // @Summary 列出运行策略集 // @Description 管理端返回可分配给平台、模型或用户组的运行策略集。 // @Tags runtime // @Produce json // @Security BearerAuth // @Success 200 {object} RuntimePolicySetListResponse // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/policy-sets [get] func (s *Server) listRuntimePolicySets(w http.ResponseWriter, r *http.Request) { items, err := s.store.ListRuntimePolicySets(r.Context()) if err != nil { s.logger.Error("list runtime policy sets failed", "error", err) writeError(w, http.StatusInternalServerError, "list runtime policy sets failed") return } writeJSON(w, http.StatusOK, map[string]any{"items": items}) } // getRunnerPolicy godoc // @Summary 获取 Runner 策略 // @Description 管理端获取当前生效的默认 Runner 调度策略。 // @Tags runtime // @Produce json // @Security BearerAuth // @Success 200 {object} store.RunnerPolicy // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/runner-policy [get] func (s *Server) getRunnerPolicy(w http.ResponseWriter, r *http.Request) { item, err := s.store.GetActiveRunnerPolicy(r.Context()) if err != nil { s.logger.Error("get runner policy failed", "error", err) writeError(w, http.StatusInternalServerError, "get runner policy failed") return } writeJSON(w, http.StatusOK, item) } // updateRunnerPolicy godoc // @Summary 更新 Runner 策略 // @Description 管理端写入默认 Runner 调度策略。 // @Tags runtime // @Accept json // @Produce json // @Security BearerAuth // @Param input body store.RunnerPolicyInput true "Runner 策略请求" // @Success 200 {object} store.RunnerPolicy // @Failure 400 {object} ErrorEnvelope // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/runner-policy [patch] func (s *Server) updateRunnerPolicy(w http.ResponseWriter, r *http.Request) { var input store.RunnerPolicyInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid json body") return } if err := validateRunnerPolicyInput(input); err != nil { writeError(w, http.StatusBadRequest, err.Error(), "invalid_parameter") return } item, err := s.store.UpsertDefaultRunnerPolicy(r.Context(), input) if err != nil { s.logger.Error("update runner policy failed", "error", err) writeError(w, http.StatusInternalServerError, "update runner policy failed") return } writeJSON(w, http.StatusOK, item) } func validateRunnerPolicyInput(input store.RunnerPolicyInput) error { rawRules, exists := input.FailoverPolicy["actionRules"] if !exists { return nil } rules, ok := rawRules.([]any) if !ok { return errors.New("failoverPolicy.actionRules must be an array") } validActions := map[string]bool{ "stop": true, "next": true, "cooldown_and_next": true, "demote_and_next": true, "disable_and_next": true, } for index, rawRule := range rules { rule, ok := rawRule.(map[string]any) if !ok { return fmt.Errorf("failoverPolicy.actionRules[%d] must be an object", index) } action := strings.TrimSpace(stringValue(rule["action"])) if !validActions[action] { return fmt.Errorf("failoverPolicy.actionRules[%d].action is invalid", index) } if target, present := rule["target"]; present { targetValue := strings.TrimSpace(stringValue(target)) if targetValue != "model" && targetValue != "platform" { return fmt.Errorf("failoverPolicy.actionRules[%d].target is invalid", index) } } if !runnerActionRuleHasMatch(rule) { return fmt.Errorf("failoverPolicy.actionRules[%d] must include at least one match condition", index) } if action == "cooldown_and_next" && positiveJSONInteger(rule["cooldownSeconds"]) <= 0 { return fmt.Errorf("failoverPolicy.actionRules[%d].cooldownSeconds must be a positive integer", index) } if action == "demote_and_next" && positiveJSONInteger(rule["demoteSteps"]) <= 0 { return fmt.Errorf("failoverPolicy.actionRules[%d].demoteSteps must be a positive integer", index) } } return nil } func runnerActionRuleHasMatch(rule map[string]any) bool { for _, key := range []string{"categories", "codes", "errorCodes", "statusCodes", "keywords"} { switch values := rule[key].(type) { case []any: for _, value := range values { if strings.TrimSpace(fmt.Sprint(value)) != "" { return true } } case []string: for _, value := range values { if strings.TrimSpace(value) != "" { return true } } } } return false } func positiveJSONInteger(value any) int { switch typed := value.(type) { case int: if typed > 0 { return typed } case int64: if typed > 0 { return int(typed) } case float64: if typed > 0 && typed == float64(int(typed)) { return int(typed) } case json.Number: parsed, err := typed.Int64() if err == nil && parsed > 0 { return int(parsed) } } return 0 } type updatePlatformDynamicPriorityRequest struct { DynamicPriority *int `json:"dynamicPriority" example:"10"` Reset bool `json:"reset" example:"false"` } // updatePlatformDynamicPriority godoc // @Summary 更新平台动态优先级 // @Description 管理端调整平台运行时动态优先级;reset 为 true 时清空动态值。 // @Tags runtime // @Accept json // @Produce json // @Security BearerAuth // @Param platformID path string true "平台 ID" // @Param input body updatePlatformDynamicPriorityRequest true "动态优先级请求" // @Success 200 {object} store.Platform // @Failure 400 {object} ErrorEnvelope // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 404 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/platforms/{platformID}/dynamic-priority [patch] func (s *Server) updatePlatformDynamicPriority(w http.ResponseWriter, r *http.Request) { var input updatePlatformDynamicPriorityRequest if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid json body") return } var dynamicPriority *int if input.Reset { dynamicPriority = nil } else { if input.DynamicPriority == nil { writeError(w, http.StatusBadRequest, "dynamicPriority is required unless reset is true") return } if *input.DynamicPriority < 0 { writeError(w, http.StatusBadRequest, "dynamicPriority must be greater than or equal to 0") return } dynamicPriority = input.DynamicPriority } item, err := s.store.UpdatePlatformDynamicPriority(r.Context(), r.PathValue("platformID"), dynamicPriority) if err != nil { if store.IsNotFound(err) { writeError(w, http.StatusNotFound, "platform not found") return } s.logger.Error("update platform dynamic priority failed", "error", err) writeError(w, http.StatusInternalServerError, "update platform dynamic priority failed") return } writeJSON(w, http.StatusOK, item) } // restorePlatformModelRuntimeStatus godoc // @Summary 恢复平台模型运行状态 // @Description 管理端手动解除平台模型停用、模型冷却、平台冷却或平台禁用状态,使其重新参与路由。 // @Tags runtime // @Produce json // @Security BearerAuth // @Param platformModelID path string true "平台模型 ID" // @Success 200 {object} store.ModelRateLimitStatus // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 404 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/model-rate-limits/{platformModelID}/restore [post] func (s *Server) restorePlatformModelRuntimeStatus(w http.ResponseWriter, r *http.Request) { item, err := s.store.RestorePlatformModelRuntimeStatus(r.Context(), r.PathValue("platformModelID")) if err != nil { if store.IsNotFound(err) { writeError(w, http.StatusNotFound, "platform model not found") return } s.logger.Error("restore platform model runtime status failed", "error", err) writeError(w, http.StatusInternalServerError, "restore platform model runtime status failed") return } writeJSON(w, http.StatusOK, item) } // createRuntimePolicySet godoc // @Summary 创建运行策略集 // @Description 管理端创建运行策略集,policyKey 和 name 必填。 // @Tags runtime // @Accept json // @Produce json // @Security BearerAuth // @Param input body store.RuntimePolicySetInput true "运行策略集请求" // @Success 201 {object} store.RuntimePolicySet // @Failure 400 {object} ErrorEnvelope // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 409 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/policy-sets [post] func (s *Server) createRuntimePolicySet(w http.ResponseWriter, r *http.Request) { var input store.RuntimePolicySetInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid json body") return } if !validRuntimePolicyInput(input) { writeError(w, http.StatusBadRequest, "policyKey and name are required") return } item, err := s.store.CreateRuntimePolicySet(r.Context(), input) if err != nil { if store.IsUniqueViolation(err) { writeError(w, http.StatusConflict, "runtime policy key already exists") return } s.logger.Error("create runtime policy set failed", "error", err) writeError(w, http.StatusInternalServerError, "create runtime policy set failed") return } writeJSON(w, http.StatusCreated, item) } // updateRuntimePolicySet godoc // @Summary 更新运行策略集 // @Description 管理端更新运行策略集及其限流、重试、超时等策略配置。 // @Tags runtime // @Accept json // @Produce json // @Security BearerAuth // @Param policySetID path string true "运行策略集 ID" // @Param input body store.RuntimePolicySetInput true "运行策略集请求" // @Success 200 {object} store.RuntimePolicySet // @Failure 400 {object} ErrorEnvelope // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 404 {object} ErrorEnvelope // @Failure 409 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/policy-sets/{policySetID} [patch] func (s *Server) updateRuntimePolicySet(w http.ResponseWriter, r *http.Request) { var input store.RuntimePolicySetInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeError(w, http.StatusBadRequest, "invalid json body") return } if !validRuntimePolicyInput(input) { writeError(w, http.StatusBadRequest, "policyKey and name are required") return } item, err := s.store.UpdateRuntimePolicySet(r.Context(), r.PathValue("policySetID"), input) if err != nil { if store.IsNotFound(err) { writeError(w, http.StatusNotFound, "runtime policy set not found") return } if store.IsUniqueViolation(err) { writeError(w, http.StatusConflict, "runtime policy key already exists") return } s.logger.Error("update runtime policy set failed", "error", err) writeError(w, http.StatusInternalServerError, "update runtime policy set failed") return } writeJSON(w, http.StatusOK, item) } // deleteRuntimePolicySet godoc // @Summary 删除运行策略集 // @Description 管理端删除非默认运行策略集;默认策略集受保护。 // @Tags runtime // @Produce json // @Security BearerAuth // @Param policySetID path string true "运行策略集 ID" // @Success 204 "No Content" // @Failure 401 {object} ErrorEnvelope // @Failure 403 {object} ErrorEnvelope // @Failure 404 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/admin/runtime/policy-sets/{policySetID} [delete] func (s *Server) deleteRuntimePolicySet(w http.ResponseWriter, r *http.Request) { if err := s.store.DeleteRuntimePolicySet(r.Context(), r.PathValue("policySetID")); err != nil { if store.IsNotFound(err) { writeError(w, http.StatusNotFound, "runtime policy set not found") return } if errors.Is(err, store.ErrProtectedDefault) { writeError(w, http.StatusForbidden, "default runtime policy set cannot be deleted") return } s.logger.Error("delete runtime policy set failed", "error", err) writeError(w, http.StatusInternalServerError, "delete runtime policy set failed") return } w.WriteHeader(http.StatusNoContent) } func validRuntimePolicyInput(input store.RuntimePolicySetInput) bool { return strings.TrimSpace(input.PolicyKey) != "" && strings.TrimSpace(input.Name) != "" }