Files
Agent-System/internal/server/chat_history_auth.go
T
weichenandCursor f6813cc272 feat(api): add additive HTTP/SSE error envelopes
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>
2026-09-15 14:40:56 +00:00

252 lines
6.7 KiB
Go

package server
import (
"context"
"net/http"
"sort"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/projectinstance"
)
// chatHistoryAuth 会话历史读取授权上下文。
type chatHistoryAuth struct {
SessionID string
SessionOwnerID int64
ProjectID int64
ViewerUserID int64
StageAccess *projectinstance.StageAccess
}
func (s *Server) authorizeSessionRead(
w http.ResponseWriter,
r *http.Request,
sessionID string,
user *auth.UserAccount,
) (*chatHistoryAuth, bool) {
if user == nil {
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
return nil, false
}
if !s.assertSessionOwnedByUser(w, r, sessionID, user.ID) {
return nil, false
}
sessionOwnerID := user.ID
if s.ctxMgr != nil {
if ownerID, err := s.ctxMgr.GetSessionOwnerID(r.Context(), sessionID); err == nil && ownerID > 0 {
sessionOwnerID = ownerID
}
}
authCtx := &chatHistoryAuth{
SessionID: sessionID,
SessionOwnerID: sessionOwnerID,
ViewerUserID: user.ID,
}
if s.ctxMgr != nil {
if projectID, err := s.ctxMgr.GetSessionProjectID(r.Context(), sessionID); err == nil {
authCtx.ProjectID = projectID
}
}
if authCtx.ProjectID > 0 {
access, err := s.resolveStageAccess(r.Context(), authCtx.ProjectID, user.ID)
if err != nil && s.failClosedAuthz() {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "WORKFLOW_ACCESS_DENIED",
"message": "无权访问该阶段内容",
})
return nil, false
}
authCtx.StageAccess = access
}
return authCtx, true
}
func (a *chatHistoryAuth) messageVisible(msg ctxmgr.ChatMessageRecord) bool {
if a == nil {
return false
}
return projectinstance.MessageRecordVisible(
msg.ScopeKind,
msg.StageKey,
msg.Metadata,
a.StageAccess,
a.SessionOwnerID,
a.ViewerUserID,
)
}
func (s *Server) filterChatMessagesByAuth(
authCtx *chatHistoryAuth,
msgs []ctxmgr.ChatMessageRecord,
) []ctxmgr.ChatMessageRecord {
if authCtx == nil || len(msgs) == 0 {
return msgs
}
out := make([]ctxmgr.ChatMessageRecord, 0, len(msgs))
for _, msg := range msgs {
if authCtx.messageVisible(msg) {
out = append(out, msg)
}
}
return out
}
func (s *Server) filterChatSearchHitsByAuth(
authCtx *chatHistoryAuth,
hits []ctxmgr.ChatSearchHit,
) []ctxmgr.ChatSearchHit {
if authCtx == nil || len(hits) == 0 {
return hits
}
out := make([]ctxmgr.ChatSearchHit, 0, len(hits))
for _, hit := range hits {
meta := map[string]any{"stage_key": hit.StageKey}
if projectinstance.MessageRecordVisible("", hit.StageKey, meta, authCtx.StageAccess, authCtx.SessionOwnerID, authCtx.ViewerUserID) {
out = append(out, hit)
}
}
return out
}
func (s *Server) applyChatWindowAuthFilter(authCtx *chatHistoryAuth, res *ctxmgr.ChatWindowResult) {
if res == nil || authCtx == nil {
return
}
filtered := make([]ctxmgr.ChatDayBucket, 0, len(res.Days))
for i := range res.Days {
msgs := s.filterChatMessagesByAuth(authCtx, res.Days[i].Messages)
if len(msgs) == 0 {
continue
}
res.Days[i].Messages = msgs
res.Days[i].LoadedCount = len(msgs)
res.Days[i].Count = len(msgs)
res.Days[i].FirstMessageID = msgs[0].ID
res.Days[i].LastMessageID = msgs[len(msgs)-1].ID
filtered = append(filtered, res.Days[i])
}
res.Days = filtered
if len(filtered) > 0 {
res.OldestLoadedDay = filtered[0].DayKey
} else {
res.OldestLoadedDay = ""
}
}
func aggregateDateStats(msgs []ctxmgr.ChatMessageRecord) []ctxmgr.ChatDateStat {
statMap := make(map[string]*ctxmgr.ChatDateStat)
var orderedKeys []string
for _, msg := range msgs {
dKey := msg.DayKey
if dKey == "" {
continue
}
st, ok := statMap[dKey]
if !ok {
st = &ctxmgr.ChatDateStat{
DayKey: dKey,
Categories: make(map[string]int),
}
statMap[dKey] = st
orderedKeys = append(orderedKeys, dKey)
}
st.Count++
cat := msg.Category
if cat == "" {
cat = ctxmgr.CategoryUncategorized
}
st.Categories[cat]++
}
sort.Strings(orderedKeys)
res := make([]ctxmgr.ChatDateStat, 0, len(orderedKeys))
for _, k := range orderedKeys {
res = append(res, *statMap[k])
}
return res
}
func aggregateCategoryStats(msgs []ctxmgr.ChatMessageRecord) []ctxmgr.ChatCategoryStat {
statMap := make(map[string]*ctxmgr.ChatCategoryStat)
for _, msg := range msgs {
cat := msg.Category
if cat == "" {
cat = ctxmgr.CategoryUncategorized
}
st, ok := statMap[cat]
if !ok {
st = &ctxmgr.ChatCategoryStat{
Category: cat,
Label: ctxmgr.GetCategoryLabel(cat),
}
statMap[cat] = st
}
st.Count++
if st.FirstMessageID == 0 || msg.ID < st.FirstMessageID {
st.FirstMessageID = msg.ID
}
if msg.ID > st.LastMessageID {
st.LastMessageID = msg.ID
}
}
res := make([]ctxmgr.ChatCategoryStat, 0, len(statMap))
for _, st := range statMap {
res = append(res, *st)
}
sort.Slice(res, func(i, j int) bool {
return res[i].Count > res[j].Count
})
return res
}
func (s *Server) listVisibleSessionMessages(ctx context.Context, authCtx *chatHistoryAuth) ([]ctxmgr.ChatMessageRecord, error) {
if s.ctxMgr == nil || authCtx == nil {
return nil, nil
}
msgs, err := s.ctxMgr.ListSessionMessagesForAuth(ctx, authCtx.SessionID)
if err != nil {
return nil, err
}
return s.filterChatMessagesByAuth(authCtx, msgs), nil
}
func (s *Server) resolveTaskMessageScope(ctx context.Context, projectID int64, taskKey string) (*projectinstance.MessageScope, error) {
if s.instSvc == nil || !s.instSvc.Available() {
if s.cfg != nil && s.cfg.AllowInMemory {
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopePreActivation}, nil
}
return nil, projectinstance.ErrTaskScopeNotResolved
}
return s.instSvc.ResolveMessageScopeForTask(ctx, projectID, taskKey)
}
func (s *Server) resolveChatMessageScope(ctx context.Context, projectID int64, activeStageKey string) (*projectinstance.MessageScope, error) {
if s.instSvc == nil || !s.instSvc.Available() {
if projectID <= 0 {
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopeNonProjectPrivate}, nil
}
return &projectinstance.MessageScope{ScopeKind: projectinstance.ScopePreActivation}, nil
}
return s.instSvc.ResolveMessageScopeForChat(ctx, projectID, activeStageKey)
}
func (s *Server) applyScopeToMeta(ctx context.Context, meta map[string]any, projectID int64, activeStageKey, taskKey string) error {
if meta == nil {
return nil
}
var scope *projectinstance.MessageScope
var err error
if taskKey != "" {
scope, err = s.resolveTaskMessageScope(ctx, projectID, taskKey)
} else {
scope, err = s.resolveChatMessageScope(ctx, projectID, activeStageKey)
}
if err != nil {
return err
}
if scope != nil {
projectinstance.ApplyScopeToMetadata(meta, scope)
}
return nil
}