Files
Cyrene/backend/gateway/internal/handler/memory_handler.go
T
AskaEth 91c9ee4b2d fix: 修复 AI 回复无法送达发送者 + 重复消息 + action角色泄露 + OS环境支持
广播逻辑重构:
- AI 回复 (stream_start/response/stream_segments/multi_message/stream_end) 改用 broadcastToUser 发送给所有客户端
- 用户消息回显保持 broadcastToUserExcept 排除发送者

消息去重与角色修复:
- CacheMessage(user) 移至回复生成后,避免本轮 LLM 调用出现重复用户消息
- action 角色消息在 DB 存储时映射为 assistant,DeepSeek 等模型不支持自定义角色
- stream_end defer 机制确保错误路径也会终止客户端思考指示器

OS 完整环境支持:
- host 包重构为 HostBackend 接口 + Direct/WSL/Docker 三种后端
- 新增 os_exec/os_file/os_system 工具供 AI 在完整 Linux 环境中自由操作

其他:
- 视觉模型注入 + 图片预处理后清空 Images 避免传给 Chat 模型
- 图片 URL 相对路径→绝对 URL 转换
- DevTools 链路追踪页面 + 重启修复
- 记忆搜索模糊匹配增强
- 后台思考定时调度支持
- 管理后台页面 (模型配置/用户管理等)
- docs/api 更新广播机制说明

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:46:17 +08:00

226 lines
6.3 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"fmt"
"html"
"io"
"github.com/yourname/cyrene-ai/pkg/logger"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
)
// MemoryHandler 记忆查询处理器 — 代理到 Memory-Service
type MemoryHandler struct {
memoryServiceURL string
client *http.Client
}
// NewMemoryHandler 创建记忆处理器
func NewMemoryHandler(memoryServiceURL string) *MemoryHandler {
return &MemoryHandler{
memoryServiceURL: memoryServiceURL,
client: &http.Client{
Timeout: 15 * time.Second,
},
}
}
// Query 搜索用户记忆 — 代理 POST /api/v1/memories/query
// 管理员可通过 user_id 查询参数查询任意用户的记忆
func (h *MemoryHandler) Query(c *gin.Context) {
authUserID := middleware.GetUserID(c)
userID := c.Query("user_id")
// 非管理员只能查询自己的记忆;管理员可通过查询参数指定目标用户
if authUserID != "admin" || userID == "" {
userID = authUserID
}
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "查询参数q不能为空"})
return
}
// 使用 memory-service 的 POST /api/v1/memories/query 端点
reqBody, _ := json.Marshal(map[string]interface{}{
"user_id": userID,
"query_text": query,
"limit": 10,
})
url := fmt.Sprintf("%s/api/v1/memories/query", h.memoryServiceURL)
httpReq, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建请求失败"})
return
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(httpReq)
if err != nil {
logger.Printf("[memory] Memory-Service 不可达 (Query): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
"hint": "Memory-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Memory-Service",
})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
// List 列出用户所有记忆 — 代理 GET /api/v1/memories?user_id=...
// 管理员可通过 user_id 查询参数查询任意用户的记忆
func (h *MemoryHandler) List(c *gin.Context) {
authUserID := middleware.GetUserID(c)
userID := c.Query("user_id")
// 非管理员只能查询自己的记忆;管理员可通过查询参数指定目标用户
if authUserID != "admin" || userID == "" {
userID = authUserID
}
limit := c.Query("limit")
offset := c.Query("offset")
url := fmt.Sprintf("%s/api/v1/memories?user_id=%s", h.memoryServiceURL, userID)
if limit != "" {
url += "&limit=" + limit
}
if offset != "" {
url += "&offset=" + offset
}
resp, err := h.client.Get(url)
if err != nil {
logger.Printf("[memory] Memory-Service 不可达 (List): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
"hint": "Memory-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Memory-Service",
})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
// Add 手动添加记忆 — 代理 POST /api/v1/memories
// 管理员可通过请求体中的 user_id 字段为任意用户添加记忆
func (h *MemoryHandler) Add(c *gin.Context) {
authUserID := middleware.GetUserID(c)
var req struct {
UserID string `json:"user_id"`
Content string `json:"content" binding:"required"`
Category string `json:"category"`
Priority int `json:"priority"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
return
}
if req.Category == "" {
req.Category = "other"
}
if req.Priority <= 0 {
req.Priority = 1
}
// 管理员可通过请求体指定目标用户,否则使用认证用户
userID := authUserID
if authUserID == "admin" && req.UserID != "" {
userID = req.UserID
}
// 转发到 Memory-Service(对用户输入进行 HTML 转义防 XSS)
memReq := map[string]interface{}{
"user_id": userID,
"content": html.EscapeString(req.Content),
"category": html.EscapeString(req.Category),
"priority": req.Priority,
}
reqBody, _ := json.Marshal(memReq)
url := fmt.Sprintf("%s/api/v1/memories", h.memoryServiceURL)
httpReq, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建请求失败"})
return
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(httpReq)
if err != nil {
logger.Printf("[memory] Memory-Service 不可达 (Add): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
"hint": "Memory-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Memory-Service",
})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
// Delete 删除单条记忆 — 代理 DELETE /api/v1/memories/:id
func (h *MemoryHandler) Delete(c *gin.Context) {
memoryID := c.Query("id")
if memoryID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 id 参数"})
return
}
url := fmt.Sprintf("%s/api/v1/memories/%s", h.memoryServiceURL, memoryID)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建请求失败"})
return
}
resp, err := h.client.Do(req)
if err != nil {
logger.Printf("[memory] Memory-Service 不可达 (Delete): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": fmt.Sprintf("Memory-Service 不可达: %v", err),
"errorType": "memory_service_unreachable",
"hint": "Memory-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Memory-Service",
})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}