package platform import ( "context" "encoding/json" "errors" "fmt" "infinite-canvas/backend/internal/kernel" "strings" "time" "infinite-canvas/backend/internal/model" "gorm.io/gorm" ) const runtimePolicySettingKey = "runtime_policy" const ( maxRuntimeUploadMB int64 = 999 maxRuntimeStorageGB int64 = 999 maxRuntimeDataMB int64 = 999_999 maxRuntimeCount int64 = 999_999_999 maxRuntimeRate = 999_999 maxRuntimeConcurrency = 999 maxRuntimeTimeoutMinutes = 9_999 ) type RuntimeResourcePolicy struct { ResourceUploadMB int64 `json:"resourceUploadMB"` GeneratedFileMB int64 `json:"generatedFileMB"` DailyUploadMB int64 `json:"dailyUploadMB"` StoredFileGB int64 `json:"storedFileGB"` StructuredDataMB int64 `json:"structuredDataMB"` TaskDataGB int64 `json:"taskDataGB"` AssetCount int64 `json:"assetCount"` CanvasCount int64 `json:"canvasCount"` TaskCount int64 `json:"taskCount"` APICallLogCount int64 `json:"apiCallLogCount"` RecycleBinRetentionDays int `json:"recycleBinRetentionDays"` } type RuntimeTaskPolicy struct { WorkerConcurrency int `json:"workerConcurrency"` ChannelConcurrency int `json:"channelConcurrency"` ActiveTaskLimit int `json:"activeTaskLimit"` ImageTimeoutMinutes int `json:"imageTimeoutMinutes"` TextTimeoutMinutes int `json:"textTimeoutMinutes"` AudioTimeoutMinutes int `json:"audioTimeoutMinutes"` VideoTimeoutMinutes int `json:"videoTimeoutMinutes"` StoryboardTimeoutMinutes int `json:"storyboardTimeoutMinutes"` DefaultTimeoutMinutes int `json:"defaultTimeoutMinutes"` } type RuntimeRequestPolicy struct { TaskCreatePerMinute int `json:"taskCreatePerMinute"` ResourceUploadPerMinute int `json:"resourceUploadPerMinute"` ResourceImportPerMinute int `json:"resourceImportPerMinute"` AssetWritePerMinute int `json:"assetWritePerMinute"` CanvasWritePerMinute int `json:"canvasWritePerMinute"` RegisterPerHour int `json:"registerPerHour"` EmailCodePerHour int `json:"emailCodePerHour"` LoginIPPerTenMinutes int `json:"loginIPPerTenMinutes"` LoginAccountPerTenMinutes int `json:"loginAccountPerTenMinutes"` SystemRelayPerMinute int `json:"systemRelayPerMinute"` CustomRelayPerMinute int `json:"customRelayPerMinute"` CustomRelayConcurrency int `json:"customRelayConcurrency"` CustomRelayRequestMB int64 `json:"customRelayRequestMB"` CustomRelayResponseMB int64 `json:"customRelayResponseMB"` CustomRelayTimeoutMinutes int `json:"customRelayTimeoutMinutes"` SystemRelayRequestMB int64 `json:"systemRelayRequestMB"` SystemRelayResponseMB int64 `json:"systemRelayResponseMB"` ChannelCircuitFailureCount int `json:"channelCircuitFailureCount"` ChannelCircuitOpenSeconds int `json:"channelCircuitOpenSeconds"` } type RuntimePolicySetting struct { Resource RuntimeResourcePolicy `json:"resource"` Task RuntimeTaskPolicy `json:"task"` Request RuntimeRequestPolicy `json:"request"` } type PublicRuntimePolicySetting struct { RuntimePolicySetting Configured bool `json:"configured"` UpdatedBy string `json:"updatedBy"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type PublicRuntimeLimits struct { ActiveTaskLimit int `json:"activeTaskLimit"` ResourceUploadMB int64 `json:"resourceUploadMB"` RecycleBinRetentionDays int `json:"recycleBinRetentionDays"` } // DefaultRuntimePolicy returns the effective baseline policy, including // deployment-level environment overrides. It is exported so composition-root // compatibility code and domain tests do not need to reach into platform's // private implementation. func DefaultRuntimePolicy() RuntimePolicySetting { return RuntimePolicySetting{ Resource: RuntimeResourcePolicy{ ResourceUploadMB: 50, GeneratedFileMB: 64, DailyUploadMB: 2048, StoredFileGB: 20, StructuredDataMB: 256, TaskDataGB: 1, AssetCount: 2_000, CanvasCount: 1_000, TaskCount: 20_000, APICallLogCount: 100_000, RecycleBinRetentionDays: 30, }, Task: RuntimeTaskPolicy{ WorkerConcurrency: effectiveChannelConcurrencyLimit(envInt("CANVAS_WORKER_CONCURRENCY", TaskWorkerConcurrency)), ChannelConcurrency: defaultChannelConcurrencyLimit(), ActiveTaskLimit: 5, ImageTimeoutMinutes: 8, TextTimeoutMinutes: 8, AudioTimeoutMinutes: 8, VideoTimeoutMinutes: 60, StoryboardTimeoutMinutes: 20, DefaultTimeoutMinutes: 10, }, Request: RuntimeRequestPolicy{ TaskCreatePerMinute: 30, ResourceUploadPerMinute: 30, ResourceImportPerMinute: 30, AssetWritePerMinute: 120, CanvasWritePerMinute: 120, RegisterPerHour: 30, EmailCodePerHour: 60, LoginIPPerTenMinutes: 50, LoginAccountPerTenMinutes: 10, SystemRelayPerMinute: 120, CustomRelayPerMinute: 120, CustomRelayConcurrency: 4, CustomRelayRequestMB: 32, CustomRelayResponseMB: 32, CustomRelayTimeoutMinutes: 10, SystemRelayRequestMB: 64, SystemRelayResponseMB: 128, ChannelCircuitFailureCount: min(envInt("CANVAS_CHANNEL_CIRCUIT_FAILURES", 5), maxRuntimeConcurrency), ChannelCircuitOpenSeconds: min(envInt("CANVAS_CHANNEL_CIRCUIT_SECONDS", 60), 86_400), }, } } func selfUseRuntimePolicy() RuntimePolicySetting { value := DefaultRuntimePolicy() value.Resource = RuntimeResourcePolicy{ ResourceUploadMB: maxRuntimeUploadMB, GeneratedFileMB: maxRuntimeUploadMB, DailyUploadMB: maxRuntimeDataMB, StoredFileGB: maxRuntimeStorageGB, StructuredDataMB: maxRuntimeDataMB, TaskDataGB: maxRuntimeStorageGB, AssetCount: maxRuntimeCount, CanvasCount: maxRuntimeCount, TaskCount: maxRuntimeCount, APICallLogCount: maxRuntimeCount, RecycleBinRetentionDays: 0, } value.Task = RuntimeTaskPolicy{ WorkerConcurrency: maxRuntimeConcurrency, ChannelConcurrency: maxRuntimeConcurrency, ActiveTaskLimit: maxRuntimeConcurrency, ImageTimeoutMinutes: maxRuntimeTimeoutMinutes, TextTimeoutMinutes: maxRuntimeTimeoutMinutes, AudioTimeoutMinutes: maxRuntimeTimeoutMinutes, VideoTimeoutMinutes: maxRuntimeTimeoutMinutes, StoryboardTimeoutMinutes: maxRuntimeTimeoutMinutes, DefaultTimeoutMinutes: maxRuntimeTimeoutMinutes, } value.Request = RuntimeRequestPolicy{ TaskCreatePerMinute: maxRuntimeRate, ResourceUploadPerMinute: maxRuntimeRate, ResourceImportPerMinute: maxRuntimeRate, AssetWritePerMinute: maxRuntimeRate, CanvasWritePerMinute: maxRuntimeRate, RegisterPerHour: maxRuntimeRate, EmailCodePerHour: maxRuntimeRate, LoginIPPerTenMinutes: maxRuntimeRate, LoginAccountPerTenMinutes: maxRuntimeRate, SystemRelayPerMinute: maxRuntimeRate, CustomRelayPerMinute: maxRuntimeRate, CustomRelayConcurrency: maxRuntimeConcurrency, CustomRelayRequestMB: maxRuntimeUploadMB, CustomRelayResponseMB: maxRuntimeUploadMB, CustomRelayTimeoutMinutes: maxRuntimeTimeoutMinutes, SystemRelayRequestMB: maxRuntimeUploadMB, SystemRelayResponseMB: maxRuntimeUploadMB, ChannelCircuitFailureCount: maxRuntimeConcurrency, ChannelCircuitOpenSeconds: 1, } return value } func SelfUseRuntimePolicy() RuntimePolicySetting { return selfUseRuntimePolicy() } func (s *Service) RuntimePolicy() (RuntimePolicySetting, error) { _, value, err := s.readRuntimePolicy() return value, err } func (s *Service) RuntimeConcurrencySetting() (RuntimeTaskPolicy, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() return s.concurrencyCache.Get(ctx, runtimePolicySettingKey, func(ctx context.Context) (RuntimeTaskPolicy, int, error) { reader := &Service{repo: s.repo.WithContext(ctx), host: s.host, Coordinator: s.Coordinator, concurrencyCache: s.concurrencyCache} policy, err := reader.RuntimePolicy() return policy.Task, 256, err }) } func (s *Service) PublicRuntimeLimits() (*PublicRuntimeLimits, error) { policy, err := s.RuntimePolicy() if err != nil { return nil, err } return &PublicRuntimeLimits{ ActiveTaskLimit: policy.Task.ActiveTaskLimit, ResourceUploadMB: policy.Resource.ResourceUploadMB, RecycleBinRetentionDays: policy.Resource.RecycleBinRetentionDays, }, nil } func (s *Service) AdminRuntimePolicySetting(actor *model.User) (*PublicRuntimePolicySetting, error) { if err := s.requireAdmin(actor); err != nil { return nil, err } setting, value, err := s.readRuntimePolicy() if err != nil { return nil, err } return publicRuntimePolicy(setting, value), nil } func (s *Service) AdminSelfUseRuntimePolicy(actor *model.User) (*PublicRuntimePolicySetting, error) { if err := s.requireAdmin(actor); err != nil { return nil, err } return publicRuntimePolicy(nil, selfUseRuntimePolicy()), nil } func (s *Service) UpdateRuntimePolicySetting(actor *model.User, value RuntimePolicySetting) (*PublicRuntimePolicySetting, error) { if err := s.requireAdmin(actor); err != nil { return nil, err } if err := validateRuntimePolicy(value); err != nil { return nil, err } current, before, err := s.readRuntimePolicy() if err != nil { return nil, err } encoded, err := json.Marshal(value) if err != nil { return nil, err } setting := model.SystemSetting{Key: runtimePolicySettingKey, ValueJSON: string(encoded), UpdatedBy: actor.ID} if current != nil { setting.CreatedAt = current.CreatedAt } if err := s.repo.SaveSystemSetting(&setting); err != nil { return nil, err } s.concurrencyCache.Clear() if err := s.appendAdminAudit(actor, "runtime_policy.update", "system_setting", runtimePolicySettingKey, "更新资源与请求策略", map[string]any{"before": before, "after": value}); err != nil { return nil, err } return publicRuntimePolicy(&setting, value), nil } func (s *Service) ResetRuntimePolicySetting(actor *model.User) (*PublicRuntimePolicySetting, error) { if err := s.requireAdmin(actor); err != nil { return nil, err } _, before, err := s.readRuntimePolicy() if err != nil { return nil, err } if err := s.repo.DeleteSystemSetting(runtimePolicySettingKey); err != nil { return nil, err } s.concurrencyCache.Clear() after := DefaultRuntimePolicy() if err := s.appendAdminAudit(actor, "runtime_policy.reset", "system_setting", runtimePolicySettingKey, "重置资源与请求策略", map[string]any{"before": before, "after": after}); err != nil { return nil, err } return publicRuntimePolicy(nil, after), nil } func (s *Service) readRuntimePolicy() (*model.SystemSetting, RuntimePolicySetting, error) { setting, err := s.repo.SystemSetting(runtimePolicySettingKey) if errors.Is(err, gorm.ErrRecordNotFound) { value := DefaultRuntimePolicy() return nil, value, validateRuntimePolicy(value) } if err != nil { return nil, RuntimePolicySetting{}, err } value := DefaultRuntimePolicy() if strings.TrimSpace(setting.ValueJSON) == "" || json.Unmarshal([]byte(setting.ValueJSON), &value) != nil { return nil, RuntimePolicySetting{}, errors.New("资源与请求策略配置格式无效") } if err := validateRuntimePolicy(value); err != nil { return nil, RuntimePolicySetting{}, err } return setting, value, nil } func validateRuntimePolicy(value RuntimePolicySetting) error { resource := value.Resource for label, item := range map[string]int64{ "普通资源单文件": resource.ResourceUploadMB, "单个生成资源": resource.GeneratedFileMB, } { if item < 1 || item > maxRuntimeUploadMB { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d MB 的整数", label, maxRuntimeUploadMB)) } } if resource.DailyUploadMB < 1 || resource.DailyUploadMB > maxRuntimeDataMB { return kernel.BadAuthRequest(fmt.Sprintf("每日上传量必须是 1-%d MB 的整数", maxRuntimeDataMB)) } if resource.StoredFileGB < 1 || resource.StoredFileGB > maxRuntimeStorageGB || resource.TaskDataGB < 1 || resource.TaskDataGB > maxRuntimeStorageGB { return kernel.BadAuthRequest(fmt.Sprintf("账号文件与任务数据容量必须是 1-%d GB 的整数", maxRuntimeStorageGB)) } if resource.StructuredDataMB < 1 || resource.StructuredDataMB > maxRuntimeDataMB { return kernel.BadAuthRequest(fmt.Sprintf("结构化数据容量必须是 1-%d MB 的整数", maxRuntimeDataMB)) } storedMB := resource.StoredFileGB * 1024 if resource.ResourceUploadMB > storedMB || resource.GeneratedFileMB > storedMB { return kernel.BadAuthRequest("单文件上限不能大于账号文件总容量") } for label, item := range map[string]int64{ "素材数量": resource.AssetCount, "画布数量": resource.CanvasCount, "任务历史数量": resource.TaskCount, "请求日志数量": resource.APICallLogCount, } { if item < 1 || item > maxRuntimeCount { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d 的整数", label, maxRuntimeCount)) } } if resource.RecycleBinRetentionDays < 0 || resource.RecycleBinRetentionDays > 365 { return kernel.BadAuthRequest("回收站保留天数必须是 0-365 的整数 (0 表示不自动清理)") } task := value.Task for label, item := range map[string]int{ "Worker 并发数": task.WorkerConcurrency, "全局渠道并发数": task.ChannelConcurrency, "活动任务上限": task.ActiveTaskLimit, } { if item < 1 || item > maxRuntimeConcurrency { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d 的整数", label, maxRuntimeConcurrency)) } } for label, item := range map[string]int{ "图片任务超时": task.ImageTimeoutMinutes, "文本任务超时": task.TextTimeoutMinutes, "音频任务超时": task.AudioTimeoutMinutes, "视频任务超时": task.VideoTimeoutMinutes, "分镜任务超时": task.StoryboardTimeoutMinutes, "默认任务超时": task.DefaultTimeoutMinutes, } { if item < 1 || item > maxRuntimeTimeoutMinutes { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d 分钟的整数", label, maxRuntimeTimeoutMinutes)) } } request := value.Request for label, item := range map[string]int{ "任务创建频控": request.TaskCreatePerMinute, "资源上传频控": request.ResourceUploadPerMinute, "资源导入频控": request.ResourceImportPerMinute, "素材写入频控": request.AssetWritePerMinute, "画布写入频控": request.CanvasWritePerMinute, "注册频控": request.RegisterPerHour, "验证码频控": request.EmailCodePerHour, "登录 IP 频控": request.LoginIPPerTenMinutes, "登录账号频控": request.LoginAccountPerTenMinutes, "系统渠道频控": request.SystemRelayPerMinute, "自定义渠道频控": request.CustomRelayPerMinute, } { if item < 1 || item > maxRuntimeRate { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d 的整数", label, maxRuntimeRate)) } } if request.CustomRelayConcurrency < 1 || request.CustomRelayConcurrency > maxRuntimeConcurrency { return kernel.BadAuthRequest(fmt.Sprintf("自定义渠道并发必须是 1-%d 的整数", maxRuntimeConcurrency)) } for label, item := range map[string]int64{ "自定义渠道请求体": request.CustomRelayRequestMB, "自定义渠道响应体": request.CustomRelayResponseMB, "系统渠道请求体": request.SystemRelayRequestMB, "系统渠道响应体": request.SystemRelayResponseMB, } { if item < 1 || item > maxRuntimeUploadMB { return kernel.BadAuthRequest(fmt.Sprintf("%s必须是 1-%d MB 的整数", label, maxRuntimeUploadMB)) } } if request.CustomRelayTimeoutMinutes < 1 || request.CustomRelayTimeoutMinutes > maxRuntimeTimeoutMinutes { return kernel.BadAuthRequest(fmt.Sprintf("自定义渠道超时必须是 1-%d 分钟的整数", maxRuntimeTimeoutMinutes)) } if request.ChannelCircuitFailureCount < 1 || request.ChannelCircuitFailureCount > maxRuntimeConcurrency { return kernel.BadAuthRequest(fmt.Sprintf("渠道熔断失败次数必须是 1-%d 的整数", maxRuntimeConcurrency)) } if request.ChannelCircuitOpenSeconds < 1 || request.ChannelCircuitOpenSeconds > 86_400 { return kernel.BadAuthRequest("渠道熔断时长必须是 1-86400 秒的整数") } return nil } func ValidateRuntimePolicy(value RuntimePolicySetting) error { return validateRuntimePolicy(value) } func publicRuntimePolicy(setting *model.SystemSetting, value RuntimePolicySetting) *PublicRuntimePolicySetting { result := &PublicRuntimePolicySetting{RuntimePolicySetting: value, Configured: setting != nil} if setting != nil { result.UpdatedBy = setting.UpdatedBy result.CreatedAt = setting.CreatedAt result.UpdatedAt = setting.UpdatedAt } return result } func megabytes(value int64) int64 { return kernel.Megabytes(value) } func gigabytes(value int64) int64 { return kernel.Gigabytes(value) }