feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs

- Fix: Session history flash (race condition + WS guard)
- Fix: Chat background overlay + sidebar transparency
- Fix: IoT device control (Chinese action names, status field)
- Feat: Independent memory-service (port 8091, 13 endpoints)
- Feat: Independent tool-engine service (port 8092, 13 tools)
- Feat: Tool call logs with paginated DevTools panel
- Feat: Thinking log records with DevTools panel
- Feat: Future development roadmap document
- Chore: Updated .gitignore, go.work, DevTools config
- Chore: 5-service health check, project review docs
This commit is contained in:
2026-05-18 20:05:14 +08:00
parent b6ec36886c
commit 78e3f450c2
54 changed files with 7846 additions and 106 deletions
@@ -0,0 +1,316 @@
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
}
if entry.Priority < 0 || entry.Priority > 3 {
entry.Priority = model.MemoryNormal
}
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_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)
}