fix(vision): 图像 URL 取回失败的 WARN 带来源主机(可诊断且不外泄路径/查询串/凭据)
- loop 解析点用结构断言取 SourceFailure 的主机(errors.As 穿透 %w 包装), 不导入 vision 包、不引入方向性依赖;拿不到来源时留空串而不编造。 - 新增 TestImageSourceHost 四例(nil/普通错误/包装后来源/空主机), 变异验证:把返回值改成 "" 该用例必红(已复跑确认)。
This commit is contained in:
1 parent
910d154d35
commit
127c32b440
2 files changed
+52
-1
No files matched your search
@@ -2,11 +2,46 @@ package loop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fakeSourceErr 模拟 vision 包的 SourceFailure:错误链上能报出来源主机。
|
||||||
|
type fakeSourceErr struct{ host string }
|
||||||
|
|
||||||
|
func (e *fakeSourceErr) Error() string { return "取回失败" }
|
||||||
|
func (e *fakeSourceErr) ImageSourceHost() string {
|
||||||
|
return e.host
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestImageSourceHost 钉住「日志能定位到来源主机」这条契约:错误链上
|
||||||
|
// 带 ImageSourceHost 的证据要穿透 %w 包装被取出;其它错误一律空串
|
||||||
|
// (空串表示无从定位,而不是假装有来源)。
|
||||||
|
func TestImageSourceHost(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "nil 错误", err: nil, want: ""},
|
||||||
|
{name: "普通错误", err: errors.New("解析失败"), want: ""},
|
||||||
|
{name: "包装后的来源错误", err: fmt.Errorf("resolve: %w", &fakeSourceErr{host: "cdn.example.com"}), want: "cdn.example.com"},
|
||||||
|
{name: "包装但主机为空", err: fmt.Errorf("resolve: %w", &fakeSourceErr{}), want: ""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := imageSourceHost(c.err); got != c.want {
|
||||||
|
t.Fatalf("imageSourceHost = %q, want %q", got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRun_ImageResolverFoldsToText(t *testing.T) {
|
func TestRun_ImageResolverFoldsToText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Model: "m", Content: "Done."}}}}
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Model: "m", Content: "Done."}}}}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package loop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||||
)
|
)
|
||||||
@@ -47,7 +48,10 @@ func (l *Loop) resolveImages(ctx context.Context, messages []model.Message) []mo
|
|||||||
}
|
}
|
||||||
resolved, err := l.imageResolver.Resolve(ctx, m)
|
resolved, err := l.imageResolver.Resolve(ctx, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.log.Warn("image resolve", "error", err)
|
// 图像解析失败(尤其:URL 来源已失效)在这里留下可诊断信号 ——
|
||||||
|
// 原消息照旧保留、回合继续,但不再静默:host 是来源 URL 的主机,
|
||||||
|
// 不含路径、查询串与凭据(由 SourceFailure 保证)。
|
||||||
|
l.log.Warn("image resolve", "error", err, "host", imageSourceHost(err))
|
||||||
out[i] = m
|
out[i] = m
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -60,6 +64,18 @@ func (l *Loop) resolveImages(ctx context.Context, messages []model.Message) []mo
|
|||||||
return out
|
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 是否有任何内容部件是图像部件。
|
// messageHasImage 报告 msg 是否有任何内容部件是图像部件。
|
||||||
func messageHasImage(m model.Message) bool {
|
func messageHasImage(m model.Message) bool {
|
||||||
for _, part := range m.Content {
|
for _, part := range m.Content {
|
||||||
|
|||||||
Reference in new issue
Block a user