diff --git a/internal/application/repository/chunk.go b/internal/application/repository/chunk.go index 1bfd16ca..1e9a8ab1 100644 --- a/internal/application/repository/chunk.go +++ b/internal/application/repository/chunk.go @@ -90,7 +90,7 @@ func (r *chunkRepository) ListPagedChunksByKnowledgeID( baseFilter := func(db *gorm.DB) *gorm.DB { db = db.Where("tenant_id = ? AND knowledge_id = ? AND chunk_type IN (?) AND status in (?)", tenantID, knowledgeID, chunkType, []int{int(types.ChunkStatusIndexed), int(types.ChunkStatusDefault)}) - if tagID == "__untagged__" { + if tagID == types.UntaggedTagID { // Special value to filter entries without a tag db = db.Where("tag_id = ''") } else if tagID != "" { @@ -601,6 +601,7 @@ func (r *chunkRepository) UpdateChunkFlagsBatch( // UpdateChunkFieldsByTagID updates fields for all chunks with the specified tag ID. // Returns the list of affected chunk IDs for syncing with retriever engines. +// newTagID: if not nil, updates tag_id to this value (empty string means uncategorized) func (r *chunkRepository) UpdateChunkFieldsByTagID( ctx context.Context, tenantID uint64, @@ -609,6 +610,7 @@ func (r *chunkRepository) UpdateChunkFieldsByTagID( isEnabled *bool, setFlags types.ChunkFlags, clearFlags types.ChunkFlags, + newTagID *string, excludeIDs []string, ) ([]string, error) { // First, get the IDs of chunks that will be affected (for is_enabled sync) @@ -619,7 +621,7 @@ func (r *chunkRepository) UpdateChunkFieldsByTagID( Select("id"). Where("tenant_id = ? AND knowledge_base_id = ? AND chunk_type = ?", tenantID, kbID, types.ChunkTypeFAQ) - if tagID == "" { + if tagID == "" || tagID == types.UntaggedTagID { query = query.Where("(tag_id = '' OR tag_id IS NULL)") } else { query = query.Where("tag_id = ?", tagID) @@ -648,11 +650,20 @@ func (r *chunkRepository) UpdateChunkFieldsByTagID( updates["is_enabled"] = *isEnabled } + // Handle newTagID update (__untagged__ is stored as empty string) + if newTagID != nil { + if *newTagID == types.UntaggedTagID { + updates["tag_id"] = "" + } else { + updates["tag_id"] = *newTagID + } + } + query := r.db.WithContext(ctx).Model(&types.Chunk{}). Where("tenant_id = ? AND knowledge_base_id = ? AND chunk_type = ?", tenantID, kbID, types.ChunkTypeFAQ) - if tagID == "" { + if tagID == "" || tagID == types.UntaggedTagID { query = query.Where("(tag_id = '' OR tag_id IS NULL)") } else { query = query.Where("tag_id = ?", tagID) diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go index 926d31a2..b0d744e8 100644 --- a/internal/application/service/knowledge.go +++ b/internal/application/service/knowledge.go @@ -3570,7 +3570,7 @@ func (s *knowledgeService) UpdateFAQEntryFieldsBatch(ctx context.Context, // Update all chunks with this tag affectedIDs, err := s.chunkRepo.UpdateChunkFieldsByTagID( ctx, tenantID, kb.ID, tagID, - update.IsEnabled, setFlags, clearFlags, req.ExcludeIDs, + update.IsEnabled, setFlags, clearFlags, update.TagID, req.ExcludeIDs, ) if err != nil { return err diff --git a/internal/types/faq.go b/internal/types/faq.go index 912653dc..2fa4cbea 100644 --- a/internal/types/faq.go +++ b/internal/types/faq.go @@ -228,6 +228,9 @@ type FAQSearchRequest struct { MatchCount int `json:"match_count"` } +// UntaggedTagID is the special tag ID representing uncategorized entries +const UntaggedTagID = "__untagged__" + // FAQEntryFieldsUpdate 单个FAQ条目的字段更新 type FAQEntryFieldsUpdate struct { IsEnabled *bool `json:"is_enabled,omitempty"` @@ -243,7 +246,7 @@ type FAQEntryFieldsUpdate struct { type FAQEntryFieldsBatchUpdate struct { // ByID 按条目ID更新,key为条目ID ByID map[string]FAQEntryFieldsUpdate `json:"by_id,omitempty"` - // ByTag 按Tag批量更新,key为TagID(空字符串表示未分类) + // ByTag 按Tag批量更新,key为TagID(__untagged__表示未分类) ByTag map[string]FAQEntryFieldsUpdate `json:"by_tag,omitempty"` // ExcludeIDs 在ByTag操作中需要排除的ID列表 ExcludeIDs []string `json:"exclude_ids,omitempty"` diff --git a/internal/types/interfaces/chunk.go b/internal/types/interfaces/chunk.go index 1e6d09bd..72826053 100644 --- a/internal/types/interfaces/chunk.go +++ b/internal/types/interfaces/chunk.go @@ -64,8 +64,9 @@ type ChunkRepository interface { // clearFlags: map of chunk ID to flags to clear (AND NOT operation) UpdateChunkFlagsBatch(ctx context.Context, tenantID uint64, kbID string, setFlags map[string]types.ChunkFlags, clearFlags map[string]types.ChunkFlags) error // UpdateChunkFieldsByTagID updates fields for all chunks with the specified tag ID. - // Supports updating is_enabled and flags fields. - UpdateChunkFieldsByTagID(ctx context.Context, tenantID uint64, kbID string, tagID string, isEnabled *bool, setFlags types.ChunkFlags, clearFlags types.ChunkFlags, excludeIDs []string) ([]string, error) + // Supports updating is_enabled, flags, and tag_id fields. + // newTagID: if not nil, updates tag_id to this value (empty string means uncategorized) + UpdateChunkFieldsByTagID(ctx context.Context, tenantID uint64, kbID string, tagID string, isEnabled *bool, setFlags types.ChunkFlags, clearFlags types.ChunkFlags, newTagID *string, excludeIDs []string) ([]string, error) // FAQChunkDiff compares FAQ chunks between two knowledge bases and returns the differences. // Returns: chunksToAdd (content_hash in src but not in dst), chunksToDelete (content_hash in dst but not in src) FAQChunkDiff(ctx context.Context, srcTenantID uint64, srcKBID string, dstTenantID uint64, dstKBID string) (chunksToAdd []string, chunksToDelete []string, err error)