Files
Agent-System/internal/server/chat_window_api.go
T
weichenandCursor 4b8d8bf6d6 fix(chat): unify history read authorization and stats filtering
Apply session ownership plus scope_kind visibility across window, day,
search, dates, categories, unread, and digest endpoints; add PG refresh
regression test for pre-activation and task-scoped messages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 03:06:11 +00:00

254 lines
6.8 KiB
Go

package server
import (
"encoding/json"
"net/http"
"os"
"strconv"
"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.svc == nil || s.svc.ContextManager() == 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
}
authCtx, ok := s.authorizeSessionRead(w, r, sessionID, user)
if !ok {
return
}
daysStr := r.URL.Query().Get("days")
days, _ := strconv.Atoi(daysStr)
if days <= 0 {
days = ctxmgr.DefaultWindowDays
}
beforeDay := r.URL.Query().Get("before_day")
res, err := s.svc.ContextManager().GetChatWindow(r.Context(), ctxmgr.ChatWindowQuery{
SessionID: sessionID,
Days: days,
BeforeDay: beforeDay,
})
if err != nil {
http.Error(w, "get chat window failed: "+err.Error(), http.StatusInternalServerError)
return
}
s.applyChatWindowAuthFilter(authCtx, res)
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.svc == nil || s.svc.ContextManager() == 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
}
authCtx, ok := s.authorizeSessionRead(w, r, sessionID, user)
if !ok {
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
}
beforeIDStr := r.URL.Query().Get("before_id")
beforeID, _ := strconv.ParseInt(beforeIDStr, 10, 64)
afterIDStr := r.URL.Query().Get("after_id")
afterID, _ := strconv.ParseInt(afterIDStr, 10, 64)
limitStr := r.URL.Query().Get("limit")
limit, _ := strconv.Atoi(limitStr)
if limit <= 0 {
limit = 100
}
res, err := s.svc.ContextManager().GetChatDayMessages(r.Context(), sessionID, date, anchorID, beforeID, afterID, limit)
if err != nil {
http.Error(w, "get chat day messages failed: "+err.Error(), http.StatusInternalServerError)
return
}
res.Messages = s.filterChatMessagesByAuth(authCtx, res.Messages)
res.LoadedCount = len(res.Messages)
res.TotalCount = len(res.Messages)
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.svc == nil || s.svc.ContextManager() == 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
}
authCtx, ok := s.authorizeSessionRead(w, r, sessionID, user)
if !ok {
return
}
visible, err := s.listVisibleSessionMessages(r.Context(), authCtx)
if err != nil {
http.Error(w, "list chat dates failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, aggregateDateStats(visible))
}
// handleListChatCategories 列出会话各分类消息统计 (用于分类筛选条)
// GET /api/v1/chat/categories?session_id=
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
if s.svc == nil || s.svc.ContextManager() == 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
}
authCtx, ok := s.authorizeSessionRead(w, r, sessionID, user)
if !ok {
return
}
visible, err := s.listVisibleSessionMessages(r.Context(), authCtx)
if err != nil {
http.Error(w, "list chat categories failed: "+err.Error(), http.StatusInternalServerError)
return
}
writeJSONResponse(w, http.StatusOK, aggregateCategoryStats(visible))
}
// 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),
})
}