package server import ( "encoding/json" "net/http" "strconv" "chat-agent-system/internal/ctxmgr" ) // ----------------------------------------------------------------------------- // 多级上下文与偏好记忆 RESTful 控制器 // ----------------------------------------------------------------------------- // handleGetUserContext 获取单用户私有上下文与个性化偏好 // user_id 仅允许等于当前认证用户本人(或系统管理员),不再默认回落 user id=1。 func (s *Server) handleGetUserContext(w http.ResponseWriter, r *http.Request) { user, ok := s.requirePrincipal(w, r) if !ok { return } userIDStr := r.URL.Query().Get("user_id") projectIDStr := r.URL.Query().Get("project_id") sessionID := r.URL.Query().Get("session_id") userID, _ := strconv.ParseInt(userIDStr, 10, 64) if userID <= 0 { userID = user.ID } if userID != user.ID && !s.isSystemAdminUser(r.Context(), user) { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权查询其他用户的上下文", }) return } projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) if s.ctxMgr() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return } // FIND-SEC-01 回归:session_id 必须归属于目标用户本人,否则拒绝。 // 会话不存在与归属他人同样返回 403,避免泄露会话存在性。 if sessionID != "" { ownerID, err := s.ctxMgr().GetSessionOwnerID(r.Context(), sessionID) if err != nil || ownerID <= 0 || ownerID != userID { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该会话上下文", }) return } } uCtx, err := s.ctxMgr().GetUserContext(r.Context(), projectID, userID, sessionID) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } writeJSONResponse(w, http.StatusOK, uCtx) } // handleUpdateUserPreference 定期整理或前端保存用户习惯偏好 func (s *Server) handleUpdateUserPreference(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } var req UpdateUserPreferenceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeDomainError(w, r, http.StatusBadRequest, err) return } if req.UserID <= 0 { req.UserID = user.ID } if req.UserID != user.ID && !s.isSystemAdminUser(r.Context(), user) { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权修改其他用户的偏好", }) return } if s.ctxMgr() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return } pref := &ctxmgr.UserPreference{ UserID: req.UserID, CommunicationStyle: req.CommunicationStyle, FocusAreas: req.FocusAreas, OutputFormatPref: req.OutputFormatPref, CustomInstructions: req.CustomInstructions, } if err := s.ctxMgr().UpdateUserPreference(r.Context(), pref); err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } writeJSONResponse(w, http.StatusOK, map[string]any{"status": "ok", "user_id": req.UserID}) } // handleGetProjectContext 获取项目级多方协同事实黑板与里程碑决策流 func (s *Server) handleGetProjectContext(w http.ResponseWriter, r *http.Request) { user, ok := s.requireAuthenticatedUser(w, r) if !ok { return } projectIDStr := r.URL.Query().Get("project_id") projectID, _ := strconv.ParseInt(projectIDStr, 10, 64) if projectID <= 0 { writeSafeError(w, r, http.StatusBadRequest, "missing or invalid project_id query param") return } // Membership first: allowAnyStageAccess assumes the caller already passed // project membership (see ResolveStageAccess). Without this guard any // authenticated user can read an activated project's collaborative context. if !s.enforceChatProjectAccess(w, r, user, projectID) { return } if !s.allowAnyStageAccess(r.Context(), projectID, user.ID) { s.writeWorkflowAccessDenied(w) return } if s.ctxMgr() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return } pCtx, err := s.ctxMgr().GetProjectContext(r.Context(), projectID) if err != nil { writeDomainError(w, r, http.StatusInternalServerError, err) return } writeJSONResponse(w, http.StatusOK, pCtx) }