- 抽共享存储核心:internal/runtime/documents/store.go(22 项白名单 + 字节嗅探唯一裁决 + 内容寻址 sha256[:32]+ext + 去重 + 限额 + 路径双检)+ store_meta.go(documents 表唯一写入实现)。
- gateway 改为调用 store,**对外行为零变化**(机器证明:既有 documents_test/chat_documents_test/chat_documents_bounds_test 三个文件 0 行改动且全绿;状态码/响应字段/错误码/审计动作名/限额文档口径全部逐字保留;maxChatDocuments 改为引用 documents.MaxPerMessage)。
- IM 入站附件摄取(channelbridge):文件类媒体 → store.Save → 挂 model.DocumentMessage(RoleUser) 部件 ⇒ 后续 resolveDocuments 自动抽取/注入/清单;挂载点在「会话解析 + 黑白名单 + 最小打扰」三重闸门之后(被拦消息绝不在盘上留文件);**不支持的格式不静默**(诚实提示行 + 带来源日志,只记 name/mime/size,名字做展示净化防伪造行);图片/语音保持既有链路。
- 主会话独立验收:隔离 worktree(M1 的 12 文件)build/vet 绿、全树 59 包 0 FAIL、双平台交叉编译绿、12 条 Inbound 用例 PASS;变异独立复现(关闭 IM 摄取 ⇒ 5 条红,含「落盘文件数 = [] want 1」)。
- ⚠️ 真机边界(需裁决):现网四个适配器上报的文件媒体**只带 URL**(telegram FileID / qq-obv11 file|url / weixin·kook CDN URL),Media.Data 恒空 ⇒ 本切片只摄**内联字节**,URL 一律「诚实提示,不代取远程」(避免第三份 SSRF 实现)。真机要让「她读到 IM 文件」,需各适配器入站时下载并内联(凭据在各自渠道里,推荐)或新增共享受守卫抓取器。
- 另登记:IM 附件暂未写 chat_messages.document 列(二手回合看不到引用,建议并入 S7);channelbridge 既有测试夹具在 -race 下有数据竞争(stubSessions.GetOrCreate,与本次无关)。
123 lines
5.1 KiB
Go
123 lines
5.1 KiB
Go
package channelbridge
|
||
|
||
// 本文件承载 IM 入站附件摄取的**诚实性与边界**用例(design/24 §4.4;切片 M1),
|
||
// 与 inbound_documents_test.go 是同一批夹具的搬家(夹具留在那边,同包可见):
|
||
//
|
||
// - 拒绝原因必须**分类正确**:超限说超限,绝不笼统说成「格式不支持」;
|
||
// - 渠道只给远程引用(目前适配器的现实形态)时不代取、不静默;
|
||
// - 图片类媒体保持现状(不走文档摄取);
|
||
// - 组合根未接线时照旧跑回合,但模型知道附件读不到。
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
|
||
)
|
||
|
||
// TestInboundOversizeFileIsHonestAboutSize 钉住诚实口径的**分类**:体积超限必须
|
||
// 说「超过体积上限」,绝不能笼统说成「格式不支持」——把原因说错也是不诚实。
|
||
func TestInboundOversizeFileIsHonestAboutSize(t *testing.T) {
|
||
t.Parallel()
|
||
env := newIngestEnv(t, documents.StoreConfig{MaxBytes: 8}, true)
|
||
msg := fileMessage("这个大吗", "big.txt", []byte("这是明显超过八个字节的正文\n"), "text/plain")
|
||
|
||
env.bridge.process(context.Background(), msg)
|
||
|
||
if names := docDirNames(t, env.uploads); len(names) != 0 {
|
||
t.Fatalf("超限附件被落库了:%v", names)
|
||
}
|
||
visible := env.model.visibleText()
|
||
if !strings.Contains(visible, "体积上限") {
|
||
t.Fatalf("超限附件的提示没有点明体积上限:%q", visible)
|
||
}
|
||
if strings.Contains(visible, "格式不在可读白名单内") {
|
||
t.Fatalf("超限附件被说成了「格式不支持」(原因说错):%q", visible)
|
||
}
|
||
if log := env.logs.String(); !strings.Contains(log, "inbound attachment rejected") {
|
||
t.Fatalf("缺少带来源的拒绝日志:%q", log)
|
||
}
|
||
}
|
||
|
||
// TestInboundRemoteOnlyMediaIsHonestWithoutFetch 钉住「渠道只给远程引用」这一现实形态
|
||
// (适配器目前上报 URL/平台文件 id,而不是内联字节):本切片**不代取**,但绝不静默。
|
||
func TestInboundRemoteOnlyMediaIsHonestWithoutFetch(t *testing.T) {
|
||
t.Parallel()
|
||
env := newIngestEnv(t, documents.StoreConfig{}, true)
|
||
msg := directMessage()
|
||
msg.Content = "这个文件里是什么"
|
||
msg.Media = []adapter.Media{{
|
||
Kind: adapter.MediaFile, URL: "https://cdn.example.com/secret-token=abc/report.pdf",
|
||
Name: "report.pdf", Size: 1024, MIMEType: "application/pdf",
|
||
}}
|
||
|
||
env.bridge.process(context.Background(), msg)
|
||
|
||
if names := docDirNames(t, env.uploads); len(names) != 0 {
|
||
t.Fatalf("远程引用的附件被落库了:%v", names)
|
||
}
|
||
visible := env.model.visibleText()
|
||
if !strings.Contains(visible, "report.pdf") || !strings.Contains(visible, "远程引用") {
|
||
t.Fatalf("模型可见视图里没有诚实提示行:%q", visible)
|
||
}
|
||
log := env.logs.String()
|
||
if !strings.Contains(log, "no inlined bytes") {
|
||
t.Fatalf("缺少「无内联字节」的带来源日志:%q", log)
|
||
}
|
||
// 远程引用可能带凭据,绝不进日志。
|
||
if strings.Contains(log, "secret-token=abc") {
|
||
t.Fatalf("日志里出现了远程引用原串(可能含凭据):%q", log)
|
||
}
|
||
}
|
||
|
||
// TestInboundImageMediaKeepsExistingPath 钉住边界:图片类媒体**不**走文档摄取
|
||
// (design/24 §4.2 末:图片走既有视觉链路),因此不落库、不挂文档部件、不产生提示行。
|
||
func TestInboundImageMediaKeepsExistingPath(t *testing.T) {
|
||
t.Parallel()
|
||
env := newIngestEnv(t, documents.StoreConfig{}, true)
|
||
msg := directMessage()
|
||
msg.Content = "看这张图"
|
||
msg.Media = []adapter.Media{{
|
||
Kind: adapter.MediaImage, Name: "photo.png", MIMEType: "image/png",
|
||
Data: []byte("\x89PNG\r\n\x1a\n"), Size: 8,
|
||
}}
|
||
|
||
env.bridge.process(context.Background(), msg)
|
||
|
||
if names := docDirNames(t, env.uploads); len(names) != 0 {
|
||
t.Fatalf("图片类媒体被文档摄取落库了:%v", names)
|
||
}
|
||
visible := env.model.visibleText()
|
||
if strings.Contains(visible, "[附件 ") || strings.Contains(visible, "无法读取") {
|
||
t.Fatalf("图片类媒体产生了文档清单/提示行:%q", visible)
|
||
}
|
||
if !strings.Contains(visible, "看这张图") {
|
||
t.Fatalf("用户文本丢了:%q", visible)
|
||
}
|
||
if log := env.logs.String(); strings.Contains(log, "attachment") {
|
||
t.Fatalf("图片类媒体产生了附件日志:%q", log)
|
||
}
|
||
}
|
||
|
||
// TestInboundFileMediaWithoutStoreIsHonest 钉住「未接线」路径:组合根没给落盘配置时,
|
||
// 带文件的消息照旧能跑(模型收到诚实提示),只是不落库 —— 绝不假装读到了。
|
||
func TestInboundFileMediaWithoutStoreIsHonest(t *testing.T) {
|
||
t.Parallel()
|
||
env := newIngestEnv(t, documents.StoreConfig{}, false)
|
||
msg := fileMessage("这个文件呢", "notes.txt", []byte("正文\n"), "text/plain")
|
||
|
||
env.bridge.process(context.Background(), msg)
|
||
|
||
if env.model.calls() == 0 {
|
||
t.Fatal("未接线时回合没有跑到模型")
|
||
}
|
||
if !strings.Contains(env.model.visibleText(), "未接线文档存储") {
|
||
t.Fatalf("未接线时没有诚实提示:%q", env.model.visibleText())
|
||
}
|
||
if log := env.logs.String(); !strings.Contains(log, "document store not wired") {
|
||
t.Fatalf("未接线时没有带来源的日志:%q", log)
|
||
}
|
||
}
|