package server // chat_unread_api.go — P3 / Step 2 双轨制未读消息与位点推进 API // // GET /api/v1/chat/unread?session_id=...&user_id=...&since_message_id=... // POST /api/v1/chat/read import ( "encoding/json" "net/http" "strconv" "chat-agent-system/internal/ctxmgr" ) func (s *Server) handleChatUnread(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } sessionID := r.URL.Query().Get("session_id") if sessionID == "" { http.Error(w, "missing session_id", http.StatusBadRequest) return } authCtx, ok := s.authorizeSessionRead(w, r, sessionID, user) if !ok { return } var userID int64 if uStr := r.URL.Query().Get("user_id"); uStr != "" { if uid, err := strconv.ParseInt(uStr, 10, 64); err == nil { userID = uid } } var sinceID int64 if sinceStr := r.URL.Query().Get("since_message_id"); sinceStr != "" { if id, err := strconv.ParseInt(sinceStr, 10, 64); err == nil { sinceID = id } } if s.svc == nil || s.svc.ContextManager() == nil { writeJSONResponse(w, http.StatusOK, ctxmgr.ChatUnreadSummary{ Total: 0, NextMessageID: 0, UnreadMessageIDs: []int64{}, Items: []ctxmgr.UnreadDayItem{}, }) return } summary, err := s.svc.ContextManager().GetChatUnreadSummary(r.Context(), sessionID, userID, sinceID) if err != nil { writeJSONResponse(w, http.StatusOK, ctxmgr.ChatUnreadSummary{ Total: 0, NextMessageID: 0, UnreadMessageIDs: []int64{}, Items: []ctxmgr.UnreadDayItem{}, }) return } if authCtx.StageAccess != nil && authCtx.StageAccess.Restricted { visible, err := s.listVisibleSessionMessages(r.Context(), authCtx) if err == nil { allowed := make(map[int64]bool, len(visible)) for _, msg := range visible { allowed[msg.ID] = true } filteredIDs := make([]int64, 0, len(summary.UnreadMessageIDs)) for _, id := range summary.UnreadMessageIDs { if allowed[id] { filteredIDs = append(filteredIDs, id) } } summary.UnreadMessageIDs = filteredIDs summary.Total = len(filteredIDs) } } writeJSONResponse(w, http.StatusOK, summary) } func (s *Server) handleChatRead(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req ctxmgr.MarkChatReadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest) return } if req.SessionID == "" { http.Error(w, "missing session_id", http.StatusBadRequest) return } if _, ok := s.authorizeSessionRead(w, r, req.SessionID, user); !ok { return } if s.svc == nil || s.svc.ContextManager() == nil { http.Error(w, "context manager not initialized", http.StatusInternalServerError) return } pos, err := s.svc.ContextManager().MarkChatRead(r.Context(), req) if err != nil { http.Error(w, "mark chat read failed: "+err.Error(), http.StatusInternalServerError) return } writeJSONResponse(w, http.StatusOK, map[string]any{ "ok": true, "session_id": pos.SessionID, "user_id": pos.UserID, "last_read_message_id": pos.LastReadMessageID, "dismissed_unread_ids": pos.DismissedUnreadIDs, "updated_at": pos.UpdatedAt, }) }