Files
Agent-System/internal/server/project_members_handlers.go
T
WeiCHEN 9071c272bf refactor(server): drop Server pool; route health/heal/vault via Runtime
Remove production *pgxpool.Pool from Server, centralize domain→HTTP
status mapping, and expose Runtime ports for task-store readiness,
duty heal, and artifact vault registry. Allow app→docduty for heal wiring.
2026-09-22 21:44:56 +08:00

217 lines
7.2 KiB
Go

package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
)
var errProjectRoleRequired = errors.New("project_role required when member does not exist")
func resolveUpdateProjectRole(requested string, existing *authz.ProjectMemberRecord) (string, error) {
if requested != "" {
return requested, nil
}
if existing != nil && existing.ProjectRole != "" {
return existing.ProjectRole, nil
}
return "", errProjectRoleRequired
}
func writeDutySyncError(w http.ResponseWriter) {
writeSafeError(w, nil, http.StatusInternalServerError, `{"error":"ERR_DUTY_SYNC","message":"成员已保存,但受控文档职责同步失败"}`)
}
func (s *Server) healProjectDuties(ctx context.Context, projectID int64) error {
if s == nil || s.svc == nil || s.rt == nil || projectID <= 0 {
return nil
}
return s.rt.HealProjectDuties(ctx, projectID)
}
type projectMemberRequest struct {
UserID int64 `json:"user_id"`
ProjectRole string `json:"project_role"`
DocumentDuty *string `json:"document_duty,omitempty"`
Status string `json:"status,omitempty"`
}
func (s *Server) handleProjectMembers(w http.ResponseWriter, r *http.Request) {
user, ok := s.requirePrincipal(w, r)
if !ok {
return
}
projectID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || projectID <= 0 {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"invalid project id"}`)
return
}
switch r.Method {
case http.MethodGet:
s.handleListProjectMembers(w, r, projectID)
case http.MethodPost:
s.handleAddProjectMember(w, r, user, projectID)
case http.MethodPut:
s.handleUpdateProjectMember(w, r, user, projectID)
case http.MethodDelete:
s.handleRemoveProjectMember(w, r, user, projectID)
default:
writeSafeError(w, r, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) handleListProjectMembers(w http.ResponseWriter, r *http.Request, projectID int64) {
if _, ok := s.requireProjectAccess(w, r, projectID); !ok {
return
}
members, err := s.authorizer.ListProjectMembers(r.Context(), projectID)
if err != nil {
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
return
}
if members == nil {
members = []authz.ProjectMemberRecord{}
}
writeJSONResponse(w, http.StatusOK, map[string]any{"members": members})
}
func (s *Server) requireProjectMemberManage(w http.ResponseWriter, r *http.Request, user *auth.UserAccount, projectID int64) bool {
can, err := s.authzSvc.CanManageProjectMembers(r.Context(), projectID, user.ID, user.RoleID)
if err != nil {
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
return false
}
if !can {
writeSafeError(w, r, http.StatusForbidden, `{"error":"ERR_FORBIDDEN","message":"无权管理项目成员"}`)
return false
}
return true
}
func (s *Server) handleAddProjectMember(w http.ResponseWriter, r *http.Request, user *auth.UserAccount, projectID int64) {
if !s.requireProjectMemberManage(w, r, user, projectID) {
return
}
var req projectMemberRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id and project_role required"}`)
return
}
if req.ProjectRole == "" {
req.ProjectRole = authz.RoleMember
}
if err := s.authorizer.UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
ProjectRole: req.ProjectRole,
DocumentDuty: req.DocumentDuty,
}); err != nil {
writeSafeError(w, r, http.StatusBadRequest, err.Error())
return
}
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
writeDutySyncError(w)
return
}
s.ensureMemberChatSession(r.Context(), projectID, req.UserID)
writeJSONResponse(w, http.StatusCreated, map[string]bool{"success": true})
}
// ensureMemberChatSession 为项目成员补齐私有会话(sess-p{id}-u{uid}),失败仅记日志。
func (s *Server) ensureMemberChatSession(ctx context.Context, projectID, memberUserID int64) {
if s == nil || s.ctxMgr == nil || projectID <= 0 || memberUserID <= 0 {
return
}
title := ""
if s.pwSvc != nil {
if proj, err := s.pwSvc.GetProject(ctx, projectID, 0); err == nil && proj != nil {
title = proj.Name
}
}
sessionID := fmt.Sprintf("sess-p%d-u%d", projectID, memberUserID)
if _, err := s.ctxMgr.EnsureChatSessionOwned(ctx, sessionID, memberUserID, projectID, title); err != nil {
log.Printf("[members] ensure chat session for project=%d user=%d failed: %v", projectID, memberUserID, err)
}
}
func (s *Server) handleUpdateProjectMember(w http.ResponseWriter, r *http.Request, user *auth.UserAccount, projectID int64) {
if !s.requireProjectMemberManage(w, r, user, projectID) {
return
}
var req projectMemberRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id required"}`)
return
}
var existing *authz.ProjectMemberRecord
if s.authorizer != nil {
rec, err := s.authorizer.GetProjectMember(r.Context(), projectID, req.UserID)
if err != nil {
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
return
}
existing = rec
} else if req.ProjectRole == "" && req.DocumentDuty != nil {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"project_role required with document_duty"}`)
return
}
wrote := false
if req.ProjectRole != "" || req.DocumentDuty != nil {
role, err := resolveUpdateProjectRole(req.ProjectRole, existing)
if err != nil {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"project_role required when member does not exist"}`)
return
}
if err := s.authorizer.UpsertProjectMemberSpec(r.Context(), projectID, req.UserID, authz.MemberUpsert{
ProjectRole: role,
DocumentDuty: req.DocumentDuty,
}); err != nil {
writeSafeError(w, r, http.StatusBadRequest, err.Error())
return
}
wrote = true
}
if req.Status != "" {
if err := s.authorizer.SetProjectMemberStatus(r.Context(), projectID, req.UserID, req.Status); err != nil {
writeSafeError(w, r, http.StatusBadRequest, err.Error())
return
}
wrote = true
}
if wrote {
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
writeDutySyncError(w)
return
}
}
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleRemoveProjectMember(w http.ResponseWriter, r *http.Request, user *auth.UserAccount, projectID int64) {
if !s.requireProjectMemberManage(w, r, user, projectID) {
return
}
userIDStr := r.URL.Query().Get("user_id")
targetID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || targetID <= 0 {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"user_id query required"}`)
return
}
if err := s.authorizer.SetProjectMemberStatus(r.Context(), projectID, targetID, authz.StatusExited); err != nil {
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
return
}
if err := s.healProjectDuties(r.Context(), projectID); err != nil {
writeDutySyncError(w)
return
}
writeJSONResponse(w, http.StatusOK, map[string]bool{"success": true})
}