feat(channels): IM 入站图片采集层(图片白名单 + 微信图片来源 + 复用 vision 内联入口)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- mediafetch 新增**图片白名单**(扩展名 + **字节嗅探兜底**;与文档/音频**三份名单互不污染**),
  取回/裁决/上限全部走既有 Fetch(**来源白名单 deny-by-default** + 逐跳重定向校验 + 边读边限 + 超时)
  ⇒ 没有新造第四份取回实现
- 微信:给图片 URL 配来源白名单(沿用该渠道 CDN 根口径)
- vision 的 `data:` URI 内联入口为**只读复用**(未改 vision 一行)
- **行为变更**:图片现在**会**被尝试取回(**字节嗅探是唯一裁决权** —— 不是图片字节就不写 `Data`);
  其它渠道尚无图片来源白名单 ⇒ 由 deny-by-default 拒绝(**安全**,只是暂读不到,走诚实降级)——
  故同步更新了 kook 的既有测试(原断言「图片保持既有路径 = 不取回」已随口径演进)
- ⚠️ **`channelbridge` 一个字没碰**(接线点在切片报告 §7),等监工接线 + 补其它渠道白名单
- 测试:3 个新文件;变异 5 条
This commit is contained in:
AskaEth committed 2026-10-05 11:04:41 +08:00
1 parent 5de708a560
commit 6a545badbd
8 files changed
+1004 -37

No files matched your search

+36 -13
View File
@@ -230,20 +230,43 @@ func TestInboundBodyURLIsNeverFetched(t *testing.T) {
}
}
// TestInboundImageMediaKeepsExistingPath 钉住边界:图片类媒体不走文档内联。
func TestInboundImageMediaKeepsExistingPath(t *testing.T) {
// kookPNG 是一份「字节嗅探认得出来」的 PNG 载荷(`http.DetectContentType` 只认 8 字节签名)。
var kookPNG = []byte("\x89PNG\r\n\x1a\n" + "fake-png-body")
// TestInboundImageMediaIsInlinedOnlyWhenBytesAreImages 钉住切片 IM-IMG 之后的新边界:
// 图片类媒体**现在也进内联链路**(取回字节,供桥接器内联成 data: URI 进视觉链路),但
// 「字节嗅探是唯一裁决权」——假服务端回文本时它**绝不**被写进 `Data`,回真图字节时才写。
// ⚠️ KOOK 自己的图片来源白名单与上游接线属后续切片(本切片只做微信)。
func TestInboundImageMediaIsInlinedOnlyWhenBytesAreImages(t *testing.T) {
t.Parallel()
s := newKookAssetServer(t)
a, c, _ := newInlineKookAdapter(t, s)
t.Run("字节不是图片: 取回但不内联", func(t *testing.T) {
t.Parallel()
s := newKookAssetServer(t) // 默认正文是文本(Content-Type: text/plain)
a, c, _ := newInlineKookAdapter(t, s)
if err := a.handleEvent(context.Background(), kookEventJSON(kindImage, s.srv.URL+"/asset/photo.png"), nil); err != nil {
t.Fatalf("handleEvent: %v", err)
}
if err := a.handleEvent(context.Background(), kookEventJSON(kindImage, s.srv.URL+"/asset/photo.png"), nil); err != nil {
t.Fatalf("handleEvent: %v", err)
}
if s.hits.Load() != 0 {
t.Fatalf("图片媒体被文档内联取回了 %d 次,want 0", s.hits.Load())
}
if len(c.message(t).Media[0].Data) != 0 {
t.Fatal("图片媒体被写入了字节(应留给视觉链路)")
}
if s.hits.Load() != 1 {
t.Fatalf("图片声明的媒体取回次数 = %d, want 1(图片现在也进内联链路)", s.hits.Load())
}
if len(c.message(t).Media[0].Data) != 0 {
t.Fatal("非图片字节被写入了字节(嗅探是唯一裁决权)")
}
})
t.Run("字节是真图: 内联", func(t *testing.T) {
t.Parallel()
s := newKookAssetServer(t)
s.body = kookPNG
a, c, _ := newInlineKookAdapter(t, s)
if err := a.handleEvent(context.Background(), kookEventJSON(kindImage, s.srv.URL+"/asset/photo.png"), nil); err != nil {
t.Fatalf("handleEvent: %v", err)
}
if !bytes.Equal(c.message(t).Media[0].Data, kookPNG) {
t.Fatalf("Media.Data = %q, want 取回的图片字节", c.message(t).Media[0].Data)
}
})
}