From 60bad8cad4fdfb2b2e4f5e0e85204c9bdaba5a4e Mon Sep 17 00:00:00 2001 From: VicoHu Date: Thu, 17 Sep 2026 18:12:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(protocol):=20Seedream=20=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E5=BC=8F=E7=BB=93=E6=9E=9C=20-=20=E8=A3=B8=20b64=5Fjson=20?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=20data=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 声明式解析把 Ark/Seedream 的裸 b64_json 写成 DataURL 时未加 data: 前缀,导致图片任务在上游成功后仍报媒体结果下载失败。 Co-authored-by: Cursor --- backend/internal/app/provider_audio.go | 2 +- backend/internal/protocol/manifest.go | 46 +++++++++++++++++++ .../protocol/official_catalog_test.go | 24 ++++++++++ docs/content/docs/progress/pending-test.mdx | 8 ++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/backend/internal/app/provider_audio.go b/backend/internal/app/provider_audio.go index 31c7db436..03ddcef91 100644 --- a/backend/internal/app/provider_audio.go +++ b/backend/internal/app/provider_audio.go @@ -172,7 +172,7 @@ func asyncAudioErrorMessage(state map[string]interface{}) string { func decodeProviderDataURL(value string) (string, []byte, error) { header, encoded, ok := strings.Cut(value, ",") if !ok || !strings.HasPrefix(header, "data:") || !strings.HasSuffix(strings.ToLower(header), ";base64") { - return "", nil, errors.New("音频 data URL 格式无效") + return "", nil, errors.New("data URL 格式无效") } mimeType := strings.TrimSuffix(strings.TrimPrefix(header, "data:"), ";base64") data, err := base64.StdEncoding.DecodeString(encoded) diff --git a/backend/internal/protocol/manifest.go b/backend/internal/protocol/manifest.go index f38e890c0..7bd34b76c 100644 --- a/backend/internal/protocol/manifest.go +++ b/backend/internal/protocol/manifest.go @@ -711,6 +711,8 @@ func mediaReferencesFromManifestValue(value any, kind string, ephemeral bool) [] if reference.DataURL == "" { reference.DataURL = firstString(typed, "data_url", "b64_json") } + // OpenAI / Ark 等渠道常直接返回裸 b64_json;声明式结果下载要求 data URL。 + reference.DataURL = normalizeManifestInlineDataURL(reference.DataURL, reference.Kind, firstString(typed, "output_format", "mime_type", "mimeType")) if reference.URL != "" || reference.DataURL != "" { result = append(result, reference) } @@ -719,6 +721,50 @@ func mediaReferencesFromManifestValue(value any, kind string, ephemeral bool) [] return result } +func normalizeManifestInlineDataURL(value, kind, formatHint string) string { + value = strings.TrimSpace(value) + if value == "" || strings.HasPrefix(value, "data:") { + return value + } + return "data:" + manifestInlineMediaMIME(kind, formatHint) + ";base64," + value +} + +func manifestInlineMediaMIME(kind, formatHint string) string { + hint := strings.ToLower(strings.TrimSpace(formatHint)) + switch hint { + case "image/png", "image/jpeg", "image/webp", "image/gif", "audio/mpeg", "audio/wav", "audio/ogg", "video/mp4", "video/webm": + return hint + case "image/jpg": + return "image/jpeg" + case "audio/mp3": + return "audio/mpeg" + case "png": + return "image/png" + case "jpeg", "jpg": + return "image/jpeg" + case "webp": + return "image/webp" + case "gif": + return "image/gif" + case "mp3", "mpeg": + return "audio/mpeg" + case "wav": + return "audio/wav" + case "mp4": + return "video/mp4" + case "webm": + return "video/webm" + } + switch strings.ToLower(strings.TrimSpace(kind)) { + case "audio": + return "audio/mpeg" + case "video": + return "video/mp4" + default: + return "image/png" + } +} + var ( manifestMDImageRegex = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`) manifestHTMLVideoRegex = regexp.MustCompile(`(?i)]+src=['"]([^'"]+)['"]`) diff --git a/backend/internal/protocol/official_catalog_test.go b/backend/internal/protocol/official_catalog_test.go index db64ef790..a7839099c 100644 --- a/backend/internal/protocol/official_catalog_test.go +++ b/backend/internal/protocol/official_catalog_test.go @@ -784,3 +784,27 @@ func TestOfficialOpenAIAudioSpeedDefaultsInvalidAndZeroValues(t *testing.T) { }) } } + +func TestOfficialArkSeedreamParsesB64JSONAsDataURL(t *testing.T) { + for _, tc := range []struct { + packageName, providerID string + }{ + {"volcengine-ark-seedream.yingce-plugin", "volcengine-ark-image"}, + {"volcengine-ark-agent-plan-seedream.yingce-plugin", "volcengine-ark-agent-plan-image"}, + } { + t.Run(tc.providerID, func(t *testing.T) { + adapter := officialPackageAdapter(t, tc.packageName, tc.providerID) + result, err := adapter.ParseCreate(context.Background(), []byte(`{"created":1,"data":[{"b64_json":"aW1hZ2U=","output_format":"jpeg","size":"1824x1024"}]}`)) + if err != nil { + t.Fatal(err) + } + if result.Result == nil || len(result.Result.Images) != 1 { + t.Fatalf("result = %#v", result.Result) + } + dataURL := result.Result.Images[0].DataURL + if dataURL != "data:image/jpeg;base64,aW1hZ2U=" { + t.Fatalf("DataURL = %q, want jpeg data URL from b64_json + output_format", dataURL) + } + }) + } +} diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index f2a2e2347..77373a84c 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -12,6 +12,14 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 - 宿主已把 Agent Plan 协议纳入与官方 Ark 相同的能力默认值、Token 计费校验、方舟素材引用、取消任务与前端 payload 分支。 - 待管理端安装这两个 `.yingce-plugin` 后:分别创建图片/视频渠道,确认默认 Base URL 为 `/api/plan/v3`;用 Agent Plan Key 文生图、参考图生图、Seedance 文生视频与全模态参考成功;故意混用官方 Key 应失败;官方 `volcengine-ark-*` 渠道行为不受影响。 +## Seedream 声明式结果裸 b64_json(2026-09-17) + +根因:`volcengine-ark-seedream` / `volcengine-ark-agent-plan-seedream` 响应把 `response.data` 原样映射为 images;解析回退把裸 `b64_json` 写入 `DataURL` 却未加 `data:` 前缀,随后 `decodeProviderDataURL` 失败。UI 报「声明式协议媒体结果下载失败:音频 data URL 格式无效」(旧错误文案误写「音频」),但管理端请求明细显示上游 HTTP 200 且已返回内嵌编码。修复:声明式媒体解析对裸 base64 按 `output_format`/kind 补全 data URL;错误文案改为通用「data URL 格式无效」。协议单测已覆盖。需重建后端容器后浏览器验收: + +- [ ] Agent Plan / 官方 Seedream 图片渠道(`response_format=b64_json`)文生图、参考图生图成功落盘,不再出现「媒体结果下载失败」。 +- [ ] 上游返回 `output_format=jpeg` 时结果可正常预览。 +- [ ] 管理端该任务对应 create 日志仍为 succeeded / 200,与前端成功态一致。 + ## Agent Plan Seedream 误走可信素材(2026-09-17) 根因:`isArkPlanVideoConfig` / 前端 `usesArkVideoAssetReference` 仅按 Base URL 含 `/api/plan/v3` 判断,Agent Plan Seedream 与 Seedance 共用该路径,图片生成被误判为视频可信素材路径,自动 CreateAsset 并把参考图改成 `asset://`,上游图片接口拒绝。修复:后端仅 `mode=video` 且非图片协议时同步可信素材;前端仅视频 mode + 视频协议(或非图片协议的 Plan BaseURL)才提交 `asset://`。后端单测已覆盖。需浏览器验收: