From 8430b9fa2d1f9bfa6127e089340b247af7f8a083 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Wed, 24 Dec 2025 11:00:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EFAQ=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=89=E6=9B=B4=E6=96=B0=E6=97=B6=E9=97=B4=E5=8D=87=E5=BA=8F?= =?UTF-8?q?/=E9=99=8D=E5=BA=8F=E6=8E=92=E5=88=97=EF=BC=9B=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=A0=87=E7=AD=BE=E5=90=8D=E7=A7=B0=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=EF=BC=8C=E6=8F=90=E5=8D=87=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=95=88=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/faq.go | 7 ++- internal/agent/tools/get_document_info.go | 2 +- internal/agent/tools/knowledge_search.go | 2 +- internal/agent/tools/list_knowledge_chunks.go | 2 +- internal/application/repository/chunk.go | 9 +++- internal/application/repository/tag.go | 14 ++++++ internal/application/service/agent_service.go | 2 +- internal/application/service/chunk.go | 1 + internal/application/service/knowledge.go | 47 ++++++++++++++++++- internal/handler/faq.go | 4 +- internal/types/faq.go | 1 + internal/types/interfaces/chunk.go | 2 + internal/types/interfaces/knowledge.go | 2 + internal/types/interfaces/tag.go | 2 + 14 files changed, 88 insertions(+), 9 deletions(-) diff --git a/client/faq.go b/client/faq.go index 5f2f32c9c..2e27ba377 100644 --- a/client/faq.go +++ b/client/faq.go @@ -17,6 +17,7 @@ type FAQEntry struct { KnowledgeID string `json:"knowledge_id"` KnowledgeBaseID string `json:"knowledge_base_id"` TagID string `json:"tag_id"` + TagName string `json:"tag_name"` IsEnabled bool `json:"is_enabled"` IsRecommended bool `json:"is_recommended"` StandardQuestion string `json:"standard_question"` @@ -139,8 +140,9 @@ type faqSimpleResponse struct { } // ListFAQEntries returns paginated FAQ entries under a knowledge base. +// sortOrder: "asc" for time ascending (updated_at ASC), default is time descending (updated_at DESC) func (c *Client) ListFAQEntries(ctx context.Context, - knowledgeBaseID string, page, pageSize int, tagID string, keyword string, + knowledgeBaseID string, page, pageSize int, tagID string, keyword string, sortOrder string, ) (*FAQEntriesPage, error) { path := fmt.Sprintf("/api/v1/knowledge-bases/%s/faq/entries", knowledgeBaseID) query := url.Values{} @@ -156,6 +158,9 @@ func (c *Client) ListFAQEntries(ctx context.Context, if keyword != "" { query.Add("keyword", keyword) } + if sortOrder != "" { + query.Add("sort_order", sortOrder) + } resp, err := c.doRequest(ctx, http.MethodGet, path, nil, query) if err != nil { diff --git a/internal/agent/tools/get_document_info.go b/internal/agent/tools/get_document_info.go index ff5c0360c..69111c135 100644 --- a/internal/agent/tools/get_document_info.go +++ b/internal/agent/tools/get_document_info.go @@ -139,7 +139,7 @@ func (t *GetDocumentInfoTool) Execute(ctx context.Context, args map[string]inter ListPagedChunksByKnowledgeID(ctx, t.tenantID, id, &types.Pagination{ Page: 1, PageSize: 1000, - }, []types.ChunkType{"text"}, "", "") + }, []types.ChunkType{"text"}, "", "", "") if err != nil { mu.Lock() results[id] = &docInfo{ diff --git a/internal/agent/tools/knowledge_search.go b/internal/agent/tools/knowledge_search.go index 79482b900..3a68fd746 100644 --- a/internal/agent/tools/knowledge_search.go +++ b/internal/agent/tools/knowledge_search.go @@ -1052,7 +1052,7 @@ func (t *KnowledgeSearchTool) formatOutput( _, total, err := t.chunkService.GetRepository().ListPagedChunksByKnowledgeID(ctx, t.tenantID, result.KnowledgeID, &types.Pagination{Page: 1, PageSize: 1}, - []types.ChunkType{types.ChunkTypeText}, "", "", + []types.ChunkType{types.ChunkTypeText}, "", "", "", ) if err != nil { logger.Warnf( diff --git a/internal/agent/tools/list_knowledge_chunks.go b/internal/agent/tools/list_knowledge_chunks.go index 839d957bc..6ec00117d 100644 --- a/internal/agent/tools/list_knowledge_chunks.go +++ b/internal/agent/tools/list_knowledge_chunks.go @@ -117,7 +117,7 @@ func (t *ListKnowledgeChunksTool) Execute(ctx context.Context, args map[string]i } chunks, total, err := t.chunkService.GetRepository().ListPagedChunksByKnowledgeID(ctx, - t.tenantID, knowledgeID, pagination, []types.ChunkType{types.ChunkTypeText, types.ChunkTypeFAQ}, "", "") + t.tenantID, knowledgeID, pagination, []types.ChunkType{types.ChunkTypeText, types.ChunkTypeFAQ}, "", "", "") if err != nil { return &types.ToolResult{ Success: false, diff --git a/internal/application/repository/chunk.go b/internal/application/repository/chunk.go index 03fa97ccc..10c5c9c7f 100644 --- a/internal/application/repository/chunk.go +++ b/internal/application/repository/chunk.go @@ -78,6 +78,7 @@ func (r *chunkRepository) ListPagedChunksByKnowledgeID( chunkType []types.ChunkType, tagID string, keyword string, + sortOrder string, ) ([]*types.Chunk, int64, error) { var chunks []*types.Chunk var total int64 @@ -109,8 +110,14 @@ func (r *chunkRepository) ListPagedChunksByKnowledgeID( // Then query the paginated data dataQuery := baseFilter(r.db.WithContext(ctx)) + // Default is time descending, "asc" for time ascending + orderClause := "updated_at DESC" + if sortOrder == "asc" { + orderClause = "updated_at ASC" + } + if err := dataQuery. - Order("chunk_index ASC"). + Order(orderClause). Offset(page.Offset()). Limit(page.Limit()). Find(&chunks).Error; err != nil { diff --git a/internal/application/repository/tag.go b/internal/application/repository/tag.go index 8dbcac092..c47564a30 100644 --- a/internal/application/repository/tag.go +++ b/internal/application/repository/tag.go @@ -40,6 +40,20 @@ func (r *knowledgeTagRepository) GetByID(ctx context.Context, tenantID uint64, i return &tag, nil } +// GetByIDs retrieves multiple tags by their IDs in a single query +func (r *knowledgeTagRepository) GetByIDs(ctx context.Context, tenantID uint64, ids []string) ([]*types.KnowledgeTag, error) { + if len(ids) == 0 { + return []*types.KnowledgeTag{}, nil + } + var tags []*types.KnowledgeTag + if err := r.db.WithContext(ctx). + Where("tenant_id = ? AND id IN (?)", tenantID, ids). + Find(&tags).Error; err != nil { + return nil, err + } + return tags, nil +} + // GetByName gets a knowledge tag by name func (r *knowledgeTagRepository) GetByName(ctx context.Context, tenantID uint64, kbID string, name string) (*types.KnowledgeTag, error) { var tag types.KnowledgeTag diff --git a/internal/application/service/agent_service.go b/internal/application/service/agent_service.go index deb445796..b6cb19401 100644 --- a/internal/application/service/agent_service.go +++ b/internal/application/service/agent_service.go @@ -337,7 +337,7 @@ func (s *agentService) getKnowledgeBaseInfos(ctx context.Context, kbIDs []string pageResult, err := s.knowledgeService.ListFAQEntries(ctx, kbID, &types.Pagination{ Page: 1, PageSize: 10, - }, "", "") + }, "", "", "") if err == nil && pageResult != nil { docCount = int(pageResult.Total) if entries, ok := pageResult.Data.([]*types.FAQEntry); ok { diff --git a/internal/application/service/chunk.go b/internal/application/service/chunk.go index 938232e22..7c37afe5c 100644 --- a/internal/application/service/chunk.go +++ b/internal/application/service/chunk.go @@ -151,6 +151,7 @@ func (s *chunkService) ListPagedChunksByKnowledgeID(ctx context.Context, chunkType, "", "", + "", ) if err != nil { logger.ErrorWithFields(ctx, err, map[string]interface{}{ diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go index 6c1e2662f..8d87686e7 100644 --- a/internal/application/service/knowledge.go +++ b/internal/application/service/knowledge.go @@ -2548,6 +2548,7 @@ func (s *knowledgeService) CloneChunk(ctx context.Context, src, dst *types.Knowl chunkType, "", "", + "", ) chunkPage++ if err != nil { @@ -2642,7 +2643,7 @@ func (s *knowledgeService) CloneChunk(ctx context.Context, src, dst *types.Knowl // ListFAQEntries lists FAQ entries under a FAQ knowledge base. func (s *knowledgeService) ListFAQEntries(ctx context.Context, - kbID string, page *types.Pagination, tagID string, keyword string, + kbID string, page *types.Pagination, tagID string, keyword string, sortOrder string, ) (*types.PageResult, error) { if page == nil { page = &types.Pagination{} @@ -2662,11 +2663,33 @@ func (s *knowledgeService) ListFAQEntries(ctx context.Context, } chunkType := []types.ChunkType{types.ChunkTypeFAQ} chunks, total, err := s.chunkRepo.ListPagedChunksByKnowledgeID( - ctx, tenantID, faqKnowledge.ID, page, chunkType, tagID, keyword, + ctx, tenantID, faqKnowledge.ID, page, chunkType, tagID, keyword, sortOrder, ) if err != nil { return nil, err } + + // Build tag ID to name mapping for all unique tag IDs (batch query) + tagNameMap := make(map[string]string) + tagIDs := make([]string, 0) + tagIDSet := make(map[string]struct{}) + for _, chunk := range chunks { + if chunk.TagID != "" { + if _, exists := tagIDSet[chunk.TagID]; !exists { + tagIDSet[chunk.TagID] = struct{}{} + tagIDs = append(tagIDs, chunk.TagID) + } + } + } + if len(tagIDs) > 0 { + tags, err := s.tagRepo.GetByIDs(ctx, tenantID, tagIDs) + if err == nil { + for _, tag := range tags { + tagNameMap[tag.ID] = tag.Name + } + } + } + kb.EnsureDefaults() entries := make([]*types.FAQEntry, 0, len(chunks)) for _, chunk := range chunks { @@ -2674,6 +2697,10 @@ func (s *knowledgeService) ListFAQEntries(ctx context.Context, if err != nil { return nil, err } + // Set tag name from mapping + if entry.TagID != "" { + entry.TagName = tagNameMap[entry.TagID] + } entries = append(entries, entry) } return types.NewPageResult(total, page, entries), nil @@ -3315,6 +3342,14 @@ func (s *knowledgeService) CreateFAQEntry(ctx context.Context, return nil, err } + // 查询TagName + if entry.TagID != "" { + tag, tagErr := s.tagRepo.GetByID(ctx, tenantID, entry.TagID) + if tagErr == nil && tag != nil { + entry.TagName = tag.Name + } + } + return entry, nil } @@ -3356,6 +3391,14 @@ func (s *knowledgeService) GetFAQEntry(ctx context.Context, return nil, err } + // 查询TagName + if entry.TagID != "" { + tag, tagErr := s.tagRepo.GetByID(ctx, tenantID, entry.TagID) + if tagErr == nil && tag != nil { + entry.TagName = tag.Name + } + } + return entry, nil } diff --git a/internal/handler/faq.go b/internal/handler/faq.go index 8a7ef2566..ca3354f0b 100644 --- a/internal/handler/faq.go +++ b/internal/handler/faq.go @@ -33,6 +33,7 @@ func NewFAQHandler(knowledgeService interfaces.KnowledgeService) *FAQHandler { // @Param page_size query int false "每页数量" // @Param tag_id query string false "标签ID筛选" // @Param keyword query string false "关键词搜索" +// @Param sort_order query string false "排序方式: asc(按更新时间正序), 默认按更新时间倒序" // @Success 200 {object} map[string]interface{} "FAQ列表" // @Failure 400 {object} errors.AppError "请求参数错误" // @Security Bearer @@ -49,8 +50,9 @@ func (h *FAQHandler) ListEntries(c *gin.Context) { tagID := secutils.SanitizeForLog(c.Query("tag_id")) keyword := secutils.SanitizeForLog(c.Query("keyword")) + sortOrder := secutils.SanitizeForLog(c.Query("sort_order")) - result, err := h.knowledgeService.ListFAQEntries(ctx, secutils.SanitizeForLog(c.Param("id")), &page, tagID, keyword) + result, err := h.knowledgeService.ListFAQEntries(ctx, secutils.SanitizeForLog(c.Param("id")), &page, tagID, keyword, sortOrder) if err != nil { logger.ErrorWithFields(ctx, err, nil) c.Error(err) diff --git a/internal/types/faq.go b/internal/types/faq.go index 7729ba304..96f1c03bc 100644 --- a/internal/types/faq.go +++ b/internal/types/faq.go @@ -180,6 +180,7 @@ type FAQEntry struct { KnowledgeID string `json:"knowledge_id"` KnowledgeBaseID string `json:"knowledge_base_id"` TagID string `json:"tag_id"` + TagName string `json:"tag_name"` IsEnabled bool `json:"is_enabled"` IsRecommended bool `json:"is_recommended"` StandardQuestion string `json:"standard_question"` diff --git a/internal/types/interfaces/chunk.go b/internal/types/interfaces/chunk.go index 57c5a90a6..400b35d21 100644 --- a/internal/types/interfaces/chunk.go +++ b/internal/types/interfaces/chunk.go @@ -18,6 +18,7 @@ type ChunkRepository interface { ListChunksByKnowledgeID(ctx context.Context, tenantID uint64, knowledgeID string) ([]*types.Chunk, error) // ListPagedChunksByKnowledgeID lists paged chunks by knowledge id. // When tagID is non-empty, results are filtered by tag_id. + // sortOrder: "asc" for time ascending (updated_at ASC), default is time descending (updated_at DESC) ListPagedChunksByKnowledgeID( ctx context.Context, tenantID uint64, @@ -26,6 +27,7 @@ type ChunkRepository interface { chunkType []types.ChunkType, tagID string, keyword string, + sortOrder string, ) ([]*types.Chunk, int64, error) ListChunkByParentID(ctx context.Context, tenantID uint64, parentID string) ([]*types.Chunk, error) // UpdateChunk updates a chunk diff --git a/internal/types/interfaces/knowledge.go b/internal/types/interfaces/knowledge.go index ad0a25b56..bd50bda87 100644 --- a/internal/types/interfaces/knowledge.go +++ b/internal/types/interfaces/knowledge.go @@ -74,12 +74,14 @@ type KnowledgeService interface { UpdateImageInfo(ctx context.Context, knowledgeID string, chunkID string, imageInfo string) error // ListFAQEntries lists FAQ entries under a FAQ knowledge base. // When tagID is non-empty, results are filtered by tag_id on FAQ chunks. + // sortOrder: "asc" for time ascending (updated_at ASC), default is time descending (updated_at DESC) ListFAQEntries( ctx context.Context, kbID string, page *types.Pagination, tagID string, keyword string, + sortOrder string, ) (*types.PageResult, error) // UpsertFAQEntries imports or appends FAQ entries asynchronously. // Returns task ID (Knowledge ID) for tracking import progress. diff --git a/internal/types/interfaces/tag.go b/internal/types/interfaces/tag.go index 471c2d2e3..9f6e04119 100644 --- a/internal/types/interfaces/tag.go +++ b/internal/types/interfaces/tag.go @@ -26,6 +26,8 @@ type KnowledgeTagRepository interface { Create(ctx context.Context, tag *types.KnowledgeTag) error Update(ctx context.Context, tag *types.KnowledgeTag) error GetByID(ctx context.Context, tenantID uint64, id string) (*types.KnowledgeTag, error) + // GetByIDs retrieves multiple tags by their IDs in a single query. + GetByIDs(ctx context.Context, tenantID uint64, ids []string) ([]*types.KnowledgeTag, error) GetByName(ctx context.Context, tenantID uint64, kbID string, name string) (*types.KnowledgeTag, error) ListByKB( ctx context.Context,