- 虚拟滚动:接入 @tanstack/react-virtual,封装 useVirtualMessages 动态测高与 overscan 预渲染,迁移 anchorNavigator 锚点定位与 SSR 兼容降级 - 聊天记录导出:基于既有 docx 与 xlsx 依赖实现 chatExporter 与 ChatExportModal,新增服务端 /api/v1/chat/export/messages 支持按条件导出 - 协同未读标记:新增 useUnreadMarks 30s 轮询与服务端 /api/v1/chat/unread 接口,日历格与工具栏支持橙色未读徽标提示 - 测试与验证:新增 chat_unread_and_export_api_test.go,前端全量 126 项单元/组件测试及生产打包 100% 通过
92 lines
2.0 KiB
Go
92 lines
2.0 KiB
Go
package server
|
|
|
|
// chat_unread_api.go — P3 未读消息 API
|
|
//
|
|
// GET /api/v1/chat/unread?session_id=...&since_message_id=...
|
|
//
|
|
// 返回 since_message_id 之后的新消息按日聚合未读数。
|
|
// 内存模式(MemoryStore)下直接按消息列表计数;
|
|
// DB 模式查询 chat_messages 表。
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
)
|
|
|
|
type unreadItem struct {
|
|
Date string `json:"date"`
|
|
Count int `json:"count"`
|
|
LastMessageID int64 `json:"last_message_id"`
|
|
}
|
|
|
|
func (s *Server) handleChatUnread(w http.ResponseWriter, r *http.Request) {
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
sinceStr := r.URL.Query().Get("since_message_id")
|
|
var sinceID int64
|
|
if sinceStr != "" {
|
|
id, err := strconv.ParseInt(sinceStr, 10, 64)
|
|
if err == nil {
|
|
sinceID = id
|
|
}
|
|
}
|
|
|
|
if s.svc == nil || s.svc.ContextManager() == nil {
|
|
writeJSONResponse(w, http.StatusOK, []unreadItem{})
|
|
return
|
|
}
|
|
|
|
msgs, err := s.svc.ContextManager().GetChatHistory(r.Context(), sessionID, 3000, 0)
|
|
if err != nil {
|
|
writeJSONResponse(w, http.StatusOK, []unreadItem{})
|
|
return
|
|
}
|
|
|
|
type dayAgg struct {
|
|
count int
|
|
lastMessageID int64
|
|
}
|
|
dayMap := make(map[string]*dayAgg)
|
|
var dayOrder []string
|
|
|
|
for _, msg := range msgs {
|
|
if msg.ID == 0 || msg.ID <= sinceID {
|
|
continue
|
|
}
|
|
if msg.Role != "user" {
|
|
continue
|
|
}
|
|
dayKey := msg.DayKey
|
|
if dayKey == "" && !msg.CreatedAt.IsZero() {
|
|
dayKey = msg.CreatedAt.Format("2006-01-02")
|
|
}
|
|
if dayKey == "" {
|
|
continue
|
|
}
|
|
if _, ok := dayMap[dayKey]; !ok {
|
|
dayMap[dayKey] = &dayAgg{}
|
|
dayOrder = append(dayOrder, dayKey)
|
|
}
|
|
dayMap[dayKey].count++
|
|
if msg.ID > dayMap[dayKey].lastMessageID {
|
|
dayMap[dayKey].lastMessageID = msg.ID
|
|
}
|
|
}
|
|
|
|
items := make([]unreadItem, 0, len(dayOrder))
|
|
for _, dk := range dayOrder {
|
|
agg := dayMap[dk]
|
|
items = append(items, unreadItem{
|
|
Date: dk,
|
|
Count: agg.count,
|
|
LastMessageID: agg.lastMessageID,
|
|
})
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, items)
|
|
}
|