diff --git a/backend/internal/app/creation.go b/backend/internal/app/creation.go index 6548d37ff..d1abbe7e0 100644 --- a/backend/internal/app/creation.go +++ b/backend/internal/app/creation.go @@ -524,7 +524,20 @@ func validateCreationSubmissionScope(run *model.CreationRun, version int64, req if key == "videoSeconds" { metadataKey = "seconds" } - if approved := stringValue(meta[metadataKey]); approved != "" && approved != stringValue(config[key]) { + approved := strings.TrimSpace(stringValue(meta[metadataKey])) + candidate := strings.TrimSpace(stringValue(config[key])) + if metadataKey == "quality" { + approvedNorm := strings.ToLower(approved) + candidateNorm := strings.ToLower(candidate) + // auto/any 与空缺在图片生成中等价,前端 omittedImageQuality 可能会省略默认 quality + if (approvedNorm == "auto" || approvedNorm == "any" || approvedNorm == "") && (candidateNorm == "auto" || candidateNorm == "any" || candidateNorm == "") { + continue + } + if approvedNorm == candidateNorm { + continue + } + } + if approved != "" && approved != candidate { return creationConflict("生成规格与已批准方案不同") } } diff --git a/backend/internal/app/task_creation.go b/backend/internal/app/task_creation.go index 2f04632e6..be2a517ff 100644 --- a/backend/internal/app/task_creation.go +++ b/backend/internal/app/task_creation.go @@ -388,7 +388,28 @@ func (s *Service) resolveSystemChannelModelSelection(input map[string]any, taskT } } - priceTier := channelModelPriceTierForIntent(*channelModel, intent) + pricingIntent := intent + if normalizeCapability(channelModel.Capability) == "image" { + pricingOptions := make(map[string]any, len(intent.Options)+2) + for k, v := range intent.Options { + pricingOptions[k] = v + } + rawQuality := strings.ToLower(strings.TrimSpace(fmt.Sprint(nextConfig["quality"]))) + if rawQuality != "" && rawQuality != "" && rawQuality != "auto" && rawQuality != "any" { + pricingOptions["quality"] = rawQuality + } else if pricingOptions["quality"] == nil || pricingOptions["quality"] == "" || pricingOptions["quality"] == "auto" { + pricingOptions["quality"] = "1k" + } + if rawSize := strings.ToLower(strings.TrimSpace(fmt.Sprint(nextConfig["size"]))); rawSize != "" && rawSize != "" && rawSize != "auto" { + pricingOptions["size"] = rawSize + } + pricingIntent.Options = pricingOptions + } + + priceTier := channelModelPriceTierForIntent(*channelModel, pricingIntent) + if priceTier == nil { + priceTier = channelModelPriceTierForIntent(*channelModel, intent) + } if priceTier == nil || !ValidatePriceTierPrice(priceTier, channelModel.Capability, channelModel.Protocol) { return input, ModelPriceNotConfigured("指定的模型未配置当前规格的有效价格") } diff --git a/backend/internal/app/task_creation_test.go b/backend/internal/app/task_creation_test.go index 31d347f3a..421502c93 100644 --- a/backend/internal/app/task_creation_test.go +++ b/backend/internal/app/task_creation_test.go @@ -319,3 +319,56 @@ func newSelectionPriceTier(id string, selector string, providerModelKey string, BillingMode: billingMode, UnitPriceMicrocredits: 10, PriceConfigured: true, Enabled: true, } } + +func TestImageResolutionPricingOnSystemChannel(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceGeminiImage), "gemini-image-model") + profile.Image.Quality = ImageQualityConfig{Supported: false, Default: "auto"} + profile.Image.Size = ImageSizeConfig{ + Parameter: "aspect_ratio", + Values: []string{"1:1", "16:9"}, + Default: "1:1", + Presets: []ImageSizePreset{ + {Tier: "1k", Ratio: "16:9", Size: "1824x1024", Width: 1824, Height: 1024}, + {Tier: "2k", Ratio: "16:9", Size: "2752x1536", Width: 2752, Height: 1536}, + {Tier: "4k", Ratio: "16:9", Size: "3840x2160", Width: 3840, Height: 2160}, + }, + } + svc, _, channel, channelModel := createSystemChannelSelectionFixture(t, "image", model.ChannelInterfaceGeminiImage, profile, []model.ChannelModelPriceTier{ + newSelectionPriceTier("tier-1k", `{"quality":"1k"}`, "provider-1k", "fixed_request"), + newSelectionPriceTier("tier-2k", `{"quality":"2k"}`, "provider-2k", "fixed_request"), + newSelectionPriceTier("tier-4k", `{"quality":"4k"}`, "provider-4k", "fixed_request"), + }) + + for _, tc := range []struct { + name string + quality string + size string + wantTier string + }{ + {"1k specified", "1k", "16:9", "tier-1k"}, + {"2k specified", "2k", "16:9", "tier-2k"}, + {"4k specified", "4k", "16:9", "tier-4k"}, + {"auto specified", "auto", "16:9", "tier-1k"}, + {"empty specified", "", "16:9", "tier-1k"}, + } { + t.Run(tc.name, func(t *testing.T) { + input := map[string]any{ + "mode": "image", + "config": map[string]any{ + "channelId": channel.ID, + "model": channelModel.ModelKey, + "size": tc.size, + "quality": tc.quality, + }, + } + resolved, err := svc.resolveSystemChannelModelSelection(input, "canvas_image", "") + if err != nil { + t.Fatalf("resolve error: %v", err) + } + cfg := resolved["config"].(map[string]any) + if cfg["priceTierId"] != tc.wantTier { + t.Fatalf("priceTierId = %#v, want %s", cfg["priceTierId"], tc.wantTier) + } + }) + } +}