package loop import ( "context" "errors" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // ImageResolver 是循环通向多模态视觉层的接缝(design/06 // §0)。它在 Run 开始时对每条携带图像内容部件的消息调用一次, // 并可以改写该消息,使主模型能对图像进行推理。具体 // resolver 位于 vision 包,它通过 vision Router 决定 // 是保留图像给支持视觉的主模型,还是折叠为 // vision/OCR 工具产出的文字描述(「先 // 看图再对话」)。为 nil 时图像内容保持原样,直接流向 // 支持视觉的主模型。 type ImageResolver interface { // Resolve 改写 msg,其中至少携带一个图像内容部件。它 // 返回(可能已改写的)消息。出错时保留原始 // 消息,使该 turn 仍能继续。 Resolve(ctx context.Context, msg model.Message) (model.Message, error) } // ImageResolveFunc 把一个函数适配为 ImageResolver 接口。 type ImageResolveFunc func(ctx context.Context, msg model.Message) (model.Message, error) // Resolve 实现 ImageResolver。 func (f ImageResolveFunc) Resolve(ctx context.Context, msg model.Message) (model.Message, error) { return f(ctx, msg) } // resolveImages 用注入的 resolver 改写 messages 中每条 // 携带图像的消息。未配置 resolver、没有图像、或某条消息 // 解析失败(失败会被记录日志并保留原消息,使该 turn 仍能 // 运行)时,它原样返回输入切片。任一消息发生变化时 // 返回一个新切片。 func (l *Loop) resolveImages(ctx context.Context, messages []model.Message) []model.Message { if l.imageResolver == nil { return messages } out := make([]model.Message, len(messages)) changed := false for i, m := range messages { if !messageHasImage(m) { out[i] = m continue } resolved, err := l.imageResolver.Resolve(ctx, m) if err != nil { // 图像解析失败(尤其:URL 来源已失效)在这里留下可诊断信号 —— // 原消息照旧保留、回合继续,但不再静默:host 是来源 URL 的主机, // 不含路径、查询串与凭据(由 SourceFailure 保证)。 l.log.Warn("image resolve", "error", err, "host", imageSourceHost(err)) out[i] = m continue } out[i] = resolved changed = true } if !changed { return messages } return out } // imageSourceHost 从图像解析失败错误里取出来源主机(vision 的取回失败 // 错误以结构断言暴露它),使 WARN 能定位到具体来源。错误不带这个信息 // (解析失败、非图像、无解析器等)时返回空串。用结构断言而不是导入 // vision 包:loop 只需知道「有个东西能报主机」,不需要依赖取回实现。 func imageSourceHost(err error) string { var withHost interface{ ImageSourceHost() string } if errors.As(err, &withHost) { return withHost.ImageSourceHost() } return "" } // messageHasImage 报告 msg 是否有任何内容部件是图像部件。 func messageHasImage(m model.Message) bool { for _, part := range m.Content { if part.Type == model.ContentTypeImage { return true } } return false }