Files
Cyrene/backend/memory-service/internal/service/memory_service.go
T
AskaEth 1fc2b41d36 fix: 将管理员 user_id 从动态 admin_{username} 改为固定 admin
根因:admin user_id 由 admin_ + req.Username 动态拼接,
当 .env 中 ADMIN_USERNAME 更改时,新登录会生成不同的 user_id,
导致旧会话成为孤儿且消息历史不可见。

修复方案 (Plan A):
- auth_handler.go: Login 时 userID 固定为 admin
- auth.go: IsAdminKey 从 HasPrefix(admin_) 改为 == admin
- chat_handler.go: 主对话管理员检查改为 userID == admin
- memory_handler.go: 3处 admin_ 前缀检查改为 == admin
- briefing_handler.go: 3处 admin_ 前缀检查改为 != admin
- sessionStore.ts: isAdminUser 从 startsWith 改为 ===
- MessageBubble.tsx: UserAvatar 管理员判断改为 ===
- main.go: 添加旧管理员用户清理逻辑 (ListUsers+DeleteUser)
- user_store.go: 新增 ListUsers 和 DeleteUser 函数
- ai-core/main.go: adminUserID 从 admin_admin 改为 admin
- memory-service/store.go: 默认 user_id 改为 admin
- memory-service/memory_service.go: 默认 UserID 改为 admin
- devtools/src/index.js: URL 参数 user_id=admin

验证: Go build 通过 (gateway/ai-core/memory-service),
tsc --noEmit 通过, vite build 通过
2026-05-20 22:13:21 +08:00

322 lines
8.4 KiB
Go

package service
import (
"context"
"fmt"
"log"
"strings"
"github.com/yourname/cyrene-ai/memory-service/internal/model"
"github.com/yourname/cyrene-ai/memory-service/internal/store"
)
// deDupThreshold 去重相似度阈值
const deDupThreshold = 0.75
// MemoryService 记忆业务逻辑
type MemoryService struct {
store *store.Store
}
// NewMemoryService 创建记忆服务
func NewMemoryService(s *store.Store) *MemoryService {
return &MemoryService{store: s}
}
// CreateMemory 创建/保存记忆
func (svc *MemoryService) CreateMemory(ctx context.Context, entry *model.MemoryEntry) error {
if entry.UserID == "" {
return fmt.Errorf("user_id 不能为空")
}
if entry.Content == "" {
return fmt.Errorf("content 不能为空")
}
if entry.Category == "" {
entry.Category = model.CategoryKnowledge
}
if entry.Importance < 1 {
entry.Importance = 5
}
// Priority 范围检查:扩展为 [0, 10] 以支持用户自定义高优先级记忆
// 低于 0 的视为无效,重置为 normal;高于 10 的截断到 10
if entry.Priority < 0 {
entry.Priority = model.MemoryNormal
}
if entry.Priority > 10 {
entry.Priority = 10
}
if entry.Source == "" {
entry.Source = "manual"
}
// 去重检查
similar, err := svc.findSimilar(ctx, entry.UserID, entry)
if err == nil && similar != nil {
// 合并到已有记忆
return svc.mergeMemory(ctx, similar, entry)
}
return svc.store.Save(ctx, entry)
}
// GetMemory 获取单个记忆
func (svc *MemoryService) GetMemory(ctx context.Context, id string) (*model.MemoryEntry, error) {
return svc.store.GetByID(ctx, id)
}
// ListMemories 列出用户所有记忆
func (svc *MemoryService) ListMemories(ctx context.Context, userID string, category string, minImportance int, limit int) ([]model.MemoryEntry, error) {
if limit <= 0 {
limit = 50
}
q := model.MemoryQuery{
UserID: userID,
MinImportance: minImportance,
Limit: limit,
}
if category != "" {
q.Category = model.MemoryCategory(category)
}
return svc.store.Query(ctx, q)
}
// UpdateMemory 更新记忆
func (svc *MemoryService) UpdateMemory(ctx context.Context, entry *model.MemoryEntry) error {
if entry.ID == "" {
return fmt.Errorf("id 不能为空")
}
return svc.store.Update(ctx, entry)
}
// DeleteMemory 删除记忆
func (svc *MemoryService) DeleteMemory(ctx context.Context, id string) error {
return svc.store.Delete(ctx, id)
}
// QueryMemories 语义查询 + 关键词匹配
func (svc *MemoryService) QueryMemories(ctx context.Context, userID string, queryText string, category string, minImportance int, limit int) ([]model.MemoryEntry, error) {
if limit <= 0 {
limit = 10
}
var allEntries []model.MemoryEntry
seen := make(map[string]bool)
// 1. 关键词匹配检索
keywordEntries, err := svc.store.SearchByKeyword(ctx, userID, queryText, limit*2)
if err == nil {
for _, e := range keywordEntries {
if !seen[e.ID] {
seen[e.ID] = true
allEntries = append(allEntries, e)
}
}
}
// 2. 补充按分类/重要性查询
q := model.MemoryQuery{
UserID: userID,
MinImportance: minImportance,
Limit: limit,
}
if category != "" {
q.Category = model.MemoryCategory(category)
}
categoryEntries, err := svc.store.Query(ctx, q)
if err == nil {
for _, e := range categoryEntries {
if !seen[e.ID] {
seen[e.ID] = true
allEntries = append(allEntries, e)
}
}
}
// 3. 在应用层做内容匹配过滤
queryLower := strings.ToLower(queryText)
var matched []model.MemoryEntry
for _, entry := range allEntries {
contentLower := strings.ToLower(entry.Content)
summaryLower := strings.ToLower(entry.Summary)
if strings.Contains(contentLower, queryLower) || strings.Contains(summaryLower, queryLower) {
matched = append(matched, entry)
continue
}
for _, kw := range entry.Keywords {
if strings.Contains(queryLower, strings.ToLower(kw)) ||
strings.Contains(strings.ToLower(kw), queryLower) {
matched = append(matched, entry)
break
}
}
}
if len(matched) == 0 {
matched = allEntries
}
// 4. 去重合并
matched = svc.deduplicate(matched)
// 5. 按重要性降序
sortByImportance(matched)
if len(matched) > limit {
matched = matched[:limit]
}
return matched, nil
}
// ConsolidateMemories 合并相似记忆
func (svc *MemoryService) ConsolidateMemories(ctx context.Context, userID string) (int, error) {
return svc.store.ConsolidateMemories(ctx, userID)
}
// DecayMemories 衰减旧记忆
func (svc *MemoryService) DecayMemories(ctx context.Context, userID string) (int, int, error) {
return svc.store.DecayMemories(ctx, userID)
}
// GetCategories 获取用户分类统计
func (svc *MemoryService) GetCategories(ctx context.Context, userID string) (map[string]int, error) {
return svc.store.GetCategories(ctx, userID)
}
// findSimilar 查找相似记忆
func (svc *MemoryService) findSimilar(ctx context.Context, userID string, newMem *model.MemoryEntry) (*model.MemoryEntry, error) {
existing, err := svc.store.Query(ctx, model.MemoryQuery{
UserID: userID,
Limit: 100,
})
if err != nil {
return nil, err
}
for i := range existing {
score := existing[i].SimilarityScore(newMem)
if score >= deDupThreshold {
return &existing[i], nil
}
}
return nil, nil
}
// mergeMemory 合并新记忆到已有记忆
func (svc *MemoryService) mergeMemory(ctx context.Context, existing *model.MemoryEntry, newMem *model.MemoryEntry) error {
// 更新内容(如果新内容更有价值)
if newMem.Importance > existing.Importance || len(newMem.Content) > len(existing.Content) {
existing.Content = newMem.Content
existing.Summary = newMem.Summary
}
// 合并关键词
keywordSet := make(map[string]bool)
for _, k := range existing.Keywords {
keywordSet[k] = true
}
for _, k := range newMem.Keywords {
keywordSet[k] = true
}
mergedKeywords := make([]string, 0, len(keywordSet))
for k := range keywordSet {
mergedKeywords = append(mergedKeywords, k)
}
existing.Keywords = mergedKeywords
// 取最高重要性
if newMem.Importance > existing.Importance {
existing.Importance = newMem.Importance
}
// 取最高优先级
if newMem.Priority > existing.Priority {
existing.Priority = newMem.Priority
}
existing.AccessCount++
existing.Source = "merged"
log.Printf("[memory-service] 合并记忆 [%s|%d★]: %s (去重)", existing.Category, existing.Importance, existing.Summary)
return svc.store.Update(ctx, existing)
}
// deduplicate 去重合并
func (svc *MemoryService) deduplicate(entries []model.MemoryEntry) []model.MemoryEntry {
if len(entries) < 2 {
return entries
}
result := make([]model.MemoryEntry, 0, len(entries))
discarded := make(map[int]bool)
for i := 0; i < len(entries); i++ {
if discarded[i] {
continue
}
for j := i + 1; j < len(entries); j++ {
if discarded[j] {
continue
}
score := entries[i].SimilarityScore(&entries[j])
if score >= deDupThreshold {
if entries[j].Importance > entries[i].Importance ||
(entries[j].Importance == entries[i].Importance && entries[j].Priority > entries[i].Priority) {
discarded[i] = true
break
} else {
discarded[j] = true
}
}
}
if !discarded[i] {
result = append(result, entries[i])
}
}
return result
}
// sortByImportance 按 Importance 降序, Priority 降序排列
func sortByImportance(entries []model.MemoryEntry) {
for i := 0; i < len(entries); i++ {
for j := i + 1; j < len(entries); j++ {
if entries[j].Importance > entries[i].Importance ||
(entries[j].Importance == entries[i].Importance && entries[j].Priority > entries[i].Priority) {
entries[i], entries[j] = entries[j], entries[i]
}
}
}
}
// SaveThinkingLog 保存自主思考日志
func (svc *MemoryService) SaveThinkingLog(ctx context.Context, tl *model.ThinkingLog) error {
if tl.Content == "" {
return fmt.Errorf("content 不能为空")
}
if tl.UserID == "" {
tl.UserID = "admin"
}
return svc.store.SaveThinkingLog(ctx, tl)
}
// QueryThinkingLogs 分页查询思考日志
func (svc *MemoryService) QueryThinkingLogs(ctx context.Context, q model.ThinkingQuery) ([]model.ThinkingLog, error) {
return svc.store.QueryThinkingLogs(ctx, q)
}
// GetThinkingLogByID 获取单条思考日志
func (svc *MemoryService) GetThinkingLogByID(ctx context.Context, id string) (*model.ThinkingLog, error) {
return svc.store.GetThinkingLogByID(ctx, id)
}
// GetThinkingStats 获取思考日志统计
func (svc *MemoryService) GetThinkingStats(ctx context.Context) (*model.ThinkingStats, error) {
return svc.store.GetThinkingStats(ctx)
}