fix(upstream): vivid 上传会话宽容解析 + 错误回显原始响应

vivid 协议各实现的 /v1/uploads 响应形状不一(顶层 / data 包裹、
url|upload_url|presigned_url、key|file_key|object_key)。之前只认顶层
url+key,遇到包装或别名就报 "upload session missing url/key" 且看不
到上游实际返回。现在逐层(含嵌套 data/result/upload/session)搜寻两
个字段,仍缺失时把原始 JSON 截断进错误信息便于定位。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-14 18:15:47 +08:00
co-authored by Devin
parent effe76d83f
commit b17cc3e9c8
+44 -3
View File
@@ -189,10 +189,10 @@ func (c *Client) vividUploadRef(ctx context.Context, baseURL, apiKey string, dat
if err != nil { if err != nil {
return "", err return "", err
} }
putURL := strings.TrimSpace(stringValue(up["url"])) putURL, key := vividUploadSession(up)
key := strings.TrimSpace(stringValue(up["key"]))
if putURL == "" || key == "" { if putURL == "" || key == "" {
return "", fmt.Errorf("%w: upload session missing url/key", ErrTemporaryUpstream) raw, _ := json.Marshal(up)
return "", fmt.Errorf("%w: upload session missing url/key (got %s)", ErrTemporaryUpstream, clip(raw, 200))
} }
req, err := http.NewRequest(http.MethodPut, putURL, bytes.NewReader(data)) req, err := http.NewRequest(http.MethodPut, putURL, bytes.NewReader(data))
if err != nil { if err != nil {
@@ -212,6 +212,47 @@ func (c *Client) vividUploadRef(ctx context.Context, baseURL, apiKey string, dat
return key, nil return key, nil
} }
// vividUploadSession extracts the presigned PUT url + object key from a
// /v1/uploads response. Vivid-protocol implementations vary: some return the
// pair at top level, others wrap it in {"data":{...}} / {"result":{...}}, and
// field spellings differ (upload_url / presigned_url / put_url; key /
// file_key / object_key). Search top level first, then one layer deep.
func vividUploadSession(up map[string]any) (putURL, key string) {
urlKeys := []string{"url", "upload_url", "uploadUrl", "presigned_url", "presignedUrl", "put_url", "putUrl", "signed_url", "signedUrl"}
keyKeys := []string{"key", "file_key", "fileKey", "object_key", "objectKey", "upload_key", "uploadKey"}
pick := func(m map[string]any) (string, string) {
var u, k string
for _, f := range urlKeys {
if u = strings.TrimSpace(stringValue(m[f])); u != "" {
break
}
}
for _, f := range keyKeys {
if k = strings.TrimSpace(stringValue(m[f])); k != "" {
break
}
}
return u, k
}
levels := []map[string]any{up}
for _, nest := range []string{"data", "result", "upload", "session", "upload_session"} {
if sub, ok := up[nest].(map[string]any); ok {
levels = append(levels, sub)
}
}
// url 和 key 允许跨层出现(比如 url 在顶层、key 在 data 里)。
for _, m := range levels {
u, k := pick(m)
if putURL == "" {
putURL = u
}
if key == "" {
key = k
}
}
return putURL, key
}
// vividRefKindMime classifies a reference blob into the vivid "kind" (image / // vividRefKindMime classifies a reference blob into the vivid "kind" (image /
// video / audio) plus a concrete mime. Magic bytes first (DetectContentType // video / audio) plus a concrete mime. Magic bytes first (DetectContentType
// misses mov/m4a/ogg-audio), then sniffing, then a png default. // misses mov/m4a/ogg-audio), then sniffing, then a png default.