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 @@ + + +
+ + +