refactor+feat(documents,channelbridge): 共享存储核心 + IM 入站附件摄取(主人 2.3)
- 抽共享存储核心: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,与本次无关)。
This commit is contained in:
1 parent
51c1672355
commit
2a43035f5d
12 files changed
+1743
-158
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/channelbridge"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||
@@ -77,6 +79,9 @@ func (a *app) wireChannels() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 入站附件的目录口径与文档摄取层(wire_runtime.go 的 docResolver)同源,
|
||||
// 两处必须指向同一棵目录树,否则附件会落在一个地方、抽取去另一个地方找。
|
||||
docDataDir := resolveDataDir(a.cfg)
|
||||
bridge, berr := channelbridge.New(channelbridge.Options{
|
||||
Log: logger.WithSource(a.log, "services/channelbridge"),
|
||||
Loop: a.loop,
|
||||
@@ -85,6 +90,18 @@ func (a *app) wireChannels() error {
|
||||
History: a.history,
|
||||
QuietStartHour: a.cfg.Background.QuietStartHour,
|
||||
QuietEndHour: a.cfg.Background.QuietEndHour,
|
||||
// 入站附件摄取(design/24 §3;切片 M1):IM 收到的受支持格式文件落进与
|
||||
// Web 上传**同一个**存储核心(目录口径与 wire_runtime.go 的文档摄取层
|
||||
// 同源:`<DataDir>/uploads/documents`),元数据也写进同一张 `documents`
|
||||
// 表——`documents` 表的写入路径因此全仓只有一条(S7 的引用计数与 GC 有
|
||||
// 单一事实来源)。未配置数据目录时写入面为 nil,桥接器走「未接线」的
|
||||
// 诚实路径(模型知道有附件读不到),绝不假装读到了。
|
||||
DocumentStore: documents.StoreConfig{
|
||||
DataDir: docDataDir,
|
||||
UploadsDir: filepath.Join(docDataDir, "uploads", "documents"),
|
||||
Meta: documents.MetadataWriterFor(a.dbPath, a.log),
|
||||
Log: a.log,
|
||||
},
|
||||
// 可选的复审模型在接近单适配器上限时剔除低价值排队消息
|
||||
// (design/12 §6 策略四)。适配器为 nil 时,闸门回落到 FIFO 驱逐。
|
||||
Review: reviewFor(a.modelAdapter, logger.WithSource(a.log, "services/channelbridge/review")),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWireChannels_WiresInboundDocumentStore 是一道「接线存在性」防回归检查
|
||||
// (切片 M1;与 `TestWireRuntime_UsesTheCompositionRootSeams` 同一手法)。
|
||||
//
|
||||
// 组合根本身无法在单元测试里实例化(开库、迁移、绑端口,而后端还在运行),
|
||||
// 因此「IM 入站附件确实被接上了共享文档存储」只能在源码层钉住:列出的字符串
|
||||
// 少任何一个,桥接器都会静默退回「未接线」的诚实路径 —— 附件不再落库、S7 的 GC
|
||||
// 也看不到 IM 来源的文件,而这一切**不会**让任何既有用例变红。所以必须显式钉住。
|
||||
//
|
||||
// 最后一对断言是**成对契约**:入站附件落盘的目录与文档摄取层读原文件的目录
|
||||
// 必须逐字同源(`<DataDir>/uploads/documents`),否则附件会落在一个地方、
|
||||
// 装配期去另一个地方找 —— 症状是「附件全都读不到」,且两边各自都自洽。
|
||||
func TestWireChannels_WiresInboundDocumentStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const dirSeam = `UploadsDir: filepath.Join(docDataDir, "uploads", "documents")`
|
||||
src, err := os.ReadFile("wire_channels.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read wire_channels.go: %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"DocumentStore: documents.StoreConfig{",
|
||||
dirSeam,
|
||||
"Meta: documents.MetadataWriterFor(a.dbPath, a.log)",
|
||||
"docDataDir := resolveDataDir(a.cfg)",
|
||||
} {
|
||||
if !strings.Contains(string(src), want) {
|
||||
t.Fatalf("wire_channels.go 里找不到 %q —— IM 入站附件没有被接上共享文档存储", want)
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := os.ReadFile("wire_runtime.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read wire_runtime.go: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(reader), dirSeam) {
|
||||
t.Fatalf("文档摄取层的目录与 IM 入站附件的目录不同源(wire_runtime.go 里找不到 %q)—— "+
|
||||
"附件会落在一个地方、装配期去另一个地方找", dirSeam)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user