- 起因(Android 注册的成本项 H1):`resolveImages` 每回合都跑,而 `inlineImageSrc` 对 `data:` 短路、 对 **URL 每回合重新取回并内联** ⇒ ① 同一张图反复网络 I/O;② 来源一旦失效则该回合解析失败, 而 loop 保留原消息不中断回合 ⇒ **模型这轮静默看不到图** - 修法:按**内容哈希**记忆「URL → data URI」结果(有界 + 并发安全,独立小文件承载); **失败不写缓存**(不把坏结果钉住,第二次仍可重试);命中即不再发起网络请求 - **来源失效不再静默**:取回失败产生可诊断信号(含主机与原因,**不含图片内容/凭据**),回合照旧继续 - 复核:vision 包 build/vet/测试绿(含 `-race`);断言覆盖「同一 URL 连续解析 → 上游只被取回 1 次且结果逐字节相同」 「内容变化不被旧缓存命中」「失败不写缓存且第二次仍重试」「容量上限生效」,以及既有断言 (非图片 `ErrNotImage` / SSRF 被拒 / 长度预检)不回归;变异反证已还原
65 lines
3.1 KiB
Go
65 lines
3.1 KiB
Go
package vision
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// vision 包返回的哨兵错误。每个错误向外传递时都以 %w
|
|
// 包装,使调用方能按类别判断,同时保留原始原因。
|
|
var (
|
|
// ErrNoImage 在图像来源字符串为空时返回。
|
|
ErrNoImage = errors.New("vision: empty image source")
|
|
// ErrNotConfigured 在适配器未设置 endpoint/model 时返回。
|
|
ErrNotConfigured = errors.New("vision: adapter not configured")
|
|
// ErrUnsupportedSource 在图像来源无法处理时返回。
|
|
ErrUnsupportedSource = errors.New("vision: unsupported image source")
|
|
// ErrFetch 在 URL 图像无法下载时返回。
|
|
ErrFetch = errors.New("vision: cannot fetch image")
|
|
// ErrUpstream 在 vision/OCR 提供方请求失败时返回。
|
|
ErrUpstream = errors.New("vision: upstream request failed")
|
|
// ErrBadImage 在图像无法读取或解码时返回。
|
|
ErrBadImage = errors.New("vision: cannot read image")
|
|
// ErrNotImage 在来源声明的类型与真实字节都判不出图像时返回
|
|
// (例如把 HTML/PROXY 错误页当成图片送进模型)。
|
|
ErrNotImage = errors.New("vision: source is not an image")
|
|
// ErrBadArgs 在工具参数格式错误时由该工具返回。
|
|
ErrBadArgs = errors.New("vision: bad tool arguments")
|
|
// ErrBlockedAddress 在图像 URL 指向被拒的链路本地/云元数据地址时返回
|
|
// (见 guard.go)。错误只带主机与网段原因,不带 URL 或其中的凭据。
|
|
ErrBlockedAddress = errors.New("vision: image URL points at a blocked address")
|
|
// ErrImageTooLarge 在内联图像(base64 / data: URI)字符串超过上限时返回。
|
|
// 判定在解码与短路之前完成,错误信息只带上限与本次长度。
|
|
ErrImageTooLarge = errors.New("vision: inline image is too large")
|
|
)
|
|
|
|
// SourceFailure 描述一次图像来源取回失败。它只携带主机与底层原因:完整
|
|
// URL 可能带着查询串里的凭据、图像字节可能含隐私内容,两者都不进错误
|
|
// 文案。Host 取不到(URL 无法解析)时为空串。
|
|
//
|
|
// 它实现 Unwrap,因此底层哨兵(ErrFetch / ErrNotImage /
|
|
// ErrBlockedAddress / ErrImageTooLarge …)仍可用 errors.Is 判定;
|
|
// 调用方用 errors.As(*SourceFailure) 或结构断言 ImageSourceHost()
|
|
// 取出主机,把「来源失效」记成 WARN 而不是静默丢图(见 loop/vision.go)。
|
|
type SourceFailure struct {
|
|
// Host 是来源 URL 的主机(含端口),不含 userinfo、路径与查询串。
|
|
Host string
|
|
// Reason 是取回失败的底层原因,保留哨兵链供 errors.Is 判定。
|
|
Reason error
|
|
}
|
|
|
|
// Error 渲染主机与原因;Host 为空时只呈现原因。
|
|
func (e *SourceFailure) Error() string {
|
|
if e.Host == "" {
|
|
return e.Reason.Error()
|
|
}
|
|
return fmt.Sprintf("图像来源取回失败(主机 %s):%v", e.Host, e.Reason)
|
|
}
|
|
|
|
// Unwrap 暴露底层原因,使哨兵判定与具体错误类型照常可用。
|
|
func (e *SourceFailure) Unwrap() error { return e.Reason }
|
|
|
|
// ImageSourceHost 报告来源主机,供调用方按结构断言取用(loop 的解析点),
|
|
// 使 loop 无需依赖本包的取回实现。
|
|
func (e *SourceFailure) ImageSourceHost() string { return e.Host }
|