244 lines
7.1 KiB
Go
244 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/Tencent/WeKnora/internal/errors"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
)
|
|
|
|
// FAQHandler handles FAQ knowledge base operations.
|
|
type FAQHandler struct {
|
|
knowledgeService interfaces.KnowledgeService
|
|
}
|
|
|
|
// NewFAQHandler creates a new FAQ handler
|
|
func NewFAQHandler(knowledgeService interfaces.KnowledgeService) *FAQHandler {
|
|
return &FAQHandler{knowledgeService: knowledgeService}
|
|
}
|
|
|
|
// ListEntries lists FAQ entries under a knowledge base.
|
|
func (h *FAQHandler) ListEntries(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var page types.Pagination
|
|
if err := c.ShouldBindQuery(&page); err != nil {
|
|
logger.Error(ctx, "Failed to bind pagination query", err)
|
|
c.Error(errors.NewBadRequestError("分页参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
tagID := secutils.SanitizeForLog(c.Query("tag_id"))
|
|
keyword := secutils.SanitizeForLog(c.Query("keyword"))
|
|
|
|
result, err := h.knowledgeService.ListFAQEntries(ctx, secutils.SanitizeForLog(c.Param("id")), &page, tagID, keyword)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": result,
|
|
})
|
|
}
|
|
|
|
// UpsertEntries appends or replaces FAQ entries in batch asynchronously.
|
|
func (h *FAQHandler) UpsertEntries(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req types.FAQBatchUpsertPayload
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ upsert payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
taskID, err := h.knowledgeService.UpsertFAQEntries(ctx, secutils.SanitizeForLog(c.Param("id")), &req)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"task_id": taskID,
|
|
},
|
|
})
|
|
}
|
|
|
|
// CreateEntry creates a single FAQ entry synchronously.
|
|
func (h *FAQHandler) CreateEntry(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req types.FAQEntryPayload
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ entry payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
entry, err := h.knowledgeService.CreateFAQEntry(ctx, secutils.SanitizeForLog(c.Param("id")), &req)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": entry,
|
|
})
|
|
}
|
|
|
|
// UpdateEntry updates a single FAQ entry.
|
|
func (h *FAQHandler) UpdateEntry(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req types.FAQEntryPayload
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ entry payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := h.knowledgeService.UpdateFAQEntry(ctx,
|
|
secutils.SanitizeForLog(c.Param("id")), secutils.SanitizeForLog(c.Param("entry_id")), &req); err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
})
|
|
}
|
|
|
|
// UpdateEntryTagBatch updates tags for FAQ entries in batch.
|
|
func (h *FAQHandler) UpdateEntryTagBatch(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req faqEntryTagBatchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ entry tag batch payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
if err := h.knowledgeService.UpdateFAQEntryTagBatch(ctx,
|
|
secutils.SanitizeForLog(c.Param("id")), req.Updates); err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
})
|
|
}
|
|
|
|
// UpdateEntryFieldsBatch updates multiple fields for FAQ entries in batch.
|
|
// This is the unified API for batch updating FAQ entry fields.
|
|
// Supports updating is_enabled, is_recommended, tag_id in a single call.
|
|
func (h *FAQHandler) UpdateEntryFieldsBatch(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req types.FAQEntryFieldsBatchUpdate
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ entry fields batch payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
if err := h.knowledgeService.UpdateFAQEntryFieldsBatch(ctx,
|
|
secutils.SanitizeForLog(c.Param("id")), &req); err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
})
|
|
}
|
|
|
|
// faqDeleteRequest is a request for deleting FAQ entries in batch
|
|
type faqDeleteRequest struct {
|
|
IDs []string `json:"ids" binding:"required,min=1,dive,required"`
|
|
}
|
|
|
|
// faqEntryTagBatchRequest is a request for updating tags for FAQ entries in batch
|
|
type faqEntryTagBatchRequest struct {
|
|
Updates map[string]*string `json:"updates" binding:"required,min=1"`
|
|
}
|
|
|
|
// DeleteEntries deletes FAQ entries in batch.
|
|
func (h *FAQHandler) DeleteEntries(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req faqDeleteRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Errorf(ctx, "Failed to bind FAQ delete payload: %s", secutils.SanitizeForLog(err.Error()))
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := h.knowledgeService.DeleteFAQEntries(ctx,
|
|
secutils.SanitizeForLog(c.Param("id")),
|
|
secutils.SanitizeForLogArray(req.IDs)); err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
})
|
|
}
|
|
|
|
// SearchFAQ searches FAQ entries using hybrid search.
|
|
func (h *FAQHandler) SearchFAQ(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var req types.FAQSearchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Error(ctx, "Failed to bind FAQ search payload", err)
|
|
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
req.QueryText = secutils.SanitizeForLog(req.QueryText)
|
|
if req.MatchCount <= 0 {
|
|
req.MatchCount = 10
|
|
}
|
|
if req.MatchCount > 200 {
|
|
req.MatchCount = 200
|
|
}
|
|
entries, err := h.knowledgeService.SearchFAQEntries(ctx, secutils.SanitizeForLog(c.Param("id")), &req)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": entries,
|
|
})
|
|
}
|
|
|
|
// ExportEntries exports all FAQ entries as a CSV file.
|
|
func (h *FAQHandler) ExportEntries(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
kbID := secutils.SanitizeForLog(c.Param("id"))
|
|
|
|
csvData, err := h.knowledgeService.ExportFAQEntries(ctx, kbID)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
// Set response headers for CSV download
|
|
c.Header("Content-Type", "text/csv; charset=utf-8")
|
|
c.Header("Content-Disposition", "attachment; filename=faq_export.csv")
|
|
// Add BOM for Excel compatibility with UTF-8
|
|
bom := []byte{0xEF, 0xBB, 0xBF}
|
|
c.Data(http.StatusOK, "text/csv; charset=utf-8", append(bom, csvData...))
|
|
}
|