feat(conversations): complete manual AI takeover

This commit is contained in:
2026-08-12 21:16:49 +08:00
committed by Rogee
parent 4be52e05c7
commit 4bf6ba6b20
22 changed files with 266 additions and 54 deletions
@@ -93,6 +93,7 @@ func (h *AssignableAgentHandler) List(c *gin.Context) {
bot["assignee_type"] = "AgentBot"
bot["icon"] = "i-lucide-bot"
bot["availability_status"] = "offline"
bot["confirmed"] = true
payload = append(payload, bot)
}
}
@@ -1059,9 +1059,10 @@ func (h *ConversationHandler) AssignTeam(c *gin.Context) {
}
var req struct {
AgentID *uint `json:"agent_id"`
AssigneeID *uint `json:"assignee_id"`
TeamID *uint `json:"team_id"`
AgentID *uint `json:"agent_id"`
AssigneeID *uint `json:"assignee_id"`
AssigneeType string `json:"assignee_type"`
TeamID *uint `json:"team_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
@@ -1076,6 +1077,23 @@ func (h *ConversationHandler) AssignTeam(c *gin.Context) {
if !ok {
return
}
if req.AssigneeType == "AgentBot" {
if agentID == nil {
agentID = new(uint)
}
conversation, bot, svcErr := h.conversationSvc.AssignAgentBot(c.Request.Context(), accountID, conversation.ID, *agentID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
recordAuditMutation(c, h.auditSvc, auditMutation{AccountID: accountID, AuditableType: "Conversation", AuditableID: conversation.ID, Action: "update", AuditedChanges: gin.H{"assignee_agent_bot_id": conversation.AssigneeAgentBotID}})
if bot == nil {
c.JSON(http.StatusOK, nil)
return
}
c.JSON(http.StatusOK, serializeAgentBotSlim(bot))
return
}
conversation, svcErr := h.conversationSvc.AssignTeam(c.Request.Context(), accountID, conversation.ID, agentID, req.TeamID)
if svcErr != nil {
handleServiceError(c, svcErr)
@@ -732,7 +732,7 @@ func (s *ConversationCrudTestSuite) TestAssignAgentBot_MutuallyExclusiveAndAccou
s.Require().NoError(s.db.Create(bot).Error)
body, _ := json.Marshal(map[string]any{"assignee_id": bot.ID, "assignee_type": "AgentBot"})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, s.convURL(s.testConv.ID)+"/assign", bytes.NewReader(body))
req := httptest.NewRequest(http.MethodPost, s.convURL(s.testConv.ID)+"/assignments", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code, w.Body.String())
@@ -741,6 +741,21 @@ func (s *ConversationCrudTestSuite) TestAssignAgentBot_MutuallyExclusiveAndAccou
s.Nil(assigned.AssigneeID)
s.Require().NotNil(assigned.AssigneeAgentBotID)
s.Equal(bot.ID, *assigned.AssigneeAgentBotID)
s.Equal(string(model.ConversationStatusPending), assigned.Status)
var assignmentResponse map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &assignmentResponse))
s.Equal("AgentBot", assignmentResponse["assignee_type"])
// Repeating takeover is idempotent.
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, s.convURL(s.testConv.ID)+"/assignments", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code, w.Body.String())
s.Require().NoError(s.db.First(&assigned, s.testConv.ID).Error)
s.Require().NotNil(assigned.AssigneeAgentBotID)
s.Equal(bot.ID, *assigned.AssigneeAgentBotID)
s.Equal(string(model.ConversationStatusPending), assigned.Status)
user := &model.User{Name: "Agent", Email: "bot-switch@example.com"}
s.Require().NoError(s.db.Create(user).Error)
@@ -756,6 +771,7 @@ func (s *ConversationCrudTestSuite) TestAssignAgentBot_MutuallyExclusiveAndAccou
s.Require().NotNil(assigned.AssigneeID)
s.Equal(user.ID, *assigned.AssigneeID)
s.Nil(assigned.AssigneeAgentBotID)
s.Equal(string(model.ConversationStatusOpen), assigned.Status)
other := &model.Account{Name: "Other"}
s.Require().NoError(s.db.Create(other).Error)
@@ -767,12 +767,13 @@ func serializeCaptainAssistantSender(assistant *model.CaptainAssistant) map[stri
func serializeAgentBotSlim(bot *model.AgentBot) map[string]any {
return map[string]any{
"id": bot.ID,
"name": bot.Name,
"description": bot.Description,
"thumbnail": bot.AvatarURL,
"outgoing_url": bot.OutgoingURL,
"bot_type": bot.BotType,
"id": bot.ID,
"name": bot.Name,
"description": bot.Description,
"thumbnail": bot.AvatarURL,
"outgoing_url": bot.OutgoingURL,
"bot_type": bot.BotType,
"assignee_type": "AgentBot",
}
}