diff --git a/client/faq.go b/client/faq.go index afe108dea..c26c4b6c6 100644 --- a/client/faq.go +++ b/client/faq.go @@ -358,6 +358,7 @@ type FAQFailedEntry struct { } // FAQImportProgress represents the progress of an async FAQ import task. +// When Status is "completed", the result fields (SkippedCount, ImportMode, ImportedAt, DisplayStatus, ProcessingTime) are populated. type FAQImportProgress struct { TaskID string `json:"task_id"` KBID string `json:"kb_id"` @@ -368,6 +369,7 @@ type FAQImportProgress struct { Processed int `json:"processed"` SuccessCount int `json:"success_count"` FailedCount int `json:"failed_count"` + SkippedCount int `json:"skipped_count,omitempty"` FailedEntries []FAQFailedEntry `json:"failed_entries,omitempty"` FailedEntriesURL string `json:"failed_entries_url,omitempty"` // CSV download URL when too many failures Message string `json:"message"` @@ -375,6 +377,12 @@ type FAQImportProgress struct { CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` DryRun bool `json:"dry_run,omitempty"` // Whether this is a dry run validation + + // Result fields (populated when Status == "completed") + ImportMode string `json:"import_mode,omitempty"` + ImportedAt time.Time `json:"imported_at,omitempty"` + DisplayStatus string `json:"display_status,omitempty"` + ProcessingTime int64 `json:"processing_time,omitempty"` } // FAQImportProgressResponse wraps the FAQ import progress response. @@ -400,69 +408,3 @@ func (c *Client) GetFAQImportProgress(ctx context.Context, taskID string) (*FAQI } return response.Data, nil } - -// FAQImportResult stores statistics of the last FAQ import. -// It matches server response from GET /knowledge-bases/{id}/faq/import/last-result. -type FAQImportResult struct { - TotalEntries int `json:"total_entries"` - SuccessCount int `json:"success_count"` - FailedCount int `json:"failed_count"` - SkippedCount int `json:"skipped_count"` - - ImportMode string `json:"import_mode"` - ImportedAt time.Time `json:"imported_at"` - TaskID string `json:"task_id"` - - FailedEntriesURL string `json:"failed_entries_url,omitempty"` - - DisplayStatus string `json:"display_status"` - ProcessingTime int64 `json:"processing_time"` -} - -type faqImportResultResponse struct { - Success bool `json:"success"` - Data *FAQImportResult `json:"data"` - Message string `json:"message,omitempty"` - Code string `json:"code,omitempty"` -} - -type updateLastFAQImportResultDisplayStatusRequest struct { - DisplayStatus string `json:"display_status"` -} - -// GetLastFAQImportResult retrieves the last FAQ import result summary for a knowledge base. -func (c *Client) GetLastFAQImportResult(ctx context.Context, knowledgeBaseID string) (*FAQImportResult, error) { - path := fmt.Sprintf("/api/v1/knowledge-bases/%s/faq/import/last-result", knowledgeBaseID) - resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil) - if err != nil { - return nil, err - } - - var response faqImportResultResponse - if err := parseResponse(resp, &response); err != nil { - return nil, err - } - return response.Data, nil -} - -// UpdateLastFAQImportResultDisplayStatus updates the display status (open/close) of the last FAQ import result. -func (c *Client) UpdateLastFAQImportResultDisplayStatus(ctx context.Context, knowledgeBaseID string, displayStatus string) error { - path := fmt.Sprintf("/api/v1/knowledge-bases/%s/faq/import/last-result/display", knowledgeBaseID) - resp, err := c.doRequest(ctx, http.MethodPut, path, &updateLastFAQImportResultDisplayStatusRequest{DisplayStatus: displayStatus}, nil) - if err != nil { - return err - } - - var response faqSimpleResponse - return parseResponse(resp, &response) -} - -// GetFAQImportResult is kept for backward compatibility; it now calls GetLastFAQImportResult. -func (c *Client) GetFAQImportResult(ctx context.Context, knowledgeBaseID string) (*FAQImportResult, error) { - return c.GetLastFAQImportResult(ctx, knowledgeBaseID) -} - -// UpdateFAQImportResultDisplayStatus is kept for backward compatibility; it now calls UpdateLastFAQImportResultDisplayStatus. -func (c *Client) UpdateFAQImportResultDisplayStatus(ctx context.Context, knowledgeBaseID string, displayStatus string) error { - return c.UpdateLastFAQImportResultDisplayStatus(ctx, knowledgeBaseID, displayStatus) -} diff --git a/frontend/src/api/knowledge-base/index.ts b/frontend/src/api/knowledge-base/index.ts index 6cc33b293..4c28b3bcf 100644 --- a/frontend/src/api/knowledge-base/index.ts +++ b/frontend/src/api/knowledge-base/index.ts @@ -232,10 +232,6 @@ export function getFAQImportProgress(taskId: string) { return get(`/api/v1/faq/import/progress/${taskId}`); } -export function getFAQImportResult(knowledgeBaseId: string) { - return get(`/api/v1/knowledge-bases/${knowledgeBaseId}/faq/import/last-result`); -} - export function updateFAQImportResultDisplayStatus(knowledgeBaseId: string, displayStatus: 'open' | 'close') { return put(`/api/v1/knowledge-bases/${knowledgeBaseId}/faq/import/last-result/display`, { display_status: displayStatus diff --git a/frontend/src/views/knowledge/components/FAQEntryManager.vue b/frontend/src/views/knowledge/components/FAQEntryManager.vue index b2dd79e94..09855572e 100644 --- a/frontend/src/views/knowledge/components/FAQEntryManager.vue +++ b/frontend/src/views/knowledge/components/FAQEntryManager.vue @@ -1316,7 +1316,6 @@ import { getKnowledgeBaseById, listKnowledgeBases, getFAQImportProgress, - getFAQImportResult, updateFAQImportResultDisplayStatus, } from '@/api/knowledge-base' import * as XLSX from 'xlsx' @@ -2451,6 +2450,10 @@ const startPolling = (taskId: string) => { if (status === 'success' || status === 'failed') { stopPolling() if (status === 'success') { + // 保存已完成的 taskId 用于后续加载结果 + if (importState.taskId) { + saveLastCompletedTaskId(importState.taskId) + } MessagePlugin.success(t('knowledgeEditor.faqImport.importSuccess')) // 清除筛选条件,确保用户能看到所有新导入的数据 selectedTagId.value = '' @@ -2588,13 +2591,61 @@ const restoreImportTask = async () => { } } +// localStorage key for last completed task +const getLastCompletedTaskKey = () => { + return `faq_import_last_completed_${props.kbId}` +} + +const saveLastCompletedTaskId = (taskId: string) => { + if (!props.kbId) return + try { + localStorage.setItem(getLastCompletedTaskKey(), taskId) + } catch (error) { + console.error('Failed to save last completed taskId:', error) + } +} + +const getLastCompletedTaskId = (): string | null => { + if (!props.kbId) return null + try { + return localStorage.getItem(getLastCompletedTaskKey()) + } catch (error) { + return null + } +} + // 加载持久化的导入结果统计 const loadImportResult = async () => { if (!props.kbId) return + + const lastTaskId = getLastCompletedTaskId() + if (!lastTaskId) { + importResult.value = null + return + } + try { - const res: any = await getFAQImportResult(props.kbId) - if (res?.data) { - importResult.value = res.data + const res: any = await getFAQImportProgress(lastTaskId) + const data = res?.data + if (data && data.status === 'completed') { + // 检查后端返回的 display_status,如果是 close 则不显示 + if (data.display_status === 'close') { + importResult.value = null + return + } + // Map progress fields to importResult format + importResult.value = { + total_entries: data.total, + success_count: data.success_count, + failed_count: data.failed_count, + skipped_count: data.skipped_count || 0, + import_mode: data.import_mode || 'append', + imported_at: data.imported_at, + task_id: data.task_id, + failed_entries_url: data.failed_entries_url, + display_status: data.display_status || 'open', + processing_time: data.processing_time || 0, + } } else { importResult.value = null } diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go index c262b4c9b..7bafee43a 100644 --- a/internal/application/service/knowledge.go +++ b/internal/application/service/knowledge.go @@ -6095,40 +6095,23 @@ func (s *knowledgeService) GetFAQImportProgress(ctx context.Context, taskID stri if err := json.Unmarshal(data, &progress); err != nil { return nil, fmt.Errorf("failed to unmarshal FAQ import progress: %w", err) } - return &progress, nil -} -// GetLastFAQImportResult retrieves the latest FAQ import result for a knowledge base -func (s *knowledgeService) GetLastFAQImportResult(ctx context.Context, kbID string) (*types.FAQImportResult, error) { - // 获取当前租户ID - tenantID := ctx.Value(types.TenantIDContextKey).(uint64) - - // 查找FAQ类型的knowledge - knowledgeList, err := s.repo.ListKnowledgeByKnowledgeBaseID(ctx, tenantID, kbID) - if err != nil { - return nil, fmt.Errorf("failed to list knowledge: %w", err) - } - - // 查找FAQ类型的knowledge - var faqKnowledge *types.Knowledge - for _, k := range knowledgeList { - if k.Type == types.KnowledgeTypeFAQ { - faqKnowledge = k - break + // If task is completed, enrich with persisted result fields from database + if progress.Status == types.FAQImportStatusCompleted && progress.KnowledgeID != "" { + tenantID := ctx.Value(types.TenantIDContextKey).(uint64) + knowledge, err := s.repo.GetKnowledgeByID(ctx, tenantID, progress.KnowledgeID) + if err == nil && knowledge != nil { + if result, err := knowledge.GetLastFAQImportResult(); err == nil && result != nil { + progress.SkippedCount = result.SkippedCount + progress.ImportMode = result.ImportMode + progress.ImportedAt = result.ImportedAt + progress.DisplayStatus = result.DisplayStatus + progress.ProcessingTime = result.ProcessingTime + } } } - if faqKnowledge == nil { - return nil, werrors.NewNotFoundError("FAQ knowledge not found in this knowledge base") - } - - // 解析导入结果 - result, err := faqKnowledge.GetLastFAQImportResult() - if err != nil { - return nil, fmt.Errorf("failed to parse FAQ import result: %w", err) - } - - return result, nil + return &progress, nil } // UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result diff --git a/internal/handler/faq.go b/internal/handler/faq.go index b283e862e..84878b17a 100644 --- a/internal/handler/faq.go +++ b/internal/handler/faq.go @@ -423,44 +423,6 @@ func (h *FAQHandler) GetImportProgress(c *gin.Context) { }) } -// GetLastImportResult godoc -// @Summary 获取FAQ最后一次导入结果统计 -// @Description 获取FAQ知识库的最后一次导入结果统计信息 -// @Tags FAQ管理 -// @Accept json -// @Produce json -// @Param id path string true "知识库ID" -// @Success 200 {object} map[string]interface{} "导入结果统计" -// @Failure 404 {object} errors.AppError "知识库不存在或无导入记录" -// @Security Bearer -// @Security ApiKeyAuth -// @Router /knowledge-bases/{id}/faq/import/last-result [get] -func (h *FAQHandler) GetLastImportResult(c *gin.Context) { - ctx := c.Request.Context() - kbID := secutils.SanitizeForLog(c.Param("id")) - - result, err := h.knowledgeService.GetLastFAQImportResult(ctx, kbID) - if err != nil { - logger.ErrorWithFields(ctx, err, nil) - c.Error(err) - return - } - - if result == nil { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": nil, - "message": "暂无导入记录", - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": result, - }) -} - // updateLastFAQImportResultDisplayStatusRequest is the request payload for UpdateLastImportResultDisplayStatus type updateLastFAQImportResultDisplayStatusRequest struct { DisplayStatus string `json:"display_status" binding:"required,oneof=open close"` diff --git a/internal/router/router.go b/internal/router/router.go index f2775ec9a..f228634bf 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -194,8 +194,7 @@ func RegisterFAQRoutes(r *gin.RouterGroup, handler *handler.FAQHandler) { faq.PUT("/entries/tags", handler.UpdateEntryTagBatch) faq.DELETE("/entries", handler.DeleteEntries) faq.POST("/search", handler.SearchFAQ) - // FAQ import result API - faq.GET("/import/last-result", handler.GetLastImportResult) + // FAQ import result display status faq.PUT("/import/last-result/display", handler.UpdateLastImportResultDisplayStatus) } // FAQ import progress route (outside of knowledge-base scope) diff --git a/internal/types/faq.go b/internal/types/faq.go index d34b2e840..7d910c17b 100644 --- a/internal/types/faq.go +++ b/internal/types/faq.go @@ -293,6 +293,7 @@ const ( ) // FAQImportProgress represents the progress of an FAQ import task stored in Redis +// When Status is "completed", the result fields (SkippedCount, ImportMode, ImportedAt, DisplayStatus, ProcessingTime) are populated. type FAQImportProgress struct { TaskID string `json:"task_id"` // UUID for the import task KBID string `json:"kb_id"` // Knowledge Base ID @@ -303,6 +304,7 @@ type FAQImportProgress struct { Processed int `json:"processed"` // Entries processed so far SuccessCount int `json:"success_count"` // 成功导入/验证通过的条目数 FailedCount int `json:"failed_count"` // 失败的条目数 + SkippedCount int `json:"skipped_count,omitempty"` // 跳过的条目数(如重复等) FailedEntries []FAQFailedEntry `json:"failed_entries,omitempty"` // 失败条目详情(少量时直接返回) FailedEntriesURL string `json:"failed_entries_url,omitempty"` // 失败条目CSV下载URL(大量时返回URL) Message string `json:"message"` // Status message @@ -310,6 +312,12 @@ type FAQImportProgress struct { CreatedAt int64 `json:"created_at"` // Task creation timestamp UpdatedAt int64 `json:"updated_at"` // Last update timestamp DryRun bool `json:"dry_run,omitempty"` // 是否为 dry run 模式 + + // Result fields (populated when Status == "completed") + ImportMode string `json:"import_mode,omitempty"` // 导入模式:append 或 replace + ImportedAt time.Time `json:"imported_at,omitempty"` // 导入完成时间 + DisplayStatus string `json:"display_status,omitempty"` // 显示状态:open 或 close + ProcessingTime int64 `json:"processing_time,omitempty"` // 处理耗时(毫秒) } // FAQImportMetadata 存储在Knowledge.Metadata中的FAQ导入任务信息 diff --git a/internal/types/interfaces/knowledge.go b/internal/types/interfaces/knowledge.go index 47fc293d6..08326367f 100644 --- a/internal/types/interfaces/knowledge.go +++ b/internal/types/interfaces/knowledge.go @@ -126,8 +126,6 @@ type KnowledgeService interface { SaveKBCloneProgress(ctx context.Context, progress *types.KBCloneProgress) error // GetFAQImportProgress retrieves the progress of an FAQ import task GetFAQImportProgress(ctx context.Context, taskID string) (*types.FAQImportProgress, error) - // GetLastFAQImportResult retrieves the latest FAQ import result for a knowledge base - GetLastFAQImportResult(ctx context.Context, kbID string) (*types.FAQImportResult, error) // UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result UpdateLastFAQImportResultDisplayStatus(ctx context.Context, kbID string, displayStatus string) error // SearchKnowledge searches knowledge items by keyword across the tenant.