From fab49fdd9b573ed18ae028d6b2e62aae61223044 Mon Sep 17 00:00:00 2001 From: xiaowei Date: Fri, 29 May 2026 15:42:07 +0800 Subject: [PATCH] feat: knowledge graph visualization - Add GetGraph method to GraphStore interface - Implement SQLiteGraphStore.GetGraph for full graph export - Add GET /api/v1/graph/export endpoint with namespace filter - Add static file serving (/static/ path) - Create D3.js force-directed graph visualization: - Namespace switcher (all/hermes-main/openclaw-main/shared) - Node colors by type, size by weight - Edge labels on hover, node detail popup on click - Dark theme, zoom/pan, force simulation --- go/internal/api/server.go | 19 + go/internal/governance/graph_mem.go | 5 + go/internal/governance/graph_sqlite.go | 52 ++ go/internal/governance/graph_store.go | 3 + go/static/graph.html | 637 +++++++++++++++++++++++++ 5 files changed, 716 insertions(+) create mode 100644 go/static/graph.html diff --git a/go/internal/api/server.go b/go/internal/api/server.go index b1c6b32..e1517a9 100644 --- a/go/internal/api/server.go +++ b/go/internal/api/server.go @@ -213,6 +213,25 @@ func NewServer() http.Handler { w.Write(data) }) + // 图谱可视化导出 + mux.HandleFunc("/api/v1/graph/export", func(w http.ResponseWriter, r *http.Request) { + ns := r.URL.Query().Get("namespace") + nodes, edges := graphStore.GetGraph(ns) + respondJSON(w, 200, map[string]interface{}{ + "nodes": nodes, + "edges": edges, + "count": map[string]int{"nodes": len(nodes), "edges": len(edges)}, + }) + }) + + // 静态文件服务(知识图谱可视化 HTML) + staticDir := os.Getenv("STATIC_DIR") + if staticDir == "" { + staticDir = "/home/muc/projects/memoryweave/go/static" + } + fileServer := http.FileServer(http.Dir(staticDir)) + mux.Handle("/static/", http.StripPrefix("/static/", fileServer)) + // 冲突 mux.HandleFunc("/api/v1/conflicts", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { diff --git a/go/internal/governance/graph_mem.go b/go/internal/governance/graph_mem.go index 19832ca..6288765 100644 --- a/go/internal/governance/graph_mem.go +++ b/go/internal/governance/graph_mem.go @@ -239,3 +239,8 @@ func containsRelation(rel, substr string) bool { } return rel == substr } + +// GetGraph 内存图谱的导出(空实现) +func (g *InMemoryGraph) GetGraph(namespace string) ([]map[string]interface{}, []map[string]interface{}) { + return []map[string]interface{}{}, []map[string]interface{}{} +} diff --git a/go/internal/governance/graph_sqlite.go b/go/internal/governance/graph_sqlite.go index a2c2f35..9a57308 100644 --- a/go/internal/governance/graph_sqlite.go +++ b/go/internal/governance/graph_sqlite.go @@ -11,6 +11,7 @@ package governance import "C" import ( + "encoding/json" "fmt" "sync" "unsafe" @@ -422,3 +423,54 @@ func escape(s string) string { } return result } + +// GetGraph 返回指定 namespace 的所有节点和边(供可视化用) +func (gs *SQLiteGraphStore) GetGraph(namespace string) ([]map[string]interface{}, []map[string]interface{}) { + nodes := []map[string]interface{}{} + edges := []map[string]interface{}{} + + // 根据 namespace 构建查询(使用 escape() 防注入) + var nodeSQL string + if namespace == "" || namespace == "all" { + nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes" + } else { + nodeSQL = "SELECT id, name, type, namespace, properties FROM graph_nodes WHERE namespace = '" + escape(namespace) + "'" + } + + rows := queryRows(gs.db, nodeSQL) + for _, n := range rows { + props := n["properties"].(string) + var properties map[string]interface{} + if props != "" { + json.Unmarshal([]byte(props), &properties) + } + nodes = append(nodes, map[string]interface{}{ + "id": n["id"], + "name": n["name"], + "type": n["type"], + "namespace": n["namespace"], + "properties": properties, + }) + } + + var edgeSQL string + if namespace == "" || namespace == "all" { + edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges" + } else { + edgeSQL = "SELECT id, source, target, relation, weight, namespace FROM graph_edges WHERE namespace = '" + escape(namespace) + "'" + } + + edgeRows := queryRows(gs.db, edgeSQL) + for _, e := range edgeRows { + edges = append(edges, map[string]interface{}{ + "id": e["id"], + "source": e["source"], + "target": e["target"], + "relation": e["relation"], + "weight": e["weight"], + "namespace": e["namespace"], + }) + } + + return nodes, edges +} diff --git a/go/internal/governance/graph_store.go b/go/internal/governance/graph_store.go index bd0982e..a174b53 100644 --- a/go/internal/governance/graph_store.go +++ b/go/internal/governance/graph_store.go @@ -26,4 +26,7 @@ type GraphStore interface { // 多 Agent 分析 PageRank(damping float64, iterations int) map[string]float64 EvidenceCount(entity string) int + + // 导出完整图谱(供可视化) + GetGraph(namespace string) (nodes []map[string]interface{}, edges []map[string]interface{}) } diff --git a/go/static/graph.html b/go/static/graph.html new file mode 100644 index 0000000..6869b63 --- /dev/null +++ b/go/static/graph.html @@ -0,0 +1,637 @@ + + + + + + 织忆 MemoryWeave — 知识图谱可视化 + + + + + + +
+
+ + +
+
+ +
+
+ 节点 + 0 +
+
+ + 0 +
+
+ +
+ +
+
节点类型
+
entity
+
fact
+
decision
+
skill
+
event
+
+ +
+
加载中...
+
+ + + + \ No newline at end of file