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) } }