diff --git a/backend/internal/app/cloud_agent_media.go b/backend/internal/app/cloud_agent_media.go index 5c035a1d7..137afc489 100644 --- a/backend/internal/app/cloud_agent_media.go +++ b/backend/internal/app/cloud_agent_media.go @@ -291,6 +291,10 @@ func cloudAgentWrapMediaAdmissionError(err error) error { if errors.As(err, &admissionErr) { return err } + // 能力校验拒绝的参数可以由模型自己改正,见 cloud_agent_media_capability_error.go。 + if argument := cloudAgentMediaCapabilityArgumentError(err); argument != nil { + return argument + } retryable := cloudAgentMediaAdmissionRetryable(err) action := "report_to_user" if retryable { diff --git a/backend/internal/app/cloud_agent_media_capability_error.go b/backend/internal/app/cloud_agent_media_capability_error.go new file mode 100644 index 000000000..f6acbc92c --- /dev/null +++ b/backend/internal/app/cloud_agent_media_capability_error.go @@ -0,0 +1,39 @@ +package app + +import "strings" + +// 模型能力校验(model_capability_validate.go)拒绝的生成参数属于「模型填错了参数」, +// 不是模型、权限或预算问题。此前它们和其它准入失败一样被包成不可重试的 +// media_admission_failed:工具结果只有一句「工具执行失败」,整轮直接终止, +// 模型既不知道错在哪个参数,也没有机会改正;用户要求重试时它只能原样再错一次。 +// +// 这里把这类错误改报为字段级参数错误:结果里带着具体原因和取值来源, +// 走已有的 fix_arguments 纠正通道(次数仍受 cloudAgentToolAttemptLimit 限制), +// 没有任何任务被提交,也不会产生费用。 +var cloudAgentMediaCapabilityArgumentHints = []struct { + marker string + field string + hint string +}{ + {"图片尺寸不在当前模型支持范围内", "size", "size 必须取 model_list 返回的该模型 options.image.size.values 中的一个值(通常是像素尺寸,例如 1536x2752),不要写 9:16 这样的比例"}, + {"当前分辨率不支持所选图片宽高比", "size", "所选 quality 档位下没有这个宽高比;按 model_list 返回的该模型 options.image.size.presets 选一组匹配的比例与分辨率"}, + {"图片质量不在当前模型支持范围内", "quality", "quality 必须取 model_list 返回的该模型 options.image.quality.values 中的一个值,或省略"}, + {"视频时长不在当前模型支持范围内", "durationSeconds", "durationSeconds 必须落在 model_list 返回的该模型 options.video.duration 给出的范围或固定值内"}, + {"画面比例不在当前模型支持范围内", "size", "视频的 size 必须取 model_list 返回的该模型 options.video.ratios 中的一个比例"}, + {"输出分辨率不在当前模型支持范围内", "quality", "视频的 quality 必须取 model_list 返回的该模型 options.video.resolutions 中的一个值,大小写保持一致"}, +} + +// cloudAgentMediaCapabilityArgumentError 识别能力校验错误并转成可纠正的参数错误; +// 其它错误返回 nil,由调用方按原口径处理。 +func cloudAgentMediaCapabilityArgumentError(err error) error { + if err == nil { + return nil + } + message := err.Error() + for _, item := range cloudAgentMediaCapabilityArgumentHints { + if strings.Contains(message, item.marker) { + return cloudAgentFieldError(item.field, "unsupported_value", item.marker+";"+item.hint+"。未提交生成任务,改正后可直接重新调用") + } + } + return nil +} diff --git a/backend/internal/app/cloud_agent_media_capability_error_test.go b/backend/internal/app/cloud_agent_media_capability_error_test.go new file mode 100644 index 000000000..0b68b4a7c --- /dev/null +++ b/backend/internal/app/cloud_agent_media_capability_error_test.go @@ -0,0 +1,57 @@ +package app + +import ( + "errors" + "strings" + "testing" +) + +// 复现:generate_media 把 size 写成 "9:16" 这类比例值,而模型只接受像素尺寸; +// 修复前结果只有「工具执行失败」,整轮终止,重试时模型原样再错。 +func TestCloudAgentMediaCapabilityErrorBecomesCorrectableArgumentError(t *testing.T) { + wrapped := cloudAgentWrapMediaAdmissionError(BadAuthRequest("图片尺寸不在当前模型支持范围内")) + + var argumentErr *cloudAgentArgumentError + if !errors.As(wrapped, &argumentErr) { + t.Fatalf("capability rejection is not an argument error: %#v", wrapped) + } + var fieldErr *cloudAgentFieldArgumentError + if !errors.As(wrapped, &fieldErr) || fieldErr.Field != "size" { + t.Fatalf("field = %#v", fieldErr) + } + class, retryable, action := cloudAgentToolErrorClass(CloudAgentRequest{}, cloudAgentCall{}, wrapped, true) + if class != cloudAgentToolErrorSchemaError || !retryable || action != "fix_arguments" { + t.Fatalf("class=%s retryable=%v action=%s", class, retryable, action) + } + text := cloudAgentSafeToolError(wrapped) + if !strings.Contains(text, "图片尺寸不在当前模型支持范围内") || !strings.Contains(text, "model_list") { + t.Fatalf("model does not see the reason: %q", text) + } +} + +func TestCloudAgentMediaCapabilityErrorCoversDurationRatioAndResolution(t *testing.T) { + for message, field := range map[string]string{ + "视频时长不在当前模型支持范围内": "durationSeconds", + "画面比例不在当前模型支持范围内": "size", + "输出分辨率不在当前模型支持范围内": "quality", + "图片质量不在当前模型支持范围内": "quality", + } { + var fieldErr *cloudAgentFieldArgumentError + if !errors.As(cloudAgentWrapMediaAdmissionError(BadAuthRequest(message)), &fieldErr) || fieldErr.Field != field { + t.Fatalf("%s → %#v, want field %s", message, fieldErr, field) + } + } +} + +// 模型、权限、预算等其它准入失败保持原口径:不可自动重试,交给用户。 +func TestCloudAgentMediaAdmissionErrorStaysTerminalForOtherFailures(t *testing.T) { + wrapped := cloudAgentWrapMediaAdmissionError(BadAuthRequest("已超过本轮视频时长预算")) + var admissionErr *cloudAgentMediaAdmissionError + if !errors.As(wrapped, &admissionErr) || admissionErr.Retryable || admissionErr.RequiredAction != "report_to_user" { + t.Fatalf("budget failure changed class: %#v", wrapped) + } + var argumentErr *cloudAgentArgumentError + if errors.As(wrapped, &argumentErr) { + t.Fatal("budget failure became a correctable argument error") + } +} diff --git a/backend/internal/app/cloud_agent_runtime_media.go b/backend/internal/app/cloud_agent_runtime_media.go index f6f63d564..6cd03dd44 100644 --- a/backend/internal/app/cloud_agent_runtime_media.go +++ b/backend/internal/app/cloud_agent_runtime_media.go @@ -216,6 +216,8 @@ func (s *Service) cloudAgentMediaError(run *model.CloudAgentExecution, state *cl return cloudAgentSave(current, state) } if phase == "admission" && !submitted { + // 工具结果里的原因会脱敏;原始原因只进服务端日志,排查准入失败时以它为准。 + log.Printf("[Agent] media admission rejected run=%s: %v", run.ID, err) err = cloudAgentWrapMediaAdmissionError(err) } toolName := state.Calls[state.CallIndex].Function.Name