From 48a764b866fc58d0fb218211255922bb19ccfced Mon Sep 17 00:00:00 2001 From: nullkey Date: Wed, 29 Apr 2026 11:47:59 +0800 Subject: [PATCH] refactor(knowledge-base): address review feedback Backend - BatchDeleteKnowledge handler now enqueues an asynq TypeKnowledgeListDelete task instead of calling DeleteKnowledgeList synchronously, matching the pattern used by ClearKnowledgeBaseContents. Avoids long HTTP timeouts on large batches and shares the existing async cleanup pipeline. - Extract a small enqueueKnowledgeListDelete helper on KnowledgeHandler so BatchDeleteKnowledge and ClearKnowledgeBaseContents share the payload/marshal/enqueue boilerplate. Frontend (DocumentListView) - softer outer border and lighter row separators - header background switched to the page tone with placeholder-color text, reducing institutional gray - selected row uses an inset left accent bar and a quieter brand tint instead of a saturated background; hover stays neutral - consolidate the .row-more-btn { opacity: 1 } rule that was duplicated across the hover and selected blocks --- .../knowledge/components/DocumentListView.vue | 35 ++++++----- internal/handler/knowledge.go | 58 ++++++++++++------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/frontend/src/views/knowledge/components/DocumentListView.vue b/frontend/src/views/knowledge/components/DocumentListView.vue index 80f890991..fef9fa619 100644 --- a/frontend/src/views/knowledge/components/DocumentListView.vue +++ b/frontend/src/views/knowledge/components/DocumentListView.vue @@ -265,8 +265,8 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know flex-direction: column; width: 100%; background: var(--td-bg-color-container, #fff); - border: 1px solid var(--td-component-border, #e7e7e7); - border-radius: 10px; + border: 1px solid var(--td-component-stroke, #f0f0f0); + border-radius: 8px; overflow: hidden; } @@ -291,12 +291,13 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know position: sticky; top: 0; z-index: 2; - height: 40px; + height: 36px; font-size: 12px; font-weight: 500; - color: var(--td-text-color-secondary, #888); - background: var(--td-bg-color-secondarycontainer, #fafafa); - border-bottom: 1px solid var(--td-component-border, #e7e7e7); + letter-spacing: 0.02em; + color: var(--td-text-color-placeholder, #a6a6a6); + background: var(--td-bg-color-page, #fafbfc); + border-bottom: 1px solid var(--td-component-stroke, #f0f0f0); } .doc-list-body { @@ -305,25 +306,31 @@ const handleAction = (action: 'edit' | 'reparse' | 'move' | 'delete', item: Know } .doc-list-row { + position: relative; height: 48px; font-size: 13px; color: var(--td-text-color-primary, #232323); - border-bottom: 1px solid var(--td-component-stroke, #f0f0f0); + border-bottom: 1px solid var(--td-component-stroke, #f3f3f3); cursor: pointer; - transition: background-color 0.12s ease; + transition: background-color 0.12s ease, box-shadow 0.12s ease; &:last-child { border-bottom: 0; } - &:hover, - &.menu-open { - background: var(--td-bg-color-container-hover, #f6f8fa); - .row-more-btn { opacity: 1; } + &:hover:not(.selected), + &.menu-open:not(.selected) { + background: var(--td-bg-color-page, #f7f8fa); } &.selected { - background: var(--td-brand-color-1, #f0f6ff); - &:hover { background: var(--td-brand-color-2, #e6f0ff); } + background: var(--td-brand-color-1, #f2f5fc); + box-shadow: inset 3px 0 0 var(--td-brand-color, #0052d9); + + &:hover { background: var(--td-brand-color-light, #e8eefc); } } + + &:hover .row-more-btn, + &.menu-open .row-more-btn, + &.selected .row-more-btn { opacity: 1; } } .cell { diff --git a/internal/handler/knowledge.go b/internal/handler/knowledge.go index 77822c56a..116cd959b 100644 --- a/internal/handler/knowledge.go +++ b/internal/handler/knowledge.go @@ -187,6 +187,29 @@ func (h *KnowledgeHandler) handleDuplicateKnowledgeError(c *gin.Context, return false } +// enqueueKnowledgeListDelete enqueues an async batch-delete task for the +// given knowledge IDs and returns the asynq task ID. +func (h *KnowledgeHandler) enqueueKnowledgeListDelete( + ctx context.Context, tenantID uint64, ids []string, +) (string, error) { + payload := types.KnowledgeListDeletePayload{ + TenantID: tenantID, + KnowledgeIDs: ids, + } + langfuse.InjectTracing(ctx, &payload) + payloadBytes, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("marshal payload: %w", err) + } + task := asynq.NewTask(types.TypeKnowledgeListDelete, payloadBytes, + asynq.Queue("low"), asynq.MaxRetry(3)) + info, err := h.asynqClient.Enqueue(task) + if err != nil { + return "", fmt.Errorf("enqueue task: %w", err) + } + return info.ID, nil +} + // CreateKnowledgeFromFile godoc // @Summary 从文件创建知识 // @Description 上传文件并创建知识条目 @@ -715,16 +738,23 @@ func (h *KnowledgeHandler) BatchDeleteKnowledge(c *gin.Context) { } } - logger.Infof(ctx, "Batch deleting %d knowledge entries from KB %s", len(ids), secutils.SanitizeForLog(kbID)) - if err := h.kgService.DeleteKnowledgeList(ctx, ids); err != nil { - logger.ErrorWithFields(ctx, err, nil) - c.Error(errors.NewInternalServerError(err.Error())) + taskID, err := h.enqueueKnowledgeListDelete(ctx, effectiveTenantID, ids) + if err != nil { + logger.Errorf(ctx, "Failed to enqueue batch knowledge delete task: %v", err) + c.Error(errors.NewInternalServerError("Failed to enqueue batch delete task")) return } + logger.Infof(ctx, "Batch knowledge delete task enqueued: %s, kb_id: %s, count: %d", + taskID, secutils.SanitizeForLog(kbID), len(ids)) + c.JSON(http.StatusOK, gin.H{ "success": true, - "deleted": len(ids), + "message": "Batch delete task submitted", + "data": gin.H{ + "task_id": taskID, + "deleted_count": len(ids), + }, }) } @@ -782,21 +812,7 @@ func (h *KnowledgeHandler) ClearKnowledgeBaseContents(c *gin.Context) { knowledgeIDs = append(knowledgeIDs, knowledge.ID) } - payload := types.KnowledgeListDeletePayload{ - TenantID: effectiveTenantID, - KnowledgeIDs: knowledgeIDs, - } - langfuse.InjectTracing(ctx, &payload) - payloadBytes, err := json.Marshal(payload) - if err != nil { - logger.Errorf(ctx, "Failed to marshal knowledge list delete payload: %v", err) - c.Error(errors.NewInternalServerError("Failed to create cleanup task")) - return - } - - task := asynq.NewTask(types.TypeKnowledgeListDelete, payloadBytes, - asynq.Queue("low"), asynq.MaxRetry(3)) - info, err := h.asynqClient.Enqueue(task) + taskID, err := h.enqueueKnowledgeListDelete(ctx, effectiveTenantID, knowledgeIDs) if err != nil { logger.Errorf(ctx, "Failed to enqueue knowledge list delete task: %v", err) c.Error(errors.NewInternalServerError("Failed to enqueue cleanup task")) @@ -804,7 +820,7 @@ func (h *KnowledgeHandler) ClearKnowledgeBaseContents(c *gin.Context) { } logger.Infof(ctx, "Knowledge base contents clear task enqueued: %s, kb_id: %s, count: %d", - info.ID, secutils.SanitizeForLog(kbID), len(knowledgeIDs)) + taskID, secutils.SanitizeForLog(kbID), len(knowledgeIDs)) c.JSON(http.StatusOK, gin.H{ "success": true,