1152 lines
39 KiB
Go
1152 lines
39 KiB
Go
// HTTP 服务器 — 多 Agent 架构(FileGraph + NetworkEventBus + 跨Agent缓存失效)
|
||
package api
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"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/selfoptimize"
|
||
"github.com/xiaoxue/memoryweave/internal/storage"
|
||
)
|
||
|
||
// normalizeEntity 规整实体名:去特殊字符 + n_前缀
|
||
func normalizeEntity(entity string) string {
|
||
if strings.HasPrefix(entity, "n_") {
|
||
entity = entity[2:]
|
||
}
|
||
// 过滤特殊字符
|
||
clean := strings.Map(func(r rune) rune {
|
||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||
return r
|
||
}
|
||
return '_'
|
||
}, strings.ToLower(strings.TrimSpace(entity)))
|
||
// 合并连续下划线
|
||
for strings.Contains(clean, "__") {
|
||
clean = strings.ReplaceAll(clean, "__", "_")
|
||
}
|
||
return strings.Trim(clean, "_")
|
||
}
|
||
|
||
// 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)
|
||
|
||
// 存储后端选择:LanceDB (Rust IPC) → SQLite(CGO)→ 内存
|
||
var ldb storage.LanceDB
|
||
backend := os.Getenv("STORAGE_BACKEND")
|
||
switch backend {
|
||
case "lancedb":
|
||
sockPath := os.Getenv("LANCEDB_SOCKET")
|
||
if sockPath == "" {
|
||
sockPath = "/tmp/zhiyi-ipc.sock"
|
||
}
|
||
ldb = storage.NewRustLanceDBClient(sockPath, emb)
|
||
log.Printf("[zhiyid] 存储后端: LanceDB (Rust IPC) — %s", sockPath)
|
||
case "sqlite":
|
||
dbPath := os.Getenv("SQLITE_PATH")
|
||
sqliteDB, err := storage.NewSQLiteClient(dbPath)
|
||
if err != nil {
|
||
log.Printf("[zhiyid] SQLite 初始化失败 (%v),降级为内存存储", err)
|
||
ldb = storage.NewMemLanceClient(emb)
|
||
} else {
|
||
ldb = sqliteDB
|
||
log.Printf("[zhiyid] 存储后端: SQLite (CGO) — %s", dbPath)
|
||
}
|
||
default:
|
||
ldb = storage.NewMemLanceClient(emb)
|
||
log.Printf("[zhiyid] 存储后端: 内存(零依赖)")
|
||
}
|
||
|
||
// 启动时数据目录一致性检查(防止路径混乱导致读取废弃数据)
|
||
runStartupChecks(backend)
|
||
|
||
// G7.2: 注册 LDB getter(供 skill crystallize 使用)
|
||
routes.RegisterLDBGetter(func() storage.LanceDB { return ldb })
|
||
|
||
// 冲突检测器(E4.1: Commit 时矛盾检测依赖此实例)
|
||
conflictDetector := governance.NewConflictDetector()
|
||
api := routes.NewAPI(ldb, emb, rerank, conflictDetector)
|
||
|
||
// 启动时初始化 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"])
|
||
}
|
||
}()
|
||
|
||
// 图谱:SQLite (graph_nodes/graph_edges) — 设计要求,非 FileGraph JSON
|
||
graphPath := os.Getenv("GRAPH_PATH")
|
||
if graphPath == "" {
|
||
graphPath = "/var/lib/memoryweave/graph.db"
|
||
}
|
||
var graphStore governance.GraphStore
|
||
gs, err := governance.NewSQLiteGraphStore(graphPath)
|
||
if err != nil {
|
||
log.Printf("[zhiyid] WARN: SQLite 图谱初始化失败 (%v),降级为 InMemoryGraph", err)
|
||
graphStore = governance.NewInMemoryGraph()
|
||
} else {
|
||
graphStore = gs
|
||
log.Printf("[zhiyid] 图谱后端: SQLiteGraphStore — %s", graphPath)
|
||
}
|
||
graphUpdater := governance.NewAutoGraphUpdater(graphStore)
|
||
graphAPI := routes.NewGraphAPI(graphStore)
|
||
|
||
// G1: Recall 管线挂图谱扩展
|
||
api.Pipeline.SetGraphExpander(graphStore)
|
||
|
||
// G4: 自动蒸馏 → 注入图谱更新器
|
||
routes.SetGraphUpdater(graphUpdater)
|
||
|
||
// 冲突
|
||
conflictAPI := routes.NewConflictAPI(conflictDetector)
|
||
|
||
// ─── 缺口
|
||
gapDetector := selfoptimize.NewGapDetector(emb, ldb)
|
||
gapDetector.EnableGapRedisPersistence()
|
||
|
||
// 因果追踪持久化
|
||
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)
|
||
for _, fact := range result.Facts {
|
||
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,
|
||
}
|
||
if err := ldb.InsertMemory(mem); err != nil {
|
||
log.Printf("[distill] commit fact failed: %v", err)
|
||
}
|
||
}
|
||
// 冲突检测:加载同 namespace 已有记忆进行比较
|
||
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,
|
||
}
|
||
}
|
||
selfoptimize.Validator.Validate(input.Content, validMems)
|
||
// 被动验证结果写回 quality_score
|
||
records := selfoptimize.Validator.GetRecords()
|
||
for _, r := range records {
|
||
if r.Confidence > 0 {
|
||
_ = 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)
|
||
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")
|
||
go func() {
|
||
// 本地处理 cache.invalidate 事件(通过 HTTP self-call)
|
||
http.HandleFunc("/_internal/cache/invalidate", func(w http.ResponseWriter, r *http.Request) {
|
||
var evt struct {
|
||
Payload map[string]string `json:"payload"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&evt)
|
||
ns := evt.Payload["namespace"]
|
||
storage.SearchCacheInstance.Invalidate(ns)
|
||
log.Printf("[cache] 跨 Agent 失效: %s", ns)
|
||
w.WriteHeader(200)
|
||
})
|
||
}()
|
||
|
||
// ─── 路由注册 ──────────────────────────────
|
||
|
||
// ─── 静态文件服务(Web UI)─────────────────────
|
||
// webUIRoot 在函数顶部声明
|
||
var webUIRoot string
|
||
// 后续在 catch-all 中处理(见文件末尾)
|
||
|
||
mux.HandleFunc("/health", routes.HandleHealth)
|
||
// ─── 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)
|
||
// ─── WebSocket 实时推送 ──────────────────────
|
||
mux.HandleFunc("/api/v1/ws/", routes.WSHandler)
|
||
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)
|
||
// 新增: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(normalizeEntity(entity), maxHops, "", nil)
|
||
if err == nil {
|
||
paths = p
|
||
entities := []string{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),
|
||
})
|
||
})
|
||
|
||
// 静态文件服务(知识图谱可视化 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"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&req)
|
||
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"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&req)
|
||
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"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&evt)
|
||
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" {
|
||
json.NewDecoder(r.Body).Decode(&req)
|
||
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"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&req)
|
||
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)
|
||
}
|
||
})
|
||
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) {
|
||
cleaned := []string{}
|
||
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"`
|
||
}
|
||
json.NewDecoder(r.Body).Decode(&evt)
|
||
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.Run()
|
||
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
|
||
})
|
||
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 {
|
||
ldb.SoftDelete(mem.ID, "ephemeral_expired")
|
||
}
|
||
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", "consolidation", "backtrack":
|
||
// 深度整合含蒸馏质量回溯(Step 4)
|
||
_, err = consolPipe.Run()
|
||
case "merge":
|
||
_, err = consolPipe.RunMerge()
|
||
case "prune":
|
||
graphStore.Prune(0.15)
|
||
case "decay":
|
||
// 扫描所有记忆,按衰减率计算 freshness
|
||
memories, e := ldb.GetCandidatesForForgetting()
|
||
if e == nil {
|
||
for _, m := range memories {
|
||
// 解析 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
|
||
}
|
||
}
|
||
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")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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 数据库文件大小
|
||
if fi, err := os.Stat(graphPath); 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
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|