63 lines
2.5 KiB
Go
63 lines
2.5 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex"
|
||
)
|
||
|
||
// 本文件是「项目记忆 → 项目索引」链接的写入侧:把项目作用域记忆携带的
|
||
// ProjectID 落成一条真实的项目索引条目,使 design/03 §3 的
|
||
// 「项目索引 → 项目描述 → 章节内容」三层在第一层就有数据可解析。
|
||
//
|
||
// 背景(审计 2026-10-05 §4/§7):project / project_chapter 两表数据为零,
|
||
// 而实库里 project 域记忆的 project_id 大量悬空 —— Retriever.projectFor
|
||
// 对这些条目恒返回 nil,模型够不着项目/章节。项目索引的权威写入源是
|
||
// 知识库导入(admin RAG,PutProject + AddChapter);但一条**先写下来**的
|
||
// 项目记忆(LLM 提取或手动添加)若引用一个尚未导入的项目 id,链接即断。
|
||
// 这里在记忆写入时确保该索引条目存在(只补不覆盖),把悬空链接就地接通,
|
||
// 而不新建知识内容(记忆 ≠ 资料,design §3)。
|
||
|
||
// attachProjectIndex 把项目索引存储接到该 store 的写入路径:此后每条经
|
||
// Add 落库的项目作用域记忆都会确保其 ProjectID 对应的索引条目存在。nil
|
||
// 索引是空操作。可并发调用,last-writer-wins。
|
||
func (s *Store) attachProjectIndex(idx *projectindex.Store) {
|
||
if s == nil || idx == nil {
|
||
return
|
||
}
|
||
s.projMu.Lock()
|
||
defer s.projMu.Unlock()
|
||
s.proj = idx
|
||
}
|
||
|
||
// projectIndex 返回当前接入的项目索引存储,未接入时为 nil。
|
||
func (s *Store) projectIndex() *projectindex.Store {
|
||
if s == nil {
|
||
return nil
|
||
}
|
||
s.projMu.RLock()
|
||
defer s.projMu.RUnlock()
|
||
return s.proj
|
||
}
|
||
|
||
// ensureProjectIndex 为项目作用域且带 ProjectID 的条目确保项目索引条目
|
||
// 存在。其它作用域、空 ProjectID、未接入项目索引时都是空操作。
|
||
//
|
||
// 失败只记日志:项目链接是元数据,接通失败绝不使记忆写入失败(与
|
||
// indexItem 的语义一致);条目的关系行已经在库里,Retriever.projectFor
|
||
// 仍有日志可循(见 retrieve.go 的静默吞点修复)。
|
||
func (s *Store) ensureProjectIndex(ctx context.Context, item MemoryItem) {
|
||
if item.Scope != ScopeProject || strings.TrimSpace(item.ProjectID) == "" {
|
||
return
|
||
}
|
||
idx := s.projectIndex()
|
||
if idx == nil {
|
||
return
|
||
}
|
||
if err := idx.EnsureProject(ctx, item.ProjectID, item.ProjectID); err != nil {
|
||
s.log.Warn("memory: ensure project index entry failed, link left dangling",
|
||
"project_id", item.ProjectID, "id", item.ID, "error", err)
|
||
}
|
||
}
|