Files
Agent-System/internal/server/chat_window_api.go
T
weichenandCursor 3f42149457 feat(bizdev): snapshot collaborators onto Runtime and Server ports
Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 13:00:33 +00:00

230 lines
6.0 KiB
Go

package server
import (
"encoding/json"
"net/http"
"os"
"strconv"
"chat-agent-system/internal/app/chatflow"
"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.chatLog == 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
}
res, err := s.chatLog.GetWindow(r.Context(), s.chatPrincipal(user, chatflow.ActionReadWindow), ctxmgr.ChatWindowQuery{
SessionID: sessionID,
Days: days,
BeforeDay: r.URL.Query().Get("before_day"),
})
if err != nil {
s.writeChatLogError(w, err)
return
}
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.chatLog == 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
}
beforeID, _ := strconv.ParseInt(r.URL.Query().Get("before_id"), 10, 64)
afterID, _ := strconv.ParseInt(r.URL.Query().Get("after_id"), 10, 64)
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 100
}
res, err := s.chatLog.GetDay(r.Context(), s.chatPrincipal(user, chatflow.ActionReadDay), chatflow.DayQuery{
SessionID: sessionID,
Date: date,
AnchorID: anchorID,
BeforeID: beforeID,
AfterID: afterID,
Limit: limit,
})
if err != nil {
s.writeChatLogError(w, err)
return
}
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.chatLog == 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
}
res, err := s.chatLog.DateStats(r.Context(), s.chatPrincipal(user, chatflow.ActionReadStats), sessionID)
if err != nil {
s.writeChatLogError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, res)
}
// handleListChatCategories 列出会话各分类消息统计 (用于分类筛选条)
// GET /api/v1/chat/categories?session_id=
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
if s.chatLog == 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
}
res, err := s.chatLog.CategoryStats(r.Context(), s.chatPrincipal(user, chatflow.ActionReadStats), sessionID)
if err != nil {
s.writeChatLogError(w, err)
return
}
writeJSONResponse(w, http.StatusOK, res)
}
// handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用)
// POST /api/v1/chat/messages/import
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
if s.ctxMgr == 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.ctxMgr.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),
})
}