From 426af51365b7d0e58749eb0649ea166a5f52d23a Mon Sep 17 00:00:00 2001 From: weichen Date: Fri, 18 Sep 2026 03:39:08 +0000 Subject: [PATCH] =?UTF-8?q?fix(ctxmgr):=20EnsureChatSession=20=E5=BD=92?= =?UTF-8?q?=E5=B1=9E=20COALESCE=20=E6=94=B9=E4=B8=BA=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E5=80=BC=E4=BC=98=E5=85=88=EF=BC=88R-1c=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 防止跨用户通过 upsert/消息落库覆盖已绑定的 chat_sessions.user_id;新增 EnsureChatSessionOwned。 Co-authored-by: Cursor --- internal/ctxmgr/chat_history.go | 20 ++++++++++++++++++-- internal/ctxmgr/manager.go | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/ctxmgr/chat_history.go b/internal/ctxmgr/chat_history.go index 253ed6ef..54a189e1 100644 --- a/internal/ctxmgr/chat_history.go +++ b/internal/ctxmgr/chat_history.go @@ -32,7 +32,7 @@ func (m *ContextManager) EnsureChatSession(ctx context.Context, sessionID string VALUES ($1, $2, $3, $4, now(), now()) ON CONFLICT (session_id) DO UPDATE SET title = COALESCE(NULLIF($4, ''), chat_sessions.title), - user_id = COALESCE($2, chat_sessions.user_id), + user_id = COALESCE(chat_sessions.user_id, $2), project_id = COALESCE($3, chat_sessions.project_id), updated_at = now() `, sessionID, uid, pid, title) @@ -79,7 +79,7 @@ func (m *ContextManager) EnsureChatSession(ctx context.Context, sessionID string if title != "" { sess.Title = title } - if userID > 0 { + if userID > 0 && sess.UserID <= 0 { sess.UserID = userID } if projectID > 0 { @@ -97,6 +97,22 @@ func (m *ContextManager) EnsureChatSession(ctx context.Context, sessionID string return ©, nil } +// ErrSessionOwnerMismatch 表示调用者试图操作已绑定给其他用户的会话。 +var ErrSessionOwnerMismatch = errors.New("ctxmgr: session owner mismatch") + +// EnsureChatSessionOwned 在写入前校验归属:已绑定且不匹配调用者时返回 +// ErrSessionOwnerMismatch;未绑定或本就是调用者本人时正常落地。 +func (m *ContextManager) EnsureChatSessionOwned( + ctx context.Context, sessionID string, callerUserID, projectID int64, title string, +) (*ChatSessionSummary, error) { + if callerUserID > 0 { + if owner, err := m.GetSessionOwnerID(ctx, sessionID); err == nil && owner > 0 && owner != callerUserID { + return nil, ErrSessionOwnerMismatch + } + } + return m.EnsureChatSession(ctx, sessionID, callerUserID, projectID, title) +} + // ListChatSessions 分页查询历史会话列表 func (m *ContextManager) ListChatSessions(ctx context.Context, filter SessionFilter) ([]ChatSessionSummary, error) { if filter.Limit <= 0 { diff --git a/internal/ctxmgr/manager.go b/internal/ctxmgr/manager.go index 430e60e5..80a80ba4 100644 --- a/internal/ctxmgr/manager.go +++ b/internal/ctxmgr/manager.go @@ -190,13 +190,13 @@ func (m *ContextManager) RecordUserMessageWithID(ctx context.Context, sessionID } } - // 确保会话存在 + // 确保会话存在(user_id 已有值优先,防止跨用户覆盖归属) _, _ = m.pool.Exec(ctx, ` INSERT INTO chat_sessions (session_id, user_id, project_id, title, created_at, updated_at) VALUES ($1, $2, $3, '对话协同会话', now(), now()) ON CONFLICT (session_id) DO UPDATE SET updated_at = now(), - user_id = COALESCE($2, chat_sessions.user_id), + user_id = COALESCE(chat_sessions.user_id, $2), project_id = COALESCE($3, chat_sessions.project_id) `, sessionID, uid, pid)