Public errors now emit code, safe message, and request_id while keeping the legacy error field. Raw SQL, paths, tokens, and XSS payloads are stripped; the frontend branches on code instead of substring matching. Co-authored-by: Cursor <cursoragent@cursor.com>
107 lines
2.6 KiB
Go
107 lines
2.6 KiB
Go
package server
|
|
|
|
// chat_unread_api.go — P3 / Step 2 双轨制未读消息与位点推进 API
|
|
//
|
|
// GET /api/v1/chat/unread?session_id=...&since_message_id=...
|
|
// POST /api/v1/chat/read
|
|
//
|
|
// user_id 只取自登录 Principal,忽略 query/body 中的身份字段。
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"chat-agent-system/internal/app/chatflow"
|
|
"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 == "" {
|
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
|
return
|
|
}
|
|
|
|
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.chatLog == nil {
|
|
writeJSONResponse(w, http.StatusOK, ctxmgr.ChatUnreadSummary{
|
|
Total: 0,
|
|
NextMessageID: 0,
|
|
UnreadMessageIDs: []int64{},
|
|
Items: []ctxmgr.UnreadDayItem{},
|
|
})
|
|
return
|
|
}
|
|
|
|
summary, err := s.chatLog.Unread(r.Context(), s.chatPrincipal(user, chatflow.ActionUnread), chatflow.UnreadQuery{
|
|
SessionID: sessionID,
|
|
SinceMessageID: sinceID,
|
|
})
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
return
|
|
}
|
|
if summary == nil {
|
|
summary = &ctxmgr.ChatUnreadSummary{
|
|
UnreadMessageIDs: []int64{},
|
|
Items: []ctxmgr.UnreadDayItem{},
|
|
}
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, summary)
|
|
}
|
|
|
|
func (s *Server) handleChatRead(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
|
|
user, ok := s.requireAuthenticatedUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req ctxmgr.MarkChatReadRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
if req.SessionID == "" {
|
|
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
|
return
|
|
}
|
|
|
|
if s.chatLog == nil {
|
|
writeSafeError(w, r, http.StatusInternalServerError, "context manager not initialized")
|
|
return
|
|
}
|
|
|
|
pos, err := s.chatLog.MarkRead(r.Context(), s.chatPrincipal(user, chatflow.ActionUnread), req)
|
|
if err != nil {
|
|
s.writeChatLogError(w, err)
|
|
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,
|
|
})
|
|
}
|