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