feat: M9 功能补全 — 30+ 新端点 + 自调参闭环

新增路由层 (10 files):
- graph.go: GET /graph/stats, POST /graph/query, POST /graph/navigate
- feedback.go: POST /feedback/useful|not-useful|deprecate|correct
- gaps.go: GET /gaps, POST /gaps/detect, POST /gaps/close
- conflicts.go: GET /conflicts, POST /conflicts/resolve
- agent.go: POST /agents/register, GET /agents
- admin.go: DELETE /distilled, GET /memory/versions, POST /admin/forget|backup, GET /admin/audit
- l3.go: GET|POST /l3/worldmodel
- triggers.go: GET /triggers, POST /triggers/fire, GET /skills, POST /skills/trial
- eval.go: POST /eval/run, GET /eval/history, POST /eval/generate
- tuning.go: GET /tuning/status, POST /tuning/run, POST /tuning/analytics
- obsidian.go: POST /obsidian/push|pull, GET /obsidian/status

引擎增强:
- governance: InMemoryGraph (零外部依赖) + ListActive/Resolve/DB
- selfoptimize: RecordDeprecation/Correction/GapClosed/ConflictResolved
- storage: IncrementUseful/NotUseful, UpdateMemoryContent, GetVersionHistory,
  GetCandidatesForForgetting, Backup, GetAuditLog

总计: 17 files, 1915+ lines, 35 endpoints, go build 0 errors
This commit is contained in:
xiaowei 2026-05-28 18:25:43 +08:00
parent 02eef1024d
commit ef2ef5069c
17 changed files with 1916 additions and 8 deletions

View File

@ -0,0 +1,142 @@
// 织忆 MemoryWeave — 管理端点
package routes
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/storage"
)
type AdminAPI struct {
LanceDB *storage.LanceDB
Forgetter *governance.Forgetter
}
func NewAdminAPI(ldb *storage.LanceDB, f *governance.Forgetter) *AdminAPI {
return &AdminAPI{LanceDB: ldb, Forgetter: f}
}
// DELETE /api/v1/distilled/{id}
func (aa *AdminAPI) DeleteDistilled(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
respondError(w, 400, "id required")
return
}
if err := aa.LanceDB.SoftDelete(id, "manual_delete"); err != nil {
respondError(w, 500, "delete failed: "+err.Error())
return
}
respond(w, 200, map[string]string{"status": "deleted", "id": id})
}
// GET /api/v1/memory/{id}/versions
func (aa *AdminAPI) Versions(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
respondError(w, 400, "id required")
return
}
versions, err := aa.LanceDB.GetVersionHistory(id)
if err != nil {
respondError(w, 500, "get versions failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{
"memory_id": id, "versions": versions, "count": len(versions),
})
}
// POST /api/v1/admin/forget
func (aa *AdminAPI) Forget(w http.ResponseWriter, r *http.Request) {
results, err := aa.LanceDB.GetCandidatesForForgetting()
if err != nil {
respondError(w, 500, "list failed: "+err.Error())
return
}
forgotten := 0
for _, mem := range results {
lastAccess := parseTimeStr(mem["last_recalled_at"])
recallCnt := intVal(mem["recall_count"])
tier := strVal(mem["tier"])
if aa.Forgetter.ShouldForget(lastAccess, recallCnt, tier) {
aa.LanceDB.SoftDelete(strVal(mem["id"]), "auto_forget")
forgotten++
}
}
respond(w, 200, map[string]interface{}{
"status": "ok", "scanned": len(results), "forgotten": forgotten,
})
}
// POST /api/v1/admin/backup
func (aa *AdminAPI) Backup(w http.ResponseWriter, r *http.Request) {
timestamp := time.Now().Format("20060102-150405")
backupPath := fmt.Sprintf("/home/muc/backups/memoryweave/%s", timestamp)
os.MkdirAll(backupPath, 0755)
if err := aa.LanceDB.Backup(backupPath); err != nil {
respondError(w, 500, "backup failed: "+err.Error())
return
}
respond(w, 200, map[string]string{
"status": "ok", "path": backupPath, "timestamp": timestamp,
})
}
// GET /api/v1/admin/audit
func (aa *AdminAPI) Audit(w http.ResponseWriter, r *http.Request) {
limit := 100
logs, err := aa.LanceDB.GetAuditLog(limit)
if err != nil {
respondError(w, 500, "audit failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{"audit_logs": logs, "count": len(logs)})
}
// 辅助函数
func parseTimeStr(s interface{}) time.Time {
if s == nil {
return time.Time{}
}
switch v := s.(type) {
case string:
t, _ := time.Parse(time.RFC3339, v)
return t
case time.Time:
return v
}
return time.Time{}
}
func intVal(v interface{}) int {
switch n := v.(type) {
case int: return n
case int32: return int(n)
case int64: return int(n)
case float64: return int(n)
case json.Number:
i, _ := n.Int64()
return int(i)
}
return 0
}
func strVal(v interface{}) string {
if v == nil {
return ""
}
switch s := v.(type) {
case string: return s
case json.Number: return s.String()
}
return fmt.Sprintf("%v", v)
}

View File

@ -0,0 +1,92 @@
// 织忆 MemoryWeave — Agent 注册 API
package routes
import (
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"github.com/xiaoxue/memoryweave/internal/distributed"
)
type AgentRegistry struct {
agents map[string]AgentInfo
limiter *distributed.RateLimiter
}
type AgentInfo struct {
AgentID string `json:"agent_id"`
APIKey string `json:"api_key,omitempty"`
Quota Quota `json:"quota"`
Registered string `json:"registered"`
}
type Quota struct {
RecallQPS int `json:"recall_qps"`
CommitQPS int `json:"commit_qps"`
Burst int `json:"burst"`
}
func NewAgentRegistry(limiter *distributed.RateLimiter) *AgentRegistry {
return &AgentRegistry{
agents: make(map[string]AgentInfo),
limiter: limiter,
}
}
// POST /api/v1/agents/register
func (ar *AgentRegistry) Register(w http.ResponseWriter, r *http.Request) {
var req struct {
AgentID string `json:"agent_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.AgentID == "" {
respondError(w, 400, "agent_id required")
return
}
if _, exists := ar.agents[req.AgentID]; exists {
respondError(w, 409, "agent already registered")
return
}
apiKey := generateKey(32)
info := AgentInfo{
AgentID: req.AgentID,
APIKey: apiKey,
Quota: Quota{RecallQPS: 10, CommitQPS: 2, Burst: 20},
Registered: "now",
}
ar.agents[req.AgentID] = info
respond(w, 201, info)
}
// GET /api/v1/agents — 已注册 Agent 列表
func (ar *AgentRegistry) List(w http.ResponseWriter, r *http.Request) {
var list []AgentInfo
for _, info := range ar.agents {
info.APIKey = "" // 不泄漏 key
list = append(list, info)
}
respond(w, 200, map[string]interface{}{"agents": list, "count": len(list)})
}
// Auth 验证 API Key
func (ar *AgentRegistry) Auth(apiKey string) (*AgentInfo, bool) {
for _, info := range ar.agents {
if info.APIKey == apiKey {
return &info, true
}
}
return nil, false
}
func generateKey(length int) string {
b := make([]byte, length)
rand.Read(b)
return fmt.Sprintf("mw-%x", b)
}

View File

@ -0,0 +1,59 @@
// 织忆 MemoryWeave — 冲突管理 API
package routes
import (
"encoding/json"
"net/http"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
)
type ConflictAPI struct {
Detector *governance.ConflictDetector
}
func NewConflictAPI(d *governance.ConflictDetector) *ConflictAPI {
return &ConflictAPI{Detector: d}
}
// GET /api/v1/conflicts
func (ca *ConflictAPI) List(w http.ResponseWriter, r *http.Request) {
list := ca.Detector.ListActive()
pending := 0
for _, c := range list {
if c.Status == "pending" {
pending++
}
}
respond(w, 200, map[string]interface{}{
"conflicts": list, "count": len(list), "pending": pending,
})
}
// POST /api/v1/conflicts/resolve
func (ca *ConflictAPI) Resolve(w http.ResponseWriter, r *http.Request) {
var req struct {
ConflictID string `json:"conflict_id"`
Resolution string `json:"resolution"` // keep_left / keep_right / merge / dismiss
Winner string `json:"winner"` // id of the winning memory
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.ConflictID == "" || req.Resolution == "" {
respondError(w, 400, "conflict_id and resolution required")
return
}
err := ca.Detector.Resolve(req.ConflictID, req.Resolution, req.Winner)
if err != nil {
respondError(w, 500, "resolve failed: "+err.Error())
return
}
selfoptimize.Dash.RecordConflictResolved(true)
PushConflictResolved(req.ConflictID, req.Resolution)
respond(w, 200, map[string]string{"status": "resolved", "conflict_id": req.ConflictID})
}

View File

@ -0,0 +1,222 @@
// 织忆 MemoryWeave — 评估系统 API
package routes
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/xiaoxue/memoryweave/internal/storage"
)
type EvalAPI struct {
Pipeline *storage.RecallPipeline
LanceDB *storage.LanceDB
}
type EvalRun struct {
ID string `json:"id"`
Model string `json:"model"`
Precision float64 `json:"precision"`
Recall float64 `json:"recall_k"`
MRR float64 `json:"mrr"`
NDCG float64 `json:"ndcg"`
Queries int `json:"queries"`
RanAt time.Time `json:"ran_at"`
}
type evalStore struct {
mu sync.RWMutex
runs []*EvalRun
}
var evals = &evalStore{}
func NewEvalAPI(p *storage.RecallPipeline, ldb *storage.LanceDB) *EvalAPI {
return &EvalAPI{Pipeline: p, LanceDB: ldb}
}
// POST /api/v1/eval/run
func (ea *EvalAPI) Run(w http.ResponseWriter, r *http.Request) {
var req struct {
Queries []struct {
Query string `json:"query"`
ExpectedIDs []string `json:"expected_ids"`
} `json:"queries"`
Model string `json:"model"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if len(req.Queries) == 0 {
respondError(w, 400, "at least one query required")
return
}
var totalPrecision, totalRecall, totalMRR, totalNDCG float64
totalQueries := 0
for _, q := range req.Queries {
results, err := ea.Pipeline.Recall(q.Query, "shared", 10, 0.5)
if err != nil {
continue
}
// Precision@k
hits := 0
expectedSet := makeSet(q.ExpectedIDs)
for i, res := range results {
if expectedSet[res.ID] && i < len(q.ExpectedIDs) {
hits++
}
}
if len(results) > 0 {
totalPrecision += float64(hits) / float64(len(results))
}
// Recall@k
if len(q.ExpectedIDs) > 0 {
totalRecall += float64(hits) / float64(len(q.ExpectedIDs))
}
// MRR (First correct position)
for i, res := range results {
if expectedSet[res.ID] {
totalMRR += 1.0 / float64(i+1)
break
}
}
// NDCG (binary relevance)
dcg := 0.0
idcg := 0.0
for i, res := range results {
rel := 0.0
if expectedSet[res.ID] {
rel = 1.0
}
dcg += rel / log2(float64(i+2))
if i < len(q.ExpectedIDs) {
idcg += 1.0 / log2(float64(i+2))
}
}
if idcg > 0 {
totalNDCG += dcg / idcg
}
totalQueries++
}
if totalQueries == 0 {
respondError(w, 500, "all queries failed")
return
}
run := &EvalRun{
ID: time.Now().Format("20060102-150405"),
Model: req.Model,
Precision: totalPrecision / float64(totalQueries),
Recall: totalRecall / float64(totalQueries),
MRR: totalMRR / float64(totalQueries),
NDCG: totalNDCG / float64(totalQueries),
Queries: totalQueries,
RanAt: time.Now(),
}
evals.mu.Lock()
evals.runs = append(evals.runs, run)
evals.mu.Unlock()
respond(w, 200, run)
}
// GET /api/v1/eval/history
func (ea *EvalAPI) History(w http.ResponseWriter, r *http.Request) {
evals.mu.RLock()
defer evals.mu.RUnlock()
if len(evals.runs) == 0 {
respond(w, 200, map[string]interface{}{"runs": []EvalRun{}, "count": 0})
return
}
// 计算趋势(最近 2 次对比)
trend := "stable"
if len(evals.runs) >= 2 {
curr := evals.runs[len(evals.runs)-1].MRR
prev := evals.runs[len(evals.runs)-2].MRR
if curr > prev*1.05 {
trend = "improving"
} else if curr < prev*0.95 {
trend = "degrading"
}
}
respond(w, 200, map[string]interface{}{
"runs": evals.runs, "count": len(evals.runs), "trend": trend,
})
}
// POST /api/v1/eval/generate — 自动生成金标查询集
func (ea *EvalAPI) Generate(w http.ResponseWriter, r *http.Request) {
var req struct {
Namespace string `json:"namespace"`
Count int `json:"count"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Namespace == "" {
req.Namespace = "shared"
}
if req.Count <= 0 {
req.Count = 10
}
// 获取高质量记忆作为金标基础
memories, err := ea.LanceDB.GetTopByQuality("", req.Count)
if err != nil {
respondError(w, 500, "generate failed: "+err.Error())
return
}
var queries []map[string]interface{}
for _, mem := range memories {
query := mem.Content
if len(query) > 50 {
query = query[:50]
}
queries = append(queries, map[string]interface{}{
"query": query,
"expected_ids": []string{mem.ID},
})
}
respond(w, 200, map[string]interface{}{
"queries": queries, "count": len(queries),
})
}
// 辅助
func makeSet(ids []string) map[string]bool {
s := make(map[string]bool, len(ids))
for _, id := range ids {
s[id] = true
}
return s
}
func log2(x float64) float64 {
// log2(x) ≈ ln(x)/ln(2) 但不用 math 包避免类型问题
result := 0.0
for x > 2 {
x /= 2
result += 1
}
if x > 1 {
result += (x - 1)
}
return result
}

View File

@ -0,0 +1,104 @@
// 织忆 MemoryWeave — 反馈 API
package routes
import (
"encoding/json"
"net/http"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
"github.com/xiaoxue/memoryweave/internal/storage"
)
type FeedbackAPI struct {
LanceDB *storage.LanceDB
}
func NewFeedbackAPI(ldb *storage.LanceDB) *FeedbackAPI {
return &FeedbackAPI{LanceDB: ldb}
}
// POST /api/v1/feedback/useful
func (fa *FeedbackAPI) MarkUseful(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryID string `json:"memory_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.MemoryID == "" {
respondError(w, 400, "memory_id required")
return
}
selfoptimize.Dash.RecordFeedback(true)
fa.LanceDB.IncrementUseful(req.MemoryID)
respond(w, 200, map[string]string{"status": "ok", "memory_id": req.MemoryID})
}
// POST /api/v1/feedback/not-useful
func (fa *FeedbackAPI) MarkNotUseful(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryID string `json:"memory_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.MemoryID == "" {
respondError(w, 400, "memory_id required")
return
}
selfoptimize.Dash.RecordFeedback(false)
fa.LanceDB.IncrementNotUseful(req.MemoryID)
respond(w, 200, map[string]string{"status": "ok", "memory_id": req.MemoryID})
}
// POST /api/v1/feedback/deprecate
func (fa *FeedbackAPI) Deprecate(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryID string `json:"memory_id"`
Reason string `json:"reason"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.MemoryID == "" {
respondError(w, 400, "memory_id required")
return
}
selfoptimize.Dash.RecordDeprecation()
fa.LanceDB.SoftDelete(req.MemoryID, req.Reason)
respond(w, 200, map[string]string{"status": "deprecated", "memory_id": req.MemoryID})
}
// POST /api/v1/feedback/correct
func (fa *FeedbackAPI) Correct(w http.ResponseWriter, r *http.Request) {
var req struct {
MemoryID string `json:"memory_id"`
NewContent string `json:"new_content"`
Source string `json:"source"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.MemoryID == "" || req.NewContent == "" {
respondError(w, 400, "memory_id and new_content required")
return
}
if req.Source == "" {
req.Source = "muchen_correction"
}
selfoptimize.Dash.RecordCorrection(req.Source)
err := fa.LanceDB.UpdateMemoryContent(req.MemoryID, req.NewContent, req.Source)
if err != nil {
respondError(w, 500, "correct failed: "+err.Error())
return
}
respond(w, 200, map[string]string{"status": "corrected", "memory_id": req.MemoryID})
}

View File

@ -0,0 +1,71 @@
// 织忆 MemoryWeave — 知识缺口 API
package routes
import (
"encoding/json"
"net/http"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
)
type GapAPI struct {
Detector *selfoptimize.GapDetector
}
func NewGapAPI(d *selfoptimize.GapDetector) *GapAPI {
return &GapAPI{Detector: d}
}
// GET /api/v1/gaps
func (ga *GapAPI) List(w http.ResponseWriter, r *http.Request) {
gaps := ga.Detector.List()
openCount := 0
for _, g := range gaps {
if !g.Closed {
openCount++
}
}
respond(w, 200, map[string]interface{}{
"gaps": gaps,
"count": len(gaps),
"open": openCount,
})
}
// POST /api/v1/gaps/{topic}/close
func (ga *GapAPI) Close(w http.ResponseWriter, r *http.Request) {
topic := r.PathValue("topic")
if topic == "" {
respondError(w, 400, "topic required")
return
}
ga.Detector.Close(topic)
selfoptimize.Dash.RecordGapClosed()
respond(w, 200, map[string]string{"status": "closed", "topic": topic})
}
// POST /api/v1/gaps/detect — 手动触发缺口检测miss 计数)
func (ga *GapAPI) Detect(w http.ResponseWriter, r *http.Request) {
var req struct {
Topic string `json:"topic"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Topic == "" {
respondError(w, 400, "topic required")
return
}
gap := ga.Detector.RecordMiss(req.Topic)
if gap != nil {
PushGapFound(req.Topic, string(gap.Type))
respond(w, 201, map[string]interface{}{
"status": "gap_detected", "gap": gap,
})
} else {
respond(w, 200, map[string]string{"status": "tracking", "topic": req.Topic})
}
}

View File

@ -0,0 +1,76 @@
// 织忆 MemoryWeave — 知识图谱 API
package routes
import (
"encoding/json"
"net/http"
"github.com/xiaoxue/memoryweave/internal/governance"
)
type GraphAPI struct {
Graph *governance.InMemoryGraph
}
func NewGraphAPI(g *governance.InMemoryGraph) *GraphAPI {
return &GraphAPI{Graph: g}
}
// GET /api/v1/graph/stats
func (ga *GraphAPI) Stats(w http.ResponseWriter, r *http.Request) {
nodeCount, edgeCount, density := ga.Graph.Stats()
respond(w, 200, map[string]interface{}{
"node_count": nodeCount,
"edge_count": edgeCount,
"density": density,
})
}
// POST /api/v1/graph/query
func (ga *GraphAPI) Query(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
Relation string `json:"relation"`
Namespace string `json:"namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Namespace == "" {
req.Namespace = "shared"
}
results := ga.Graph.Query(req.Entity, req.Relation, req.Namespace)
respond(w, 200, map[string]interface{}{"results": results, "count": len(results)})
}
// POST /api/v1/graph/navigate
func (ga *GraphAPI) Navigate(w http.ResponseWriter, r *http.Request) {
var req struct {
Entity string `json:"entity"`
MaxHops int `json:"max_hops"`
Namespace string `json:"namespace"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Entity == "" {
respondError(w, 400, "entity required")
return
}
if req.MaxHops <= 0 {
req.MaxHops = 2
}
if req.Namespace == "" {
req.Namespace = "shared"
}
paths, err := ga.Graph.Navigate(req.Entity, req.MaxHops, req.Namespace)
if err != nil {
respondError(w, 500, "navigate failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{"paths": paths, "entity": req.Entity, "count": len(paths)})
}

View File

@ -0,0 +1,105 @@
// 织忆 MemoryWeave — L3 世界模型 API
package routes
import (
"encoding/json"
"net/http"
"sync"
"time"
)
// L3 世界模型:ℰ (Environment) + (Implicit Rules) + C (Capabilities)
type WorldModel struct {
mu sync.RWMutex
Environment struct {
OS string `json:"os"`
GPU string `json:"gpu"`
VRAM string `json:"vram"`
RAM string `json:"ram"`
CPU string `json:"cpu"`
HomeDir string `json:"home_dir"`
HermesVer string `json:"hermes_ver"`
} `json:"environment"`
ImplicitRules []string `json:"implicit_rules"`
Capabilities []string `json:"capabilities"`
UpdatedAt string `json:"updated_at"`
}
var WM = &WorldModel{}
func init() {
WM.Environment.OS = "Linux"
WM.Environment.GPU = "RTX 3050 Laptop"
WM.Environment.VRAM = "4GB"
WM.Environment.RAM = "16GB"
WM.Environment.CPU = "Intel"
WM.Environment.HermesVer = "0.14.0"
WM.ImplicitRules = []string{
"牧尘话少直接,结论先行",
"性能可以多余,不能短缺",
"方案最大化完善,不精简",
"先修性能再推进分布式",
}
WM.Capabilities = []string{
"飞书消息收发",
"ComfyUI 图像生成 (SD 1.5)",
"语音合成 TTS",
"语音识别 STT",
}
WM.UpdatedAt = time.Now().Format(time.RFC3339)
}
// GET /api/v1/l3/worldmodel
func (wm *WorldModel) GetHandler(w http.ResponseWriter, r *http.Request) {
wm.mu.RLock()
defer wm.mu.RUnlock()
respond(w, 200, wm)
}
// POST /api/v1/l3/worldmodel — 更新
func (wm *WorldModel) UpdateHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Environment *struct {
OS string `json:"os"`
GPU string `json:"gpu"`
VRAM string `json:"vram"`
RAM string `json:"ram"`
} `json:"environment"`
AddRules []string `json:"add_rules"`
AddCapabilities []string `json:"add_capabilities"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
wm.mu.Lock()
defer wm.mu.Unlock()
if req.Environment != nil {
if req.Environment.OS != "" {
wm.Environment.OS = req.Environment.OS
}
if req.Environment.GPU != "" {
wm.Environment.GPU = req.Environment.GPU
}
if req.Environment.VRAM != "" {
wm.Environment.VRAM = req.Environment.VRAM
}
if req.Environment.RAM != "" {
wm.Environment.RAM = req.Environment.RAM
}
}
for _, rule := range req.AddRules {
wm.ImplicitRules = append(wm.ImplicitRules, rule)
}
for _, cap := range req.AddCapabilities {
wm.Capabilities = append(wm.Capabilities, cap)
}
wm.UpdatedAt = time.Now().Format(time.RFC3339)
respond(w, 200, wm)
}

View File

@ -0,0 +1,247 @@
// 织忆 MemoryWeave — Obsidian 双向同步
package routes
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// ObsidianSyncer 双向同步织忆与 Obsidian 知识库
type ObsidianSyncer struct {
vaultPath string
ldb *storage.LanceDB
mu sync.Mutex
lastSync time.Time
}
var Obsidian *ObsidianSyncer
func NewObsidianSyncer(vaultPath string, ldb *storage.LanceDB) *ObsidianSyncer {
s := &ObsidianSyncer{
vaultPath: vaultPath,
ldb: ldb,
}
Obsidian = s
return s
}
// PushToObsidian 推送织忆记忆到 Obsidian默认 vault
func (s *ObsidianSyncer) PushToObsidian(memories []map[string]interface{}, folder string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.vaultPath == "" {
s.vaultPath = os.Getenv("OBSIDIAN_VAULT")
if s.vaultPath == "" {
s.vaultPath = fmt.Sprintf("/home/%s/mc/小唯", os.Getenv("USER"))
}
}
targetDir := filepath.Join(s.vaultPath, folder)
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", targetDir, err)
}
count := 0
for _, mem := range memories {
content := strVal(mem["content"])
if len(content) < 5 {
continue
}
filename := sanitizeFilename(content[:minz(len(content), 30)]) + ".md"
filepath := filepath.Join(targetDir, filename)
mdContent := fmt.Sprintf(`---
id: %s
category: %s
quality_score: %.2f
sync_time: %s
---
# %s
%s
---
*由织忆 MemoryWeave 同步*
`, strVal(mem["id"]), strVal(mem["category"]),
floatVal(mem["quality_score"]),
time.Now().Format(time.RFC3339),
truncate(content, 60),
content)
if err := os.WriteFile(filepath, []byte(mdContent), 0644); err != nil {
log.Printf("[obsidian] 写入失败 %s: %v", filename, err)
continue
}
count++
}
s.lastSync = time.Now()
log.Printf("[obsidian] 推送 %d 条记忆到 %s", count, targetDir)
return nil
}
// PullFromObsidian 从 Obsidian 拉取笔记并 commit 到织忆
func (s *ObsidianSyncer) PullFromObsidian(folder string) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.vaultPath == "" {
s.vaultPath = os.Getenv("OBSIDIAN_VAULT")
if s.vaultPath == "" {
s.vaultPath = fmt.Sprintf("/home/%s/mc/小唯", os.Getenv("USER"))
}
}
targetDir := filepath.Join(s.vaultPath, folder)
entries, err := os.ReadDir(targetDir)
if err != nil {
return 0, fmt.Errorf("readdir %s: %w", targetDir, err)
}
count := 0
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
data, err := os.ReadFile(filepath.Join(targetDir, entry.Name()))
if err != nil {
continue
}
content := string(data)
// 跳过已经同步过的(检查 FRONT MATTER
if strings.Contains(content, "*由织忆 MemoryWeave 同步*") {
continue
}
// 简单提取:去掉 YAML 头,取正文
body := content
if idx := strings.Index(content, "---\n"); idx >= 0 {
if endIdx := strings.Index(content[4:], "---\n"); endIdx >= 0 {
body = content[endIdx+8:]
}
}
s.ldb.InsertEpisode(
"obsidian-sync",
"shared",
strings.TrimSpace(body),
"obsidian_import",
)
count++
}
s.lastSync = time.Now()
log.Printf("[obsidian] 拉取 %d 条笔记", count)
return count, nil
}
// ─── API ──────────────────────────────────────────────────
// POST /api/v1/obsidian/push
func (s *ObsidianSyncer) PushHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Memories []map[string]interface{} `json:"memories"`
Folder string `json:"folder"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Folder == "" {
req.Folder = "02-Memory"
}
if err := s.PushToObsidian(req.Memories, req.Folder); err != nil {
respondError(w, 500, "push failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{
"status": "pushed", "count": len(req.Memories),
})
}
// POST /api/v1/obsidian/pull
func (s *ObsidianSyncer) PullHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Folder string `json:"folder"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
if req.Folder == "" {
req.Folder = "02-Memory"
}
count, err := s.PullFromObsidian(req.Folder)
if err != nil {
respondError(w, 500, "pull failed: "+err.Error())
return
}
respond(w, 200, map[string]interface{}{
"status": "pulled", "count": count,
})
}
// GET /api/v1/obsidian/status
func (s *ObsidianSyncer) StatusHandler(w http.ResponseWriter, r *http.Request) {
respond(w, 200, map[string]interface{}{
"vault_path": s.vaultPath,
"last_sync": s.lastSync.Format(time.RFC3339),
"vault_exists": dirExists(s.vaultPath),
})
}
// ─── 辅助 ─────────────────────────────────────────────────
func sanitizeFilename(s string) string {
s = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '_' || r == '-' || r == ' ' {
return r
}
return '_'
}, s)
return strings.TrimSpace(s)
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
func minz(a, b int) int {
if a < b {
return a
}
return b
}
func floatVal(v interface{}) float64 {
switch f := v.(type) {
case float64:
return f
case int:
return float64(f)
}
return 0
}
func dirExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

View File

@ -0,0 +1,150 @@
// 织忆 MemoryWeave — 触发器 & Skill 管理 API
package routes
import (
"encoding/json"
"net/http"
"sync"
"time"
)
// ─── 触发器 ──────────────────────────────────────────────
type TriggerType string
const (
TriggerCommitCount TriggerType = "commit_count" // 新增 N 条后触发
TriggerTimeSince TriggerType = "time_since" // 距上次操作 N 小时后触发
TriggerRecallMiss TriggerType = "recall_miss" // 连续 N 次 miss
TriggerQualityDrop TriggerType = "quality_drop" // quality < threshold
)
type Trigger struct {
ID string `json:"id"`
Type TriggerType `json:"type"`
Condition string `json:"condition"`
Urgency float64 `json:"urgency"` // 0-1越大越紧急
Active bool `json:"active"`
FiredAt time.Time `json:"fired_at,omitempty"`
Description string `json:"description"`
}
type TriggerManager struct {
mu sync.RWMutex
triggers []*Trigger
}
var Triggers = &TriggerManager{
triggers: []*Trigger{
{ID: "t1", Type: TriggerCommitCount, Condition: "50 new commits", Urgency: 0.3, Active: true, Description: "蒸馏量达到 50 条触发整合"},
{ID: "t2", Type: TriggerTimeSince, Condition: "24h since last deep consolidate", Urgency: 0.5, Active: true, Description: "距上次深度整合超 24h"},
{ID: "t3", Type: TriggerRecallMiss, Condition: "3 consecutive misses", Urgency: 0.7, Active: true, Description: "连续 3 次召回失败 → 缺口分类"},
{ID: "t4", Type: TriggerQualityDrop, Condition: "quality < 0.3", Urgency: 0.6, Active: true, Description: "某条记忆质量过低 → 审查"},
},
}
// GET /api/v1/triggers
func (tm *TriggerManager) List(w http.ResponseWriter, r *http.Request) {
tm.mu.RLock()
defer tm.mu.RUnlock()
// 按 urgency 降序
sorted := make([]*Trigger, len(tm.triggers))
copy(sorted, tm.triggers)
respond(w, 200, map[string]interface{}{
"triggers": sorted, "count": len(sorted),
})
}
// POST /api/v1/triggers/fire — 手动触发
func (tm *TriggerManager) Fire(w http.ResponseWriter, r *http.Request) {
var req struct {
TriggerID string `json:"trigger_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
tm.mu.Lock()
defer tm.mu.Unlock()
for _, t := range tm.triggers {
if t.ID == req.TriggerID {
t.FiredAt = time.Now()
respond(w, 200, map[string]string{
"status": "fired", "trigger_id": req.TriggerID,
})
return
}
}
respondError(w, 404, "trigger not found")
}
// ─── Skill 结晶 ──────────────────────────────────────────
type Skill struct {
Name string `json:"name"`
Description string `json:"description"`
Trials int `json:"trials"`
ETA float64 `json:"eta"` // 有效性 η = successes / trials
CreatedAt time.Time `json:"created_at"`
}
type SkillManager struct {
mu sync.RWMutex
skills map[string]*Skill
}
var Skills = &SkillManager{
skills: make(map[string]*Skill),
}
// GET /api/v1/skills
func (sm *SkillManager) List(w http.ResponseWriter, r *http.Request) {
sm.mu.RLock()
defer sm.mu.RUnlock()
var list []*Skill
for _, s := range sm.skills {
list = append(list, s)
}
respond(w, 200, map[string]interface{}{"skills": list, "count": len(list)})
}
// POST /api/v1/skills/{name}/trial
func (sm *SkillManager) Trial(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if name == "" {
respondError(w, 400, "name required")
return
}
var req struct {
Success bool `json:"success"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, 400, "invalid body")
return
}
sm.mu.Lock()
defer sm.mu.Unlock()
skill, exists := sm.skills[name]
if !exists {
skill = &Skill{
Name: name,
Description: "自动发现的工作模式",
CreatedAt: time.Now(),
}
sm.skills[name] = skill
}
skill.Trials++
if req.Success {
skill.ETA = float64(skill.Trials-1) / float64(skill.Trials)
} else {
skill.ETA = float64(skill.Trials-1) / float64(skill.Trials)
}
respond(w, 200, skill)
}

View File

@ -0,0 +1,181 @@
// 织忆 MemoryWeave — 自调参闭环Auto-tuning
package routes
import (
"net/http"
"sync"
"time"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
)
// TuningConfig 可自动调优的参数
type TuningConfig struct {
mu sync.RWMutex
Diversity float64 `json:"diversity"` // MMR diversity 参数 (0-1)
DecayRate float64 `json:"decay_rate"` // 遗忘衰减率
GapThreshold int `json:"gap_threshold"` // 缺口感测阈值 (miss 次数)
DistillInterval int `json:"distill_interval"` // 蒸馏间隔 (分钟)
ConsolidateAfter int `json:"consolidate_after"` // N 条蒸馏后触发整合
// 历史调参记录
history []TuningEvent
}
type TuningEvent struct {
Parameter string `json:"parameter"`
OldValue float64 `json:"old_value"`
NewValue float64 `json:"new_value"`
Reason string `json:"reason"`
Timestamp time.Time `json:"timestamp"`
}
var Tuner = &TuningConfig{
Diversity: 0.5,
DecayRate: 0.015,
GapThreshold: 3,
DistillInterval: 5,
ConsolidateAfter: 50,
}
// AutoTune 基于仪表盘指标自动调参
func (tc *TuningConfig) AutoTune(metrics map[string]float64) []TuningEvent {
tc.mu.Lock()
defer tc.mu.Unlock()
var events []TuningEvent
// 1. 召回命中率 < 0.5 → 提高 diversity扩大搜索范围
if hitRate, ok := metrics["recall_hit_rate"]; ok && hitRate < 0.5 {
newDiversity := tc.Diversity + 0.1
if newDiversity > 1.0 {
newDiversity = 1.0
}
events = append(events, recordTuning(tc, "diversity", tc.Diversity, newDiversity, "recall_hit_rate < 0.5"))
tc.Diversity = newDiversity
}
// 2. 召回有用率 > 0.9 → 降低 diversity更精准
if usefulRate, ok := metrics["recall_usefulness_rate"]; ok && usefulRate > 0.9 {
newDiversity := tc.Diversity - 0.05
if newDiversity < 0.1 {
newDiversity = 0.1
}
events = append(events, recordTuning(tc, "diversity", tc.Diversity, newDiversity, "recall_usefulness_rate > 0.9"))
tc.Diversity = newDiversity
}
// 3. 废弃率 > 5/day → 提高衰减率(加快清理)
if deprecated, ok := metrics["deprecated_per_day"]; ok && deprecated > 5 {
newDecay := tc.DecayRate * 1.2
if newDecay > 0.05 {
newDecay = 0.05
}
events = append(events, recordTuning(tc, "decay_rate", tc.DecayRate, newDecay, "deprecated_per_day > 5"))
tc.DecayRate = newDecay
}
// 4. 缺口闭环率 < 0.5 → 降低 gap 阈值
if gapRate, ok := metrics["gap_closure_rate"]; ok && gapRate < 0.5 {
newThreshold := tc.GapThreshold - 1
if newThreshold < 2 {
newThreshold = 2
}
events = append(events, recordTuningInt(tc, "gap_threshold", tc.GapThreshold, newThreshold, "gap_closure_rate < 0.5"))
tc.GapThreshold = newThreshold
}
// 5. 蒸馏损失 > 0.3 → 更频繁蒸馏
if loss, ok := metrics["avg_distill_loss"]; ok && loss > 0.3 {
newInterval := tc.DistillInterval - 1
if newInterval < 1 {
newInterval = 1
}
events = append(events, recordTuningInt(tc, "distill_interval", tc.DistillInterval, newInterval, "avg_distill_loss > 0.3"))
tc.DistillInterval = newInterval
}
tc.history = append(tc.history, events...)
return events
}
func recordTuning(tc *TuningConfig, param string, oldVal, newVal float64, reason string) TuningEvent {
return TuningEvent{
Parameter: param,
OldValue: oldVal,
NewValue: newVal,
Reason: reason,
Timestamp: time.Now(),
}
}
func recordTuningInt(tc *TuningConfig, param string, oldVal, newVal int, reason string) TuningEvent {
return TuningEvent{
Parameter: param,
OldValue: float64(oldVal),
NewValue: float64(newVal),
Reason: reason,
Timestamp: time.Now(),
}
}
// ─── API ──────────────────────────────────────────────────
// GET /api/v1/tuning/status
func (tc *TuningConfig) StatusHandler(w http.ResponseWriter, r *http.Request) {
tc.mu.RLock()
defer tc.mu.RUnlock()
respond(w, 200, map[string]interface{}{
"config": map[string]interface{}{
"diversity": tc.Diversity,
"decay_rate": tc.DecayRate,
"gap_threshold": tc.GapThreshold,
"distill_interval": tc.DistillInterval,
"consolidate_after": tc.ConsolidateAfter,
},
"history": tc.history,
})
}
// POST /api/v1/tuning/run — 手动触发一次自动调参
func (tc *TuningConfig) RunHandler(w http.ResponseWriter, r *http.Request) {
metrics := selfoptimize.Dash.Metrics()
events := tc.AutoTune(metrics)
if len(events) == 0 {
respond(w, 200, map[string]string{"status": "no_changes_needed"})
} else {
respond(w, 200, map[string]interface{}{
"status": "tuned",
"events": events,
"count": len(events),
})
}
}
// POST /api/v1/tuning/analytics — 运行因果分析
func (tc *TuningConfig) AnalyticsHandler(w http.ResponseWriter, r *http.Request) {
// 基于指标间的关系给出建议
m := selfoptimize.Dash.Metrics()
suggestions := []string{}
if m["recall_hit_rate"] < 0.5 && m["avg_distill_loss"] > 0.3 {
suggestions = append(suggestions, "蒸馏质量低导致召回率低 — 建议检查 Embedding 模型")
}
if m["deprecated_per_day"] > 10 {
suggestions = append(suggestions, "废弃率过高 — 正在流失大量记忆,检查冲突检测阈值")
}
if m["gap_closure_rate"] < 0.3 {
suggestions = append(suggestions, "缺口闭环率过低 — Agent 未回应缺口学习请求")
}
if m["auto_resolve_rate"] < 0.5 {
suggestions = append(suggestions, "自动裁决率低 — 规则覆盖不全,考虑扩展策略")
}
respond(w, 200, map[string]interface{}{
"metrics": m,
"suggestions": suggestions,
})
}

View File

@ -62,11 +62,9 @@ func (s *SSEManager) SSEHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("[sse] agent %s connected", agentID)
// 欢迎消息
fmt.Fprintf(w, "data: {\"type\":\"connected\",\"agent_id\":\"%s\"}\n\n", agentID)
flusher.Flush()
// 心跳
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
@ -89,7 +87,8 @@ func (s *SSEManager) SSEHandler(w http.ResponseWriter, r *http.Request) {
}
}
// 便捷推送方法
// ─── 便捷推送方法 ──────────────────────────────────────────
func PushMemoryCommitted(agentID, namespace, memoryID string) {
SSEBus.Push(SSEMessage{
Type: "memory_committed",
@ -117,3 +116,10 @@ func PushConsolidationDone(summary string) {
Payload: summary,
})
}
func PushConflictResolved(conflictID, resolution string) {
SSEBus.Push(SSEMessage{
Type: "conflict_resolved",
Payload: map[string]string{"conflict_id": conflictID, "resolution": resolution},
})
}

View File

@ -2,12 +2,15 @@
package api
import (
"encoding/json"
"log"
"net/http"
"os"
"github.com/xiaoxue/memoryweave/internal/api/middleware"
"github.com/xiaoxue/memoryweave/internal/api/routes"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/selfoptimize"
"github.com/xiaoxue/memoryweave/internal/storage"
)
@ -15,13 +18,44 @@ import (
func NewServer() http.Handler {
mux := http.NewServeMux()
// 初始化存储层
// ─── 初始化依赖 ──────────────────────────────
ldb := storage.NewLanceClient()
emb := storage.NewEmbedder(os.Getenv("VLLM_ENDPOINT"))
rerank := storage.NewReranker(os.Getenv("RERANK_ENDPOINT"))
// 核心 API
api := routes.NewAPI(ldb, emb, rerank)
// 知识图谱(零外部依赖 = 内存实现)
graphStore := governance.NewInMemoryGraph()
graphAPI := routes.NewGraphAPI(graphStore)
// 冲突检测器
conflictDetector := governance.NewConflictDetector()
conflictAPI := routes.NewConflictAPI(conflictDetector)
// 自优化
gapDetector := selfoptimize.NewGapDetector()
gapAPI := routes.NewGapAPI(gapDetector)
// 反馈
feedbackAPI := routes.NewFeedbackAPI(ldb)
// 管理
forgetter := governance.NewForgetter()
adminAPI := routes.NewAdminAPI(ldb, forgetter)
// Agent 注册
agentRegistry := routes.NewAgentRegistry(nil)
// 评估
evalAPI := routes.NewEvalAPI(api.Pipeline, ldb)
// L3
l3API := routes.WM
// ─── 路由注册 ──────────────────────────────
// 公共路由
mux.HandleFunc("/health", routes.HandleHealth)
@ -32,12 +66,104 @@ func NewServer() http.Handler {
mux.HandleFunc("/api/v1/stats", api.Stats)
mux.HandleFunc("/api/v1/batch-commit", api.BatchCommit)
// M3: SSE 实时推送
// SSE 实时推送
mux.HandleFunc("/api/v1/ws/", routes.SSEBus.SSEHandler)
// M8: 深度整合
mux.HandleFunc("/api/v1/admin/consolidate", routes.HandleConsolidate)
// 知识图谱
mux.HandleFunc("/api/v1/graph/stats", graphAPI.Stats)
mux.HandleFunc("/api/v1/graph/query", graphAPI.Query)
mux.HandleFunc("/api/v1/graph/navigate", graphAPI.Navigate)
log.Println("[zhiyid] 路由注册: /health /commit /recall /bootstrap /stats /batch-commit /ws/ /admin/consolidate")
// 冲突管理
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)
// 反馈
mux.HandleFunc("/api/v1/feedback/useful", feedbackAPI.MarkUseful)
mux.HandleFunc("/api/v1/feedback/not-useful", feedbackAPI.MarkNotUseful)
mux.HandleFunc("/api/v1/feedback/deprecate", feedbackAPI.Deprecate)
mux.HandleFunc("/api/v1/feedback/correct", feedbackAPI.Correct)
// 知识缺口
mux.HandleFunc("/api/v1/gaps", gapAPI.List)
mux.HandleFunc("/api/v1/gaps/detect", gapAPI.Detect)
mux.HandleFunc("/api/v1/gaps/close/", gapAPI.Close)
// Agent 注册
mux.HandleFunc("/api/v1/agents/register", agentRegistry.Register)
mux.HandleFunc("/api/v1/agents", agentRegistry.List)
// 管理端点
mux.HandleFunc("/api/v1/admin/consolidate", routes.HandleConsolidate)
mux.HandleFunc("/api/v1/admin/forget", adminAPI.Forget)
mux.HandleFunc("/api/v1/admin/backup", adminAPI.Backup)
mux.HandleFunc("/api/v1/admin/audit", adminAPI.Audit)
mux.HandleFunc("/api/v1/distilled/", 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)
// Skill 结晶
mux.HandleFunc("/api/v1/skills", routes.Skills.List)
mux.HandleFunc("/api/v1/skills/", func(w http.ResponseWriter, r *http.Request) {
routes.Skills.Trial(w, r)
})
// 评估
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)
// 自优化仪表盘
mux.HandleFunc("/api/v1/metrics/self", func(w http.ResponseWriter, r *http.Request) {
metrics := selfoptimize.Dash.Metrics()
data, _ := json.Marshal(metrics)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
})
// Obsidian 同步
obsidian := routes.NewObsidianSyncer("", ldb)
mux.HandleFunc("/api/v1/obsidian/push", obsidian.PushHandler)
mux.HandleFunc("/api/v1/obsidian/pull", obsidian.PullHandler)
mux.HandleFunc("/api/v1/obsidian/status", obsidian.StatusHandler)
log.Println("[zhiyid] 路由注册: /health /commit /recall /bootstrap /stats /batch-commit /ws/* /graph/* /conflicts/* /feedback/* /gaps/* /agents/* /admin/* /l3/* /triggers/* /skills/* /eval/* /tuning/* /obsidian/*")
return middleware.Auth(mux)
}

View File

@ -253,6 +253,35 @@ func (gs *GraphStore) Navigate(entity string, maxHops int, namespace string) ([]
return paths, nil
}
// DB 返回底层 sql.DB供路由层直接查询
func (gs *GraphStore) DB() *sql.DB {
return gs.db
}
// ListActive 返回所有活跃冲突
func (cd *ConflictDetector) ListActive() []*Conflict {
cd.mu.RLock()
defer cd.mu.RUnlock()
var list []*Conflict
for _, c := range cd.active {
if c.Status == "pending" {
list = append(list, c)
}
}
return list
}
// Resolve 解决冲突
func (cd *ConflictDetector) Resolve(id, resolution, winner string) error {
cd.mu.Lock()
defer cd.mu.Unlock()
if c, ok := cd.active[id]; ok {
c.Status = "resolved"
c.Strategy = resolution
}
return nil
}
// Prune 修剪图谱(删除孤立节点、低权重边)
func (gs *GraphStore) Prune(minWeight float64) error {
_, err := gs.db.Exec("DELETE FROM graph_edges WHERE weight < ?", minWeight)

View File

@ -0,0 +1,165 @@
// 织忆 MemoryWeave — 内存知识图谱(零外部依赖)
package governance
import (
"fmt"
"sync"
)
// InMemoryGraph 纯内存知识图谱,替代 SQLite 依赖
type InMemoryGraph struct {
mu sync.RWMutex
nodes map[string]*GraphNode
edges []*GraphEdge
}
type GraphNode struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Namespace string `json:"namespace"`
}
type GraphEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Relation string `json:"relation"`
Weight float64 `json:"weight"`
Namespace string `json:"namespace"`
}
func NewInMemoryGraph() *InMemoryGraph {
return &InMemoryGraph{
nodes: make(map[string]*GraphNode),
}
}
func (g *InMemoryGraph) AddNode(id, name, nodeType, namespace string) error {
g.mu.Lock()
defer g.mu.Unlock()
g.nodes[id] = &GraphNode{ID: id, Name: name, Type: nodeType, Namespace: namespace}
return nil
}
func (g *InMemoryGraph) AddEdge(id, source, target, relation, namespace string, weight float64) error {
g.mu.Lock()
defer g.mu.Unlock()
g.edges = append(g.edges, &GraphEdge{
ID: id, Source: source, Target: target,
Relation: relation, Weight: weight, Namespace: namespace,
})
return nil
}
// Navigate 多跳 BFS 导航
func (g *InMemoryGraph) Navigate(entity string, maxHops int, namespace string) ([]map[string]interface{}, error) {
g.mu.RLock()
defer g.mu.RUnlock()
visited := map[string]bool{entity: true}
queue := []string{entity}
var paths []map[string]interface{}
for hop := 1; hop <= maxHops && len(queue) > 0; hop++ {
var nextQueue []string
for _, current := range queue {
for _, e := range g.edges {
if e.Namespace != namespace {
continue
}
neighbor := ""
if e.Source == current {
neighbor = e.Target
} else if e.Target == current {
neighbor = e.Source
}
if neighbor == "" || visited[neighbor] {
continue
}
visited[neighbor] = true
nextQueue = append(nextQueue, neighbor)
paths = append(paths, map[string]interface{}{
"edge_id": e.ID,
"source": current,
"target": neighbor,
"relation": e.Relation,
"weight": e.Weight,
"hop": hop,
})
}
}
queue = nextQueue
}
return paths, nil
}
// Stats 返回图谱统计
func (g *InMemoryGraph) Stats() (nodeCount, edgeCount int, density float64) {
g.mu.RLock()
defer g.mu.RUnlock()
nodeCount = len(g.nodes)
edgeCount = len(g.edges)
if nodeCount > 1 {
density = float64(edgeCount) / float64(nodeCount*(nodeCount-1))
}
return
}
// Prune 删除低权重边和孤立节点
func (g *InMemoryGraph) Prune(minWeight float64) {
g.mu.Lock()
defer g.mu.Unlock()
// 删除低权重边
var kept []*GraphEdge
for _, e := range g.edges {
if e.Weight >= minWeight {
kept = append(kept, e)
}
}
g.edges = kept
// 删除孤立节点
connected := make(map[string]bool)
for _, e := range g.edges {
connected[e.Source] = true
connected[e.Target] = true
}
for id := range g.nodes {
if !connected[id] {
delete(g.nodes, id)
}
}
}
// Query 按实体和关系查询
func (g *InMemoryGraph) Query(entity, relation, namespace string) []map[string]interface{} {
g.mu.RLock()
defer g.mu.RUnlock()
var results []map[string]interface{}
for _, e := range g.edges {
if e.Namespace != namespace {
continue
}
if e.Source == entity || e.Target == entity {
if relation == "" || containsRelation(e.Relation, relation) {
results = append(results, map[string]interface{}{
"edge_id": e.ID,
"source": e.Source,
"target": e.Target,
"relation": e.Relation,
"weight": e.Weight,
})
}
}
}
return results
}
func containsRelation(rel, substr string) bool {
return len(substr) == 0 || fmt.Sprintf("%s", rel) != ""
// 简化实现:总是匹配
}

View File

@ -292,6 +292,40 @@ func (pg *PrefetchGraph) RecordCoAccess(a, b string) {
}
// GetPrefetch 获取某个 query 的预取候选项
// RecordDeprecation 记录废弃操作
func (d *Dashboard) RecordDeprecation() {
d.mu.Lock()
defer d.mu.Unlock()
d.DeprecatedToday++
}
// RecordCorrection 记录用户修正
func (d *Dashboard) RecordCorrection(source string) {
d.mu.Lock()
defer d.mu.Unlock()
d.TotalFixes++
if source == "muchen_correction" {
d.CascadeFixedTotal++
}
}
// RecordGapClosed 记录缺口关闭
func (d *Dashboard) RecordGapClosed() {
d.mu.Lock()
defer d.mu.Unlock()
d.ClosedGaps++
}
// RecordConflictResolved 记录冲突解决
func (d *Dashboard) RecordConflictResolved(auto bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.TotalConflicts++
if auto {
d.AutoResolvedConflicts++
}
}
func (pg *PrefetchGraph) GetPrefetch(query string) []string {
pg.mu.RLock()
defer pg.mu.RUnlock()

View File

@ -218,5 +218,104 @@ func (c *LanceClient) InsertMemory(m models.MemoryRecord) error {
return c.Insert("memories", m)
}
// IncrementUseful 增加 useful 计数
func (c *LanceClient) IncrementUseful(id string) {
c.Update("memories", id, map[string]any{
"useful_count": "useful_count + 1",
"recall_count": "recall_count + 1",
})
}
// IncrementNotUseful 增加 not-useful 计数
func (c *LanceClient) IncrementNotUseful(id string) {
c.Update("memories", id, map[string]any{
"not_useful_count": "not_useful_count + 1",
})
}
// UpdateMemoryContent 更新记忆内容并记录版本历史
func (c *LanceClient) UpdateMemoryContent(id, newContent, source string) error {
return c.Update("memories", id, map[string]any{
"content": newContent,
"source": source,
"version": "version + 1",
})
}
// GetVersionHistory 获取记忆版本历史
func (c *LanceClient) GetVersionHistory(id string) ([]map[string]interface{}, error) {
// 从 memory 的 version_history JSON 字段读取
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/v1/table/memories/query", c.baseURL), nil)
q := req.URL.Query()
q.Set("filter", fmt.Sprintf("id = '%s'", id))
q.Set("columns", "id,version_history")
req.URL.RawQuery = q.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var results []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&results)
return results, nil
}
// GetCandidatesForForgetting 获取可遗忘的候选记忆
func (c *LanceClient) GetCandidatesForForgetting() ([]map[string]interface{}, error) {
reqBody := map[string]interface{}{
"filter": "is_deleted = false AND tier != 'core'",
"order": "last_recalled_at ASC",
"top_k": 100,
}
body, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/table/memories/query", c.baseURL), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var results []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&results)
return results, nil
}
// Backup 备份数据到指定目录
func (c *LanceClient) Backup(path string) error {
// 调用 LanceDB 原生备份接口
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/backup", c.baseURL),
bytes.NewReader([]byte(fmt.Sprintf(`{"path":"%s"}`, path))))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// GetAuditLog 获取最近 N 条审计日志
func (c *LanceClient) GetAuditLog(limit int) ([]map[string]interface{}, error) {
// 从 audits 表或 tombstones 表取
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/v1/table/tombstones/query", c.baseURL), nil)
q := req.URL.Query()
q.Set("top_k", fmt.Sprintf("%d", limit))
q.Set("order", "deleted_at DESC")
req.URL.RawQuery = q.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var results []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&results)
return results, nil
}
// LanceDB 类型别名,兼容路由层引用
type LanceDB = LanceClient