feat(settings): align assignable agents payloads
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -80,5 +80,39 @@ func (h *AssignableAgentHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, agents)
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeAssignableAgents(accountID, agents)})
|
||||
}
|
||||
|
||||
func serializeAssignableAgents(accountID uint, agents []service.AssignableAgentDTO) []map[string]any {
|
||||
payload := make([]map[string]any, 0, len(agents))
|
||||
for i := range agents {
|
||||
agent := agents[i]
|
||||
availableName := agent.DisplayName
|
||||
if availableName == "" {
|
||||
availableName = agent.Name
|
||||
}
|
||||
provider := agent.Provider
|
||||
if provider == "" {
|
||||
provider = "email"
|
||||
}
|
||||
customRoleID := any(nil)
|
||||
if agent.CustomRoleID != 0 {
|
||||
customRoleID = agent.CustomRoleID
|
||||
}
|
||||
payload = append(payload, map[string]any{
|
||||
"id": agent.ID,
|
||||
"account_id": accountID,
|
||||
"availability_status": agent.AvailabilityStatus,
|
||||
"auto_offline": agent.AutoOffline,
|
||||
"confirmed": agent.Confirmed,
|
||||
"email": agent.Email,
|
||||
"provider": provider,
|
||||
"available_name": availableName,
|
||||
"name": agent.Name,
|
||||
"role": agent.Role,
|
||||
"thumbnail": agent.AvatarURL,
|
||||
"custom_role_id": customRoleID,
|
||||
})
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ type AssignableAgentHandlerTestSuite struct {
|
||||
router *gin.Engine
|
||||
handler *AssignableAgentHandler
|
||||
|
||||
account *model.Account
|
||||
inbox1 *model.Inbox
|
||||
inbox2 *model.Inbox
|
||||
inbox3 *model.Inbox
|
||||
user1 *model.User // agent in inbox1
|
||||
user2 *model.User // agent in inbox1 + inbox2
|
||||
user3 *model.User // administrator of account
|
||||
user4 *model.User // agent only in inbox2
|
||||
account *model.Account
|
||||
inbox1 *model.Inbox
|
||||
inbox2 *model.Inbox
|
||||
inbox3 *model.Inbox
|
||||
user1 *model.User // agent in inbox1
|
||||
user2 *model.User // agent in inbox1 + inbox2
|
||||
user3 *model.User // administrator of account
|
||||
user4 *model.User // agent only in inbox2
|
||||
}
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) SetupSuite() {
|
||||
@@ -168,6 +168,15 @@ func (s *AssignableAgentHandlerTestSuite) TearDownTest() {
|
||||
s.reseedData()
|
||||
}
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) decodeAssignablePayload(w *httptest.ResponseRecorder) []interface{} {
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Require().NotContains(resp, "success")
|
||||
payload, ok := resp["payload"].([]interface{})
|
||||
s.Require().True(ok, "expected Chatwoot payload array, got %v", resp)
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) reseedData() {
|
||||
// Re-create the AccountUser + InboxMember associations
|
||||
s.Require().NoError(s.db.Create(&model.AccountUser{
|
||||
@@ -211,13 +220,16 @@ func (s *AssignableAgentHandlerTestSuite) TestList_SingleInbox_ReturnsInboxMembe
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Should include user1, user2 (inbox members) + user3 (administrator) = 3
|
||||
s.Len(data, 3)
|
||||
agent := data[0].(map[string]interface{})
|
||||
s.Contains(agent, "availability_status")
|
||||
s.Contains(agent, "available_name")
|
||||
s.Contains(agent, "auto_offline")
|
||||
s.Contains(agent, "confirmed")
|
||||
s.Contains(agent, "thumbnail")
|
||||
s.Contains(agent, "custom_role_id")
|
||||
}
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) TestList_InboxWithOnlyOneMember() {
|
||||
@@ -229,11 +241,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_InboxWithOnlyOneMember() {
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// user2, user4 (inbox2 members) + user3 (administrator) = 3
|
||||
s.Len(data, 3)
|
||||
}
|
||||
@@ -247,11 +255,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_InboxWithNoMembers_ReturnsOnl
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Only administrator user3
|
||||
s.Len(data, 1)
|
||||
}
|
||||
@@ -268,11 +272,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_MultipleInboxIDsQueryParam()
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Intersection: user2. Admin: user3. Total: 2
|
||||
s.Len(data, 2)
|
||||
}
|
||||
@@ -289,11 +289,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_MultipleInboxIDsQueryParams_N
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Empty intersection + admin user3
|
||||
s.Len(data, 1)
|
||||
}
|
||||
@@ -309,11 +305,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_QueryParamSameAsPrimaryInboxI
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Same as single inbox1: user1, user2, user3
|
||||
s.Len(data, 3)
|
||||
}
|
||||
@@ -332,11 +324,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_MultipleAdditionalInboxIDs()
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
s.Len(data, 1)
|
||||
}
|
||||
|
||||
@@ -398,11 +386,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_InvalidQueryParamInboxIDs_Ign
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Same as just inbox1: user1, user2, user3
|
||||
s.Len(data, 3)
|
||||
}
|
||||
@@ -417,11 +401,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_NonExistentInboxID() {
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// No inbox members, only administrators: user3
|
||||
s.Len(data, 1)
|
||||
}
|
||||
@@ -437,11 +417,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_NonExistentAccountID() {
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Inbox members of inbox1: user1, user2. No admins for account 99999.
|
||||
s.Len(data, 2)
|
||||
}
|
||||
@@ -543,10 +519,8 @@ func (s *AssignableAgentHandlerTestSuite) TestList_NilService_PanicRecovered() {
|
||||
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) TestList_ResponseStructure() {
|
||||
// Verify the response structure matches: {success: true, data: [...]}
|
||||
// Verify the response structure matches Chatwoot: {payload: [...]}
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET",
|
||||
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
||||
@@ -557,10 +531,9 @@ func (s *AssignableAgentHandlerTestSuite) TestList_ResponseStructure() {
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
|
||||
// Must have "success" key set to true
|
||||
s.True(resp["success"].(bool))
|
||||
// Must have "data" key
|
||||
s.NotNil(resp["data"])
|
||||
s.NotContains(resp, "success")
|
||||
s.NotContains(resp, "data")
|
||||
s.NotNil(resp["payload"])
|
||||
// No "meta" key for non-paginated response
|
||||
s.Nil(resp["meta"])
|
||||
// No "error" key for success response
|
||||
@@ -581,11 +554,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_AdministratorDeduplication()
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// user1, user2 (inbox members), user3 (admin AND inbox member) - deduplicated
|
||||
// Should be 3, not 4 (user3 counted once)
|
||||
s.Len(data, 3)
|
||||
@@ -609,11 +578,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_AccountWithNoAdministrators()
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
// Only inbox member user1 (no administrators added)
|
||||
s.Len(data, 1)
|
||||
}
|
||||
@@ -627,9 +592,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_LargeAccountID() {
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
s.decodeAssignablePayload(w)
|
||||
}
|
||||
|
||||
func (s *AssignableAgentHandlerTestSuite) TestList_NegativeAccountID_ParseError() {
|
||||
@@ -705,11 +668,7 @@ func (s *AssignableAgentHandlerTestSuite) TestList_TwoInboxesWithPartialIntersec
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
s.Len(data, 2)
|
||||
}
|
||||
|
||||
@@ -728,15 +687,11 @@ func (s *AssignableAgentHandlerTestSuite) TestList_ThreeInboxesIntersectionWithT
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.True(resp["success"].(bool))
|
||||
|
||||
data := resp["data"].([]interface{})
|
||||
data := s.decodeAssignablePayload(w)
|
||||
s.Len(data, 2)
|
||||
}
|
||||
|
||||
// Run the test suite
|
||||
func TestAssignableAgentHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(AssignableAgentHandlerTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,35 +14,41 @@ import (
|
||||
// 逻辑: 对每个inbox在inbox_ids[]中的成员取交集,加上account管理员,去重。
|
||||
// 增强功能: 计算每个agent的workload (open conversations数量),按workload升序排序。
|
||||
type AssignableAgentService struct {
|
||||
inboxMemberRepo *repository.InboxMemberRepo
|
||||
userRepo *repository.UserRepo
|
||||
accountRepo *repository.AccountRepo
|
||||
conversationRepo *repository.ConversationRepo
|
||||
inboxMemberRepo *repository.InboxMemberRepo
|
||||
userRepo *repository.UserRepo
|
||||
accountRepo *repository.AccountRepo
|
||||
conversationRepo *repository.ConversationRepo
|
||||
}
|
||||
|
||||
// NewAssignableAgentService 创建新的AssignableAgentService。
|
||||
func NewAssignableAgentService(inboxMemberRepo *repository.InboxMemberRepo, userRepo *repository.UserRepo, accountRepo *repository.AccountRepo, conversationRepo *repository.ConversationRepo) *AssignableAgentService {
|
||||
return &AssignableAgentService{
|
||||
inboxMemberRepo: inboxMemberRepo,
|
||||
userRepo: userRepo,
|
||||
accountRepo: accountRepo,
|
||||
conversationRepo: conversationRepo,
|
||||
inboxMemberRepo: inboxMemberRepo,
|
||||
userRepo: userRepo,
|
||||
accountRepo: accountRepo,
|
||||
conversationRepo: conversationRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// AssignableAgentDTO 是可分配agent的API响应结构,包含workload信息。
|
||||
// Reference: Chatwoot assignable_agents API — 返回agent列表及其当前workload
|
||||
type AssignableAgentDTO struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Active bool `json:"active"`
|
||||
Available bool `json:"available"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
AvailabilityStatus string `json:"availability_status"` // online, offline, busy
|
||||
Workload int64 `json:"workload"` // 当前open conversations数量
|
||||
IsAdministrator bool `json:"is_administrator"` // 是否为account管理员
|
||||
ID uint `json:"id"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Active bool `json:"active"`
|
||||
Available bool `json:"available"`
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"available_name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
AutoOffline bool `json:"auto_offline"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
CustomRoleID uint `json:"custom_role_id,omitempty"`
|
||||
AvailabilityStatus string `json:"availability_status"` // online, offline, busy
|
||||
Workload int64 `json:"workload"` // 当前open conversations数量
|
||||
IsAdministrator bool `json:"is_administrator"` // 是否为account管理员
|
||||
}
|
||||
|
||||
// FindAssignableAgents 返回可以被分配到指定inbox对话中的agents。
|
||||
@@ -131,36 +137,48 @@ func (s *AssignableAgentService) GetAssignableAgents(ctx context.Context, accoun
|
||||
}
|
||||
}
|
||||
|
||||
// 获取account的管理员IDs用于标记is_administrator
|
||||
adminIDs, _ := s.findAdministratorIDs(ctx, accountID)
|
||||
adminSet := make(map[uint]bool, len(adminIDs))
|
||||
for _, id := range adminIDs {
|
||||
adminSet[id] = true
|
||||
}
|
||||
|
||||
// 获取每个agent的availability status (从account_user表中获取)
|
||||
availabilityMap := make(map[uint]string)
|
||||
// 获取每个agent的AccountUser字段,用于复用Chatwoot _agent serializer shape。
|
||||
accountUserMap := make(map[uint]model.AccountUser)
|
||||
accountUsers, _, auErr := s.accountRepo.FindAgentsByAccount(ctx, accountID, 0, 1000)
|
||||
if auErr == nil {
|
||||
for _, au := range accountUsers {
|
||||
availabilityMap[au.UserID] = au.Availability
|
||||
accountUserMap[au.UserID] = au
|
||||
}
|
||||
}
|
||||
|
||||
// 构建DTO列表
|
||||
dtos := make([]AssignableAgentDTO, len(users))
|
||||
for i, u := range users {
|
||||
accountUser := accountUserMap[u.ID]
|
||||
role := accountUser.Role
|
||||
if role == "" {
|
||||
role = u.Role
|
||||
}
|
||||
availabilityStatus := accountUser.Availability
|
||||
if availabilityStatus == "" {
|
||||
if u.Available {
|
||||
availabilityStatus = "online"
|
||||
} else {
|
||||
availabilityStatus = "offline"
|
||||
}
|
||||
}
|
||||
dtos[i] = AssignableAgentDTO{
|
||||
ID: u.ID,
|
||||
AccountID: accountID,
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
Role: u.Role,
|
||||
Role: role,
|
||||
Active: u.Active,
|
||||
Available: u.Available,
|
||||
Provider: u.Provider,
|
||||
DisplayName: u.DisplayName,
|
||||
AvatarURL: u.AvatarURL,
|
||||
AvailabilityStatus: availabilityMap[u.ID],
|
||||
AutoOffline: accountUser.AutoOffline,
|
||||
Confirmed: u.ConfirmedAt != nil,
|
||||
CustomRoleID: accountUser.CustomRoleID,
|
||||
AvailabilityStatus: availabilityStatus,
|
||||
Workload: workloadMap[u.ID],
|
||||
IsAdministrator: adminSet[u.ID],
|
||||
IsAdministrator: role == "administrator",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,4 +250,4 @@ func union(a, b []uint) []uint {
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user