Files
Cyrene/backend/memory-service/internal/handler/memory_handler.go
T
AskaEth 71f0a1abdb feat: Go模块路径迁移 + Docker生产部署适配 + ethend Docker兼容
- 所有Go模块路径从 github.com/yourname/cyrene-ai 迁移到 git.yeij.top/AskaEth/Cyrene
- 5个Go Dockerfile添加 GOPROXY=https://goproxy.cn,direct 解决国内构建问题
- ai-core go.mod 添加 pkg/plugins replace 指令
- Caddyfile 简化为 http:// 通配 + handle 保留 /api 前缀
- ethend Dockerfile 适配 (npm install + 仅 COPY package.json)
- ethend 新增 RUNNING_IN_DOCKER 环境变量,健康检查改用Docker服务名
- ethend 数据库状态检查支持Docker hostname (postgres/redis/qdrant/minio)
- process-manager 新增 CONTAINER_SVC_MAP + Docker模式自动检测
- 统一 docker-compose.dev.db.yml 卷名 (pg_data/redis_data/qdrant_data/minio_data)
- docker-compose.yml ethend服务挂载docker.sock + 端口变量化
- 清理 .env 统一后的残留文件与提示信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:43:22 +08:00

548 lines
15 KiB
Go

package handler
import (
"encoding/json"
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
"net/http"
"strings"
"git.yeij.top/AskaEth/Cyrene/memory-service/internal/model"
"git.yeij.top/AskaEth/Cyrene/memory-service/internal/service"
)
// MemoryHandler HTTP API 处理器
type MemoryHandler struct {
svc *service.MemoryService
}
// NewMemoryHandler 创建记忆处理器
func NewMemoryHandler(svc *service.MemoryService) *MemoryHandler {
return &MemoryHandler{svc: svc}
}
// RegisterRoutes 注册所有路由到 mux
func (h *MemoryHandler) RegisterRoutes(mux *http.ServeMux) {
// POST /api/v1/memories - 创建/保存记忆
mux.HandleFunc("/api/v1/memories", h.handleMemories)
// GET/DELETE/PUT /api/v1/memories/... (带 ID)
mux.HandleFunc("/api/v1/memories/", h.handleMemoryByID)
// POST /api/v1/memories/query - 语义查询
mux.HandleFunc("/api/v1/memories/query", h.handleQuery)
// POST /api/v1/memories/consolidate - 合并相似记忆
mux.HandleFunc("/api/v1/memories/consolidate", h.handleConsolidate)
// POST /api/v1/memories/decay - 衰减旧记忆
mux.HandleFunc("/api/v1/memories/decay", h.handleDecay)
// GET /api/v1/memories/categories - 获取类别统计
mux.HandleFunc("/api/v1/memories/categories", h.handleCategories)
// 自主思考日志 API
mux.HandleFunc("/api/v1/thinking", h.handleThinking)
mux.HandleFunc("/api/v1/thinking/", h.handleThinkingByID)
mux.HandleFunc("/api/v1/thinking/stats", h.handleThinkingStats)
}
// handleMemories 处理 /api/v1/memories
// GET - 列出用户记忆 (?user_id=xxx&category=xxx&min_importance=xxx&limit=xxx)
// POST - 创建记忆
func (h *MemoryHandler) handleMemories(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.listMemories(w, r)
case http.MethodPost:
h.createMemory(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
// listMemories GET /api/v1/memories?user_id=xxx
func (h *MemoryHandler) listMemories(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
if userID == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id 参数")
return
}
category := r.URL.Query().Get("category")
limit := queryInt(r, "limit", 50)
offset := queryInt(r, "offset", 0)
minImportance := queryInt(r, "min_importance", 0)
memories, err := h.svc.ListMemories(r.Context(), userID, category, minImportance, limit, offset)
if err != nil {
logger.Printf("[memory-handler] 列出记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if memories == nil {
memories = []model.MemoryEntry{}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"user_id": userID,
"memories": memories,
"total": len(memories),
})
}
// createMemory POST /api/v1/memories
func (h *MemoryHandler) createMemory(w http.ResponseWriter, r *http.Request) {
var req struct {
UserID string `json:"user_id"`
Content string `json:"content"`
Summary string `json:"summary"`
Category string `json:"category"`
Priority int `json:"priority"`
Importance int `json:"importance"`
Keywords []string `json:"keywords"`
SessionID string `json:"session_id"`
Source string `json:"source"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
if req.UserID == "" || req.Content == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id 或 content")
return
}
entry := &model.MemoryEntry{
UserID: req.UserID,
Content: req.Content,
Summary: req.Summary,
Category: model.MemoryCategory(req.Category),
Priority: model.MemoryPriority(req.Priority),
Importance: req.Importance,
Keywords: req.Keywords,
SessionID: req.SessionID,
Source: req.Source,
}
if err := h.svc.CreateMemory(r.Context(), entry); err != nil {
logger.Printf("[memory-handler] 创建记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"status": "saved",
"memory": entry,
})
}
// handleMemoryByID 处理 /api/v1/memories/{id}
// GET - 获取单个记忆
// PUT - 更新记忆
// DELETE - 删除记忆
func (h *MemoryHandler) handleMemoryByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/v1/memories/")
// 排除子路径 (query, consolidate, decay, categories)
switch id {
case "query", "consolidate", "decay", "categories":
return // 这些有自己独立的处理器
}
if id == "" {
writeError(w, http.StatusBadRequest, "缺少记忆 ID")
return
}
switch r.Method {
case http.MethodGet:
h.getMemory(w, r, id)
case http.MethodPut:
h.updateMemory(w, r, id)
case http.MethodDelete:
h.deleteMemory(w, r, id)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
// getMemory GET /api/v1/memories/:id
func (h *MemoryHandler) getMemory(w http.ResponseWriter, r *http.Request, id string) {
entry, err := h.svc.GetMemory(r.Context(), id)
if err != nil {
logger.Printf("[memory-handler] 获取记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if entry == nil {
writeError(w, http.StatusNotFound, "记忆不存在")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"memory": entry,
})
}
// updateMemory PUT /api/v1/memories/:id
func (h *MemoryHandler) updateMemory(w http.ResponseWriter, r *http.Request, id string) {
var req struct {
Content string `json:"content"`
Summary string `json:"summary"`
Category string `json:"category"`
Priority int `json:"priority"`
Importance int `json:"importance"`
Keywords []string `json:"keywords"`
Source string `json:"source"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
entry := &model.MemoryEntry{
ID: id,
Content: req.Content,
Summary: req.Summary,
Category: model.MemoryCategory(req.Category),
Priority: model.MemoryPriority(req.Priority),
Importance: req.Importance,
Keywords: req.Keywords,
Source: req.Source,
}
if err := h.svc.UpdateMemory(r.Context(), entry); err != nil {
logger.Printf("[memory-handler] 更新记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "updated",
"memory_id": id,
})
}
// deleteMemory DELETE /api/v1/memories/:id
func (h *MemoryHandler) deleteMemory(w http.ResponseWriter, r *http.Request, id string) {
if err := h.svc.DeleteMemory(r.Context(), id); err != nil {
logger.Printf("[memory-handler] 删除记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "deleted",
"memory_id": id,
})
}
// handleQuery POST /api/v1/memories/query
func (h *MemoryHandler) handleQuery(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
UserID string `json:"user_id"`
QueryText string `json:"query_text"`
Category string `json:"category"`
MinImportance int `json:"min_importance"`
Limit int `json:"limit"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
if req.UserID == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id")
return
}
if req.Limit <= 0 {
req.Limit = 10
}
memories, err := h.svc.QueryMemories(r.Context(), req.UserID, req.QueryText, req.Category, req.MinImportance, req.Limit)
if err != nil {
logger.Printf("[memory-handler] 查询记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if memories == nil {
memories = []model.MemoryEntry{}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"user_id": req.UserID,
"query": req.QueryText,
"memories": memories,
"total": len(memories),
})
}
// handleConsolidate POST /api/v1/memories/consolidate
func (h *MemoryHandler) handleConsolidate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
UserID string `json:"user_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
if req.UserID == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id")
return
}
merged, err := h.svc.ConsolidateMemories(r.Context(), req.UserID)
if err != nil {
logger.Printf("[memory-handler] 合并记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "consolidated",
"user_id": req.UserID,
"merged": merged,
"message": "记忆整理完成",
})
}
// handleDecay POST /api/v1/memories/decay
func (h *MemoryHandler) handleDecay(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
UserID string `json:"user_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
if req.UserID == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id")
return
}
decayed, deleted, err := h.svc.DecayMemories(r.Context(), req.UserID)
if err != nil {
logger.Printf("[memory-handler] 衰减记忆失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "decayed",
"user_id": req.UserID,
"decayed": decayed,
"deleted": deleted,
"message": "记忆衰减完成",
})
}
// handleCategories GET /api/v1/memories/categories?user_id=xxx
func (h *MemoryHandler) handleCategories(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
userID := r.URL.Query().Get("user_id")
if userID == "" {
writeError(w, http.StatusBadRequest, "缺少 user_id 参数")
return
}
categories, err := h.svc.GetCategories(r.Context(), userID)
if err != nil {
logger.Printf("[memory-handler] 获取分类统计失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"user_id": userID,
"categories": categories,
})
}
// --- 辅助函数 ---
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]interface{}{
"error": message,
})
}
func queryInt(r *http.Request, key string, fallback int) int {
v := r.URL.Query().Get(key)
if v == "" {
return fallback
}
var result int
for _, c := range v {
if c < '0' || c > '9' {
return fallback
}
result = result*10 + int(c-'0')
}
return result
}
// handleThinking 处理 /api/v1/thinking
// GET - 分页查询思考日志 (?user_id=xxx&limit=xxx&offset=xxx)
// POST - 保存思考日志
func (h *MemoryHandler) handleThinking(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.listThinkingLogs(w, r)
case http.MethodPost:
h.createThinkingLog(w, r)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
// createThinkingLog POST /api/v1/thinking
func (h *MemoryHandler) createThinkingLog(w http.ResponseWriter, r *http.Request) {
var req struct {
UserID string `json:"user_id"`
Content string `json:"content"`
ToolCalls string `json:"tool_calls"`
ToolCallCount int `json:"tool_call_count"`
ContentLength int `json:"content_length"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "请求格式错误: "+err.Error())
return
}
if req.Content == "" {
writeError(w, http.StatusBadRequest, "缺少 content")
return
}
tl := &model.ThinkingLog{
UserID: req.UserID,
Content: req.Content,
ToolCalls: req.ToolCalls,
ToolCallCount: req.ToolCallCount,
ContentLength: req.ContentLength,
}
if err := h.svc.SaveThinkingLog(r.Context(), tl); err != nil {
logger.Printf("[memory-handler] 保存思考日志失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"status": "saved",
"thinking": tl,
})
}
// listThinkingLogs GET /api/v1/thinking?user_id=xxx&limit=xxx&offset=xxx
func (h *MemoryHandler) listThinkingLogs(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
limit := queryInt(r, "limit", 20)
offset := queryInt(r, "offset", 0)
logs, err := h.svc.QueryThinkingLogs(r.Context(), model.ThinkingQuery{
UserID: userID,
Limit: limit,
Offset: offset,
})
if err != nil {
logger.Printf("[memory-handler] 查询思考日志失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if logs == nil {
logs = []model.ThinkingLog{}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"logs": logs,
"total": len(logs),
})
}
// handleThinkingByID 处理 /api/v1/thinking/{id}
// GET - 获取单条思考日志
func (h *MemoryHandler) handleThinkingByID(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/v1/thinking/")
// 排除子路径
if id == "stats" || id == "" {
return
}
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
thinkingLog, err := h.svc.GetThinkingLogByID(r.Context(), id)
if err != nil {
logger.Printf("[memory-handler] 获取思考日志失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if thinkingLog == nil {
writeError(w, http.StatusNotFound, "思考日志不存在")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"thinking": thinkingLog,
})
}
// handleThinkingStats GET /api/v1/thinking/stats
func (h *MemoryHandler) handleThinkingStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
userID := r.URL.Query().Get("user_id")
stats, err := h.svc.GetThinkingStats(r.Context(), userID)
if err != nil {
logger.Printf("[memory-handler] 获取思考日志统计失败: %v", err)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"total_logs": stats.TotalLogs,
"total_tool_calls": stats.TotalToolCalls,
"avg_content_length": stats.AvgContentLen,
"latest_at": stats.LatestAt,
})
}