fix(media): 统一图片结果 URL 化并限制同步 Base64

将上游 URL 直接持久化,内联媒体经对象存储后仅保留 URL 与内部定位元数据;异步轮询、任务详情和幂等重放统一使用零对象读取的 URL 投影,并增加 64KiB 响应门禁。

OpenAI 图片接口接受 url 与 b64_json,同步 Base64 限制为 20MiB 和每 Pod 2 并发;新增历史结果迁移清零门禁、结果指标和 API GOMEMLIMIT。

验证:API go test ./...、go vet、聚焦 race、pnpm openapi、pnpm lint/test/build、迁移安全检查与 docker compose config 均通过。
This commit is contained in:
2026-08-05 18:15:06 +08:00
parent f9b945e4aa
commit b13392ef50
22 changed files with 1367 additions and 176 deletions
+64 -14
View File
@@ -55,6 +55,43 @@ func TestGeneratedAssetDecisionUploadsInlineImageBase64(t *testing.T) {
}
}
func TestUploadGeneratedAssetsReusesDuplicateSHAWithinTask(t *testing.T) {
putCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "unexpected method", http.StatusMethodNotAllowed)
return
}
putCount++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := &Service{}
service.directOSS = &directOSSUploader{
endpoint: server.URL, bucket: "bucket", accessKeyID: "access-id", accessKeySecret: "access-secret", objectPrefix: "media",
}
payload := append([]byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}, bytes.Repeat([]byte{0}, 32)...)
encoded := base64.StdEncoding.EncodeToString(payload)
result := map[string]any{"data": []any{
map[string]any{"b64_json": encoded, "mime_type": "image/png"},
map[string]any{"b64_json": encoded, "mime_type": "image/png"},
}}
stored, err := service.uploadGeneratedAssets(t.Context(), "task-duplicate", "images.generations", "", result)
if err != nil {
t.Fatal(err)
}
items := stored["data"].([]any)
first := items[0].(map[string]any)
second := items[1].(map[string]any)
if putCount != 1 || stringFromAny(first["url"]) == "" || first["url"] != second["url"] {
t.Fatalf("duplicate SHA was not reused: puts=%d first=%#v second=%#v", putCount, first, second)
}
if TaskResultHasInlineBinary(stored) || first["b64_json"] != nil || second["b64_json"] != nil {
t.Fatalf("inline payload remained after deduplicated upload: %#v", stored)
}
}
func TestMediaResultMaterializationConcurrencyIsBounded(t *testing.T) {
service := New(config.Config{MediaMaterializationConcurrency: 1}, nil, nil)
result := map[string]any{
@@ -227,6 +264,15 @@ func TestGeneratedAssetDecisionUploadsURLWhenPolicyUploadAll(t *testing.T) {
}
}
func TestGeneratedAssetDecisionRejectsUnsupportedURLScheme(t *testing.T) {
_, err := generatedAssetDecisionForItem("images.generations", map[string]any{
"url": "ftp://files.example/result.png", "type": "image",
}, defaultGeneratedAssetUploadPolicy())
if clients.ErrorCode(err) != "invalid_upstream_result" {
t.Fatalf("unexpected error: %v", err)
}
}
func TestGeneratedAssetUploadPolicyFromName(t *testing.T) {
tests := []struct {
name string
@@ -329,13 +375,15 @@ func TestFinalizeGeneratedAssetsUploadsNestedInlineBinaryUnderDefaultPolicy(t *t
t.Fatal("finalized result still contains inline binary")
}
nested := finalized["provider_result"].(map[string]any)
reference, ok := nested["binary_data_base64"].(map[string]any)
if !ok {
t.Fatalf("nested binary should be replaced by an asset reference: %+v", nested)
if _, exists := nested["binary_data_base64"]; exists {
t.Fatalf("nested Base64 field was retained: %+v", nested)
}
if urlValue := stringFromAny(reference["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") {
if urlValue := stringFromAny(nested["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") {
t.Fatalf("unexpected object storage URL: %s", urlValue)
}
if nested["upload"] == nil {
t.Fatalf("nested upload metadata was not retained: %+v", nested)
}
}
func TestFinalizeGeneratedAssetsMaterializesDetectedMediaAndKeepsOpaqueMetadata(t *testing.T) {
@@ -368,7 +416,7 @@ func TestFinalizeGeneratedAssetsMaterializesDetectedMediaAndKeepsOpaqueMetadata(
t.Fatalf("finalized result still contains generated media: %#v", finalized)
}
reference, ok := finalized["provider_payload"].(map[string]any)
if !ok || reference["assetRef"] == nil || reference["upload"] == nil {
if !ok || reference["url"] == nil || reference["upload"] == nil || reference["assetRef"] != nil {
t.Fatalf("detected media was not objectified: %#v", finalized["provider_payload"])
}
if finalized["thought_signature"] != opaque {
@@ -407,9 +455,11 @@ func TestFinalizeGeneratedAssetsMaterializesGeminiImageData(t *testing.T) {
}
data := finalized["data"].([]any)
item := data[0].(map[string]any)
reference, ok := item["b64_json"].(map[string]any)
if !ok || reference["assetRef"] == nil || reference["upload"] == nil {
t.Fatalf("Gemini b64_json was not replaced by an object reference: %#v", item)
if _, exists := item["b64_json"]; exists {
t.Fatalf("Gemini b64_json was retained: %#v", item)
}
if stringFromAny(item["url"]) == "" || item["upload"] == nil || item["assetRef"] != nil {
t.Fatalf("Gemini b64_json was not replaced by a URL result: %#v", item)
}
}
@@ -474,7 +524,7 @@ func TestUploadGeneratedAudioFailsWithoutObjectStorage(t *testing.T) {
}
}
func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithAssetRef(t *testing.T) {
func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithURL(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer server.Close()
service := &Service{}
@@ -516,9 +566,9 @@ func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithAssetRef(t *tes
if !ok {
t.Fatalf("inlineData.data should be an asset reference, got %+v", inlineData["data"])
}
ref, _ := data["assetRef"].(map[string]any)
if ref["sha256"] == "" || ref["contentType"] != "image/png" || ref["size"] != len(payload) {
t.Fatalf("unexpected asset ref: %+v", ref)
upload, _ := data["upload"].(map[string]any)
if upload["sha256"] == "" || upload["contentType"] != "image/png" || upload["size"] != len(payload) || data["assetRef"] != nil {
t.Fatalf("unexpected URL storage metadata: %+v", data)
}
if urlValue := stringFromAny(data["url"]); !strings.HasPrefix(urlValue, "https://cdn.example.com/media/image_result/") || !strings.HasSuffix(urlValue, ".png") {
t.Fatalf("unexpected raw media URL: %s", urlValue)
@@ -528,7 +578,7 @@ func TestUploadGeneratedRawMediaValueReplacesGeminiInlineDataWithAssetRef(t *tes
}
}
func TestUploadGeneratedRawMediaValueReplacesBufferAndBytesWithAssetRefs(t *testing.T) {
func TestUploadGeneratedRawMediaValueReplacesBufferAndBytesWithURLs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
defer server.Close()
service := &Service{}
@@ -551,7 +601,7 @@ func TestUploadGeneratedRawMediaValueReplacesBufferAndBytesWithAssetRefs(t *test
next := uploaded.(map[string]any)
for _, key := range []string{"buffer", "audio_bytes", "direct"} {
item, ok := next[key].(map[string]any)
if !ok || item["assetRef"] == nil || item["upload"] == nil {
if !ok || item["url"] == nil || item["upload"] == nil || item["assetRef"] != nil {
t.Fatalf("%s was not objectified: %#v", key, next[key])
}
}