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>
134 lines
3.4 KiB
Go
134 lines
3.4 KiB
Go
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,
|
|
})
|
|
}
|