From 6a118ae93b253a5fea3da28b3adc5660a24ab0d2 Mon Sep 17 00:00:00 2001 From: ddcat666 <26564566+ddcat666@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:17:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E5=9B=BE=E7=89=87=E5=B0=BA?= =?UTF-8?q?=E5=AF=B8=20-=20=E4=BF=AE=E5=A4=8D=E6=AF=94=E4=BE=8B=20preset?= =?UTF-8?q?=20=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/app/image_parameters.go | 32 ++++++++++++++- backend/internal/app/image_parameters_test.go | 31 ++++++++++++++ .../internal/app/model_capability_validate.go | 14 +++++++ backend/internal/app/task_creation.go | 4 +- backend/internal/app/task_creation_test.go | 41 +++++++++++++++++++ 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/backend/internal/app/image_parameters.go b/backend/internal/app/image_parameters.go index 7d3e603c6..d87063f40 100644 --- a/backend/internal/app/image_parameters.go +++ b/backend/internal/app/image_parameters.go @@ -262,6 +262,36 @@ func imageParameterValue(p protocol.ImageParameter, value any) any { return value } +func imageParameterUnsupportedMessage(p protocol.ImageParameter) string { + message := "图片参数超出支持范围:" + p.Label + if len(p.Values) > 0 { + values := make([]string, 0, min(len(p.Values), 12)) + for _, value := range p.Values[:min(len(p.Values), 12)] { + values = append(values, fmt.Sprint(value)) + } + message += ";支持值:" + strings.Join(values, "、") + if len(p.Values) > len(values) { + message += "等" + } + } else if p.Min != nil || p.Max != nil { + message += fmt.Sprintf(";范围:%s-%s", formatImageParameterBound(p.Min), formatImageParameterBound(p.Max)) + } + if p.Default != nil { + message += ";默认值:" + fmt.Sprint(p.Default) + } + if p.Binding == "aspectRatio" && p.Encoding == "pixels" { + message += ";若使用比例,按 model_list 的 imageSize.presets 选择对应像素尺寸" + } + return message +} + +func formatImageParameterBound(value *float64) string { + if value == nil { + return "不限" + } + return strconv.FormatFloat(*value, 'f', -1, 64) +} + func imageParameterAdmissionError(field, issue, message string) error { return NewModelError(ErrCodeModelCapabilityNotSupported, message).WithDetails(map[string]any{ "field": field, @@ -285,7 +315,7 @@ func resolveImageParameterValues(parameters []protocol.ImageParameter, values ma } value = imageParameterValue(p, value) if !p.Accepts(value) { - return nil, imageParameterAdmissionError(p.Name, "unsupported_value", "图片参数超出支持范围:"+p.Label) + return nil, imageParameterAdmissionError(p.Name, "unsupported_value", imageParameterUnsupportedMessage(p)) } if value != nil && value != "" { result[name] = value diff --git a/backend/internal/app/image_parameters_test.go b/backend/internal/app/image_parameters_test.go index d959b2ca2..af7555745 100644 --- a/backend/internal/app/image_parameters_test.go +++ b/backend/internal/app/image_parameters_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "testing" "time" @@ -35,6 +36,36 @@ func imageParameterTestAdapter(t *testing.T, packageID, providerID string) proto return nil } +func TestImageParameterRatioUsesDeclaredPixelPreset(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel("openai-image", "GPT Image 2.5") + profile.Image.Parameters = []protocol.ImageParameter{{ + Name: "size", Label: "画幅 / 尺寸", Type: "string", + Values: []any{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024", + Binding: "aspectRatio", Encoding: "pixels", Pricing: true, + }} + profile.Image.Size = ImageSizeConfig{ + Parameter: "size", Values: []string{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024", + Presets: []ImageSizePreset{ + {Tier: "1k", Ratio: "16:9", Size: "1824x1024"}, + {Tier: "1k", Ratio: "9:16", Size: "1024x1824"}, + {Tier: "1k", Ratio: "1:1", Size: "1024x1024"}, + }, + } + if got := imageSizeValueForDeclaredPreset(profile.Image, "16:9", ""); got != "1824x1024" { + t.Fatalf("ratio was not encoded by the declared preset: %q", got) + } + if got := imageSizeValueForDeclaredPreset(profile.Image, "1024x1824", ""); got != "1024x1824" { + t.Fatalf("pixel preset was not preserved: %q", got) + } + values := map[string]any{"size": imageSizeValueForDeclaredPreset(profile.Image, "16:9", "")} + if _, err := resolveImageParameterValues(profile.Image.Parameters, values); err != nil { + t.Fatalf("encoded preset was rejected by image parameter admission: %v", err) + } + if _, err := resolveImageParameterValues(profile.Image.Parameters, map[string]any{"size": "16:9"}); err == nil || !strings.Contains(err.Error(), "支持值") || !strings.Contains(err.Error(), "imageSize.presets") { + t.Fatalf("unsupported ratio error lacks actionable correction guidance: %v", err) + } +} + func TestImageParametersResolutionOnlyAndLogicalReconciliation(t *testing.T) { a := imageParameterTestAdapter(t, "google-gemini-image", "gemini-image") registry, err := protocol.NewRegistry(a) diff --git a/backend/internal/app/model_capability_validate.go b/backend/internal/app/model_capability_validate.go index 5eea1cc06..0a8c6a3b5 100644 --- a/backend/internal/app/model_capability_validate.go +++ b/backend/internal/app/model_capability_validate.go @@ -490,6 +490,20 @@ func validateImagePresetSelection(profile *ImageCapabilityConfig, quality, ratio return newModelCapabilityArgumentError("size", "unsupported_value", BadAuthRequest("当前分辨率不支持所选图片宽高比")) } +func imageSizeValueForDeclaredPreset(profile *ImageCapabilityConfig, size string, quality string) string { + if profile == nil { + return "" + } + preset, ok := declaredImageSizePreset(profile.Size.Presets, size, quality, imagePresetDefaultTier(profile)) + if !ok { + return "" + } + if profile.Size.Parameter == "size" { + return strings.TrimSpace(preset.Size) + } + return normalizeDeclaredImageRatio(preset.Ratio) +} + // imageSizeRatioForDeclaredPreset 把已声明的 preset 像素值还原成它对应的比例。比例型线路 // 会把画布比例换算成 preset 像素值(OpenAI Images 只收 WIDTHxHEIGHT),换算结果必须继续 // 通过同一套尺寸与分辨率校验。只认当前档位的 preset,避免用低档价格取到高档像素。 diff --git a/backend/internal/app/task_creation.go b/backend/internal/app/task_creation.go index 509cced75..8024014cd 100644 --- a/backend/internal/app/task_creation.go +++ b/backend/internal/app/task_creation.go @@ -533,8 +533,8 @@ func (s *Service) resolveSystemChannelModelSelection(input map[string]any, taskT } if value, ok := values["size"]; ok && capabilitySpec != nil && capabilityImageSizePresetMatches(*capabilitySpec, value, values) { values = maps.Clone(values) - if ratio := imageSizeRatioForDeclaredPreset(capabilityConfig.Image, fmt.Sprint(value), fmt.Sprint(values["imageResolution"])); ratio != "" { - values["size"] = ratio + if encoded := imageSizeValueForDeclaredPreset(capabilityConfig.Image, fmt.Sprint(value), stringValue(values["imageResolution"])); encoded != "" { + values["size"] = encoded } } resolved, parameterErr := resolveImageParameterValues(parameters, values) diff --git a/backend/internal/app/task_creation_test.go b/backend/internal/app/task_creation_test.go index 9fb6a847e..fb3217bd2 100644 --- a/backend/internal/app/task_creation_test.go +++ b/backend/internal/app/task_creation_test.go @@ -6,6 +6,7 @@ import ( "testing" "yingce/backend/internal/model" + "yingce/backend/internal/protocol" "yingce/backend/internal/repository" "gorm.io/driver/sqlite" @@ -475,6 +476,46 @@ func TestImageResolutionPricingOnSystemChannel(t *testing.T) { } } +func TestParameterizedOpenAIAspectPresetSystemChannelPersistsPixelExecutionSize(t *testing.T) { + profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceOpenAIImage), "GPT Image 2.5") + profile.Image.Parameters = []protocol.ImageParameter{{ + Name: "size", Label: "画幅 / 尺寸", Type: "string", + Values: []any{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024", + Pricing: true, Binding: "aspectRatio", Encoding: "pixels", + }} + profile.Image.Size = ImageSizeConfig{ + Parameter: "size", Values: []string{"auto", "1824x1024", "1024x1824", "1024x1024"}, Default: "1824x1024", + Presets: []ImageSizePreset{ + {Tier: "1k", Ratio: "16:9", Size: "1824x1024", Width: 1824, Height: 1024}, + {Tier: "1k", Ratio: "9:16", Size: "1024x1824", Width: 1024, Height: 1824}, + {Tier: "1k", Ratio: "1:1", Size: "1024x1024", Width: 1024, Height: 1024}, + }, + } + if err := projectImageParameters(profile.Image); err != nil { + t.Fatal(err) + } + svc, _, channel, channelModel := createSystemChannelSelectionFixture(t, "image", model.ChannelInterfaceOpenAIImage, profile, []model.ChannelModelPriceTier{ + newSelectionPriceTier("tier-1k", `{"size":"1824x1024"}`, "provider-1k", "fixed_request"), + }) + input := map[string]any{ + "mode": "image", + "config": map[string]any{ + "channelId": channel.ID, + "model": channelModel.ModelKey, + "imageParameters": map[string]any{"size": "16:9"}, + }, + } + resolved, err := svc.resolveSystemChannelModelSelection(input, "canvas_image", "") + if err != nil { + t.Fatalf("resolve error = %v", err) + } + config := resolved["config"].(map[string]any) + parameters := config["imageParameters"].(map[string]any) + if parameters["size"] != "1824x1024" || config["size"] != "1824x1024" { + t.Fatalf("parameterized size = %#v, config = %#v", parameters["size"], config["size"]) + } +} + func TestOpenAIAspectPresetSystemChannelPersistsPixelExecutionSize(t *testing.T) { profile := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceOpenAIImage), "gpt-image-2.5-4k") profile.Image.Size = ImageSizeConfig{