From b7e47a1b1f12c8c7be3e6afcfd53447bd448bb3e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 09:16:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(rel):=20=E6=96=87=E6=A1=A3=E5=B1=82?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=9C=BA=E6=99=AF=EF=BC=88document=20?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=8C=82=E5=9C=BA=E6=99=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 场景贯穿流水线的 doc 层补完: - 新增 TagSceneDocument,linkBlocksToDocument 里建文档节点时一并挂场景; - RecallByScene 返回该场景下的文档 id(可枚举「这个场面有哪些文档」); - 文档场景由**来源派生**(chan:),不存冗余 Doc.Scene 字段—— 存一份会随来源改名而说谎,是同一事实的第二份真相。 go build/vet 干净,go test -count=1 ./internal/memory/ ./internal/agent/core/ 全绿。 --- internal/agent/core/graphmedia.go | 7 +++++ internal/memory/scene.go | 52 ++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/agent/core/graphmedia.go b/internal/agent/core/graphmedia.go index c0792a0..81eb0b9 100644 --- a/internal/agent/core/graphmedia.go +++ b/internal/agent/core/graphmedia.go @@ -92,6 +92,13 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock, log.Printf("[media] 写入 L3 文档节点失败 (%s): %v", docID, err) return 0 } + // 文档层与场景模型兼容:文档节点也进场景,好让「这个场面有哪些文档」 + // 可枚举、可统计(场景贯穿流水线的 doc 层落地)。 + if scene != "" { + if err := a.memory.TagSceneDocument(scene, docID); err != nil { + log.Printf("[media] 文档挂场景失败 (%s): %v", docID, err) + } + } // 文档层把场景传给块:归档进图库的块属于该文档的来源场面(QQ 归档的图 // 就该挂在 chan:qq 上),否则 L3 里这批块在场景召回中不可见。 for i := range blocks { diff --git a/internal/memory/scene.go b/internal/memory/scene.go index 4cc4189..58c10a5 100644 --- a/internal/memory/scene.go +++ b/internal/memory/scene.go @@ -134,6 +134,8 @@ type SceneRecall struct { // 为什么场景要能取回块:块是流水线里最细的子项目,而「那场对话里发过来的 // 那张图」只记住名字是没用的——场面重现时要把块本身带回来。 Blocks []MemoryBlock `json:"blocks,omitempty"` + // Documents 是该场景下的 L3 文档节点 id(文档的场景由来源派生,见 TagSceneDocument)。 + Documents []string `json:"documents,omitempty"` } // tagSceneTx 在事务内把「关系 + 实体」挂到场景上(幂等 upsert)。 @@ -434,7 +436,29 @@ func (g *GraphDB) RecallByScene(scenes []string, limit int) (*SceneRecall, error } out.Blocks = append(out.Blocks, b) } - return out, brows.Err() + if err := brows.Err(); err != nil { + return nil, err + } + + docQuery := `SELECT sr.ref_text, MAX(sr.weight) AS w + FROM scene_refs sr JOIN scenes s ON sr.scene_id = s.id + WHERE ` + where + ` AND sr.kind = 'document' + GROUP BY sr.ref_text ORDER BY w DESC LIMIT ?` + docArgs := append(append([]interface{}{}, args...), limit) + drows, err := g.db.Query(docQuery, docArgs...) + if err != nil { + return nil, err + } + defer drows.Close() + for drows.Next() { + var id string + var w float64 + if err := drows.Scan(&id, &w); err != nil { + return nil, err + } + out.Documents = append(out.Documents, id) + } + return out, drows.Err() } // SceneStats 返回各场景的规模,按引用数降序。 @@ -521,3 +545,29 @@ func (g *GraphDB) ScenesOfRelation(relationID int64) ([]string, error) { } return keys, rows.Err() } + +// TagSceneDocument 把 L3 文档节点挂到场景上(document 层与场景模型兼容)。 +// +// 文档的「场景」不由存储字段决定,而由**来源**派生(chan:)——存一份 +// 冗余的 Doc.Scene 会随来源改名而说谎,是同一事实的第二份真相。 +// 这里只登记「这份文档属于哪些场面」,供 scene 侧枚举与统计。 +func (g *GraphDB) TagSceneDocument(sceneKey, docID string) error { + if docID == "" { + return nil + } + key := NormalizeSceneKey(sceneKey) + if key == "" { + return nil + } + g.mu.Lock() + defer g.mu.Unlock() + tx, err := g.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if err := tagSceneRefTx(tx, key, "document", 0, docID, 1.0); err != nil { + return err + } + return tx.Commit() +}