package graph import ( "path/filepath" "strings" "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" base := filepath.Base(node.Path) base = strings.TrimSuffix(base, ".md") // 去掉编号前缀(如 "002-") if idx := strings.Index(base, "-"); idx >= 0 { base = base[idx+1:] } return base == link || node.Title == link }