Ignore request user_id as identity, authorize from the session Principal, and hydrate restore/memory from the same visible set so HTTP, SSE, and prompt assembly cannot amplify hidden messages. Co-authored-by: Cursor <cursoragent@cursor.com>
230 lines
6.1 KiB
Go
230 lines
6.1 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
|
|
"chat-agent-system/internal/app/chatflow"
|
|
"chat-agent-system/internal/ctxmgr"
|
|
)
|
|
|
|
// handleGetChatWindow 获取指定会话最近 N 日的消息窗口
|
|
// GET /api/v1/chat/messages/window?session_id=&days=&before_day=
|
|
func (s *Server) handleGetChatWindow(w http.ResponseWriter, r *http.Request) {
|
|
if s.chatLog == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireAuthenticatedUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
daysStr := r.URL.Query().Get("days")
|
|
days, _ := strconv.Atoi(daysStr)
|
|
if days <= 0 {
|
|
days = ctxmgr.DefaultWindowDays
|
|
}
|
|
|
|
res, err := s.chatLog.GetWindow(r.Context(), s.chatPrincipal(user, chatflow.ActionReadWindow), ctxmgr.ChatWindowQuery{
|
|
SessionID: sessionID,
|
|
Days: days,
|
|
BeforeDay: r.URL.Query().Get("before_day"),
|
|
})
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, res)
|
|
}
|
|
|
|
// handleGetChatDayMessages 获取指定日消息 (支持 anchor_id 上下文补齐或游标分页)
|
|
// GET /api/v1/chat/messages/day?session_id=&date=&anchor_id=&before_id=&after_id=&limit=
|
|
func (s *Server) handleGetChatDayMessages(w http.ResponseWriter, r *http.Request) {
|
|
if s.chatLog == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireAuthenticatedUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
date := r.URL.Query().Get("date")
|
|
if date == "" {
|
|
date = r.URL.Query().Get("day_key")
|
|
}
|
|
|
|
anchorIDStr := r.URL.Query().Get("anchor_id")
|
|
anchorID, _ := strconv.ParseInt(anchorIDStr, 10, 64)
|
|
|
|
if date == "" && anchorID <= 0 {
|
|
http.Error(w, "missing date/day_key or anchor_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
beforeID, _ := strconv.ParseInt(r.URL.Query().Get("before_id"), 10, 64)
|
|
afterID, _ := strconv.ParseInt(r.URL.Query().Get("after_id"), 10, 64)
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
|
|
res, err := s.chatLog.GetDay(r.Context(), s.chatPrincipal(user, chatflow.ActionReadDay), chatflow.DayQuery{
|
|
SessionID: sessionID,
|
|
Date: date,
|
|
AnchorID: anchorID,
|
|
BeforeID: beforeID,
|
|
AfterID: afterID,
|
|
Limit: limit,
|
|
})
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, res)
|
|
}
|
|
|
|
// handleListChatDates 列出会话有消息的全部日期 (用于日历打点)
|
|
// GET /api/v1/chat/dates?session_id=
|
|
func (s *Server) handleListChatDates(w http.ResponseWriter, r *http.Request) {
|
|
if s.chatLog == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireAuthenticatedUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
res, err := s.chatLog.DateStats(r.Context(), s.chatPrincipal(user, chatflow.ActionReadStats), sessionID)
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, res)
|
|
}
|
|
|
|
// handleListChatCategories 列出会话各分类消息统计 (用于分类筛选条)
|
|
// GET /api/v1/chat/categories?session_id=
|
|
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
|
|
if s.chatLog == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireAuthenticatedUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
res, err := s.chatLog.CategoryStats(r.Context(), s.chatPrincipal(user, chatflow.ActionReadStats), sessionID)
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, res)
|
|
}
|
|
|
|
// handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用)
|
|
// POST /api/v1/chat/messages/import
|
|
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
|
|
if s.svc == nil || s.svc.ContextManager() == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
// 安全门禁:生产环境默认禁止调用,仅在 dev 或显式开启时开放
|
|
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || s.cfg.Env == "" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
|
|
if !isDev {
|
|
http.Error(w, "chat messages import endpoint is only available in dev/demo environment", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
type importMessageItem struct {
|
|
SessionID string `json:"session_id"`
|
|
UserID int64 `json:"user_id"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Intent string `json:"intent"`
|
|
Branch string `json:"branch"`
|
|
Metadata map[string]any `json:"metadata"`
|
|
DayKey string `json:"day_key"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
var items []importMessageItem
|
|
if err := json.NewDecoder(r.Body).Decode(&items); err != nil {
|
|
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
imported := 0
|
|
for _, item := range items {
|
|
if item.SessionID == "" || item.Content == "" {
|
|
continue
|
|
}
|
|
if item.Metadata == nil {
|
|
item.Metadata = make(map[string]any)
|
|
}
|
|
if item.DayKey != "" {
|
|
item.Metadata["day_key"] = item.DayKey
|
|
}
|
|
if item.CreatedAt != "" {
|
|
item.Metadata["created_at"] = item.CreatedAt
|
|
}
|
|
|
|
_, err := s.svc.ContextManager().RecordUserMessageWithID(
|
|
r.Context(),
|
|
item.SessionID,
|
|
item.UserID,
|
|
item.Role,
|
|
item.Content,
|
|
item.Intent,
|
|
item.Branch,
|
|
item.Metadata,
|
|
)
|
|
if err == nil {
|
|
imported++
|
|
}
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, map[string]any{
|
|
"imported_count": imported,
|
|
"total": len(items),
|
|
})
|
|
}
|