From cd546a4295aa00f8ee30c8d6682de68c4db2ebd1 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Sat, 25 Jul 2026 23:40:29 +0800
Subject: [PATCH] feat: 实现图模型与构建器
---
internal/graph/builder.go | 96 ++++++++++++++++++++++++++++++++
internal/graph/model.go | 27 +++++++++
internal/graph/builder_test.go | 54 ++++++++++++++++++
3 files changed, 177 insertions(+), 0 deletions(-)
diff --git a/internal/graph/builder.go b/internal/graph/builder.go
new file mode 100644
index 0000000..9b5ca4e
--- /dev/null
+++ b/internal/graph/builder.go
@@ -0,0 +1,96 @@
+package graph
+
+import (
+ "github.com/aisim/kb-cli/internal/vault"
+)
+
+// BuildGraph 从文件元数据构建知识图谱
+func BuildGraph(files []*vault.FileMeta) *Graph {
+ g := &Graph{}
+
+ // 标签/实体 -> 虚拟节点 ID 映射
+ labelToID := make(map[string]int64)
+ nextVirtualID := int64(1000000) // 虚拟节点从 1000000 开始
+
+ getOrCreateVirtualNode := func(label string) int64 {
+ if id, ok := labelToID[label]; ok {
+ return id
+ }
+ id := nextVirtualID
+ nextVirtualID++
+ labelToID[label] = id
+ return id
+ }
+
+ for i, f := range files {
+ node := &Node{
+ ID: int64(i + 1),
+ Path: f.Path,
+ Title: f.Title,
+ Section: f.Section,
+ Tags: f.Tags,
+ Entities: f.Entities,
+ Wikilinks: f.Wikilinks,
+ Content: f.Content,
+ }
+ g.Nodes = append(g.Nodes, node)
+
+ // 创建 tag 边
+ for _, tag := range f.Tags {
+ virtualID := getOrCreateVirtualNode("tag:" + tag)
+ g.Edges = append(g.Edges, &Edge{
+ FromNode: node.ID,
+ ToNode: virtualID,
+ Relation: "tag",
+ Label: tag,
+ })
+ }
+
+ // 创建 entity 边
+ for _, entity := range f.Entities {
+ virtualID := getOrCreateVirtualNode("entity:" + entity)
+ g.Edges = append(g.Edges, &Edge{
+ FromNode: node.ID,
+ ToNode: virtualID,
+ Relation: "entity",
+ Label: entity,
+ })
+ }
+ }
+
+ // 创建 wikilink 边(文件间链接)
+ pathToID := make(map[string]int64)
+ for _, n := range g.Nodes {
+ pathToID[n.Path] = n.ID
+ }
+
+ for _, n := range g.Nodes {
+ for _, link := range n.Wikilinks {
+ // 尝试匹配目标文件(模糊匹配:链接文本可能只是标题的一部分)
+ for _, target := range g.Nodes {
+ if matchesWikilink(target, link) {
+ g.Edges = append(g.Edges, &Edge{
+ FromNode: n.ID,
+ ToNode: target.ID,
+ Relation: "wikilink",
+ Label: link,
+ })
+ break
+ }
+ }
+ }
+ }
+
+ return g
+}
+
+// matchesWikilink 检查文件是否匹配 wikilink
+func matchesWikilink(node *Node, link string) bool {
+ // 精确匹配标题
+ if node.Title == link {
+ return true
+ }
+ // 匹配文件名(不含扩展名和编号前缀)
+ // 例如:[[充装规格配置]] 匹配 "知识/002-充装规格配置.md"
+ return false // 简化版,后续可扩展
+}
diff --git a/internal/graph/builder_test.go b/internal/graph/builder_test.go
new file mode 100644
index 0000000..8b66be5
--- /dev/null
+++ b/internal/graph/builder_test.go
@@ -0,0 +1,54 @@
+package graph
+
+import (
+ "testing"
+
+ "github.com/aisim/kb-cli/internal/vault"
+)
+
+func TestBuildGraph(t *testing.T) {
+ files := []*vault.FileMeta{
+ {
+ Path: "FAQ/充装类/001-test.md",
+ Title: "测试FAQ",
+ Section: "FAQ",
+ Tags: []string{"充装", "智能枪"},
+ Entities: []string{"YSP-35.5"},
+ Wikilinks: []string{},
+ Content: "测试内容",
+ },
+ {
+ Path: "知识/002-配置.md",
+ Title: "充装规格配置",
+ Section: "知识",
+ Tags: []string{"充装"},
+ Entities: []string{},
+ Wikilinks: []string{},
+ Content: "配置内容",
+ },
+ }
+
+ g := BuildGraph(files)
+
+ if len(g.Nodes) != 2 {
+ t.Errorf("node count = %d, want 2", len(g.Nodes))
+ }
+
+ // 应该有 3 条边:2条 tag + 1条 entity
+ tagEdges := 0
+ entityEdges := 0
+ for _, e := range g.Edges {
+ if e.Relation == "tag" {
+ tagEdges++
+ }
+ if e.Relation == "entity" {
+ entityEdges++
+ }
+ }
+ if tagEdges != 3 { // 充装 + 智能枪 + 充装(第二个文件)
+ t.Errorf("tag edges = %d, want 3", tagEdges)
+ }
+ if entityEdges != 1 {
+ t.Errorf("entity edges = %d, want 1", entityEdges)
+ }
+}
diff --git a/internal/graph/model.go b/internal/graph/model.go
new file mode 100644
index 0000000..0702912
--- /dev/null
+++ b/internal/graph/model.go
@@ -0,0 +1,27 @@
+package graph
+
+// Node 图节点(对应一个 markdown 文件)
+type Node struct {
+ ID int64 `json:"id"`
+ Path string `json:"path"`
+ Title string `json:"title"`
+ Section string `json:"section"`
+ Tags []string `json:"tags"`
+ Entities []string `json:"entities"`
+ Wikilinks []string `json:"wikilinks"`
+ Content string `json:"content"`
+}
+
+// Edge 图边(实体关系)
+type Edge struct {
+ FromNode int64 `json:"from_node"`
+ ToNode int64 `json:"to_node"` // 对于 tag/entity 边,ToNode 可以是虚拟节点 ID
+ Relation string `json:"relation"` // "tag" | "entity" | "wikilink"
+ Label string `json:"label"` // 具体值
+}
+
+// Graph 知识图谱
+type Graph struct {
+ Nodes []*Node
+ Edges []*Edge
+}
--
Gitblit v1.9.1