package service import ( "encoding/json" "errors" "math" "regexp" "sort" "strings" "time" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/repository" "gorm.io/gorm" ) var logicalModelCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{1,79}$`) type LogicalModelRequest struct { Code string `json:"code"` Name string `json:"name"` Icon string `json:"icon"` Description string `json:"description"` Capability string `json:"capability"` Enabled bool `json:"enabled"` SortOrder int `json:"sortOrder"` PricePolicy string `json:"pricePolicy"` BillingMode string `json:"billingMode"` UnitPriceMicrocredits int64 `json:"unitPriceMicrocredits"` InputPriceMicrocredits int64 `json:"inputPriceMicrocredits"` OutputPriceMicrocredits int64 `json:"outputPriceMicrocredits"` CachedPriceMicrocredits int64 `json:"cachedPriceMicrocredits"` CapabilitySpec CapabilitySpec `json:"capabilitySpec"` DefaultOptions map[string]any `json:"defaultOptions"` Routes []LogicalRouteRequest `json:"routes"` } type LogicalRouteRequest struct { PhysicalVariantID string `json:"physicalVariantId"` Enabled bool `json:"enabled"` Priority int `json:"priority"` Weight int `json:"weight"` } type PhysicalVariantRequest struct { ChannelModelID string `json:"channelModelId"` Name string `json:"name"` Enabled bool `json:"enabled"` CapabilitySpec CapabilitySpec `json:"capabilitySpec"` } type PublicLogicalModel struct { ID string `json:"id"` Code string `json:"code"` Name string `json:"name"` Icon string `json:"icon"` Description string `json:"description"` Capability string `json:"capability"` SortOrder int `json:"sortOrder"` PricePolicy string `json:"pricePolicy"` BillingMode string `json:"billingMode"` UnitPriceMicrocredits int64 `json:"unitPriceMicrocredits"` InputPriceMicrocredits int64 `json:"inputPriceMicrocredits"` OutputPriceMicrocredits int64 `json:"outputPriceMicrocredits"` CachedPriceMicrocredits int64 `json:"cachedPriceMicrocredits"` CapabilitySpec CapabilitySpec `json:"capabilitySpec"` // CapabilityProfiles 是创作端可见的匿名能力组合,不暴露其背后的供应线路关系。 CapabilityProfiles []CapabilitySpec `json:"capabilityProfiles"` DefaultOptions map[string]any `json:"defaultOptions"` Available bool `json:"available"` } type AdminLogicalRoute struct { ID string `json:"id"` PhysicalVariantID string `json:"physicalVariantId"` PhysicalVariantName string `json:"physicalVariantName"` ChannelModelID string `json:"channelModelId"` ChannelID string `json:"channelId"` PhysicalModelKey string `json:"physicalModelKey"` PhysicalModelName string `json:"physicalModelName"` Enabled bool `json:"enabled"` Priority int `json:"priority"` Weight int `json:"weight"` Available bool `json:"available"` structurallyAvailable bool CapabilitySpec CapabilitySpec `json:"capabilitySpec"` } type AdminLogicalModel struct { PublicLogicalModel Enabled bool `json:"enabled"` ActiveRevisionID string `json:"activeRevisionId"` RevisionVersion int `json:"revisionVersion"` ConfigurationError string `json:"configurationError,omitempty"` AvailabilityError string `json:"availabilityError,omitempty"` Routes []AdminLogicalRoute `json:"routes"` } type AdminPhysicalVariant struct { ID string `json:"id"` ChannelModelID string `json:"channelModelId"` ChannelID string `json:"channelId"` ModelKey string `json:"modelKey"` ModelName string `json:"modelName"` Name string `json:"name"` Capability string `json:"capability"` Protocol string `json:"protocol"` Enabled bool `json:"enabled"` ModelEnabled bool `json:"modelEnabled"` UsageCount int64 `json:"usageCount"` CapabilitySpec CapabilitySpec `json:"capabilitySpec"` } type RouteSimulationCandidate struct { RouteID string `json:"routeId"` VariantID string `json:"variantId"` ChannelModelID string `json:"channelModelId"` Priority int `json:"priority"` Weight int `json:"weight"` Enabled bool `json:"enabled"` Matched bool `json:"matched"` Blocked bool `json:"blocked"` InPool bool `json:"inPool"` Reasons []string `json:"reasons,omitempty"` } type RouteSimulationResult struct { ProductMatch CapabilityMatch `json:"productMatch"` Candidates []RouteSimulationCandidate `json:"candidates"` } func (s *Service) PublicLogicalModels(intent *ModelRequestIntent) ([]PublicLogicalModel, error) { snapshot, err := s.routeCatalogSnapshot() if err != nil { return nil, err } result := make([]PublicLogicalModel, 0, len(snapshot.Ordered)) for _, id := range snapshot.Ordered { cached := snapshot.Models[id] structuralSpecs := availableCachedRouteSpecs(cached.Routes) coverageValid := logicalModelCapabilityCovered(cached.ProductSpec, structuralSpecs) available := coverageValid && hasHealthyCachedRoute(s, cached.Routes) if intent != nil { resolvedIntent := *intent resolvedIntent.Options = mergeIntentDefaults(intent.Options, cached.Defaults) productMatch := MatchCapability(cached.ProductSpec, resolvedIntent) if !productMatch.Matched { continue } available = false if coverageValid { for _, route := range cached.Routes { if route.Route.Enabled && route.Route.Weight > 0 && !s.logicalRouteBlocked(route) && MatchCapability(route.VariantSpec, resolvedIntent).Matched { available = true break } } } } result = append(result, publicLogicalModel(cached, available)) } return result, nil } func publicLogicalModel(cached cachedLogicalModel, available bool) PublicLogicalModel { item := cached.Model routeSpecs := make([]CapabilitySpec, 0, len(cached.Routes)) for _, route := range cached.Routes { routeSpecs = append(routeSpecs, route.VariantSpec) } productSpec := capabilitySpecWithRoutePresets(cached.ProductSpec, routeSpecs) variants := make([]CapabilitySpec, 0, len(cached.Routes)) seen := make(map[string]bool, len(cached.Routes)) for _, route := range cached.Routes { if !route.Route.Enabled || route.Route.Weight <= 0 { continue } key := capabilityFingerprint(route.VariantSpec) if !seen[key] { seen[key] = true variants = append(variants, route.VariantSpec) } } return PublicLogicalModel{ID: item.ID, Code: item.Code, Name: item.Name, Icon: item.Icon, Description: item.Description, Capability: item.Capability, SortOrder: item.SortOrder, PricePolicy: item.PricePolicy, BillingMode: item.BillingMode, UnitPriceMicrocredits: item.UnitPriceMicrocredits, InputPriceMicrocredits: item.InputPriceMicrocredits, OutputPriceMicrocredits: item.OutputPriceMicrocredits, CachedPriceMicrocredits: item.CachedPriceMicrocredits, CapabilitySpec: productSpec, CapabilityProfiles: variants, DefaultOptions: cached.Defaults, Available: available} } // capabilitySpecWithRoutePresets repairs old front-model snapshots that stored // only `*` for a custom size. The wildcard remains for matching custom values, // while route presets are restored for admin and creator-side selectors. func capabilitySpecWithRoutePresets(spec CapabilitySpec, routes []CapabilitySpec) CapabilitySpec { result := spec result.Options = make(map[string]OptionConstraint, len(spec.Options)) for name, constraint := range spec.Options { if !isWildcardOptionConstraint(constraint) { result.Options[name] = constraint continue } values := append([]any(nil), constraint.Values...) seen := make(map[string]bool, len(values)) for _, value := range values { seen[normalizedScalar(value)] = true } for _, route := range routes { for _, value := range route.Options[name].Values { key := normalizedScalar(value) if key != "" && !seen[key] { seen[key] = true values = append(values, value) } } } result.Options[name] = OptionConstraint{Values: values} } return result } // capabilityFingerprint 用规范化后的结构去重能力画像;不能直接依赖原始 JSON, // 因为同一组枚举能力的数组顺序不应造成重复展示。 func capabilityFingerprint(spec CapabilitySpec) string { copySpec := spec copySpec.Operations = append([]string(nil), spec.Operations...) sort.Strings(copySpec.Operations) copySpec.Inputs = make(map[string]InputConstraint, len(spec.Inputs)) for name, constraint := range spec.Inputs { copySpec.Inputs[name] = constraint } copySpec.Options = make(map[string]OptionConstraint, len(spec.Options)) for name, constraint := range spec.Options { values := append([]any(nil), constraint.Values...) sort.SliceStable(values, func(i, j int) bool { return normalizedScalar(values[i]) < normalizedScalar(values[j]) }) constraint.Values = values copySpec.Options[name] = constraint } encoded, _ := json.Marshal(copySpec) return string(encoded) } func (s *Service) AdminLogicalModels(actor *model.User) ([]AdminLogicalModel, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } items, err := s.repo.LogicalModels(true) if err != nil { return nil, err } graphs, err := s.repo.LogicalModelGraphs(items, true) if err != nil { return nil, err } systemChannelIDs := make([]string, 0) for _, graph := range graphs { if graph == nil { continue } for _, channelModel := range graph.ChannelModels { systemChannelIDs = append(systemChannelIDs, channelModel.ChannelID) } } systemChannels, err := s.repo.SystemChannelsByIDs(systemChannelIDs, true) if err != nil { return nil, err } systemChannelByID := make(map[string]model.ModelChannel, len(systemChannels)) for _, channel := range systemChannels { systemChannelByID[channel.ID] = channel } result := make([]AdminLogicalModel, 0, len(items)) for _, item := range items { graph := graphs[item.ID] if graph == nil || graph.Revision == nil { continue } admin, buildErr := s.buildAdminLogicalModel(item, graph, systemChannelByID) if buildErr != nil { continue } result = append(result, *admin) } return result, nil } func (s *Service) buildAdminLogicalModel(item model.LogicalModel, graph *repository.LogicalModelGraph, systemChannelByID map[string]model.ModelChannel) (*AdminLogicalModel, error) { productSpec, err := DecodeCapabilitySpec(graph.Revision.CapabilitySpecJSON) if err != nil { return nil, err } variantByID := make(map[string]model.PhysicalCapabilityVariant, len(graph.Variants)) for _, variant := range graph.Variants { variantByID[variant.ID] = variant } channelModelByID := make(map[string]model.ChannelModel, len(graph.ChannelModels)) for _, channelModel := range graph.ChannelModels { channelModelByID[channelModel.ID] = channelModel } admin := AdminLogicalModel{PublicLogicalModel: publicLogicalModel(cachedLogicalModel{Model: item, ProductSpec: productSpec, Defaults: map[string]any{}}, false), Enabled: item.Enabled, ActiveRevisionID: graph.Revision.ID, RevisionVersion: graph.Revision.Version, Routes: []AdminLogicalRoute{}} for _, route := range graph.Routes { variant, variantOK := variantByID[route.PhysicalVariantID] if !variantOK { continue } channelModel, channelOK := channelModelByID[variant.ChannelModelID] if !channelOK { continue } variantSpec, specErr := effectivePhysicalVariantSpec(variant, channelModel) if specErr != nil { continue } _, channelOK = systemChannelByID[channelModel.ChannelID] structurallyAvailable := route.Enabled && route.Weight > 0 && variant.Enabled && channelModel.Enabled && channelOK available := structurallyAvailable && (item.PricePolicy != "channel" || channelModel.PriceConfigured) admin.Routes = append(admin.Routes, AdminLogicalRoute{ID: route.ID, PhysicalVariantID: variant.ID, PhysicalVariantName: variant.Name, ChannelModelID: channelModel.ID, ChannelID: channelModel.ChannelID, PhysicalModelKey: channelModel.ModelKey, PhysicalModelName: channelModel.DisplayName, Enabled: route.Enabled, Priority: route.Priority, Weight: route.Weight, Available: available, structurallyAvailable: structurallyAvailable, CapabilitySpec: variantSpec}) } routeSpecs := make([]CapabilitySpec, 0, len(admin.Routes)) for _, route := range admin.Routes { routeSpecs = append(routeSpecs, route.CapabilitySpec) } productSpec = capabilitySpecWithRoutePresets(productSpec, routeSpecs) defaults, err := decodeLogicalDefaults(graph.Revision.DefaultOptionsJSON, productSpec) if err != nil { return nil, err } admin.PublicLogicalModel = publicLogicalModel(cachedLogicalModel{Model: item, ProductSpec: productSpec, Defaults: defaults}, false) // publicLogicalModel above has no route list; admin routes are already attached // and the enriched product spec is the source used by the editor. admin.CapabilitySpec = productSpec admin.DefaultOptions = defaults structuralRouteSpecs := structuralAdminRouteSpecs(admin.Routes) settlementRouteSpecs := settlementReadyAdminRouteSpecs(admin.Routes) admin.ConfigurationError = logicalModelConfigurationError(productSpec, structuralRouteSpecs) admin.AvailabilityError = logicalModelAvailabilityError(item.PricePolicy, productSpec, structuralRouteSpecs, settlementRouteSpecs) admin.Available = len(settlementRouteSpecs) > 0 && admin.ConfigurationError == "" && admin.AvailabilityError == "" return &admin, nil } func countAvailableAdminRoutes(routes []AdminLogicalRoute) int { count := 0 for _, route := range routes { if route.Available { count++ } } return count } func structuralAdminRouteSpecs(routes []AdminLogicalRoute) []CapabilitySpec { result := make([]CapabilitySpec, 0, len(routes)) for _, route := range routes { if route.structurallyAvailable { result = append(result, route.CapabilitySpec) } } return result } func settlementReadyAdminRouteSpecs(routes []AdminLogicalRoute) []CapabilitySpec { result := make([]CapabilitySpec, 0, countAvailableAdminRoutes(routes)) for _, route := range routes { if route.Available { result = append(result, route.CapabilitySpec) } } return result } func availableCachedRouteSpecs(routes []cachedLogicalRoute) []CapabilitySpec { result := make([]CapabilitySpec, 0, len(routes)) for _, route := range routes { if route.Route.Enabled && route.Route.Weight > 0 { result = append(result, route.VariantSpec) } } return result } func hasHealthyCachedRoute(s *Service, routes []cachedLogicalRoute) bool { for _, route := range routes { if route.Route.Enabled && route.Route.Weight > 0 && !s.logicalRouteBlocked(route) { return true } } return false } func logicalModelCapabilityCovered(product CapabilitySpec, routeSpecs []CapabilitySpec) bool { return len(routeSpecs) > 0 && validateProductSpecWithinRoutes(product, routeSpecs) == nil } func logicalModelConfigurationError(product CapabilitySpec, routeSpecs []CapabilitySpec) string { if len(routeSpecs) == 0 || logicalModelCapabilityCovered(product, routeSpecs) { return "" } return "供应线路已无法完整覆盖创作端能力,请调整线路或能力范围" } func logicalModelAvailabilityError(pricePolicy string, product CapabilitySpec, structuralRouteSpecs, settlementRouteSpecs []CapabilitySpec) string { if pricePolicy != "channel" || len(structuralRouteSpecs) == 0 { return "" } if len(settlementRouteSpecs) == 0 { return "供应线路尚未配置可结算价格,请先完善渠道模型价格" } if !logicalModelCapabilityCovered(product, settlementRouteSpecs) { return "部分创作端能力只能由未配置价格的渠道模型承接,请完善对应价格" } return "" } func (s *Service) SaveAdminLogicalModel(actor *model.User, id string, req LogicalModelRequest) (*AdminLogicalModel, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } item, revision, routes, creating, err := s.logicalModelBundle(actor, id, req) if err != nil { return nil, err } if err := s.repo.SaveLogicalModelBundle(item, revision, routes, creating); err != nil { return nil, err } s.invalidateRouteCatalog() _ = s.appendAdminAudit(actor, map[bool]string{true: "logical_model.create", false: "logical_model.update"}[creating], "logical_model", item.ID, "保存前台模型及供应线路", map[string]any{"revisionId": revision.ID, "routeCount": len(routes)}) graph, err := s.repo.LogicalModelGraph(item.ID, true) if err != nil { return nil, err } channelIDs := make([]string, 0, len(graph.ChannelModels)) for _, channelModel := range graph.ChannelModels { channelIDs = append(channelIDs, channelModel.ChannelID) } systemChannels, err := s.repo.SystemChannelsByIDs(channelIDs, true) if err != nil { return nil, err } systemChannelByID := make(map[string]model.ModelChannel, len(systemChannels)) for _, channel := range systemChannels { systemChannelByID[channel.ID] = channel } return s.buildAdminLogicalModel(*item, graph, systemChannelByID) } func (s *Service) logicalModelBundle(actor *model.User, id string, req LogicalModelRequest) (*model.LogicalModel, *model.LogicalModelRevision, []model.LogicalModelRoute, bool, error) { code := strings.ToLower(strings.TrimSpace(req.Code)) name := strings.TrimSpace(req.Name) capability := normalizeCapability(req.Capability) if !logicalModelCodePattern.MatchString(code) { return nil, nil, nil, false, BadAuthRequest("模型 code 需为 2-80 位小写字母、数字、点、下划线或连字符") } if name == "" || len([]rune(name)) > 120 { return nil, nil, nil, false, BadAuthRequest("请填写 1-120 个字符的模型名称") } normalizedSpec, err := NormalizeCapabilitySpec(req.CapabilitySpec) if err != nil { return nil, nil, nil, false, err } req.CapabilitySpec = normalizedSpec if normalizeCapability(req.CapabilitySpec.Capability) != capability { return nil, nil, nil, false, BadAuthRequest("前台模型类型与能力配置不一致") } normalizedDefaults, err := normalizeLogicalDefaults(req.CapabilitySpec, req.DefaultOptions) if err != nil { return nil, nil, nil, false, err } req.DefaultOptions = normalizedDefaults if req.UnitPriceMicrocredits < 0 || req.InputPriceMicrocredits < 0 || req.OutputPriceMicrocredits < 0 || req.CachedPriceMicrocredits < 0 { return nil, nil, nil, false, BadAuthRequest("用户价格不能为负数") } pricePolicy := strings.TrimSpace(req.PricePolicy) if pricePolicy != "channel" && pricePolicy != "unified" { return nil, nil, nil, false, BadAuthRequest("请选择跟随供应价格或统一定价") } billingMode := strings.TrimSpace(req.BillingMode) if pricePolicy == "channel" { billingMode = "fixed_request" req.UnitPriceMicrocredits, req.InputPriceMicrocredits, req.OutputPriceMicrocredits, req.CachedPriceMicrocredits = 0, 0, 0, 0 } else if billingMode != "fixed_request" && billingMode != "per_second" && billingMode != "token" { return nil, nil, nil, false, BadAuthRequest("前台模型计费方式仅支持按次、按秒或 Token") } if pricePolicy == "unified" && billingMode == "per_second" && capability != "video" { return nil, nil, nil, false, BadAuthRequest("只有视频前台模型可以按秒计费") } if pricePolicy == "unified" && billingMode == "token" && capability != "text" { return nil, nil, nil, false, BadAuthRequest("当前仅文本前台模型支持 Token 计费") } creating := strings.TrimSpace(id) == "" var item *model.LogicalModel if creating { id, err = s.repo.NextPrefixedID("LMODEL") if err != nil { return nil, nil, nil, false, err } item = &model.LogicalModel{ID: id, CreatedAt: time.Now()} } else { item, err = s.repo.LogicalModel(id) if err != nil { return nil, nil, nil, false, err } } item.Code, item.Name, item.Icon, item.Description, item.Capability = code, name, strings.TrimSpace(req.Icon), strings.TrimSpace(req.Description), capability item.Enabled, item.SortOrder, item.PricePolicy, item.BillingMode = req.Enabled, req.SortOrder, pricePolicy, billingMode item.UnitPriceMicrocredits, item.InputPriceMicrocredits, item.OutputPriceMicrocredits, item.CachedPriceMicrocredits = req.UnitPriceMicrocredits, req.InputPriceMicrocredits, req.OutputPriceMicrocredits, req.CachedPriceMicrocredits item.UpdatedAt = time.Now() revisionID, err := s.repo.NextPrefixedID("REVISION") if err != nil { return nil, nil, nil, false, err } specJSON, _ := json.Marshal(req.CapabilitySpec) defaultsJSON, _ := json.Marshal(defaultMap(req.DefaultOptions)) revision := &model.LogicalModelRevision{ID: revisionID, LogicalModelID: item.ID, CapabilitySpecJSON: string(specJSON), DefaultOptionsJSON: string(defaultsJSON), CreatedBy: actor.ID, CreatedAt: time.Now()} routes := make([]model.LogicalModelRoute, 0, len(req.Routes)) seenVariants := make(map[string]bool, len(req.Routes)) structuralRouteSpecs := make([]CapabilitySpec, 0, len(req.Routes)) settlementRouteSpecs := make([]CapabilitySpec, 0, len(req.Routes)) for _, input := range req.Routes { variantID := strings.TrimSpace(input.PhysicalVariantID) if variantID == "" || seenVariants[variantID] { return nil, nil, nil, false, BadAuthRequest("供应线路必须选择不重复的可用配置") } seenVariants[variantID] = true variant, variantErr := s.repo.PhysicalVariant(variantID) if variantErr != nil { return nil, nil, nil, false, BadAuthRequest("供应线路引用的可用配置不存在") } channelModel, modelErr := s.repo.ChannelModel(variant.ChannelModelID) if modelErr != nil { return nil, nil, nil, false, BadAuthRequest("可用配置引用的渠道模型不存在") } variantSpec, specErr := effectivePhysicalVariantSpec(*variant, *channelModel) if specErr != nil || normalizeCapability(variantSpec.Capability) != capability { return nil, nil, nil, false, BadAuthRequest("供应线路能力类型与前台模型不一致") } if req.Enabled && input.Enabled && input.Weight <= 0 { return nil, nil, nil, false, BadAuthRequest("启用供应线路的同级权重必须大于 0") } if input.Weight < 0 { return nil, nil, nil, false, BadAuthRequest("供应线路的同级权重不能为负数") } if req.Enabled && input.Enabled && variant.Enabled { if err := validateVariantWithinChannelModel(*channelModel, variantSpec); err != nil { return nil, nil, nil, false, err } if channelModel.Enabled { if _, channelErr := s.repo.SystemChannel(channelModel.ChannelID); channelErr == nil { structuralRouteSpecs = append(structuralRouteSpecs, variantSpec) if pricePolicy != "channel" || channelModel.PriceConfigured { settlementRouteSpecs = append(settlementRouteSpecs, variantSpec) } } } } routeID, idErr := s.repo.NextPrefixedID("ROUTE") if idErr != nil { return nil, nil, nil, false, idErr } routes = append(routes, model.LogicalModelRoute{ID: routeID, PhysicalVariantID: variant.ID, Enabled: input.Enabled, Priority: input.Priority, Weight: input.Weight, CreatedAt: time.Now(), UpdatedAt: time.Now()}) } // 停用必须始终可执行,便于管理员立即阻止失效配置继续对外服务;重新启用时再强校验结构能力和计费可用性。 if req.Enabled { if len(structuralRouteSpecs) == 0 { return nil, nil, nil, false, BadAuthRequest("启用前台模型前至少需要一条已启用的供应线路") } if err := validateProductSpecWithinRoutes(req.CapabilitySpec, structuralRouteSpecs); err != nil { return nil, nil, nil, false, err } if availabilityError := logicalModelAvailabilityError(pricePolicy, req.CapabilitySpec, structuralRouteSpecs, settlementRouteSpecs); availabilityError != "" { return nil, nil, nil, false, BadAuthRequest(availabilityError) } } return item, revision, routes, creating, nil } func normalizeLogicalDefaults(spec CapabilitySpec, defaults map[string]any) (map[string]any, error) { result := make(map[string]any, len(defaults)) for rawName, value := range defaults { name := canonicalCapabilityOptionName(rawName) if _, exists := result[name]; exists { return nil, BadAuthRequest("默认参数存在重复别名:" + name) } constraint, ok := spec.Options[name] if !ok || !matchOptionConstraint(name, constraint, value) { return nil, BadAuthRequest("默认参数 " + name + " 不在前台模型能力范围内") } // `*` 只表示允许任意自定义值,不能作为创作端默认参数发送。 if normalizedScalar(value) == "*" { for _, candidate := range constraint.Values { if normalizedScalar(candidate) != "*" { value = candidate break } } } result[name] = value } return result, nil } func defaultMap(value map[string]any) map[string]any { if value == nil { return map[string]any{} } return value } func (s *Service) AdminPhysicalVariants(actor *model.User) ([]AdminPhysicalVariant, error) { return s.adminPhysicalVariants(actor, "") } func (s *Service) AdminPhysicalVariantsForChannelModel(actor *model.User, channelModelID string) ([]AdminPhysicalVariant, error) { return s.adminPhysicalVariants(actor, strings.TrimSpace(channelModelID)) } func (s *Service) adminPhysicalVariants(actor *model.User, channelModelID string) ([]AdminPhysicalVariant, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } var variants []model.PhysicalCapabilityVariant var err error if channelModelID != "" { if _, err := s.repo.ChannelModel(channelModelID); err != nil { return nil, BadAuthRequest("渠道模型不存在") } variants, err = s.repo.PhysicalVariants([]string{channelModelID}, true) } else { variants, err = s.repo.AllPhysicalVariants(true) } if err != nil { return nil, err } variantIDs := make([]string, 0, len(variants)) for _, variant := range variants { variantIDs = append(variantIDs, variant.ID) } usageCounts, err := s.repo.PhysicalVariantRouteCounts(variantIDs) if err != nil { return nil, err } result := make([]AdminPhysicalVariant, 0, len(variants)) for _, variant := range variants { channelModel, modelErr := s.repo.ChannelModel(variant.ChannelModelID) if modelErr != nil { continue } spec, specErr := effectivePhysicalVariantSpec(variant, *channelModel) if specErr != nil { continue } result = append(result, AdminPhysicalVariant{ID: variant.ID, ChannelModelID: channelModel.ID, ChannelID: channelModel.ChannelID, ModelKey: channelModel.ModelKey, ModelName: channelModel.DisplayName, Name: variant.Name, Capability: channelModel.Capability, Protocol: string(channelModel.Protocol), Enabled: variant.Enabled, ModelEnabled: channelModel.Enabled, UsageCount: usageCounts[variant.ID], CapabilitySpec: spec}) } return result, nil } func effectivePhysicalVariantSpec(variant model.PhysicalCapabilityVariant, channelModel model.ChannelModel) (CapabilitySpec, error) { config, configErr := DecodeModelCapabilityConfig(channelModel.CapabilityConfigJSON) if configErr == nil && config != nil { if spec, err := CapabilitySpecFromModelCapabilityConfig(config, normalizeCapability(channelModel.Capability)); err == nil { return spec, nil } } // 音频模型以及历史数据可能没有新的能力配置,保留旧快照作为只读降级,避免读路径直接丢失线路。 return DecodeCapabilitySpec(variant.CapabilitySpecJSON) } func (s *Service) SaveAdminPhysicalVariant(actor *model.User, id string, req PhysicalVariantRequest) (*AdminPhysicalVariant, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } channelModel, err := s.repo.ChannelModel(strings.TrimSpace(req.ChannelModelID)) if err != nil { return nil, BadAuthRequest("请选择有效的渠道模型") } name := strings.TrimSpace(req.Name) if name == "" || len([]rune(name)) > 120 { return nil, BadAuthRequest("请填写 1-120 个字符的可用配置名称") } creating := strings.TrimSpace(id) == "" var item *model.PhysicalCapabilityVariant if creating { existing, existingErr := s.repo.PhysicalVariants([]string{channelModel.ID}, true) if existingErr != nil { return nil, existingErr } if len(existing) > 0 { return nil, BadAuthRequest("一个渠道模型只能配置一个可用配置,请编辑现有配置") } id, err = s.repo.NextPrefixedID("VARIANT") if err != nil { return nil, err } item = &model.PhysicalCapabilityVariant{ID: id, CreatedAt: time.Now()} } else { item, err = s.repo.PhysicalVariant(id) if err != nil { return nil, err } if item.ChannelModelID != channelModel.ID { return nil, BadAuthRequest("已创建的可用配置不能换绑渠道模型,请新建可用配置") } } // variant 不再承载第二份可编辑能力配置,能力快照始终从渠道模型生成。 // 读取旧请求中的 capabilitySpec 仅为兼容旧客户端,不再作为事实来源。 // 停用是强制止损操作:即使渠道模型能力 JSON 已损坏,也必须允许管理员先停用; // 只有启用路径才要求重新生成并校验能力快照。 capabilityConfig, configErr := DecodeModelCapabilityConfig(channelModel.CapabilityConfigJSON) derivedSpec, deriveErr := CapabilitySpecFromModelCapabilityConfig(capabilityConfig, normalizeCapability(channelModel.Capability)) if req.Enabled { if configErr != nil { return nil, BadAuthRequest("渠道模型能力配置无效,请先修复渠道模型") } if deriveErr != nil { return nil, deriveErr } if err := validateVariantWithinChannelModel(*channelModel, derivedSpec); err != nil { return nil, err } } spec := derivedSpec if deriveErr != nil { // 禁用时保留已有快照,避免坏配置导致管理列表丢失;新建记录使用最小可读快照。 if existingSpec, existingErr := DecodeCapabilitySpec(item.CapabilitySpecJSON); existingErr == nil { spec = existingSpec } else { spec = CapabilitySpec{Version: 1, Capability: normalizeCapability(channelModel.Capability)} } } specJSON, _ := json.Marshal(spec) item.ChannelModelID, item.Name, item.Enabled, item.CapabilitySpecJSON, item.UpdatedAt = channelModel.ID, name, req.Enabled, string(specJSON), time.Now() if creating { err = s.repo.CreatePhysicalVariant(item) } else { err = s.repo.SavePhysicalVariant(item) } if err != nil { return nil, err } s.invalidateRouteCatalog() _ = s.appendAdminAudit(actor, map[bool]string{true: "physical_variant.create", false: "physical_variant.update"}[creating], "physical_variant", item.ID, "保存可用配置", map[string]any{"channelModelId": channelModel.ID}) variants, err := s.AdminPhysicalVariants(actor) if err != nil { return nil, err } for index := range variants { if variants[index].ID == item.ID { return &variants[index], nil } } return nil, gorm.ErrRecordNotFound } func (s *Service) SimulateLogicalModelRoute(actor *model.User, id string, intent ModelRequestIntent) (*RouteSimulationResult, error) { if err := s.RequireAdmin(actor); err != nil { return nil, err } snapshot, err := s.routeCatalogSnapshot() if err != nil { return nil, err } cached, ok := snapshot.Models[id] if !ok { return nil, BadAuthRequest("前台模型未启用或尚未发布") } intent.Options = mergeIntentDefaults(intent.Options, cached.Defaults) return &RouteSimulationResult{ProductMatch: MatchCapability(cached.ProductSpec, intent), Candidates: s.sortedRouteDiagnostics(cached.Routes, intent)}, nil } func logicalModelNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) } // 可用配置只能收窄渠道模型能力,不能声明协议适配器实际无法发送的范围。 func validateVariantWithinChannelModel(channelModel model.ChannelModel, spec CapabilitySpec) error { profile, err := DecodeModelCapabilityConfig(channelModel.CapabilityConfigJSON) if err != nil { return BadAuthRequest("渠道模型能力配置无效,请先修复渠道模型") } switch normalizeCapability(spec.Capability) { case "text": if profile == nil || profile.Text == nil { return BadAuthRequest("渠道文本模型尚未配置能力参数") } limits := map[string]int{"image": profile.Text.References.MaxImages, "video": profile.Text.References.MaxVideos} for kind, constraint := range spec.Inputs { limit, known := limits[kind] if !known && constraint.Max > 0 || known && constraint.Max > limit { return BadAuthRequest("可用配置的 " + kind + " 输入上限超过渠道模型配置") } } return validateKnownVariantOptions(spec.Options, map[string]OptionConstraint{}) case "image": if profile == nil || profile.Image == nil { return BadAuthRequest("渠道图片模型尚未配置能力参数") } image := profile.Image for kind, constraint := range spec.Inputs { switch kind { case "image": if constraint.Max > image.References.MaxImages { return BadAuthRequest("可用配置的图片输入上限超过渠道模型配置") } case "mask": if constraint.Max > 1 || constraint.Max > 0 && !image.References.MaskSupported { return BadAuthRequest("可用配置声明了渠道模型不支持的蒙版输入") } default: if constraint.Max > 0 { return BadAuthRequest("图片协议尚未接入 " + kind + " 输入") } } } physical := map[string]OptionConstraint{ "size": imageSizeOptionConstraint(image.Size), "quality": anyValues(image.Quality.Values), "transparentBackground": boolValues(image.TransparentBackground.Supported), "count": numericRange(1, float64(image.MaxOutputs), 1), } return validateKnownVariantOptions(spec.Options, physical) case "video": if profile == nil || profile.Video == nil { return BadAuthRequest("渠道视频模型尚未配置能力参数") } video := profile.Video limits := map[string]int{"image": video.References.MaxImages, "video": video.References.MaxVideos, "audio": video.References.MaxAudios} for kind, constraint := range spec.Inputs { limit, known := limits[kind] if !known && constraint.Max > 0 || known && constraint.Max > limit { return BadAuthRequest("可用配置的 " + kind + " 输入上限超过渠道模型配置") } } for _, operation := range spec.Operations { if !containsCapabilityString(video.Operations, operation) { return BadAuthRequest("可用配置包含渠道模型不支持的操作:" + operation) } } duration := OptionConstraint{} if video.Duration.Selection == "enum" { values := make([]any, 0, len(video.Duration.Values)) for _, value := range video.Duration.Values { values = append(values, value) } duration.Values = values } else { duration = numericRange(float64(video.Duration.Min), float64(video.Duration.Max), float64(video.Duration.Step)) } physical := map[string]OptionConstraint{ "videoSeconds": duration, "size": anyValues(video.Ratios), "vquality": anyValues(video.Resolutions), "videoGenerateAudio": boolValues(video.GenerateAudio.Supported), "videoWatermark": boolValues(video.Watermark.Supported), } return validateKnownVariantOptions(spec.Options, physical) case "audio": for kind, constraint := range spec.Inputs { if constraint.Max > 0 { return BadAuthRequest("音频协议尚未接入 " + kind + " 输入") } } wildcard := OptionConstraint{Values: []any{"*"}} return validateKnownVariantOptions(spec.Options, map[string]OptionConstraint{"audioVoice": wildcard, "audioFormat": wildcard, "audioSpeed": wildcard, "audioInstructions": wildcard}) } return BadAuthRequest("可用配置类型暂不支持") } // 前台模型只声明供应线路真实提供的总目录;组合是否可承接仍由匿名 capabilityProfiles 按 OR 语义判断。 func validateProductSpecWithinRoutes(product CapabilitySpec, routeSpecs []CapabilitySpec) error { for _, routeSpec := range routeSpecs { if normalizeCapability(routeSpec.Capability) != normalizeCapability(product.Capability) { return BadAuthRequest("供应线路能力类型与前台模型不一致") } } if len(product.Operations) == 0 { unrestricted := false for _, routeSpec := range routeSpecs { if len(routeSpec.Operations) == 0 { unrestricted = true break } } if !unrestricted { return BadAuthRequest("创作端生成方式必须从供应线路支持的选项中选择") } } else { for _, operation := range product.Operations { supported := false for _, routeSpec := range routeSpecs { if len(routeSpec.Operations) == 0 || containsCapabilityString(routeSpec.Operations, operation) { supported = true break } } if !supported { return BadAuthRequest("创作端生成方式不受任何供应线路支持:" + operation) } } } for name, constraint := range product.Inputs { if !inputConstraintCovered(constraint, name, routeSpecs) { return BadAuthRequest("创作端输入范围超出供应线路能力:" + name) } } for name, constraint := range product.Options { if !optionConstraintCovered(constraint, name, routeSpecs) { return BadAuthRequest("创作端参数超出供应线路能力:" + name) } } return nil } func inputConstraintCovered(candidate InputConstraint, name string, routeSpecs []CapabilitySpec) bool { next := candidate.Min for next <= candidate.Max { coveredUntil := next - 1 for _, routeSpec := range routeSpecs { constraint, exists := routeSpec.Inputs[name] if !exists { constraint = InputConstraint{Min: 0, Max: 0} } if constraint.Min <= next && constraint.Max >= next && constraint.Max > coveredUntil { coveredUntil = constraint.Max } } if coveredUntil < next { return false } next = coveredUntil + 1 } return true } func optionConstraintCovered(candidate OptionConstraint, name string, routeSpecs []CapabilitySpec) bool { routeConstraints := make([]OptionConstraint, 0, len(routeSpecs)) for _, routeSpec := range routeSpecs { if constraint, exists := routeSpec.Options[name]; exists { routeConstraints = append(routeConstraints, constraint) } } if len(routeConstraints) == 0 { return false } for _, routeConstraint := range routeConstraints { if isWildcardOptionConstraint(routeConstraint) { return true } } if len(candidate.Values) > 0 { for _, value := range candidate.Values { if !optionValueSupported(name, value, routeConstraints) { return false } } return true } if candidate.Min == nil || candidate.Max == nil { return false } if math.Abs(*candidate.Max-*candidate.Min) < 1e-9 { return optionValueSupported(name, *candidate.Min, routeConstraints) } if candidate.Step == nil { return continuousOptionRangeCovered(*candidate.Min, *candidate.Max, routeConstraints) } step := *candidate.Step count := int(math.Floor((*candidate.Max-*candidate.Min)/step+1e-9)) + 1 if count <= 10000 { for index := 0; index < count; index++ { value := *candidate.Min + float64(index)*step if !optionValueSupported(name, value, routeConstraints) { return false } } return true } // 超大离散范围不逐点展开;只有单条连续范围或步长完全兼容的线路才能作为可靠来源。 for _, routeConstraint := range routeConstraints { if routeConstraint.Min == nil || routeConstraint.Max == nil || *routeConstraint.Min > *candidate.Min || *routeConstraint.Max < *candidate.Max { continue } if routeConstraint.Step == nil { return true } startSteps := (*candidate.Min - *routeConstraint.Min) / *routeConstraint.Step stepRatio := step / *routeConstraint.Step if math.Abs(startSteps-math.Round(startSteps)) < 1e-9 && math.Abs(stepRatio-math.Round(stepRatio)) < 1e-9 { return true } } return false } func optionValueSupported(name string, value any, constraints []OptionConstraint) bool { for _, constraint := range constraints { if isWildcardOptionConstraint(constraint) { return true } if matchOptionConstraint(name, constraint, value) { return true } } return false } func isWildcardOptionConstraint(constraint OptionConstraint) bool { for _, value := range constraint.Values { if normalizedScalar(value) == "*" { return true } } return false } func continuousOptionRangeCovered(minimum float64, maximum float64, constraints []OptionConstraint) bool { next := minimum for next <= maximum+1e-9 { coveredUntil := next advanced := false for _, constraint := range constraints { if constraint.Min == nil || constraint.Max == nil || constraint.Step != nil { continue } if *constraint.Min <= next+1e-9 && *constraint.Max >= next-1e-9 && *constraint.Max > coveredUntil { coveredUntil = *constraint.Max advanced = true } } if coveredUntil >= maximum-1e-9 { return true } if !advanced { return false } next = coveredUntil } return true } func validateKnownVariantOptions(options map[string]OptionConstraint, physical map[string]OptionConstraint) error { for name, constraint := range options { limit, known := physical[name] if !known { return BadAuthRequest("参数 " + name + " 尚未接入该类型的供应商协议映射") } if !optionConstraintWithin(constraint, limit) { return BadAuthRequest("参数 " + name + " 超过渠道模型能力范围") } } return nil } func optionConstraintWithin(candidate OptionConstraint, limit OptionConstraint) bool { return optionConstraintCovered(candidate, "value", []CapabilitySpec{{Options: map[string]OptionConstraint{"value": limit}}}) } func anyValues(values []string) OptionConstraint { result := make([]any, 0, len(values)) for _, value := range values { result = append(result, value) } return OptionConstraint{Values: result} } func boolValues(supportsTrue bool) OptionConstraint { values := []any{false} if supportsTrue { values = append(values, true) } return OptionConstraint{Values: values} } func numericRange(minimum float64, maximum float64, step float64) OptionConstraint { return OptionConstraint{Min: &minimum, Max: &maximum, Step: &step} }