diff --git a/go/internal/api/routes/admin.go b/go/internal/api/routes/admin.go index f6a89aa..35c331c 100644 --- a/go/internal/api/routes/admin.go +++ b/go/internal/api/routes/admin.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "time" "github.com/xiaoxue/memoryweave/internal/governance" @@ -68,13 +69,10 @@ func (aa *AdminAPI) Forget(w http.ResponseWriter, r *http.Request) { lastAccess := parseTimeStr(mem["last_recalled_at"]) recallCnt := intVal(mem["recall_count"]) tier := strVal(mem["tier"]) + content := strVal(mem["content"]) - // E4.3: 获取实体图谱度,取 namespace 和 content 中实体的最大度 - entity := strVal(mem["namespace"]) - degree := 0 - if entity != "" { - degree = aa.GraphStore.GetEntityDegree(entity) - } + // E4.3 修复: 从 content 提取实体(而非读 namespace),取图谱最大度 + degree := extractTopEntityDegree(content, aa.GraphStore) if aa.Forgetter.ShouldForget(lastAccess, recallCnt, tier, degree) { aa.LanceDB.SoftDelete(strVal(mem["id"]), "auto_forget") @@ -141,7 +139,8 @@ func (aa *AdminAPI) Audit(w http.ResponseWriter, r *http.Request) { respond(w, 200, map[string]interface{}{"audit_logs": logs, "count": len(logs)}) } -// 辅助函数 +// ─── 辅助函数 ─────────────────────────────────────────────────────────────── + func parseTimeStr(s interface{}) time.Time { if s == nil { return time.Time{} @@ -158,10 +157,10 @@ func parseTimeStr(s interface{}) 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 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) @@ -174,8 +173,117 @@ func strVal(v interface{}) string { return "" } switch s := v.(type) { - case string: return s - case json.Number: return s.String() + case string: return s + case json.Number: return s.String() } return fmt.Sprintf("%v", v) } + +// ─── E4.3: 图谱度提取 ────────────────────────────────────────────────────── + +// extractTopEntityDegree 从 content 提取实体,返回图中度数最高的实体度数 +// 复用 distill/engine.go 的启发式逻辑(大写单词、中文实体、技术标记) +func extractTopEntityDegree(content string, gs governance.GraphStore) int { + if content == "" || gs == nil { + return 0 + } + seen := make(map[string]bool) + var candidates []string + + words := strings.Fields(content) + for _, w := range words { + w = strings.Trim(w, ",.;:!?,。;:!?、\"'()()[]【】") + if len(w) < 2 { + continue + } + // 大写字母开头的英文词(Hermes, ComfyUI, Redis 等) + runes := []rune(w) + if len(runes) >= 2 && runes[0] >= 'A' && runes[0] <= 'Z' { + normalized := strings.ToLower(w) + if !seen[normalized] && !isStopWord(normalized) { + seen[normalized] = true + candidates = append(candidates, w) + } + } + // 中文实体(2-20 个纯中文字符) + cleanChinese := stripNonChinese(w) + if len(cleanChinese) >= 2 && len(cleanChinese) <= 20 { + if !seen[cleanChinese] { + seen[cleanChinese] = true + candidates = append(candidates, cleanChinese) + } + } + // 技术标记(数字+字母组合或纯数字) + if isTechToken(w) || isAllDigits(w) { + if !seen[w] { + seen[w] = true + candidates = append(candidates, w) + } + } + } + + maxDegree := 0 + for _, entity := range candidates { + d := gs.GetEntityDegree(entity) + if d > maxDegree { + maxDegree = d + } + } + return maxDegree +} + +// isStopWord 停用词表(与 distill/engine.go 保持一致) +func isStopWord(w string) bool { + stops := []string{ + "the", "and", "for", "are", "but", "not", "you", "all", "can", "had", + "her", "was", "one", "our", "out", "this", "that", "with", "from", + "your", "what", "when", "where", "which", "their", "will", "would", + "there", "could", "other", "into", "just", "has", "have", "were", + "they", "been", "more", "than", + } + for _, s := range stops { + if w == s { + return true + } + } + return false +} + +// stripNonChinese 提取纯中文字符串 +func stripNonChinese(s string) string { + var result []rune + for _, r := range s { + if r >= 0x4E00 && r <= 0x9FFF { + result = append(result, r) + } + } + return string(result) +} + +// isTechToken 判断是否为技术标记(数字+字母混合) +func isTechToken(s string) bool { + hasDigit := false + hasLetter := false + for _, r := range s { + if r >= '0' && r <= '9' { + hasDigit = true + } + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + hasLetter = true + } + } + return hasDigit && hasLetter +} + +// isAllDigits 判断是否全为数字 +func isAllDigits(s string) bool { + if len(s) == 0 { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} \ No newline at end of file