perf(vision): 图片 URL 的「取回 + 内联」按内容哈希 memo(协同 H1:每回合重复付费/取回)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 起因(Android 注册的成本项 H1):`resolveImages` 每回合都跑,而 `inlineImageSrc` 对 `data:` 短路、
  对 **URL 每回合重新取回并内联** ⇒ ① 同一张图反复网络 I/O;② 来源一旦失效则该回合解析失败,
  而 loop 保留原消息不中断回合 ⇒ **模型这轮静默看不到图**
- 修法:按**内容哈希**记忆「URL → data URI」结果(有界 + 并发安全,独立小文件承载);
  **失败不写缓存**(不把坏结果钉住,第二次仍可重试);命中即不再发起网络请求
- **来源失效不再静默**:取回失败产生可诊断信号(含主机与原因,**不含图片内容/凭据**),回合照旧继续
- 复核:vision 包 build/vet/测试绿(含 `-race`);断言覆盖「同一 URL 连续解析 → 上游只被取回 1 次且结果逐字节相同」
  「内容变化不被旧缓存命中」「失败不写缓存且第二次仍重试」「容量上限生效」,以及既有断言
  (非图片 `ErrNotImage` / SSRF 被拒 / 长度预检)不回归;变异反证已还原
This commit is contained in:
AskaEth committed 2026-10-04 16:32:13 +08:00
1 parent b65fe05635
commit 94b882f4d9
5 files changed
+682 -8

No files matched your search

+26 -5
View File
@@ -135,21 +135,42 @@ func fetchURL(ctx context.Context, rawURL string, client *http.Client) (string,
return mime, data, nil
}
// inlinedImage 是一次「取回 + 内联」的产物。MIME 与 Raw 只为按内容哈希
// 缓存而返回(见 cache.go 的 imageCache),绝不写进日志。
type inlinedImage struct {
// URI 是可供模型直接消费的 data: URI。
URI string
// MIME 是归一化后的图像 MIME;data: 输入(纯短路路径)时为空。
MIME string
// Raw 是内联前的原始字节;data: 输入(纯短路路径)时为 nil。
Raw []byte
}
// inlineImageSrc 把图像来源字符串取回并内联为 data: URI,供主模型
// 直接消费。已是 data: URI 的原样返回(客户端现有的 base64 路径因此
// 逐字节不变),但**短路之前**也要过长度预检:这条路径不解码,不预检
// 就等于把体积上限完全交给入口层。URL 取回复用 Resolve→fetchURL 的既有
// 守卫(协议白名单 + 体积上限 + 地址守卫),并加取回超时。
func inlineImageSrc(ctx context.Context, src string) (string, error) {
img, err := inlineImage(ctx, src)
if err != nil {
return "", err
}
return img.URI, nil
}
// inlineImage 与 inlineImageSrc 同一条路径,但额外返回内联前的 MIME 与
// 原始字节,供 cache.go 按内容哈希 memo 化(同一 URL 不再每回合重复取回)。
func inlineImage(ctx context.Context, src string) (inlinedImage, error) {
if strings.HasPrefix(src, "data:") {
if err := checkInlineImageLength(src); err != nil {
return "", err
return inlinedImage{}, err
}
return src, nil
return inlinedImage{URI: src}, nil
}
ref, err := ParseImageRef(src)
if err != nil {
return "", err
return inlinedImage{}, err
}
if ref.Source == SourceURL {
var cancel context.CancelFunc
@@ -158,9 +179,9 @@ func inlineImageSrc(ctx context.Context, src string) (string, error) {
}
mime, data, err := Resolve(ctx, ref, nil)
if err != nil {
return "", err
return inlinedImage{}, err
}
return DataURI(mime, data), nil
return inlinedImage{URI: DataURI(mime, data), MIME: mime, Raw: data}, nil
}
// splitDataURI 解析 "data:<mime>;base64,<payload>" 前缀。src 不是