package vision import ( "fmt" "strings" ) // 本文件管的是 **data:/base64 入参**的内容嗅探与可行动报错 —— 与 mime.go 的 // imageMIMEFor 是同一套裁决(真实字节是否决权、声明只作参考),只是换了入口: // mime.go 管 HTTP 响应与文件读取,这里管「调用方把图片正文直接塞进参数」那条路。 // // 起因(2026-10-07 18:32:51 实测,客户端从后端逐字节取过证据):模型交给 // vision 工具的 image 是 `data:image/png;base64,` —— **只有前缀、没有载荷**。 // 旧代码把它 base64 解码得到零字节,却照样报 MIME=image/png,于是那串空载荷 // 挂着 image/png 的声明白白发给上游,换回一句「You have uploaded an unsupported // image」。报错指向不了真正的毛病(参数里根本没有图片字节),模型也无从自纠; // image-crop 在同一秒也栽在同一处,报的是 `image: unknown format`。 // 现在这类输入在本地就被明确否决,并附上「它看起来是什么」的物证与可行动提示。 // supportedFormats 是视觉上游接受的图像格式(与上游白名单同口径: // webp / png / jpeg / gif)。写进错误文案,模型据此换格式重试。 const supportedFormats = "webp, png, jpeg, gif" // reuseHint 是「把非图片当图片传」时的可行动提示。模型复述不出会话里那张图的 // 几万字符 base64(实测就是把前缀抄了下来),正确做法是传它的 URL 或本地路径。 const reuseHint = "pass the image's http(s) URL or local path instead of retyping a data: URI" // evidenceBytes 是报错时作为物证给出的头部字节数。 const evidenceBytes = 16 // checkInlinePayload 校验 data: URI 解码后的载荷,返回应当声明的 MIME 与载荷 // 本身。判据与路径/URL 来源完全一致(复用 imageMIMEFor):判出文本一律否决, // 判不出图像而声明也立不住时同样否决 —— 绝不把一段不是图片的字节挂着 // image/* 的声明送进视觉模型。 // // 返回的 MIME 取 data: 前缀里**声明**的那个,而不是嗅探结果:这条路径的既有 // 契约是「客户端送来的 data: URI 逐字节不变地继续工作」 // (url_test.go 的 TestOpenAICompatibleAdapter_Base64Unchanged),换掉声明 // 就等于改客户端的线上形状;嗅探在这里只当否决权用。 func checkInlinePayload(declared string, data []byte) (string, []byte, error) { if len(data) == 0 { return "", nil, fmt.Errorf("vision: %w: %s payload is empty — the data: URI carries no base64 bytes; hint: %s", ErrNotImage, declaredLabel(declared), reuseHint) } if _, err := imageMIMEFor(declared, data); err != nil { return "", nil, fmt.Errorf("vision: %w: %s payload is %s; supported formats: %s; leading bytes: %s; hint: %s", ErrNotImage, declaredLabel(declared), mediaType(sniffMIME(data)), supportedFormats, hexHead(data), reuseHint) } return declared, data, nil } // hexHead 把载荷开头若干字节渲染成十六进制,作为「它看起来是什么」的物证。 // 只给固定长度的头部 —— **绝不回显正文**:载荷可能很大,也可能含隐私内容 // (同 imageMIMEFor 的口径:错误信息只含类型名,不含图像字节)。 func hexHead(data []byte) string { n := min(len(data), evidenceBytes) parts := make([]string, 0, n) for _, b := range data[:n] { parts = append(parts, fmt.Sprintf("%02x", b)) } return strings.Join(parts, " ") }