feat(captain): align document response actions

This commit is contained in:
2026-06-05 12:24:16 +08:00
parent fc2cb12e63
commit afccb5f9ae
16 changed files with 877 additions and 160 deletions
@@ -5,9 +5,9 @@ import (
"strconv"
"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/pagination"
"github.com/gochat/gochat/pkg/response"
)
@@ -25,50 +25,58 @@ func NewCaptainDocumentHandler(svc *service.CaptainDocumentService) *CaptainDocu
// Create creates a new captain document.
// POST /api/v1/accounts/:account_id/captain_assistants/:assistant_id/documents
func (h *CaptainDocumentHandler) Create(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
assistantID, err := strconv.ParseUint(c.Param("assistant_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
return
}
var req service.CreateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err := bindNestedJSONPayload(c, "document", &req); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
doc, err := h.svc.Create(c.Request.Context(), uint(assistantID), uint(accountID), &req)
if err != nil {
applogger.L().Errorf("Create captain document: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create document")
assistantID := req.AssistantID
if assistantID == 0 {
assistantID, _ = parseUintParam(c, "assistant_id")
}
if assistantID == 0 {
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "Missing Assistant")
return
}
response.Created(c, doc)
doc, err := h.svc.Create(c.Request.Context(), assistantID, accountID, &req)
if err != nil {
applogger.L().Errorf("Create captain document: %v", err)
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "failed to create document")
return
}
c.JSON(http.StatusOK, captainDocumentPayload(doc))
}
// Get retrieves a captain document by ID.
// GET /api/v1/accounts/:account_id/captain_documents/:id
func (h *CaptainDocumentHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
id, err := parseUintAnyParam(c, "document_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
doc, err := h.svc.Get(c.Request.Context(), uint(id))
doc, err := h.svc.GetByAccount(c.Request.Context(), accountID, id)
if err != nil {
applogger.L().Errorf("Get captain document: %v", err)
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "document not found")
return
}
response.OK(c, doc)
c.JSON(http.StatusOK, captainDocumentPayload(doc))
}
// Update updates an existing captain document.
@@ -99,15 +107,20 @@ func (h *CaptainDocumentHandler) Update(c *gin.Context) {
// Delete deletes a captain document.
// DELETE /api/v1/accounts/:account_id/captain_documents/:id
func (h *CaptainDocumentHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
id, err := parseUintAnyParam(c, "document_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
if err := h.svc.Delete(c.Request.Context(), uint(id)); err != nil {
if err := h.svc.DeleteByAccount(c.Request.Context(), accountID, id); err != nil {
applogger.L().Errorf("Delete captain document: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete document")
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to delete document")
return
}
@@ -117,21 +130,36 @@ func (h *CaptainDocumentHandler) Delete(c *gin.Context) {
// List retrieves documents for an assistant.
// GET /api/v1/accounts/:account_id/captain_assistants/:assistant_id/documents
func (h *CaptainDocumentHandler) List(c *gin.Context) {
assistantID, err := strconv.ParseUint(c.Param("assistant_id"), 10, 64)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid assistant_id")
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
p := pagination.Parse(c)
docs, count, err := h.svc.List(c.Request.Context(), uint(assistantID), p.Offset, p.PerPage)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
assistantID, _ := parseOptionalUintQueryParam(c, "assistant_id")
if assistantID == 0 {
assistantID, _ = parseUintParam(c, "assistant_id")
}
docs, count, currentPage, err := h.svc.ListByAccount(c.Request.Context(), accountID, service.ListDocumentsRequest{
AssistantID: assistantID,
Page: page,
PerPage: 25,
Filter: c.Query("filter"),
Source: c.Query("source"),
Sort: c.Query("sort"),
SearchKey: c.Query("search_key"),
})
if err != nil {
applogger.L().Errorf("List captain documents: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list documents")
return
}
response.OKWithMeta(c, docs, p.Page, p.PerPage, count)
payload := make([]gin.H, 0, len(docs))
for i := range docs {
payload = append(payload, captainDocumentPayload(&docs[i]))
}
c.JSON(http.StatusOK, gin.H{"payload": payload, "meta": gin.H{"total_count": count, "page": currentPage}})
}
// ProcessDocument triggers document content extraction and embedding generation.
@@ -155,17 +183,68 @@ func (h *CaptainDocumentHandler) ProcessDocument(c *gin.Context) {
// SyncDocument triggers re-fetching content from the external URL.
// POST /api/v1/accounts/:account_id/captain_documents/:id/sync
func (h *CaptainDocumentHandler) SyncDocument(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
accountID := parseAccountIDParam(c)
if accountID == 0 {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
return
}
id, err := parseUintAnyParam(c, "document_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
return
}
if err := h.svc.SyncDocument(c.Request.Context(), uint(id)); err != nil {
if _, err := h.svc.MarkSyncing(c.Request.Context(), accountID, id); err != nil {
applogger.L().Errorf("SyncDocument: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to sync document")
response.AbortWithStatusError(c, captainAssistantErrorStatus(err), response.ErrInternal, "failed to sync document")
return
}
response.OK(c, gin.H{"synced": true})
}
c.Status(http.StatusAccepted)
}
func captainDocumentPayload(doc *model.CaptainDocument) gin.H {
status := doc.Status
if status == "" {
status = model.DocumentStatusPending
}
syncStatus := doc.SyncStatus
if syncStatus == model.DocumentSyncStatusPending {
syncStatus = "syncing"
}
payload := gin.H{
"account_id": doc.AccountID,
"assistant": captainDocumentAssistantPayload(doc),
"content": doc.Content,
"content_type": "text/html",
"created_at": doc.CreatedAt.Unix(),
"external_link": doc.ExternalLink,
"display_url": doc.ExternalLink,
"file_size": 0,
"pdf_document": false,
"id": doc.ID,
"name": doc.Name,
"status": status,
"sync_status": syncStatus,
"sync_in_progress": syncStatus == "syncing" || syncStatus == model.DocumentSyncStatusPending,
"last_synced_at": int64PointerValue(doc.LastSyncedAt),
"last_sync_attempted_at": int64PointerValue(doc.LastSyncAttemptedAt),
"last_sync_error_code": doc.LastSyncErrorCode,
"updated_at": doc.UpdatedAt.Unix(),
}
return payload
}
func captainDocumentAssistantPayload(doc *model.CaptainDocument) gin.H {
if doc.Assistant.ID == 0 {
return gin.H{"id": doc.AssistantID}
}
return captainAssistantPayload(&doc.Assistant)
}
func int64PointerValue(value *int64) any {
if value == nil {
return nil
}
return *value
}