Tag messages with stage_key metadata, block sends for unauthorized roles, and filter history/search/case results by allowed stages. Co-authored-by: Cursor <cursoragent@cursor.com>
225 lines
6.6 KiB
Go
225 lines
6.6 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
|
|
"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.svc == nil || s.svc.ContextManager() == 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
|
|
}
|
|
beforeDay := r.URL.Query().Get("before_day")
|
|
|
|
res, err := s.svc.ContextManager().GetChatWindow(r.Context(), ctxmgr.ChatWindowQuery{
|
|
SessionID: sessionID,
|
|
Days: days,
|
|
BeforeDay: beforeDay,
|
|
})
|
|
if err != nil {
|
|
http.Error(w, "get chat window failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if projectID, pidErr := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); pidErr == nil && projectID > 0 {
|
|
s.applyChatWindowStageFilter(r.Context(), projectID, user.ID, res)
|
|
}
|
|
|
|
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.svc == nil || s.svc.ContextManager() == 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
|
|
}
|
|
|
|
beforeIDStr := r.URL.Query().Get("before_id")
|
|
beforeID, _ := strconv.ParseInt(beforeIDStr, 10, 64)
|
|
|
|
afterIDStr := r.URL.Query().Get("after_id")
|
|
afterID, _ := strconv.ParseInt(afterIDStr, 10, 64)
|
|
|
|
limitStr := r.URL.Query().Get("limit")
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
|
|
res, err := s.svc.ContextManager().GetChatDayMessages(r.Context(), sessionID, date, anchorID, beforeID, afterID, limit)
|
|
if err != nil {
|
|
http.Error(w, "get chat day messages failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if projectID, pidErr := s.svc.ContextManager().GetSessionProjectID(r.Context(), sessionID); pidErr == nil && projectID > 0 {
|
|
res.Messages = s.filterChatMessagesByStageAccess(r.Context(), projectID, user.ID, res.Messages)
|
|
}
|
|
|
|
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.svc == nil || s.svc.ContextManager() == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
stats, err := s.svc.ContextManager().ListChatDates(r.Context(), sessionID)
|
|
if err != nil {
|
|
http.Error(w, "list chat dates failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// handleListChatCategories 列出会话各分类消息统计 (用于分类筛选条)
|
|
// GET /api/v1/chat/categories?session_id=
|
|
func (s *Server) handleListChatCategories(w http.ResponseWriter, r *http.Request) {
|
|
if s.svc == nil || s.svc.ContextManager() == nil {
|
|
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
sessionID := r.URL.Query().Get("session_id")
|
|
if sessionID == "" {
|
|
http.Error(w, "missing session_id query param", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
stats, err := s.svc.ContextManager().ListChatCategories(r.Context(), sessionID)
|
|
if err != nil {
|
|
http.Error(w, "list chat categories failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONResponse(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// handleImportChatMessages 一次性批量回填历史消息 (仅允许在 dev/demo 环境下调用)
|
|
// POST /api/v1/chat/messages/import
|
|
func (s *Server) handleImportChatMessages(w http.ResponseWriter, r *http.Request) {
|
|
if s.svc == nil || s.svc.ContextManager() == 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.svc.ContextManager().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),
|
|
})
|
|
}
|