Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/gochat/gochat/pkg/response"
|
||||
)
|
||||
|
||||
// AssignableAgentHandler handles endpoints for finding agents assignable to conversations.
|
||||
// Reference: Chatwoot app/controllers/api/v1/accounts/assignable_agents_controller.rb
|
||||
// Two routes:
|
||||
// 1. GET /api/v1/accounts/:account_id/assignable_agents (with inbox_ids[] query param)
|
||||
// 2. GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents (single inbox)
|
||||
type AssignableAgentHandler struct {
|
||||
svc *service.AssignableAgentService
|
||||
}
|
||||
|
||||
// NewAssignableAgentHandler creates a new AssignableAgentHandler.
|
||||
func NewAssignableAgentHandler(svc *service.AssignableAgentService) *AssignableAgentHandler {
|
||||
return &AssignableAgentHandler{svc: svc}
|
||||
}
|
||||
|
||||
// List returns agents that can be assigned to conversations in the specified inbox(es).
|
||||
// Supports two modes:
|
||||
// 1. Standalone resource: GET /api/v1/accounts/:account_id/assignable_agents?inbox_ids[]=1&inbox_ids[]=2
|
||||
// (Ref: Chatwoot resources :assignable_agents, only: [:index] — takes inbox_ids[] query param)
|
||||
// 2. Inbox member action: GET /api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents
|
||||
// (Ref: Chatwoot get :assignable_agents, on: :member on inboxes)
|
||||
//
|
||||
// 返回带workload信息的agent列表,按workload升序排序(最少conversations优先)。
|
||||
func (h *AssignableAgentHandler) List(c *gin.Context) {
|
||||
if h.svc == nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "assignable agent service unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
accountID := getAccountID(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine inbox IDs from either path param or query param
|
||||
inboxIDs := []uint{}
|
||||
|
||||
// Mode 2: inbox_id from URL path (GET /accounts/:account_id/inboxes/:inbox_id/assignable_agents)
|
||||
if inboxIDStr := c.Param("inbox_id"); inboxIDStr != "" {
|
||||
inboxID, parseErr := strconv.ParseUint(inboxIDStr, 10, 32)
|
||||
if parseErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid inbox_id")
|
||||
return
|
||||
}
|
||||
inboxIDs = append(inboxIDs, uint(inboxID))
|
||||
}
|
||||
|
||||
// Mode 1 & 2: additional inbox IDs from query param inbox_ids[]
|
||||
if queryInboxIDs, exists := c.GetQueryArray("inbox_ids[]"); exists {
|
||||
for _, qid := range queryInboxIDs {
|
||||
id, parseErr := strconv.ParseUint(qid, 10, 32)
|
||||
if parseErr == nil {
|
||||
// Avoid duplicate if path inbox_id matches query inbox_id
|
||||
isDup := false
|
||||
if len(inboxIDs) > 0 && uint(id) == inboxIDs[0] {
|
||||
isDup = true
|
||||
}
|
||||
if !isDup {
|
||||
inboxIDs = append(inboxIDs, uint(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
agents, svcErr := h.svc.GetAssignableAgents(c.Request.Context(), accountID, inboxIDs)
|
||||
if svcErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to list assignable agents")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user