mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、 再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个 致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上 只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。 本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆: 一、描述链彻底删除(无残留、无兼容分支) - media.Item 去掉 Description/DescribedBy 与对应列; - 删除 Store.Describe / Store.Search / Store.Pending; - 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项 core.memory.media.describe_on_ingest; - SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。 二、marker 机制删除,媒体归属改为结构化块边 - 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/ extractMediaDigests/sentenceWithMediaMarkers/docMediaContext; - memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样, 不再被 marker 污染; - 块以 sentence --contains--> block / document --contains--> block 结构边 挂到承载节点(新增 documents 表与 document 节点种类); - 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。 三、旧数据迁移(幂等) - 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest 还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行; - CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子 连同块边一起删掉。 四、向量融合:媒体按图本身被召回 - 新增 vector.FuseVectors(逐维求和 + L2 归一化); - Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合), 新增 Doc.DenseFP,指纹变化触发重算; - ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在 同一统一空间内比稠密余弦; - 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再 直接充当记忆检索结果。 五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建) - internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧 (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、 sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。 验证:go build ./... 、go vet ./...(含 -tags medialive)均通过; 在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/... 全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造 无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
This commit is contained in:
@ -3,9 +3,7 @@ package sdk
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -20,34 +18,15 @@ import (
|
||||
// 这一层原先的缺陷是**静默裁字段**:插件把 Triple / Doc 交进来,包装层只挑
|
||||
// 自己认识的几个字段转成内部结构,其余丢弃且不报错。两侧都中招:
|
||||
// - 图记忆:丢 Confidence/SubjectType/ObjectType/SentenceText,又走 Commit
|
||||
// 而非 CommitWithMedia,于是 sentences 表没有落点,媒体引用无从挂起;
|
||||
// 而非 CommitWithMedia,于是 sentences 表没有落点,媒体块无从挂接;
|
||||
// - 知识库:Query 只回 ID/Title/Content,Insert 只写这三个,读写两个方向
|
||||
// 都把媒体元数据裁掉;Remove 不解引用,媒体永久算「被引用」,GC 收不掉。
|
||||
// 都把媒体元数据裁掉。
|
||||
//
|
||||
// 现在的规则:内部结构有的字段一律透传;媒体一律走标记格式并挂到对应 owner。
|
||||
// 现在的规则:内部结构有的字段一律透传;媒体一律变成一等记忆块。
|
||||
// 媒体存储为 nil 时整条链路静默降级为纯文本行为(媒体是记忆增强,不是必需品)。
|
||||
|
||||
// ---------- 媒体标记(本层内部) ----------
|
||||
//
|
||||
// 标记是媒体在**纯文本记忆**里的表示形式:
|
||||
//
|
||||
// [image/png a1b2c3d4e5f6] 一张紫蓝红三色带图
|
||||
// └ label └ 短 digest └ 描述
|
||||
//
|
||||
// 之所以必须借文本承载:Doc.Content、sentences.text、文本记忆的 Input 全是
|
||||
// 字符串,没有字段能挂结构化数据。描述文本是持久的语义记忆(检索靠它),
|
||||
// digest 是回到字节的钥匙(反查靠它)。
|
||||
//
|
||||
// 格式与内核侧 graphmedia.go 的 mediaSummaryForEvent 一致——两边必须能互读
|
||||
// 对方写下的标记,否则插件写入的媒体在内核归档时挂不上引用,且不报错。
|
||||
|
||||
const sdkShortDigestLen = 12
|
||||
|
||||
// sdkMarkerPattern 拆解一条标记,捕获组依次为 label、短 digest、该行剩余描述。
|
||||
// digest 放宽到 8-64 位以容忍完整 digest 手写的情况;描述取到行尾而非贪婪到底,
|
||||
// 因为一条记忆可能挂多份媒体、各占一行。
|
||||
var sdkMarkerPattern = regexp.MustCompile(`\[([^\[\]\s]+)\s+([0-9a-f]{8,64})\]([^\n]*)`)
|
||||
|
||||
func sdkShortDigest(d string) string {
|
||||
if len(d) > sdkShortDigestLen {
|
||||
return d[:sdkShortDigestLen]
|
||||
@ -55,34 +34,6 @@ func sdkShortDigest(d string) string {
|
||||
return d
|
||||
}
|
||||
|
||||
// sdkMarkerFor 为一份已入库的媒体生成标记行。查不到就返回空串——
|
||||
// 媒体可能已被 GC 清掉,此时不该凭空造出一条指向虚无的标记。
|
||||
func sdkMarkerFor(ms *media.Store, digest string) string {
|
||||
it, err := ms.Stat(digest)
|
||||
if err != nil || it == nil {
|
||||
return ""
|
||||
}
|
||||
label := string(it.Kind)
|
||||
if it.MIME != "" {
|
||||
label = it.MIME
|
||||
}
|
||||
if it.Description == "" {
|
||||
// 「已入库但还没描述」与「压根没有媒体」必须可区分:
|
||||
// 描述由后台循环异步补齐,占位符保证补齐前这份媒体也不会从文本里消失。
|
||||
return fmt.Sprintf("[%s %s] (未描述)", label, sdkShortDigest(digest))
|
||||
}
|
||||
return fmt.Sprintf("[%s %s] %s", label, sdkShortDigest(digest), it.Description)
|
||||
}
|
||||
|
||||
// sdkDigestsIn 返回文本里出现过的短 digest 集合,用于避免重复追加标记。
|
||||
func sdkDigestsIn(s string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, m := range sdkMarkerPattern.FindAllStringSubmatch(s, -1) {
|
||||
out[m[2]] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sdkBlockSeq 保证块 ID 全局唯一:Graph 的 memory_blocks 以 id 为主键,
|
||||
// 不同文档里序号相同的块会在 L2→L3 迁移时相互覆盖。
|
||||
var sdkBlockSeq int64
|
||||
@ -127,27 +78,8 @@ func sdkBlockForDigest(ms *media.Store, digest string) (memory.MemoryBlock, bool
|
||||
}, true
|
||||
}
|
||||
|
||||
// sdkBlocksFromText 把文本标记里的媒体变成一等块(去重)。
|
||||
func sdkBlocksFromText(ms *media.Store, text string) []memory.MemoryBlock {
|
||||
if ms == nil || text == "" {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var blocks []memory.MemoryBlock
|
||||
for _, m := range sdkMarkerPattern.FindAllStringSubmatch(text, -1) {
|
||||
full, err := ms.ResolvePrefix(m[2])
|
||||
if err != nil || seen[full] {
|
||||
continue
|
||||
}
|
||||
seen[full] = true
|
||||
if b, ok := sdkBlockForDigest(ms, full); ok {
|
||||
blocks = append(blocks, b)
|
||||
}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// sdkBlocksFromDigests 为显式 digest 列表构造一等块(去重)。
|
||||
// digest 可以是短前缀,内部会先补全。
|
||||
func sdkBlocksFromDigests(ms *media.Store, digests []string) []memory.MemoryBlock {
|
||||
if ms == nil || len(digests) == 0 {
|
||||
return nil
|
||||
@ -155,11 +87,15 @@ func sdkBlocksFromDigests(ms *media.Store, digests []string) []memory.MemoryBloc
|
||||
seen := map[string]bool{}
|
||||
var blocks []memory.MemoryBlock
|
||||
for _, d := range digests {
|
||||
if d == "" || seen[d] {
|
||||
if d == "" {
|
||||
continue
|
||||
}
|
||||
seen[d] = true
|
||||
if b, ok := sdkBlockForDigest(ms, d); ok {
|
||||
full, err := ms.ResolvePrefix(d)
|
||||
if err != nil || seen[full] {
|
||||
continue
|
||||
}
|
||||
seen[full] = true
|
||||
if b, ok := sdkBlockForDigest(ms, full); ok {
|
||||
blocks = append(blocks, b)
|
||||
}
|
||||
}
|
||||
@ -177,10 +113,9 @@ func sdkPutAttachment(ms *media.Store, a MediaAttachment, tool string) (string,
|
||||
mime = "application/octet-stream"
|
||||
}
|
||||
return ms.Put(a.Data, media.Item{
|
||||
MIME: mime,
|
||||
Tool: tool,
|
||||
OriginPath: a.Name,
|
||||
Description: a.Description,
|
||||
MIME: mime,
|
||||
Tool: tool,
|
||||
OriginPath: a.Name,
|
||||
})
|
||||
}
|
||||
if a.Digest == "" {
|
||||
@ -193,32 +128,6 @@ func sdkPutAttachment(ms *media.Store, a MediaAttachment, tool string) (string,
|
||||
return full, nil
|
||||
}
|
||||
|
||||
// sdkAttachmentsFromText 从文本标记反解出附件元数据(不含字节),
|
||||
// 让插件不必自己写正则去认标记。
|
||||
func sdkAttachmentsFromText(ms *media.Store, s string) []MediaAttachment {
|
||||
if ms == nil || s == "" {
|
||||
return nil
|
||||
}
|
||||
var out []MediaAttachment
|
||||
seen := map[string]bool{}
|
||||
for _, m := range sdkMarkerPattern.FindAllStringSubmatch(s, -1) {
|
||||
full, err := ms.ResolvePrefix(m[2])
|
||||
if err != nil || seen[full] {
|
||||
continue
|
||||
}
|
||||
seen[full] = true
|
||||
att := MediaAttachment{Digest: full, MIME: m[1], Description: strings.TrimSpace(m[3])}
|
||||
if it, err := ms.Stat(full); err == nil && it != nil {
|
||||
att.MIME = it.MIME
|
||||
if it.Description != "" {
|
||||
att.Description = it.Description
|
||||
}
|
||||
}
|
||||
out = append(out, att)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------- 图记忆 ----------
|
||||
|
||||
type graphMemory struct {
|
||||
@ -260,10 +169,8 @@ func (m *graphMemory) Recall(query []string, depth int) ([]Entity, []Relation, e
|
||||
return entities, relations, nil
|
||||
}
|
||||
|
||||
// Commit 把插件的三元组写入图库,并把三元组句子里的媒体变成 L3 一等块。
|
||||
//
|
||||
// 媒体的落点链是 SentenceText → sentences 表 → sentence_id → 块边。
|
||||
// 旧实现丢掉 SentenceText 又走 Commit(不回 sentenceIDs),这条链一步都走不通。
|
||||
// Commit 把插件的三元组写入图库,并把结构化 MediaDigests 变成 L3 一等块。
|
||||
// 媒体通过 sentence --contains--> block 结构边挂接,不读写任何正文 marker。
|
||||
func (m *graphMemory) Commit(triples []Triple) error {
|
||||
if m.db == nil {
|
||||
return nil
|
||||
@ -278,9 +185,7 @@ func (m *graphMemory) Commit(triples []Triple) error {
|
||||
SubjectType: t.SubjectType,
|
||||
ObjectType: t.ObjectType,
|
||||
SentenceText: t.SentenceText,
|
||||
}
|
||||
if len(t.MediaDigests) > 0 {
|
||||
mt.SentenceText = m.sentenceWithMedia(mt.SentenceText, t.MediaDigests)
|
||||
MediaDigests: t.MediaDigests,
|
||||
}
|
||||
ts = append(ts, mt)
|
||||
}
|
||||
@ -289,61 +194,28 @@ func (m *graphMemory) Commit(triples []Triple) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.bindSentences(sentenceIDs)
|
||||
m.bindSentences(sentenceIDs, ts)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sentenceWithMedia 保证句子文本里带有这些 digest 的媒体标记。
|
||||
// bindSentences 把每个三元组显式携带的媒体变成 L3 一等记忆块,
|
||||
// 并以 sentence --contains--> block 结构边关联。
|
||||
//
|
||||
// 让插件填 MediaDigests 就够,不必知道标记格式——否则格式写错的后果是
|
||||
// 引用静默挂不上。已出现过的 digest 不重复追加:插件可能既手写了标记又填了
|
||||
// MediaDigests,重复标记会让同一份媒体产生两条一样的句子引用。
|
||||
func (m *graphMemory) sentenceWithMedia(sentence string, digests []string) string {
|
||||
present := sdkDigestsIn(sentence)
|
||||
var add []string
|
||||
for _, d := range digests {
|
||||
if d == "" || present[sdkShortDigest(d)] {
|
||||
continue
|
||||
}
|
||||
if m.ms == nil {
|
||||
// 没有媒体存储时也把 digest 留在文本里:拿不到描述,
|
||||
// 但将来存储可用时这条记忆仍能反查回字节。
|
||||
add = append(add, fmt.Sprintf("[media %s] (未描述)", sdkShortDigest(d)))
|
||||
present[sdkShortDigest(d)] = true
|
||||
continue
|
||||
}
|
||||
full, err := m.ms.ResolvePrefix(d)
|
||||
if err != nil {
|
||||
log.Printf("[sdk media] 插件 %s 提交的 digest %s 无法解析: %v", m.plugin, d, err)
|
||||
continue
|
||||
}
|
||||
if line := sdkMarkerFor(m.ms, full); line != "" {
|
||||
add = append(add, line)
|
||||
present[sdkShortDigest(full)] = true
|
||||
}
|
||||
}
|
||||
if len(add) == 0 {
|
||||
return sentence
|
||||
}
|
||||
if sentence == "" {
|
||||
return strings.Join(add, "\n")
|
||||
}
|
||||
return sentence + "\n" + strings.Join(add, "\n")
|
||||
}
|
||||
|
||||
// bindSentences 把每条句子里引用的媒体变成 L3 的一等记忆块,
|
||||
// 并建立 sentence --contains--> block 的结构边。
|
||||
// 不再写 media_refs:块本身就是图的一部分,不需要 owner 账本保活。
|
||||
func (m *graphMemory) bindSentences(sentenceIDs map[string]int64) {
|
||||
// 不再往句子文本里写 marker、也不再从文本反解 digest:归属由结构化字段直接给出。
|
||||
func (m *graphMemory) bindSentences(sentenceIDs map[string]int64, triples []memory.Triple) {
|
||||
if m.ms == nil || m.db == nil || len(sentenceIDs) == 0 {
|
||||
return
|
||||
}
|
||||
bound := 0
|
||||
for text, sid := range sentenceIDs {
|
||||
for _, t := range triples {
|
||||
if len(t.MediaDigests) == 0 {
|
||||
continue
|
||||
}
|
||||
sid := sentenceIDs[t.SentenceText]
|
||||
if sid == 0 {
|
||||
continue
|
||||
}
|
||||
for _, b := range sdkBlocksFromText(m.ms, text) {
|
||||
for _, b := range sdkBlocksFromDigests(m.ms, t.MediaDigests) {
|
||||
if err := m.db.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil {
|
||||
log.Printf("[sdk media] 插件 %s 写入 L3 记忆块失败: %v", m.plugin, err)
|
||||
continue
|
||||
@ -403,37 +275,21 @@ func NewTextMemoryWithMedia(plugin string, tm *text.Memory, ms *media.Store) Tex
|
||||
return &textMemoryImpl{tm: tm, ms: ms, plugin: plugin}
|
||||
}
|
||||
|
||||
// Append 追加一条文本事件;带附件时把媒体标记并进正文。
|
||||
// Append 追加一条文本事件。
|
||||
//
|
||||
// 文本记忆是追加写 JSONL,没有结构化块存储,所以媒体在这一层
|
||||
// 只能以标记形式存在。这不是妥协——描述文本才是持久的语义记忆,blob 只是缓存。
|
||||
// 文本记忆是追加写 JSONL 的原始日志,只有字符串字段,没有块容器;
|
||||
// 因此附件在这里无法结构化存下。不假装用文本标记承载媒体——
|
||||
// 需要保存媒体请用文档记忆或图记忆(它们持有一等记忆块)。
|
||||
func (m *textMemoryImpl) Append(evt TextEvent) error {
|
||||
if m.tm == nil {
|
||||
return nil
|
||||
}
|
||||
content := evt.Content
|
||||
if len(evt.Attachments) > 0 && m.ms != nil {
|
||||
var lines []string
|
||||
for _, a := range evt.Attachments {
|
||||
d, err := sdkPutAttachment(m.ms, a, "plugin_text:"+m.plugin)
|
||||
if err != nil {
|
||||
log.Printf("[sdk media] 插件 %s 文本附件入库失败: %v", m.plugin, err)
|
||||
continue
|
||||
}
|
||||
if line := sdkMarkerFor(m.ms, d); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
if content == "" {
|
||||
content = strings.Join(lines, "\n")
|
||||
} else {
|
||||
content += "\n" + strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
if len(evt.Attachments) > 0 {
|
||||
log.Printf("[sdk media] 插件 %s 向文本记忆追加了 %d 份附件,已忽略:"+
|
||||
"文本层是字符串日志,不具备块存储;请改用文档/图记忆保存媒体", m.plugin, len(evt.Attachments))
|
||||
}
|
||||
return m.tm.Append(text.Event{
|
||||
Timestamp: evt.Timestamp, Source: evt.Role, Input: content, AgentID: evt.Channel,
|
||||
Timestamp: evt.Timestamp, Source: evt.Role, Input: evt.Content, AgentID: evt.Channel,
|
||||
})
|
||||
}
|
||||
|
||||
@ -449,7 +305,6 @@ func (m *textMemoryImpl) RecentEvents(n int) ([]TextEvent, error) {
|
||||
for i, e := range got {
|
||||
out[i] = TextEvent{
|
||||
Role: e.Source, Content: e.Input, Timestamp: e.Timestamp, Channel: e.AgentID,
|
||||
Attachments: sdkAttachmentsFromText(m.ms, e.Input),
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
@ -477,12 +332,8 @@ func NewDocMemoryWithMedia(plugin string, ds *doc.Store, ms *media.Store) DocMem
|
||||
return &docMemoryImpl{ds: ds, ms: ms, plugin: plugin}
|
||||
}
|
||||
|
||||
// Query 检索文档,并补齐媒体元数据。
|
||||
//
|
||||
// 旧实现只回 ID/Title/Content,插件即便拿到一篇带媒体的文档也看不出这里有
|
||||
// 几份媒体、分别是什么。现在同时给出完整 digest 列表与 mime+描述,
|
||||
// 但**不回字节**:一次检索可能命中几十份媒体,全塞回去会把跨进程消息撑爆,
|
||||
// 需要字节时按 digest 单取。
|
||||
// Query 检索文档,并从文档持有的一等块补齐媒体元数据。
|
||||
// 只返回 digest/MIME,不返回字节或生成式描述;需要字节时按 digest 单取。
|
||||
func (m *docMemoryImpl) Query(text string, topK int) []*Doc {
|
||||
if m.ds == nil {
|
||||
return nil
|
||||
@ -498,30 +349,22 @@ func (m *docMemoryImpl) Query(text string, topK int) []*Doc {
|
||||
|
||||
// fillMedia 填充文档的媒体字段。
|
||||
//
|
||||
// 优先读一等记忆块(文档直接持有),为空时退回解析正文标记——
|
||||
// 历史文档与经旧版插件写入的文档只有标记、没有块。
|
||||
// 唯一的来源是文档直接持有的一等记忆块:媒体不靠正文标记、
|
||||
// 也不靠任何生成的描述文本。CAS 只提供 MIME 等元数据。
|
||||
func (m *docMemoryImpl) fillMedia(out *Doc, d *doc.Doc) {
|
||||
if m.ms == nil {
|
||||
if m.ms == nil || d == nil {
|
||||
return
|
||||
}
|
||||
if d != nil && len(d.Blocks) > 0 {
|
||||
for _, b := range d.Blocks {
|
||||
if b.PayloadDigest == "" {
|
||||
continue
|
||||
}
|
||||
out.MediaDigests = append(out.MediaDigests, b.PayloadDigest)
|
||||
att := MediaAttachment{Digest: b.PayloadDigest, MIME: b.MIME, Description: ""}
|
||||
if it, err := m.ms.Stat(b.PayloadDigest); err == nil && it != nil {
|
||||
att.MIME = it.MIME
|
||||
att.Description = it.Description
|
||||
}
|
||||
out.Attachments = append(out.Attachments, att)
|
||||
for _, b := range d.Blocks {
|
||||
if b.PayloadDigest == "" {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
out.Attachments = sdkAttachmentsFromText(m.ms, out.Content)
|
||||
for _, a := range out.Attachments {
|
||||
out.MediaDigests = append(out.MediaDigests, a.Digest)
|
||||
out.MediaDigests = append(out.MediaDigests, b.PayloadDigest)
|
||||
att := MediaAttachment{Digest: b.PayloadDigest, MIME: b.MIME}
|
||||
if it, err := m.ms.Stat(b.PayloadDigest); err == nil && it != nil {
|
||||
att.MIME = it.MIME
|
||||
}
|
||||
out.Attachments = append(out.Attachments, att)
|
||||
}
|
||||
}
|
||||
|
||||
@ -543,15 +386,14 @@ func appendBlocks(existing []memory.MemoryBlock, add []memory.MemoryBlock) []mem
|
||||
return existing
|
||||
}
|
||||
|
||||
// Insert 写入文档。正文里已有的媒体标记会被挂成文档级引用,
|
||||
// 避免插件写进来的媒体在下一次 GC 时被当作无主内容清掉。
|
||||
// Insert 写入文档。
|
||||
func (m *docMemoryImpl) Insert(d *Doc) error { return m.InsertWithMedia(d, nil) }
|
||||
|
||||
// InsertWithMedia 写入文档并关联媒体。
|
||||
//
|
||||
// 标记由内核补进 Content——插件不必知道标记格式,也就不会因为格式写错导致
|
||||
// 引用挂不上。补标记必须在 ds.Insert 之前完成:向量索引用 Summary+Content
|
||||
// 计算,标记进不去正文就检索不到这份媒体。
|
||||
// 媒体直接成为文档持有的一等记忆块:落进 CAS 拿到 digest,
|
||||
// 再变成块挂到文档上。不往正文写 marker——文档向量会融合这些块的
|
||||
// 媒体向量(同一统一空间),图片按自己的向量被召回。
|
||||
func (m *docMemoryImpl) InsertWithMedia(d *Doc, attachments []MediaAttachment) error {
|
||||
if m.ds == nil || d == nil {
|
||||
return nil
|
||||
@ -561,29 +403,26 @@ func (m *docMemoryImpl) InsertWithMedia(d *Doc, attachments []MediaAttachment) e
|
||||
target.Source = "plugin:" + m.plugin
|
||||
}
|
||||
|
||||
digests := m.storeAttachments(attachments, &target.Content)
|
||||
digests := m.storeAttachments(attachments)
|
||||
|
||||
// 一等记忆块:文档直接持有块本身,CAS 只提供字节与向量。
|
||||
// 不再写 media_refs——块随文档一同存活或被删除,无需 owner 账本。
|
||||
target.Blocks = appendBlocks(target.Blocks,
|
||||
append(sdkBlocksFromDigests(m.ms, digests), sdkBlocksFromText(m.ms, target.Content)...))
|
||||
target.Blocks = appendBlocks(target.Blocks, sdkBlocksFromDigests(m.ms, digests))
|
||||
|
||||
if err := m.ds.Insert(target); err != nil {
|
||||
return err
|
||||
}
|
||||
// 回填给调用方:ID 是新建时内核生成的,Content 含内核补的标记。
|
||||
// 回填给调用方:ID 是新建时内核生成的。
|
||||
d.ID = target.ID
|
||||
d.Content = target.Content
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeAttachments 把附件落库并把标记追加进 content,返回全部完整 digest。
|
||||
func (m *docMemoryImpl) storeAttachments(atts []MediaAttachment, content *string) []string {
|
||||
// storeAttachments 把附件落库,返回全部完整 digest。
|
||||
func (m *docMemoryImpl) storeAttachments(atts []MediaAttachment) []string {
|
||||
if m.ms == nil || len(atts) == 0 {
|
||||
return nil
|
||||
}
|
||||
present := sdkDigestsIn(*content)
|
||||
var digests, lines []string
|
||||
seen := map[string]bool{}
|
||||
var digests []string
|
||||
for _, a := range atts {
|
||||
full, err := sdkPutAttachment(m.ms, a, "plugin_doc:"+m.plugin)
|
||||
if err != nil {
|
||||
@ -591,21 +430,11 @@ func (m *docMemoryImpl) storeAttachments(atts []MediaAttachment, content *string
|
||||
log.Printf("[sdk media] 插件 %s 文档附件入库失败: %v", m.plugin, err)
|
||||
continue
|
||||
}
|
||||
if seen[full] {
|
||||
continue
|
||||
}
|
||||
seen[full] = true
|
||||
digests = append(digests, full)
|
||||
if present[sdkShortDigest(full)] {
|
||||
continue // 插件自己写了标记,不重复追加
|
||||
}
|
||||
present[sdkShortDigest(full)] = true
|
||||
if line := sdkMarkerFor(m.ms, full); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
if *content == "" {
|
||||
*content = strings.Join(lines, "\n")
|
||||
} else {
|
||||
*content += "\n" + strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
return digests
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user