Files
Agent-System/internal/server/chat_window_api.go
T
weichen a9c9a9182e refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go
- backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration
- task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
2026-09-25 13:17:22 +00:00

230 lines
6.1 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 {
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
return
}
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 query param")
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 {
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
return
}
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 query param")
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 {
writeSafeError(w, r, http.StatusBadRequest, "missing date/day_key or anchor_id query param")
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 {
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
return
}
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 query param")
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 {
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
return
}
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 query param")
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 {
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
return
}
// 安全门禁:生产环境默认禁止调用,仅在显式 dev 或 ENABLE_DEV_IMPORT 时开放
isDev := s.cfg.Env == "dev" || s.cfg.Env == "development" || os.Getenv("ENABLE_DEV_IMPORT") == "true"
if !isDev {
writeSafeError(w, r, http.StatusForbidden, "chat messages import endpoint is only available in dev/demo environment")
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 {
writeSafeError(w, r, http.StatusBadRequest, "invalid request body: "+err.Error())
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),
})
}