Files
gochat/backend/internal/handler/api/v1/captain_scenario_handler.go
T
rogee aeddedf2a3 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.
2026-07-07 14:44:12 +08:00

192 lines
6.7 KiB
Go

package v1
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gochat/gochat/pkg/response"
)
// CaptainScenarioHandler handles CaptainScenario REST API endpoints.
// Reference: Chatwoot enterprise/app/controllers/api/v1/captain/scenarios_controller.rb
type CaptainScenarioHandler struct {
svc *service.CaptainScenarioService
}
// NewCaptainScenarioHandler creates a new CaptainScenarioHandler.
func NewCaptainScenarioHandler(svc *service.CaptainScenarioService) *CaptainScenarioHandler {
return &CaptainScenarioHandler{svc: svc}
}
// Create creates a new scenario under an assistant.
// POST /api/v1/accounts/:account_id/captain_assistants/:assistant_id/scenarios
func (h *CaptainScenarioHandler) Create(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := parseUintParam(c, "assistant_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
var req service.CreateScenarioRequest
if err := bindNestedJSONPayload(c, "scenario", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
scenario, err := h.svc.Create(c.Request.Context(), accountID, assistantID, &req)
if err != nil {
applogger.L().Errorf("Create captain scenario: %v", err)
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to create scenario")
return
}
c.JSON(http.StatusOK, captainScenarioPayload(scenario))
}
// Get retrieves a scenario by ID.
// GET /api/v1/accounts/:account_id/captain_assistants/:assistant_id/scenarios/:id
func (h *CaptainScenarioHandler) Get(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := parseUintParam(c, "assistant_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
id, err := parseUintAnyParam(c, "scenario_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
scenario, err := h.svc.Get(c.Request.Context(), accountID, assistantID, id)
if err != nil {
applogger.L().Errorf("Get captain scenario: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "scenario not found")
return
}
c.JSON(http.StatusOK, captainScenarioPayload(scenario))
}
// Update updates an existing scenario.
// PUT /api/v1/accounts/:account_id/captain_assistants/:assistant_id/scenarios/:id
func (h *CaptainScenarioHandler) Update(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := parseUintParam(c, "assistant_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
id, err := parseUintAnyParam(c, "scenario_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
var req service.UpdateScenarioRequest
if err := bindNestedJSONPayload(c, "scenario", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
scenario, err := h.svc.UpdateScoped(c.Request.Context(), accountID, assistantID, id, &req)
if err != nil {
applogger.L().Errorf("Update captain scenario: %v", err)
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to update scenario")
return
}
c.JSON(http.StatusOK, captainScenarioPayload(scenario))
}
// Delete deletes a scenario.
// DELETE /api/v1/accounts/:account_id/captain_assistants/:assistant_id/scenarios/:id
func (h *CaptainScenarioHandler) Delete(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := parseUintParam(c, "assistant_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
id, err := parseUintAnyParam(c, "scenario_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
if err := h.svc.DeleteScoped(c.Request.Context(), accountID, assistantID, id); err != nil {
applogger.L().Errorf("Delete captain scenario: %v", err)
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to delete scenario")
return
}
response.NoContent(c)
}
// List retrieves scenarios for an assistant.
// GET /api/v1/accounts/:account_id/captain_assistants/:assistant_id/scenarios
func (h *CaptainScenarioHandler) List(c *gin.Context) {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := parseUintParam(c, "assistant_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
scenarios, count, err := h.svc.ListByAccountAssistant(c.Request.Context(), accountID, assistantID)
if err != nil {
applogger.L().Errorf("List captain scenarios: %v", err)
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to list scenarios")
return
}
payload := make([]gin.H, 0, len(scenarios))
for i := range scenarios {
payload = append(payload, captainScenarioPayload(&scenarios[i]))
}
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"total_count": count, "page": 1}})
}
func captainScenarioPayload(scenario *model.CaptainScenario) gin.H {
payload := gin.H{
"id": scenario.ID,
"title": scenario.Title,
"description": scenario.Description,
"instruction": scenario.Instruction,
"tools": rawJSONValue(scenario.Tools),
"enabled": scenario.Enabled,
"assistant_id": scenario.AssistantID,
"account_id": scenario.AccountID,
"created_at": scenario.CreatedAt,
"updated_at": scenario.UpdatedAt,
}
if scenario.Assistant.ID != 0 {
payload["assistant"] = gin.H{"id": scenario.Assistant.ID, "name": scenario.Assistant.Name}
}
return payload
}