feat(conversations): expose inbox assistant

This commit is contained in:
2026-06-06 15:07:08 +08:00
parent 7554a75427
commit 18a2cc5e83
8 changed files with 127 additions and 13 deletions
@@ -783,6 +783,34 @@ func (h *ConversationHandler) ListAttachments(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"meta": gin.H{"total_count": total}, "payload": payload})
}
// InboxAssistant returns the Captain assistant connected to the conversation inbox.
// GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant
// Reference: Chatwoot enterprise conversations_controller#inbox_assistant.
func (h *ConversationHandler) InboxAssistant(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
conversationID, err := parseUintParam(c, "conversation_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id")
return
}
assistant, svcErr := h.conversationSvc.GetInboxAssistant(c.Request.Context(), accountID, conversationID)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
if assistant == nil {
c.JSON(http.StatusOK, gin.H{"assistant": nil})
return
}
c.JSON(http.StatusOK, gin.H{"assistant": gin.H{"id": assistant.ID, "name": assistant.Name}})
}
// ToggleTyping toggles the typing status for an agent in a conversation.
// POST /api/v1/accounts/:account_id/conversations/:id/toggle_typing_status
// Reference: Chatwoot conversations_controller#toggle_typing_status (member route)
@@ -85,6 +85,8 @@ func (s *ConversationCrudTestSuite) SetupSuite() {
&model.SlaPolicy{},
&model.AppliedSLA{},
&model.SlaEvent{},
&model.CaptainAssistant{},
&model.CaptainInbox{},
)
s.Require().NoError(err)
@@ -127,6 +129,7 @@ func (s *ConversationCrudTestSuite) SetupSuite() {
conversations.POST("/filter", handler.Filter)
conversations.POST("/:conversation_id/priority", handler.UpdatePriority)
conversations.POST("/:conversation_id/assignments", handler.AssignTeam)
conversations.GET("/:conversation_id/inbox_assistant", handler.InboxAssistant)
}
}
}
@@ -182,6 +185,8 @@ func (s *ConversationCrudTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM teams")
s.db.Exec("DELETE FROM team_members")
s.db.Exec("DELETE FROM sla_policies")
s.db.Exec("DELETE FROM captain_inboxes")
s.db.Exec("DELETE FROM captain_assistants")
}
func (s *ConversationCrudTestSuite) accountURL() string {
@@ -340,6 +345,56 @@ func (s *ConversationCrudTestSuite) TestGet_NotFound() {
assert.Equal(s.T(), http.StatusNotFound, w.Code)
}
func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsBoundAssistant() {
assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Inbox Helper", Description: "Helps this inbox", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive}
s.Require().NoError(s.db.Create(assistant).Error)
s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assistantResp, ok := resp["assistant"].(map[string]interface{})
s.Require().True(ok)
assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"])
assert.Equal(s.T(), "Inbox Helper", assistantResp["name"])
assert.NotContains(s.T(), assistantResp, "description")
}
func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsNilWhenUnbound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Contains(s.T(), resp, "assistant")
assert.Nil(s.T(), resp["assistant"])
}
func (s *ConversationCrudTestSuite) TestInboxAssistant_UsesDisplayIDRoute() {
displayID := uint(77)
s.testConv.DisplayID = &displayID
s.Require().NoError(s.db.Save(s.testConv).Error)
assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Display Helper", Description: "Display route", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive}
s.Require().NoError(s.db.Create(assistant).Error)
s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/77/inbox_assistant", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assistantResp := resp["assistant"].(map[string]interface{})
assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"])
}
// ========== Update Handler Tests ==========
func (s *ConversationCrudTestSuite) TestUpdate_Success() {