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 } 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 } if projectID, pidErr := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); pidErr == nil && projectID > 0 { s.applyChatWindowStageFilter(r.Context(), projectID, user.ID, 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 } 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 } if projectID, pidErr := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); pidErr == nil && projectID > 0 { res.Messages = s.filterChatMessagesByStageAccess(r.Context(), projectID, user.ID, 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 } sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Error(w, "missing session_id query param", http.StatusBadRequest) return } stats, err := s.svc.ContextManager().ListChatDates(r.Context(), sessionID) if err != nil { http.Error(w, "list chat dates failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, stats) } // 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 } sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Error(w, "missing session_id query param", http.StatusBadRequest) return } stats, err := s.svc.ContextManager().ListChatCategories(r.Context(), sessionID) if err != nil { http.Error(w, "list chat categories failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, stats) } // 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), }) }