Files
memoryweave/go/internal/api/server.go
T

1729 lines
61 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// HTTP 服务器 — 多 Agent 架构(FileGraph + NetworkEventBus + 跨Agent缓存失效)
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/xiaoxue/memoryweave/internal/api/middleware"
"github.com/xiaoxue/memoryweave/internal/api/routes"
"github.com/xiaoxue/memoryweave/internal/distill"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/metrics"
"github.com/xiaoxue/memoryweave/internal/models"
"github.com/xiaoxue/memoryweave/internal/redact"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// cachedGraphStoreRef 全局图谱缓存引用(用于 graph write 操作时主动失效)
var cachedGraphStoreRef *storage.GraphCacheRef
// skillByNameHandler GET/DELETE /api/v1/skills/{name} 的 method 分支分发器
func skillByNameHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
routes.Skills.Get(w, r)
case http.MethodDelete:
routes.Skills.Delete(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func NewServer() http.Handler {
mux := http.NewServeMux()
// ─── 初始化依赖 ──────────────────────────────
emb := storage.NewEmbedder(os.Getenv("VLLM_ENDPOINT"))
rerank := storage.NewReranker(os.Getenv("RERANK_ENDPOINT"), emb)
// 存储后端
ldb := initStorageBackend(os.Getenv("STORAGE_BACKEND"), emb)
// 召回元数据「累积延迟更新」缓冲(2026-09-12 优化B)
// 读路径不再逐条写 LanceDB(MVCC 每写一行一个版本 → _versions 膨胀根因),
// 改为窗口内合并 + 单事务批量提交(每批 1 个版本)。
storage.InitRecallWriteBuffer(ldb)
// 软删除累积延迟更新缓冲(2026-09-12 优化C)
// 软删除操作不再逐条写 LanceDB(每写一行一个版本 → _versions 膨胀),
// 改为窗口内合并 + 单事务批量提交(每批 1 个版本)。
storage.InitSoftDeleteWriteBuffer(ldb)
// 启动时数据目录一致性检查(防止路径混乱导致读取废弃数据)
runStartupChecks(os.Getenv("STORAGE_BACKEND"))
// G7.2: 注册 LDB getter(供 skill crystallize 使用)
routes.RegisterLDBGetter(func() storage.LanceDB { return ldb })
// 冲突检测器(E4.1: Commit 时矛盾检测依赖此实例)
// 2026-09-20:改用带持久化的构造 —— 此前 active 是纯内存 map,zhiyid 重启即丢
// (实测 17:17 重启后 pending 冲突全部消失)。落盘失败不致命,降级为内存模式并记日志。
conflictDetector, cdErr := governance.NewPersistentConflictDetector("/var/lib/memoryweave/conflicts.json")
if cdErr != nil {
log.Printf("[conflict] 持久化加载失败,降级为内存模式: %v", cdErr)
}
// 注册全局实例,供 selfoptimize 触发器执行器读取(TConflict 判据)
governance.SetGlobalConflictDetector(conflictDetector)
// 图谱(带 Navigate 结果缓存:TTL 5min,容量 500,热点实体缓存命中加速)
graphStore, cachedGraphStoreRef := storage.NewCachedGraphStore(initGraphStore())
api := routes.NewAPI(ldb, emb, rerank, conflictDetector, graphStore)
// G1: Recall 管线挂图谱扩展
api.Pipeline.SetGraphExpander(graphStore)
// 启动时初始化 Prometheus 指标
go func() {
time.Sleep(1 * time.Second) // 等待 Rust sidecar 就绪
if stats, err := ldb.Stats(); err == nil {
if total, ok := stats["total_memories"].(float64); ok {
metrics.TotalMemories.Set(total)
}
if total, ok := stats["total_episodes"].(float64); ok {
metrics.TotalEpisodes.Set(total)
}
log.Printf("[metrics] 初始化指标: memories=%.0f", stats["total_memories"])
}
}()
// 图谱(带 Navigate 结果缓存:TTL 5min,容量 500,热点实体缓存命中加速)
// 已在上面初始化
graphUpdater := governance.NewAutoGraphUpdater(graphStore)
graphAPI := routes.NewGraphAPI(graphStore)
// G4: 自动蒸馏 → 注入图谱更新器
routes.SetGraphUpdater(graphUpdater)
// 冲突
conflictAPI := routes.NewConflictAPI(conflictDetector)
// ─── 缺口
gapDetector := selfoptimize.NewGapDetector(emb, ldb)
gapDetector.EnableGapRedisPersistence()
selfoptimize.SetGlobalGapDetector(gapDetector) // 注册全局实例供 recall 回调使用
// 因果追踪持久化
routes.CascadeR.Tracker().EnableCausalRedisPersistence()
gapAPI := routes.NewGapAPI(gapDetector)
routes.InitGapRepair(gapDetector, ldb) // 共享同一个 GapDetector + LanceDB
// G1a: 挂缺隙记录器(0 结果 → 自动记录 miss)
api.Pipeline.SetMissRecorder(func(query, namespace string) {
gapDetector.RecordMiss(query + " [" + namespace + "]")
})
feedbackAPI := routes.NewFeedbackAPI(ldb)
// 遗忘器:支持按 Agent 类型设置衰减率
forgetter := governance.NewForgetter()
adminAPI := routes.NewAdminAPI(ldb, forgetter, graphStore)
agentRegistry := routes.NewAgentRegistry(nil)
evalAPI := routes.NewEvalAPI(api.Pipeline, ldb)
l3API := routes.WM
consolPipe := routes.NewConsolidationPipeline(ldb, graphStore, conflictDetector)
consolPipe.SetDataDir("/var/lib/memoryweave/lancedb", "/var/lib/memoryweave/graph.db")
// Obsidian
obsidian := routes.NewObsidianSyncer("", ldb)
// ─── 蒸馏引擎 ───────────────────────────────
llmEndpoint := os.Getenv("LLM_ENDPOINT")
llmModel := os.Getenv("LLM_MODEL")
if llmModel == "" {
llmModel = "deepseek/deepseek-v4-pro"
}
llmAPIKey := os.Getenv("LLM_API_KEY")
if llmAPIKey == "" {
llmAPIKey = os.Getenv("API_KEY")
}
distillEngine := distill.NewEngine(llmEndpoint, llmModel, llmAPIKey)
routes.DistillEngineRef = distillEngine
// 蒸馏完成 → 自动图谱更新 + 冲突检测 + 被动验证 + 回写内存
distill.OnDistillComplete = func(input distill.DistillInput, result distill.DistillResult) {
entityNames := make([]string, len(result.Entities))
for i, e := range result.Entities {
entityNames[i] = e.Name
}
graphUpdater.UpdateFromDistill(&governance.DistillInput{
Content: input.Content, Facts: result.Facts,
Entities: entityNames, Namespace: input.Namespace,
EpisodeID: input.EpisodeID,
})
// 回写蒸馏产物到记忆库(LanceDB)
// 2026-09-07 方案B 质量门槛: 碎片(<20字)/噪声句(状态汇报/桥接/reflection)不入库,
// 与已有记忆完全重复跳过(蒸馏直写不走 commit exact_dup, 需本层查重)。
for _, fact := range result.Facts {
// 2026-09-10 P0:直写路径同样脱敏(这里绕过 FilterQualityFacts,
// 事故里泄漏进镜像的记忆正是从这条路径入库的)。
fact = redact.RedactSecrets(fact)
if !distill.IsQualityFact(fact) {
continue // 碎片/噪声过滤
}
// 查重: 同 namespace 已存在**语义等价**记忆 → 跳过
// 必须用 cosine 近重复判据(≥0.98),不能只比字符串相等——
// 否则 "牧尘在招商局工作" 与 "牧尘在招商局工作。"(多一个句号)
// 会被判为两条不同事实双双入库,造成近重复堆积。
var skip bool
if qvec, eErr := emb.EncodeSingle(fact); eErr == nil {
if sim, _ := ldb.Search("memories", qvec, 5, input.Namespace); len(sim) > 0 {
for _, ex := range sim {
if ex.Content == fact || cosineSimilarity(qvec, ex.Vector) >= 0.98 {
skip = true
break
}
}
}
}
if skip {
continue
}
mem := models.MemoryRecord{
ID: fmt.Sprintf("mem_%d", time.Now().UnixNano()),
AgentID: input.AgentID,
Namespace: input.Namespace,
Content: fact,
Category: "distilled",
Tier: "normal",
QualityScore: result.Overall,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DerivedFrom: input.EpisodeID,
Freshness: "fresh",
Source: "llm_distill", // 2026-09-07 方案C provenance: LLM蒸馏推断来源
}
if err := ldb.InsertMemory(mem); err != nil {
log.Printf("[distill] commit fact failed: %v", err)
}
}
// 冲突检测:加载已有记忆进行比较
// 必须用**新内容的真实向量**做语义检索取近邻;用零向量在语义索引上
// 返回的只是"范数最小"的任意行,真正的矛盾对象不在比对集里 → 永远检不出冲突。
existingMems := []models.MemoryRecord{}
if qv, eErr := emb.EncodeSingle(input.Content); eErr == nil {
if found, _ := ldb.Search("memories", qv, 100, input.Namespace); len(found) > 0 {
existingMems = found
}
}
if len(existingMems) == 0 {
zeroVec := make([]float32, 1024)
existingMems, _ = ldb.Search("memories", zeroVec, 100, input.Namespace)
}
conflictExisting := make([]map[string]interface{}, 0)
for _, m := range existingMems {
conflictExisting = append(conflictExisting, map[string]interface{}{
"content": m.Content,
"entities": entityNames, // 用当前内容提取的实体做交叉比较
})
}
for _, c := range conflictDetector.Scan(input.Content, entityNames, conflictExisting) {
if c.Strategy == "latest_wins" {
conflictDetector.AutoResolve(c)
}
}
// 被动验证:使用已加载的记忆进行 P1/P2/P3 匹配
validMems := make([]selfoptimize.MemoryForValidation, len(existingMems))
for i, m := range existingMems {
validMems[i] = selfoptimize.MemoryForValidation{
ID: m.ID, Content: m.Content, QualityScore: m.QualityScore,
}
}
validated := selfoptimize.Validator.Validate(input.Content, validMems)
// 被动验证结果写回 quality_score
// 修复(2026-09-20):原先用 GetRecords() 遍历【全部历史记录】并无条件写回 ——
// 每来一次输入就重写全库(≈6000 条),每次 Update 新增一个 LanceDB 版本快照
// (manifest 引用全部 data 文件 ⇒ O(n²)),导致版本数每日 ~1.9 万、占用每日涨十几 G。
// 现改为:只写【本次真正匹配】且【Confidence 实际发生变化】的记录。
for _, r := range validated {
if r.Confidence > 0 && r.Changed {
_ = ldb.Update("memories", r.MemoryID, map[string]any{
"quality_score": r.Confidence,
})
}
}
}
// ─── CO_OCCURS 追踪器(持久化到 Redis)───
storage.CoOccurTrackerInstance = storage.NewCoOccurTracker(storage.RedisCoOccurPersist())
storage.LoadCoOccurFromRedis()
// ─── Dashboard 持久化到 Redis(restart 不丢指标)───
selfoptimize.EnableRedisPersistence()
// ─── VProp 持久化到 Redis(restart 不丢)───
selfoptimize.VProp.EnableVPropRedisPersistence()
// ─── Skill 持久化到 Redis(G7: restart 不丢 skills)───
routes.BayesianSkills.EnableRedisPersistence()
// ─── V 值传播器(竞争性架构核心)─────────────
vPropagator := selfoptimize.VProp
// 注入 V 值查询函数到存储层(召回排序融合)
storage.VValueProvider = func(memoryID string) float64 {
return vPropagator.GetMemVValue(memoryID)
}
// 在蒸馏完成回调中记录 V 值
originalOnComplete := distill.OnDistillComplete
distill.OnDistillComplete = func(input distill.DistillInput, result distill.DistillResult) {
// 调用已有的回调
if originalOnComplete != nil {
originalOnComplete(input, result)
}
// 记录 V 值决策:蒸馏成功 = 有用
if len(result.Facts) > 0 {
vPropagator.RecordDecision(
"distill_"+input.EpisodeID,
[]string{input.EpisodeID},
"distill", "success", "",
)
}
// 更新仪表盘指标
selfoptimize.Dash.RecordUseful()
// 记录蒸馏质量损失(avg_distill_loss = 1 - Overall)
// 仅记录真实蒸馏:Overall > 0 表示 LLM 成功返回,过滤 fallback (Overall=0)
if result.Overall > 0 {
loss := 1.0 - result.Overall
if loss < 0 {
loss = 0
}
if loss > 1 {
loss = 1
}
selfoptimize.Dash.RecordDistillLoss(loss)
}
}
// ─── 跨 Agent 缓存失效回调 ───────────────────
// 收到其他 Agent 的广播 → 失效本地缓存
governance.GlobalEventBus.Subscribe("cache.invalidate", "self")
// ─── 路由注册 ──────────────────────────────
// ─── 静态文件服务(Web UI)─────────────────────
// webUIRoot 在函数顶部声明
var webUIRoot string
// 后续在 catch-all 中处理(见文件末尾)
mux.HandleFunc("/health", routes.HandleHealth)
mux.HandleFunc("/api/v1/health", routes.HandleHealth)
// 图谱缓存统计
mux.HandleFunc("/api/v1/cache/stats", func(w http.ResponseWriter, r *http.Request) {
if cachedGraphStoreRef != nil {
respondJSON(w, 200, map[string]interface{}{
"graph_cache": cachedGraphStoreRef.Stats(),
})
} else {
respondJSON(w, 200, map[string]interface{}{"graph_cache": nil})
}
})
// ─── Prometheus /metrics ─────────────────────
mux.Handle("/metrics", metrics.Handler())
// 核心 API
mux.HandleFunc("/api/v1/commit", func(w http.ResponseWriter, r *http.Request) {
api.Commit(w, r)
// 跨 Agent 广播 + 自动蒸馏
go func() {
var req struct {
AgentID string `json:"agent_id"`
Namespace string `json:"namespace"`
Content string `json:"content"`
}
if r.Body != nil {
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &req)
r.Body = io.NopCloser(bytes.NewReader(body))
}
if req.Namespace != "" {
// 通知其他 Agent:缓存失效 + 新记忆事件
governance.PushCacheInvalidate(req.Namespace, "commit")
governance.PushMemoryCommitted(req.AgentID, req.Namespace, "")
}
}()
})
mux.HandleFunc("/api/v1/recall", api.Recall)
mux.HandleFunc("/api/v1/recall/debug", api.RecallDebug)
mux.HandleFunc("/api/v1/feedback", api.Feedback)
mux.HandleFunc("/api/v1/bootstrap", api.Bootstrap)
mux.HandleFunc("/api/v1/stats", api.Stats)
mux.HandleFunc("/api/v1/batch-commit", api.BatchCommit)
// 自优化指标(Dashboard,含 avg_distill_loss)
mux.HandleFunc("/api/v1/metrics", func(w http.ResponseWriter, r *http.Request) {
m := selfoptimize.Dash.Metrics()
if stats, err := ldb.Stats(); err == nil {
if total, ok := stats["total_memories"].(float64); ok {
m["total_memories"] = total
}
if total, ok := stats["total_episodes"].(float64); ok {
m["total_episodes"] = total
}
}
respondJSON(w, 200, m)
})
// ─── WebSocket 实时推送 ──────────────────────
mux.HandleFunc("/api/v1/ws/", routes.WSHandler)
// 社区发现(Louvain + 社区摘要)
mux.HandleFunc("/api/v1/community/summarize", api.HandleCommunitySummarize)
// 深度推理(人侧数据 → 三类清单)
mux.HandleFunc("/api/v1/deep-think", api.HandleDeepThink)
mux.HandleFunc("/api/v1/ws", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok","protocol":"websocket","endpoint":"/api/v1/ws/{agent_id}"}`))
})
// 知识图谱
mux.HandleFunc("/api/v1/graph/stats", graphAPI.Stats)
mux.HandleFunc("/api/v1/graph/query", graphAPI.Query)
mux.HandleFunc("/api/v1/graph/navigate", graphAPI.Navigate)
// P2: 边反馈
mux.HandleFunc("/api/v1/graph/edge/feedback", api.EdgeFeedback)
// 新增:添加关系边(POST JSON body: {"from":"实体A","to":"实体B","relation":"关系类型","namespace":""})
mux.HandleFunc("/api/v1/graph/edge", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "POST only", 405)
return
}
var req struct {
From string `json:"from"`
To string `json:"to"`
Relation string `json:"relation"`
Namespace string `json:"namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), 400)
return
}
if req.From == "" || req.To == "" || req.Relation == "" {
http.Error(w, "from, to, relation required", 400)
return
}
ns := req.Namespace
if ns == "" {
ns = "default"
}
edgeID := fmt.Sprintf("e_%s_%s_%d", req.From, req.Relation, time.Now().UnixNano())
maxRetries := 5
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
lastErr = graphStore.AddEdge(edgeID, req.From, req.To, req.Relation, ns, 1.0)
if lastErr == nil {
break // Success, exit retry loop
}
// Check for rate limit error (429) - zhiyid returns rate_limit_exceeded
if strings.Contains(lastErr.Error(), "rate_limit_exceeded") || strings.Contains(lastErr.Error(), "429") {
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
backoff := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(backoff)
continue
}
// Any other error - don't retry
break
}
if lastErr != nil {
// Max retries exceeded - write to disk for later retry
failedPath := "/tmp/memoryweave/failed_edges.jsonl"
dir := filepath.Dir(failedPath)
os.MkdirAll(dir, 0755)
failedEdge := map[string]interface{}{
"edge_id": edgeID,
"from": req.From,
"to": req.To,
"relation": req.Relation,
"namespace": ns,
"weight": 1.0,
"timestamp": time.Now().UnixNano(),
"error": lastErr.Error(),
}
jsonData, _ := json.Marshal(failedEdge)
// Append to jsonl file
f, _ := os.OpenFile(failedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
f.Write(jsonData)
f.WriteString("\n")
// Return success with warning that edge was queued for later retry
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "queued_for_retry",
"edge_id": edgeID,
"message": "Edge write failed after max retries; queued to /tmp/memoryweave/failed_edges.jsonl",
})
return
}
// 图谱变更:失效 navigate 缓存
if cachedGraphStoreRef != nil {
cachedGraphStoreRef.InvalidateAll()
}
json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok", "edge_id": edgeID})
})
// 新增:pagerank + evidence_count
mux.HandleFunc("/api/v1/graph/pagerank", func(w http.ResponseWriter, r *http.Request) {
rank := graphStore.PageRank(0.85, 20)
data, _ := json.Marshal(map[string]interface{}{
"pagerank": rank,
"count": len(rank),
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
})
mux.HandleFunc("/api/v1/graph/evidence/", func(w http.ResponseWriter, r *http.Request) {
entity := r.URL.Path[len("/api/v1/graph/evidence/"):]
count := graphStore.EvidenceCount(entity)
data, _ := json.Marshal(map[string]interface{}{
"entity": entity,
"evidence_count": count,
})
w.Header().Set("Content-Type", "application/json")
w.Write(data)
})
// 图谱可视化导出
mux.HandleFunc("/api/v1/graph/export", func(w http.ResponseWriter, r *http.Request) {
ns := r.URL.Query().Get("namespace")
limitStr := r.URL.Query().Get("limit")
limit := 0
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
}
nodes, edges := graphStore.GetGraph(ns, limit)
respondJSON(w, 200, map[string]interface{}{
"nodes": nodes,
"edges": edges,
"count": map[string]int{"nodes": len(nodes), "edges": len(edges)},
})
})
// 图谱导航 + Obsidian 笔记关联
mux.HandleFunc("/api/v1/graph/notes", func(w http.ResponseWriter, r *http.Request) {
entity := r.URL.Query().Get("entity")
maxHops := 2
if h := r.URL.Query().Get("max_hops"); h != "" {
if parsed, err := strconv.Atoi(h); err == nil && parsed > 0 {
maxHops = parsed
}
}
maxNotes := 10
if n := r.URL.Query().Get("max_notes"); n != "" {
if parsed, err := strconv.Atoi(n); err == nil && parsed > 0 {
maxNotes = parsed
}
}
var noteResults []map[string]interface{}
var paths []map[string]interface{}
if entity != "" {
p, err := graphStore.Navigate(routes.NormalizeEntity(entity), maxHops, "", nil)
if err == nil {
paths = p
entities := []string{routes.NormalizeEntity(entity)}
for _, path := range p {
if s, ok := path["source"].(string); ok {
entities = append(entities, s)
}
if t, ok := path["target"].(string); ok {
entities = append(entities, t)
}
}
if obsidian != nil {
noteResults = obsidian.SearchNotesByEntities(entities, maxNotes)
}
}
}
respondJSON(w, 200, map[string]interface{}{
"entity": entity,
"graph_paths": paths,
"notes": noteResults,
"paths_count": len(paths),
"notes_count": len(noteResults),
})
})
// 自然语言图谱查询 /api/v1/graph/nl_query
// 输入: {"query": "织忆和牧尘是什么关系"}
// 返回: 自然语言关系描述
mux.HandleFunc("/api/v1/graph/nl_query", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
var req struct {
Query string `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
query := strings.TrimSpace(req.Query)
if query == "" {
http.Error(w, "query is required", http.StatusBadRequest)
return
}
// 1. 从 query 中提取可能的实体名(去掉问号、语气词)
cleanQuery := strings.TrimSuffix(strings.TrimSuffix(query, "?"), "?")
cleanQuery = strings.TrimPrefix(cleanQuery, "请问")
cleanQuery = strings.TrimPrefix(cleanQuery, "请说")
cleanQuery = strings.TrimSpace(cleanQuery)
// 2. 尝试识别实体对("A和B的关系"、"A与B")
var entityA, entityB string
// 模式1: "A和B是什么关系" / "A和B有什么关系" / "A和B的关系"
hasAnd := strings.Contains(cleanQuery, "和") || strings.Contains(cleanQuery, "与")
if hasAnd && (strings.Contains(cleanQuery, "关系") || strings.Contains(cleanQuery, "关联")) {
delim := "和"
if strings.Contains(cleanQuery, "与") {
delim = "与"
}
parts := strings.Split(cleanQuery, delim)
if len(parts) == 2 {
entityA = strings.TrimSpace(parts[0])
rawB := strings.TrimSpace(parts[1])
// 去掉句末的问句模式得到 entityB
entityB = strings.TrimSuffix(rawB, "是什么关系")
entityB = strings.TrimSuffix(entityB, "有什么关系")
entityB = strings.TrimSuffix(entityB, "的")
entityB = strings.TrimSuffix(entityB, "是什么")
entityB = strings.TrimSpace(entityB)
// 如果去掉问句后为空(如"织忆和牧尘" -> entityB=""),则原始 query 可能是"A和B"
if entityB == "" {
entityB = rawB
}
}
}
var result map[string]interface{}
if entityA != "" && entityB != "" {
// 双向查询:A → B 的关系
paths, _ := graphStore.Navigate(routes.NormalizeEntity(entityA), 2, "", nil)
relB, _ := graphStore.Navigate(routes.NormalizeEntity(entityB), 2, "", nil)
// 找 A → B 的直接边
var directPath map[string]interface{}
for _, p := range paths {
toNorm := routes.NormalizeEntity(p["to"].(string))
targetNorm := routes.NormalizeEntity(entityB)
if toNorm == targetNorm {
directPath = p
break
}
}
result = map[string]interface{}{
"query": query,
"entity_a": entityA,
"entity_b": entityB,
"direct_path": directPath,
"paths_from_a": len(paths),
"paths_from_b": len(relB),
"summary": fmt.Sprintf("%s 与 %s 存在 %d 条关联路径", entityA, entityB, len(paths)),
}
} else {
// 单实体查询:找 entity 的关系网
entity := cleanQuery
paths, _ := graphStore.Navigate(routes.NormalizeEntity(entity), 2, "", nil)
// 按 relation 分组
relCounts := make(map[string]int)
for _, p := range paths {
rel := p["relation"].(string)
if rel == "" {
rel = "unknown"
}
relCounts[rel]++
}
// 生成自然语言描述
var rels []string
for rel, cnt := range relCounts {
rels = append(rels, fmt.Sprintf("%s(%d条)", rel, cnt))
}
sort.Strings(rels)
result = map[string]interface{}{
"query": query,
"entity": entity,
"total_paths": len(paths),
"relation_summary": rels,
"summary": fmt.Sprintf("%s 共有 %d 条关联,分布在 %d 种关系类型", entity, len(paths), len(relCounts)),
}
}
respondJSON(w, 200, result)
})
// 图谱脏数据清理 /api/v1/graph/cleanup
mux.HandleFunc("/api/v1/graph/cleanup", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" && r.Method != "GET" {
http.Error(w, "GET or POST only", http.StatusMethodNotAllowed)
return
}
dryRun := r.URL.Query().Get("dry_run") != "false" // 默认为 dryRun
count, ids, err := graphStore.CleanupNoiseNodes(dryRun)
if err != nil {
http.Error(w, "cleanup failed: "+err.Error(), http.StatusInternalServerError)
return
}
// 实际执行后失效缓存(dry_run=true 时不执行,无需失效)
if !dryRun && cachedGraphStoreRef != nil && count > 0 {
cachedGraphStoreRef.InvalidateAll()
}
respondJSON(w, 200, map[string]interface{}{
"dry_run": dryRun,
"removed": count,
"node_ids": ids,
"message": fmt.Sprintf("Found %d noise nodes", count),
})
})
// 图谱多 agent 测试污染清理(namespace + 名称子串圈定,不碰全表编码规则)
// 2026-09-08 卫安/A03/A04 371 节点:openclaw-main/a06-main/hermes-main 中测试 agent 产生的概念/事实节点
// GET/POST /api/v1/graph/cleanup/scoped?dry_run=true&namespace=a,b,c&name=a03,a04,卫安
mux.HandleFunc("/api/v1/graph/cleanup/scoped", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" && r.Method != "GET" {
http.Error(w, "GET or POST only", http.StatusMethodNotAllowed)
return
}
dryRun := r.URL.Query().Get("dry_run") != "false" // 默认为 dryRun
nsParam := r.URL.Query().Get("namespace")
nameParam := r.URL.Query().Get("name")
if nsParam == "" || nameParam == "" {
http.Error(w, "namespace and name (comma separated) are required", http.StatusBadRequest)
return
}
namespaces := strings.Split(nsParam, ",")
nameContains := strings.Split(nameParam, ",")
type scopedCleaner interface {
CleanupScopedNodes(dryRun bool, namespaces, nameContains []string) (int, []string, error)
}
cleaner, ok := graphStore.(scopedCleaner)
if !ok {
http.Error(w, "scoped cleanup not supported by current graph backend", http.StatusNotImplemented)
return
}
count, ids, err := cleaner.CleanupScopedNodes(dryRun, namespaces, nameContains)
if err != nil {
http.Error(w, "cleanup failed: "+err.Error(), http.StatusInternalServerError)
return
}
// 实际执行后失效缓存(dry_run=true 时不执行,无需失效)
if !dryRun && cachedGraphStoreRef != nil && count > 0 {
cachedGraphStoreRef.InvalidateAll()
}
respondJSON(w, 200, map[string]interface{}{
"dry_run": dryRun,
"removed": count,
"node_ids": ids,
"namespaces": namespaces,
"name_contains": nameContains,
"message": fmt.Sprintf("Found %d scoped noise nodes", count),
})
})
// 静态文件服务(知识图谱可视化 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" {
conflictAPI.List(w, r)
} else {
http.NotFound(w, r)
}
})
mux.HandleFunc("/api/v1/conflicts/resolve", conflictAPI.Resolve)
// 反馈 — G5: 修正记忆时填充 version_history
mux.HandleFunc("/api/v1/feedback/useful", func(w http.ResponseWriter, r *http.Request) {
feedbackAPI.MarkUseful(w, r)
// 质量监控:useful → 质量上升
var req struct {
MemoryID string `json:"memory_id"`
}
if r.Body != nil {
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &req)
r.Body = io.NopCloser(bytes.NewReader(body))
if req.MemoryID != "" {
selfoptimize.Dash.RecordUseful()
// V 值:useful = success
selfoptimize.VProp.RecordDecision(
"useful_"+req.MemoryID,
[]string{req.MemoryID},
"user_feedback", "success", "",
)
selfoptimize.QualityMonitor.Check(req.MemoryID, 0, 5) // 清除潜在告警
}
}
})
mux.HandleFunc("/api/v1/feedback/not-useful", func(w http.ResponseWriter, r *http.Request) {
feedbackAPI.MarkNotUseful(w, r)
// 质量监控:not-useful → 检查是否需要降权
var req struct {
MemoryID string `json:"memory_id"`
}
if r.Body != nil {
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &req)
r.Body = io.NopCloser(bytes.NewReader(body))
if req.MemoryID != "" {
selfoptimize.Dash.RecordNotUseful()
// V 值:not-useful = failure
selfoptimize.VProp.RecordDecision(
"notuseful_"+req.MemoryID,
[]string{req.MemoryID},
"user_feedback", "failure", "",
)
score := selfoptimize.Dash.QualityScore()
if record := selfoptimize.QualityMonitor.Check(req.MemoryID, score, 5); record != nil {
_ = record // WebSocket 通知
}
}
}
})
mux.HandleFunc("/api/v1/feedback/deprecate", feedbackAPI.Deprecate)
mux.HandleFunc("/api/v1/feedback/correct", func(w http.ResponseWriter, r *http.Request) {
feedbackAPI.Correct(w, r)
var req struct {
MemoryID string `json:"memory_id"`
NewContent string `json:"new_content"`
Source string `json:"source"`
}
if r.Body != nil {
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &req)
r.Body = io.NopCloser(bytes.NewReader(body))
if req.MemoryID != "" && req.NewContent != "" {
routes.FillVersionHistory(req.MemoryID, "", req.NewContent, req.Source)
// 跨 Agent 通知
governance.PushMemoryUpdated(req.MemoryID, 0, "corrected")
}
}
})
// 缺口
mux.HandleFunc("/api/v1/gaps", gapAPI.List)
mux.HandleFunc("/api/v1/gaps/detect", gapAPI.Detect)
mux.HandleFunc("/api/v1/gaps/close/", func(w http.ResponseWriter, r *http.Request) {
gapAPI.Close(w, r)
topic := r.URL.Path[len("/api/v1/gaps/close/"):]
routes.PushGapFilled(topic, 1)
governance.PushGapFilled(topic, 1)
})
mux.HandleFunc("/api/v1/gaps/repair", routes.GapRepair.FullRepairHandler)
// Agent 注册
mux.HandleFunc("/api/v1/agents/register", agentRegistry.Register)
mux.HandleFunc("/api/v1/agents", agentRegistry.List)
// ─── 多 Agent EventBus 管理 ──────────────────
mux.HandleFunc("/api/v1/eventbus/subscribe", func(w http.ResponseWriter, r *http.Request) {
var req struct {
EventType string `json:"event_type"`
Callback string `json:"callback_url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
governance.GlobalEventBus.Subscribe(req.EventType, req.Callback)
respondJSON(w, 200, map[string]string{"status": "subscribed"})
})
mux.HandleFunc("/api/v1/eventbus/unsubscribe", func(w http.ResponseWriter, r *http.Request) {
var req struct {
EventType string `json:"event_type"`
Callback string `json:"callback_url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
governance.GlobalEventBus.Unsubscribe(req.EventType, req.Callback)
respondJSON(w, 200, map[string]string{"status": "unsubscribed"})
})
mux.HandleFunc("/api/v1/eventbus/list", func(w http.ResponseWriter, r *http.Request) {
subs := governance.GlobalEventBus.ListSubscribers()
respondJSON(w, 200, subs)
})
// 跨 Agent 缓存失效接收端点(供其他 Agent 实例 HTTP 回调)
mux.HandleFunc("/api/v1/cache/invalidate", func(w http.ResponseWriter, r *http.Request) {
var evt struct {
Type string `json:"type"`
Payload map[string]string `json:"payload"`
}
if err := json.NewDecoder(r.Body).Decode(&evt); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
ns := evt.Payload["namespace"]
storage.SearchCacheInstance.Invalidate(ns)
log.Printf("[cache] 跨 Agent 失效接收: %s", ns)
respondJSON(w, 200, map[string]string{"status": "invalidated"})
})
// 管理
// ?task=full 触发完整整合(DBSCAN + 衰减校准 + 质量回溯),默认 cluster_only
mux.HandleFunc("/api/v1/admin/consolidate", func(w http.ResponseWriter, r *http.Request) {
task := r.URL.Query().Get("task")
if task == "" {
task = "cluster_only"
}
// full 模式给 5 分钟;cluster_only 保持 30 秒
timeout := 30 * time.Second
if task == "full" {
timeout = 5 * time.Minute
}
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
done := make(chan *routes.ConsolidationReport, 1)
go func() {
report, _ := consolPipe.RunWithMode(task)
done <- report
}()
select {
case <-ctx.Done():
respondJSON(w, 408, map[string]string{"error": "consolidation timeout", "task": task})
return
case report := <-done:
if report == nil {
respondJSON(w, 500, map[string]string{"error": "consolidation failed", "task": task})
return
}
respondJSON(w, 200, report)
go governance.PushDistillationComplete(report.Merged, report.ConflictsFound)
}
})
mux.HandleFunc("/api/v1/admin/forget", adminAPI.Forget)
mux.HandleFunc("/api/v1/admin/dedup", api.Dedup)
mux.HandleFunc("/api/v1/admin/backup", adminAPI.Backup)
mux.HandleFunc("/api/v1/admin/restore", adminAPI.Restore)
mux.HandleFunc("/api/v1/admin/backups", adminAPI.ListBackups)
mux.HandleFunc("/api/v1/admin/audit", adminAPI.Audit)
// 遗忘器类型管理
mux.HandleFunc("/api/v1/admin/forgetter/type", func(w http.ResponseWriter, r *http.Request) {
var req struct {
AgentType string `json:"agent_type"`
}
if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
forgetter.SetAgentType(req.AgentType)
}
respondJSON(w, 200, map[string]interface{}{
"agent_type": forgetter.AgentType(),
"decay_rate": AgentTypeDecayOrDefault(forgetter.AgentType()),
})
})
mux.HandleFunc("/api/v1/admin/distill/force", func(w http.ResponseWriter, r *http.Request) {
if routes.DistillEngineRef == nil {
respondJSON(w, 400, map[string]string{"error": "distill engine not initialized"})
return
}
var req struct {
Content string `json:"content"`
Category string `json:"category"`
Namespace string `json:"namespace"`
AgentID string `json:"agent_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if req.Content == "" {
respondJSON(w, 400, map[string]string{"error": "content required"})
return
}
if req.Namespace == "" {
req.Namespace = "hermes-main"
}
if req.AgentID == "" {
req.AgentID = "hermes-a06"
}
epID := fmt.Sprintf("ep_manual_%d", time.Now().UnixNano())
routes.AutoDistillTrigger(epID, req.Content, req.Category, req.Namespace, req.AgentID)
respondJSON(w, 200, map[string]string{"status": "queued", "episode_id": epID})
})
// 蒸馏队列状态端点(G8-G9 配套)
mux.HandleFunc("/api/v1/distill/status", func(w http.ResponseWriter, r *http.Request) {
if routes.DistillEngineRef == nil {
respondJSON(w, 200, map[string]interface{}{"status": "not_initialized", "queue_len": 0})
return
}
respondJSON(w, 200, routes.DistillEngineRef.GetStatus())
})
mux.HandleFunc("/api/v1/distill/queue", func(w http.ResponseWriter, r *http.Request) {
if routes.DistillEngineRef == nil {
respondJSON(w, 200, map[string]interface{}{"queue": []interface{}{}, "count": 0})
return
}
items := routes.DistillEngineRef.QueueItems()
respondJSON(w, 200, map[string]interface{}{"queue": items, "count": len(items)})
})
mux.HandleFunc("/api/v1/distill/quota", func(w http.ResponseWriter, r *http.Request) {
if routes.DistillEngineRef == nil {
respondJSON(w, 200, map[string]interface{}{"remaining": 0, "used": 0, "limit": 0, "status": "not_initialized"})
return
}
respondJSON(w, 200, routes.DistillEngineRef.GetQuota())
})
mux.HandleFunc("/api/v1/distilled/{id}", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "DELETE" {
adminAPI.DeleteDistilled(w, r)
} else {
http.NotFound(w, r)
}
})
// P2: 记忆离线整合(LightMem UPDATE_PROMPT)— 手动触发
// POST /api/v1/consolidate/memory {"namespace":"hermes-main"}
mux.HandleFunc("/api/v1/consolidate/memory", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "POST only", 405)
return
}
if routes.DistillEngineRef == nil {
respondJSON(w, 400, map[string]string{"error": "distill engine not initialized"})
return
}
var req struct {
Namespace string `json:"namespace"`
Limit int `json:"limit"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if req.Namespace == "" {
req.Namespace = "hermes-main"
}
if req.Limit <= 0 {
req.Limit = 50
}
// 取全部记忆(零向量搜索)
zeroVec := make([]float32, 1024)
mems, err := ldb.Search("memories", zeroVec, req.Limit, req.Namespace)
if err != nil || len(mems) == 0 {
respondJSON(w, 200, map[string]interface{}{"status": "ok", "processed": 0, "message": "no memories to consolidate"})
return
}
// 两两比较相似度(简单 Jaccard/词重叠启发式,避免调用嵌入)
// 真实场景:应使用向量相似度,此处用词重叠近似
processed := 0
updated := 0
deleted := 0
ignored := 0
errors_ := 0
// 按目标分组整合:外层 i 为目标,内层收集所有高相似候选,
// 每个目标只调一次 LLM(UpdatePrompt 原生支持多候选),避免 O(n²) LLM 调用风暴。
// 2026-09-06 fix(t_04 ④): 原实现每对相似记忆调一次 LLM → limit=100 产生 4950 对
// → 跑 223s+ 未完成 → 每天 processed=0(不是 namespace 过滤 bug)。
for i := 0; i < len(mems); i++ {
// 收集所有与 mems[i] 高相似的候选(j > i 且未处理)
var cands []distill.MemoryCandidate
for j := i + 1; j < len(mems); j++ {
if mems[j].Content == "" {
continue
}
sim := distill.TextSimilarity(mems[i].Content, mems[j].Content)
if sim < 0.5 {
continue
}
cands = append(cands, distill.MemoryCandidate{ID: mems[j].ID, Content: mems[j].Content})
}
if len(cands) == 0 {
continue
}
// 一次 LLM 决策:目标 mems[i] + 全部高相似候选
result, err := routes.DistillEngineRef.ConsolidateMemory(
distill.MemoryCandidate{ID: mems[i].ID, Content: mems[i].Content},
cands,
)
if err != nil {
errors_++
continue
}
processed++
switch result.Action {
case "update":
if result.NewMemory != "" {
if err := ldb.UpdateMemoryContent(mems[i].ID, result.NewMemory, "consolidate"); err != nil {
log.Printf("[consolidate] update failed %s: %v", mems[i].ID, err)
} else {
updated++
}
}
case "delete":
if err := ldb.SoftDelete(mems[i].ID, "consolidate: conflict"); err != nil {
log.Printf("[consolidate] delete failed %s: %v", mems[i].ID, err)
} else {
deleted++
}
default:
ignored++
}
}
respondJSON(w, 200, map[string]interface{}{
"status": "ok",
"processed": processed,
"updated": updated,
"deleted": deleted,
"ignored": ignored,
"errors": errors_,
"total_memories": len(mems),
})
})
// GET /api/v1/memories — list all memories (zero-vector search, for plugin compat)
mux.HandleFunc("/api/v1/memories", adminAPI.ListMemories)
mux.HandleFunc("/api/v1/memories/export", adminAPI.ExportMemoriesMD)
// /api/v1/memory/{id}/versions — existing version history endpoint
mux.HandleFunc("/api/v1/memory/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
adminAPI.Versions(w, r)
} else {
http.NotFound(w, r)
}
})
// L3
mux.HandleFunc("/api/v1/l3/worldmodel", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
l3API.GetHandler(w, r)
} else if r.Method == "POST" {
l3API.UpdateHandler(w, r)
} else {
http.NotFound(w, r)
}
})
// 触发器
mux.HandleFunc("/api/v1/triggers", routes.Triggers.List)
mux.HandleFunc("/api/v1/triggers/fire", routes.Triggers.Fire)
// 触发器 pause/resume — 手动解析路径
mux.HandleFunc("/api/v1/admin/triggers/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// 格式: /api/v1/admin/triggers/{id}/{action}
parts := strings.Split(strings.TrimPrefix(path, "/api/v1/admin/triggers/"), "/")
if len(parts) != 2 {
http.NotFound(w, r)
return
}
id, action := parts[0], parts[1]
// 构造一个新请求,让 Pause/Resume 能通过 PathValue 读取
r.SetPathValue("id", id)
switch action {
case "pause":
routes.Triggers.Pause(w, r)
case "resume":
routes.Triggers.Resume(w, r)
default:
http.NotFound(w, r)
}
})
// Skills(G7)
mux.HandleFunc("/api/v1/skills", routes.Skills.List)
mux.HandleFunc("POST /api/v1/skills", routes.Skills.Register) // 注册新 skill
mux.HandleFunc("/api/v1/skills/stats", routes.Skills.Stats) // skill 统计
mux.HandleFunc("/api/v1/skills/bayes", func(w http.ResponseWriter, r *http.Request) {
list := routes.BayesianSkills.List()
respondJSON(w, 200, list)
})
mux.HandleFunc("/api/v1/skills/{name}", skillByNameHandler) // GET/DELETE /api/v1/skills/{name}
mux.HandleFunc("/api/v1/skills/{name}/trial", routes.Skills.Trial)
mux.HandleFunc("/api/v1/skills/{name}/execute", routes.ExecuteSkill) // G7.3 skill执行
// G7.2: 结晶路由
mux.HandleFunc("/api/v1/crystallize/candidates", routes.GetSkillCandidates) // 获取候选
mux.HandleFunc("/api/v1/crystallize/memory/{id}", routes.CrystallizeSkill) // 对记忆执行结晶
// 评估 + 自调参
mux.HandleFunc("/api/v1/eval/run", evalAPI.Run)
mux.HandleFunc("/api/v1/eval/history", evalAPI.History)
mux.HandleFunc("/api/v1/eval/generate", evalAPI.Generate)
mux.HandleFunc("/api/v1/tuning/status", routes.Tuner.StatusHandler)
mux.HandleFunc("/api/v1/tuning/run", routes.Tuner.RunHandler)
mux.HandleFunc("/api/v1/tuning/analytics", routes.Tuner.AnalyticsHandler)
// 仪表盘 + 验证 + V值 + 缓存 + 流水线
mux.HandleFunc("/api/v1/metrics/self", func(w http.ResponseWriter, r *http.Request) {
metrics := selfoptimize.Dash.Metrics()
respondJSON(w, 200, metrics)
})
mux.HandleFunc("/api/v1/validate/passive", func(w http.ResponseWriter, r *http.Request) {
records := selfoptimize.Validator.GetRecords()
respondJSON(w, 200, records)
})
mux.HandleFunc("/api/v1/vvalue/decisions", func(w http.ResponseWriter, r *http.Request) {
decisions := selfoptimize.VProp.ListRecentDecisions(20)
respondJSON(w, 200, decisions)
})
// G5.3: V 值查询 + 触发反向传播
mux.HandleFunc("/api/v1/vvalue/memory/", func(w http.ResponseWriter, r *http.Request) {
memID := strings.TrimPrefix(r.URL.Path, "/api/v1/vvalue/memory/")
if memID == "" {
respondJSON(w, 400, map[string]string{"error": "memory_id required"})
return
}
v := selfoptimize.VProp.GetMemVValue(memID)
respondJSON(w, 200, map[string]interface{}{"memory_id": memID, "v_value": v})
})
mux.HandleFunc("/api/v1/trace/vprop", func(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryIDs []string `json:"memory_ids"`
Action string `json:"action"`
Outcome string `json:"outcome"`
ParentID string `json:"parent_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err == nil && len(req.MemoryIDs) > 0 {
id := fmt.Sprintf("trace_%d", time.Now().UnixNano())
selfoptimize.VProp.RecordDecision(id, req.MemoryIDs, req.Action, req.Outcome, req.ParentID)
// 更新相关记忆的 quality_score
for _, mid := range req.MemoryIDs {
v := selfoptimize.VProp.GetMemVValue(mid)
lamb := 0.1
if ldb, ok := ldb.(interface {
UpdateQualityScore(id string, delta float64) error
}); ok {
_ = ldb.UpdateQualityScore(mid, lamb*v)
}
}
respondJSON(w, 200, map[string]string{"status": "propagated"})
return
}
respondJSON(w, 400, map[string]string{"error": "memory_ids and outcome required"})
})
mux.HandleFunc("/api/v1/admin/cache", func(w http.ResponseWriter, r *http.Request) {
stats := storage.SearchCacheInstance.Stats()
respondJSON(w, 200, stats)
})
mux.HandleFunc("/api/v1/admin/pipeline", func(w http.ResponseWriter, r *http.Request) {
stats := selfoptimize.Flow.Stats()
respondJSON(w, 200, stats)
})
// G6: ephemeral namespace 管理
mux.HandleFunc("/api/v1/admin/ephemeral/clean", func(w http.ResponseWriter, r *http.Request) {
now := time.Now().UnixMilli()
agents := agentRegistry.ListAgents()
var cleaned []string
for _, agent := range agents {
if now-agent.LastSeen > 5*60*1000 {
ns := agent.AgentID + "-ephemeral"
zeroVec := make([]float32, 1024)
memories, _ := ldb.Search("memories", zeroVec, 1000, ns)
for _, mem := range memories {
if err := ldb.SoftDelete(mem.ID, "ephemeral_expired"); err != nil {
log.Printf("[softdelete] failed id: %v", err)
}
}
if len(memories) > 0 {
cleaned = append(cleaned, ns)
log.Printf("[ephemeral] cleaned %d memories from %s (inactive for %ds)",
len(memories), ns, (now-agent.LastSeen)/1000)
}
}
}
respondJSON(w, 200, map[string]interface{}{
"status": "cleaned",
"cleaned_namespaces": cleaned,
})
})
// Obsidian
mux.HandleFunc("/api/v1/obsidian/push", obsidian.PushHandler)
mux.HandleFunc("/api/v1/obsidian/pull", obsidian.PullHandler)
mux.HandleFunc("/api/v1/obsidian/status", obsidian.StatusHandler)
// 内部分支:跨 Agent 缓存失效回调
mux.HandleFunc("/_internal/cache/invalidate", func(w http.ResponseWriter, r *http.Request) {
var evt struct {
Payload map[string]string `json:"payload"`
}
if err := json.NewDecoder(r.Body).Decode(&evt); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
ns := evt.Payload["namespace"]
storage.SearchCacheInstance.Invalidate(ns)
log.Printf("[cache] 内部跨 Agent 失效: %s", ns)
w.WriteHeader(200)
})
// ─── Redis 组件 ─────────────────────────────
heartbeat := storage.NewHeartbeat("zhiyid-primary")
_ = heartbeat // 后台 goroutine 自动心跳
// ─── 后台引擎启动 ──────────────────────────
selfoptimize.RegisterCommitFlow(selfoptimize.Flow)
selfoptimize.RegisterRecallFlow(selfoptimize.Flow)
selfoptimize.RegisterGapFlow(selfoptimize.Flow)
selfoptimize.RegisterCorrectFlow(selfoptimize.Flow)
selfoptimize.RegisterConsolidateFlow(selfoptimize.Flow)
// 注册真正的 consolidate 处理器:调用 ConsolidationPipeline
selfoptimize.Flow.Register("consolidate", func(task *selfoptimize.PipelineTask) error {
report, err := consolPipe.RunWithMode("full")
if err == nil && report != nil {
// 注入 consolidate 报告 → Dashboard
if report.Merged > 0 {
// 合并启发式: 按合并数比例缩放, 但限定单次贡献 ≤ 0.05, 防止污染 avg_distill_loss
loss := float64(report.Merged) * 0.002
if loss > 0.05 {
loss = 0.05
}
selfoptimize.Dash.RecordDistillLoss(loss)
}
if report.ConflictsFound > 0 {
selfoptimize.Dash.RecordConflictResolved(true)
}
for _, p := range report.Patterns {
if len(p) > 13 && p[:13] == "quality_score" {
// 解析 quality_score=0.50 → 注入 avg_distill_loss
var qs float64
if _, e := fmt.Sscanf(p, "quality_score=%f", &qs); e == nil {
selfoptimize.Dash.RecordDistillLoss(1.0 - qs)
}
}
}
}
return err
})
// 注册 conflict_muchen 处理器:自动裁决低信任差异冲突
selfoptimize.Flow.Register("conflict_muchen", func(task *selfoptimize.PipelineTask) error {
conflicts := conflictAPI.Detector.ListActive()
for _, c := range conflicts {
if c.Status != "pending" {
continue
}
// 按策略自动裁决:latest_wins / primary_wins
resolution := conflictAPI.Detector.AutoResolve(c)
if resolution == "pending" {
// 2026-09-20 修复(链路空转):**不再静默 dismiss**。
//
// 原实现把未匹配策略的冲突改写成 "dismiss" 再 Resolve,且记成
// RecordConflictResolved(true) —— 后果是每条 pending 冲突最多活 10 分钟
// 即消失、auto_resolve_rate 指标虚高,冲突从不进入任何人视野
// (实测:17:19 新建冲突,同进程几分钟后 pending:0,未重启)。
//
// 现改为:72 小时内保留 pending 供人工处置;超期才按 latest_wins 自动收敛
// (这是真裁决,才记指标)。保留窗口内不再有任何自动处置。
if time.Since(c.CreatedAt) < 72*time.Hour {
continue
}
resolution = "latest_wins"
}
if err := conflictAPI.Detector.Resolve(c.ID, resolution, ""); err == nil {
selfoptimize.Dash.RecordConflictResolved(true)
}
}
return nil
})
go selfoptimize.Flow.Start()
selfoptimize.Executor.Start(selfoptimize.Flow)
// G6: ephemeral 定时清理(每 5 分钟扫描一次,清理断开超过 5 分钟的 agent 的 ephemeral 记忆)
go func() {
for {
time.Sleep(5 * time.Minute)
now := time.Now().UnixMilli()
agents := agentRegistry.ListAgents()
for _, agent := range agents {
if now-agent.LastSeen > 5*60*1000 {
ns := agent.AgentID + "-ephemeral"
zeroVec := make([]float32, 1024)
memories, _ := ldb.Search("memories", zeroVec, 1000, ns)
for _, mem := range memories {
if err := ldb.SoftDelete(mem.ID, "ephemeral_expired"); err != nil {
log.Printf("[softdelete] failed id: %v", err)
}
}
if len(memories) > 0 {
log.Printf("[ephemeral] cleaned %d memories from %s (inactive for %ds)",
len(memories), ns, (now-agent.LastSeen)/1000)
}
}
}
}
}()
// 质量下降监控:每 30 分钟扫描所有低质量记忆
go func() {
for {
time.Sleep(30 * time.Minute)
zeroVec := make([]float32, 1024)
memories, err := ldb.Search("memories", zeroVec, 2000, "")
if err != nil || len(memories) == 0 {
continue
}
alerts := 0
for _, m := range memories {
if m.QualityScore > 0 && m.QualityScore < 0.3 {
if record := selfoptimize.QualityMonitor.Check(m.ID, m.QualityScore, 2); record != nil {
alerts++
if record.Status == "deprecating" {
log.Printf("[quality] memory %s quality=%.2f deprecating", m.ID[:20], m.QualityScore)
}
}
}
}
if alerts > 0 {
log.Printf("[quality] scan: %d low-quality memories flagged", alerts)
}
}
}()
// 图持久化 — 确保启动后写入
if saver, ok := graphStore.(interface{ Save() error }); ok {
saver.Save()
}
// ─── 触发器自动执行循环(每 30 秒检查一次)────────
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
for _, t := range []struct{ id, action string }{
{"t_distill", "distill"},
{"t_merge", "merge"},
{"t_prune", "prune"},
{"t_decay", "decay"},
{"t_backtrack", "backtrack"},
{"t_gap", "gap_scan"},
{"t_consolidation", "consolidation"},
} {
if !routes.Triggers.CanFire(t.id) {
continue
}
routes.Triggers.RecordFire(t.id)
go func(triggerID, action string) {
var err error
switch action {
case "distill":
// distill 保持 cluster_only(高频触发,只做快速聚类)
_, err = consolPipe.Run()
case "consolidation", "backtrack":
// full 模式:含 prune + decay + 质量回溯(Step 4)
// 48h cooldown 保护,不会真正每分钟跑 full(cooldown 内 CanFire 仍返回 false)
_, err = consolPipe.RunWithMode("full")
// 2026-09-21 (t_4ce53193):冲突去重 + 保留策略
// 去重:同 (L,R) 对只留最早一条,累加 dup_count,归档 + 审计
conflictDetector.DedupeActive(governance.DefaultConflictRetentionConfig())
// 保留:pending > 7d 无人处置 → 归档(不删除)
conflictDetector.RetainAndArchive(governance.DefaultConflictRetentionConfig())
case "merge":
_, err = consolPipe.RunMerge()
case "prune":
graphStore.Prune(0.15)
case "decay":
// 扫描所有记忆,按衰减率计算 freshness
// 2026-09-06 P1 fix: 每 tick 限量评估(maxDecayBatch),优先处理
// 最久未访问的(last_recalled_at 最早 → 最该遗忘),防全表单次拉取
// 内存翻倍 + 长时间阻塞触发器循环。
memories, e := ldb.GetCandidatesForForgetting()
decayScanned := 0
decayForgotten := 0
if e == nil {
const maxDecayBatch = 2000
if len(memories) > maxDecayBatch {
sort.SliceStable(memories, func(a, b int) bool {
return memoryTimeVal(memories[a], "last_recalled_at", "created_at") <
memoryTimeVal(memories[b], "last_recalled_at", "created_at")
})
memories = memories[:maxDecayBatch]
}
for _, m := range memories {
decayScanned++
// 🔒 2026-09-07 修复(误删事故): episodes(原始对话)不参与遗忘——
// 它们是蒸馏原料/审计历史, 不是可遗忘的记忆。曾因未过滤 category
// 导致 67 条 8月对话被 auto_forget 清掉(已恢复)。仅 distilled/general 等记忆可遗忘。
if cat, ok := m["category"].(string); ok && cat == "episodes" {
continue
}
// 🔒 长内容保护: distilled >200 字(有实质信息)不参与 auto 遗忘
// (曾误删 405 字 CNB 修复经验; 长记忆应由蒸馏/整合管理, 非时间遗忘)
if cat, _ := m["category"].(string); cat != "episodes" {
if cs, ok := m["content"].(string); ok && len([]rune(cs)) > 200 {
continue
}
}
// 解析 last_recalled_at
lastAccessed := time.Now().Add(-30 * 24 * time.Hour) // 默认30天前
if t, ok := m["last_recalled_at"].(string); ok && t != "" {
if parsed, err := time.Parse(time.RFC3339, t); err == nil {
lastAccessed = parsed
}
}
// 2026-09-07 方案A 碎片快速道: <30 字且 >20 天未访问 → 直接遗忘
// (绕过 ShouldForget 的 recallCount 保命——碎片 recall_count 可能虚高)。
// 碎片无记忆价值(审计: 30.9% <20字 + 43% <50字 = 蒸馏无门槛产物)。
contentStr := ""
if cs, ok := m["content"].(string); ok {
contentStr = cs
}
if len([]rune(contentStr)) < 30 && time.Since(lastAccessed).Hours()/24 > 20 {
if id, ok := m["id"].(string); ok {
_ = ldb.SoftDelete(id, "auto_forget_fragment")
decayForgotten++
}
continue
}
recallCount := 0
if rc, ok := m["recall_count"].(int); ok {
recallCount = rc
}
tier := "normal"
if ts, ok := m["tier"].(string); ok {
tier = ts
}
// E4.3: 获取实体图谱度(namespace 作为实体)
entity := ""
if ns, ok := m["namespace"].(string); ok {
entity = ns
}
degree := 0
if entity != "" {
degree = graphStore.GetEntityDegree(entity)
}
if forgetter.ShouldForget(lastAccessed, recallCount, tier, degree) {
if id, ok := m["id"].(string); ok {
forgetter.ScanAndForget(id)
// 实际执行软删除(持久化到 LanceDB)
_ = ldb.SoftDelete(id, "auto_forget")
decayForgotten++
}
}
}
}
// AC-1 验收日志(P1 修复 type 断言后 last_recalled_at 可解析;batch<=2000)
if decayScanned > 0 {
log.Printf("[decay] scanned=%d forgotten=%d (batch<=2000, 最久未访问优先)", decayScanned, decayForgotten)
}
case "gap_scan":
// 检查已有缺口:过期 7 天的自动关闭
gaps := gapDetector.List()
for _, g := range gaps {
if !g.Closed && time.Since(g.CreatedAt) > 7*24*time.Hour {
gapDetector.Close(g.Topic)
}
}
// 自动修复开放的缺口
for _, g := range gaps {
if !g.Closed && routes.GapRepair != nil {
if routes.GapRepair.AutoRepair(g) {
selfoptimize.Dash.RecordGapClosed()
}
}
}
}
if err != nil {
routes.Triggers.RecordFail(triggerID)
}
// Skill 结晶:每次蒸馏成功后记录
if action == "distill" || action == "consolidation" {
routes.Skills.RecordTrial("auto_distill", true)
}
routes.WSBus.Broadcast("trigger.fired", map[string]string{
"trigger_id": triggerID,
"action": action,
})
}(t.id, t.action)
}
}
}()
// 自优化指标定时采集(每 30 分钟)
go func() {
for {
time.Sleep(30 * time.Minute)
m := selfoptimize.Dash.Metrics()
date := time.Now().Format("2006-01-02")
for k, v := range m {
storage.GlobalMetricsStore.Set(date, k, v)
}
}
}()
// 首次指标采集
m := selfoptimize.Dash.Metrics()
date := time.Now().Format("2006-01-02")
for k, v := range m {
storage.GlobalMetricsStore.Set(date, k, v)
}
// Prometheus 指标定时同步(每 15s 采集一次系统指标)
go func() {
for {
time.Sleep(15 * time.Second)
// 同步 Dashboard → Prometheus gauges
metrics.SyncFromDashboard(selfoptimize.Dash.Metrics())
// 采集 SQLite 数据库文件大小(Linux only)
if runtime.GOOS != "windows" {
gPath := os.Getenv("GRAPH_PATH")
if gPath == "" {
gPath = "/var/lib/memoryweave/graph.db"
}
if fi, err := os.Stat(gPath); err == nil {
metrics.SQLiteDBSizeBytes.Set(float64(fi.Size()))
}
}
// 采集进程 RSS 内存
if data, err := os.ReadFile("/proc/self/status"); err == nil {
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "VmRSS:") {
// VmRSS: 12345 kB
f := strings.Fields(line)
if len(f) >= 2 {
if kb, err := strconv.ParseFloat(f[1], 64); err == nil {
metrics.MemoryRSSBytes.Set(kb * 1024)
}
}
break
}
}
}
}
}()
log.Println("[zhiyid] 多 Agent 架构 — Redis + FileGraph + EventBus + vLLM — 已启动")
// Web UI 静态文件根目录
webUIRoot = os.Getenv("ZHIYI_WEB_UI_ROOT")
if webUIRoot == "" {
webUIRoot = "../web-ui"
}
// Catch-all: 优先检查静态文件,再 JSON
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
// 静态文件服务(安全检查,不允许 .. 遍历)
if p != "/" && !strings.Contains(p, "..") {
filePath := webUIRoot + p
if _, err := os.Stat(filePath); err == nil {
http.ServeFile(w, r, filePath)
return
}
}
// 根路径 → index.html
if p == "/" || p == "" {
if _, err := os.Stat(webUIRoot + "/index.html"); err == nil {
http.ServeFile(w, r, webUIRoot+"/index.html")
return
}
respondJSON(w, 200, map[string]interface{}{
"service": "zhiyid", "status": "ok", "version": "0.1.0",
})
return
}
respondJSON(w, 404, map[string]interface{}{
"error": "endpoint not found: " + p,
})
})
return middleware.CORS()(middleware.Auth(mux))
}
// ─── 辅助 ──────────────────────────────────────────
func respondJSON(w http.ResponseWriter, code int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(data)
}
func AgentTypeDecayOrDefault(agentType string) float64 {
if rate, ok := governance.AgentTypeDecay[agentType]; ok {
return rate
}
return 0.015
}
// memoryTimeVal 取记忆时间戳用于排序(主 key 优先,空则回退副 key;均空返回 0)。
// 2026-09-06 P1: decay 分批按"最久未访问优先"排序需要;兼容 RFC3339 string / time.Time / int。
func memoryTimeVal(m map[string]interface{}, primary, fallback string) int64 {
for _, key := range []string{primary, fallback} {
v, ok := m[key]
if !ok || v == nil {
continue
}
switch val := v.(type) {
case string:
if val == "" {
continue
}
if t, err := time.Parse(time.RFC3339, val); err == nil {
return t.Unix()
}
if n, err := strconv.ParseInt(val, 10, 64); err == nil {
return n
}
case time.Time:
if !val.IsZero() {
return val.Unix()
}
case float64:
return int64(val)
case int64:
return val
case int:
return int64(val)
}
}
return 0
}
// runStartupChecks 启动时数据目录一致性检查
// 检测废弃路径(如 /home/muc/data)并警告,防止数据源混乱
func runStartupChecks(backend string) {
canonicalDataDir := "/var/lib/memoryweave"
deprecatedPaths := []string{
"/home/muc/data",
"/home/muc/.local/share/memoryweave",
}
for _, dep := range deprecatedPaths {
if _, err := os.Stat(dep); err == nil {
log.Printf("[WARN] 检测到废弃数据目录 %s,仍有数据残留(当前 canonical: %s)", dep, canonicalDataDir)
}
}
// LanceDB 后端:检查 Rust sidecar socket 是否可达
if backend == "lancedb" {
sockPath := os.Getenv("LANCEDB_SOCKET")
if sockPath == "" {
sockPath = "/tmp/zhiyi-ipc.sock"
}
if _, err := os.Stat(sockPath); os.IsNotExist(err) {
log.Printf("[WARN] LanceDB socket 不存在 (%s),Rust sidecar 可能未运行", sockPath)
} else {
log.Printf("[startup] LanceDB socket 就绪: %s", sockPath)
}
}
// canonical 目录存在性检查
if _, err := os.Stat(canonicalDataDir); os.IsNotExist(err) {
log.Printf("[WARN] canonical 数据目录不存在: %s(首次部署?)", canonicalDataDir)
} else {
log.Printf("[startup] 数据目录正常: %s", canonicalDataDir)
}
}