mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
feat: 添加FAQ导入结果持久化展示功能
This commit is contained in:
@@ -232,6 +232,16 @@ 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
|
||||
});
|
||||
}
|
||||
|
||||
export function searchKnowledge(keyword: string, offset = 0, limit = 20, fileTypes?: string[]) {
|
||||
let url = `/api/v1/knowledge/search?keyword=${encodeURIComponent(keyword)}&offset=${offset}&limit=${limit}`;
|
||||
if (fileTypes && fileTypes.length > 0) {
|
||||
|
||||
@@ -54,6 +54,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入结果统计(持久化显示) -->
|
||||
<div v-if="importResult && importResult.display_status === 'open' && !importState.taskId" class="faq-import-result-card">
|
||||
<div class="import-result-content">
|
||||
<div class="import-result-header">
|
||||
<t-icon name="check-circle" size="18px" class="result-icon" />
|
||||
<span class="result-title">最近导入结果</span>
|
||||
<span class="result-time">{{ formatImportTime(importResult.imported_at) }}</span>
|
||||
</div>
|
||||
<div class="import-result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">导入数据</span>
|
||||
<span class="stat-value">{{ importResult.total_entries }}条</span>
|
||||
</div>
|
||||
<div class="stat-item success">
|
||||
<span class="stat-label">成功</span>
|
||||
<span class="stat-value">{{ importResult.success_count }}条</span>
|
||||
</div>
|
||||
<div v-if="importResult.failed_count > 0" class="stat-item failed">
|
||||
<span class="stat-label">失败</span>
|
||||
<span class="stat-value">{{ importResult.failed_count }}条</span>
|
||||
<t-button
|
||||
v-if="importResult.failed_entries_url"
|
||||
variant="text"
|
||||
theme="primary"
|
||||
size="small"
|
||||
class="download-failed-btn"
|
||||
@click="downloadFailedEntries"
|
||||
>
|
||||
下载原因
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="importResult.skipped_count > 0" class="stat-item skipped">
|
||||
<span class="stat-label">跳过</span>
|
||||
<span class="stat-value">{{ importResult.skipped_count }}条</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-result-footer">
|
||||
<span class="import-mode">{{ importResult.import_mode === 'append' ? '追加模式' : '替换模式' }}</span>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="result-close-btn"
|
||||
@click="closeImportResult"
|
||||
>
|
||||
<t-icon name="close" size="14px" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入进度条(显示在列表页面顶部) -->
|
||||
<div v-if="importState.taskId && importState.taskStatus" class="faq-import-progress-bar">
|
||||
<div class="progress-bar-content">
|
||||
@@ -106,6 +157,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入结果统计(持久化显示) -->
|
||||
<div v-if="importResult && importResult.display_status === 'open' && !importState.taskId" class="faq-import-result-card">
|
||||
<div class="import-result-content">
|
||||
<div class="import-result-header">
|
||||
<t-icon name="check-circle" size="18px" class="result-icon" />
|
||||
<span class="result-title">最近导入结果</span>
|
||||
<span class="result-time">{{ formatImportTime(importResult.imported_at) }}</span>
|
||||
</div>
|
||||
<div class="import-result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">导入数据</span>
|
||||
<span class="stat-value">{{ importResult.total_entries }}条</span>
|
||||
</div>
|
||||
<div class="stat-item success">
|
||||
<span class="stat-label">成功</span>
|
||||
<span class="stat-value">{{ importResult.success_count }}条</span>
|
||||
</div>
|
||||
<div v-if="importResult.failed_count > 0" class="stat-item failed">
|
||||
<span class="stat-label">失败</span>
|
||||
<span class="stat-value">{{ importResult.failed_count }}条</span>
|
||||
<t-button
|
||||
v-if="importResult.failed_entries_url"
|
||||
variant="text"
|
||||
theme="primary"
|
||||
size="small"
|
||||
class="download-failed-btn"
|
||||
@click="downloadFailedEntries"
|
||||
>
|
||||
下载原因
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="importResult.skipped_count > 0" class="stat-item skipped">
|
||||
<span class="stat-label">跳过</span>
|
||||
<span class="stat-value">{{ importResult.skipped_count }}条</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-result-footer">
|
||||
<span class="import-mode">{{ importResult.import_mode === 'append' ? '追加模式' : '替换模式' }}</span>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="result-close-btn"
|
||||
@click="closeImportResult"
|
||||
>
|
||||
<t-icon name="close" size="14px" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-main">
|
||||
<aside class="faq-tag-panel">
|
||||
<div class="sidebar-header">
|
||||
@@ -519,6 +621,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入结果统计(持久化显示) -->
|
||||
<div v-if="importResult && importResult.display_status === 'open' && !importState.taskId" class="faq-import-result-card">
|
||||
<div class="import-result-content">
|
||||
<div class="import-result-header">
|
||||
<t-icon name="check-circle" size="18px" class="result-icon" />
|
||||
<span class="result-title">最近导入结果</span>
|
||||
<span class="result-time">{{ formatImportTime(importResult.imported_at) }}</span>
|
||||
</div>
|
||||
<div class="import-result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">导入数据</span>
|
||||
<span class="stat-value">{{ importResult.total_entries }}条</span>
|
||||
</div>
|
||||
<div class="stat-item success">
|
||||
<span class="stat-label">成功</span>
|
||||
<span class="stat-value">{{ importResult.success_count }}条</span>
|
||||
</div>
|
||||
<div v-if="importResult.failed_count > 0" class="stat-item failed">
|
||||
<span class="stat-label">失败</span>
|
||||
<span class="stat-value">{{ importResult.failed_count }}条</span>
|
||||
<t-button
|
||||
v-if="importResult.failed_entries_url"
|
||||
variant="text"
|
||||
theme="primary"
|
||||
size="small"
|
||||
class="download-failed-btn"
|
||||
@click="downloadFailedEntries"
|
||||
>
|
||||
下载原因
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="importResult.skipped_count > 0" class="stat-item skipped">
|
||||
<span class="stat-label">跳过</span>
|
||||
<span class="stat-value">{{ importResult.skipped_count }}条</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-result-footer">
|
||||
<span class="import-mode">{{ importResult.import_mode === 'append' ? '追加模式' : '替换模式' }}</span>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="result-close-btn"
|
||||
@click="closeImportResult"
|
||||
>
|
||||
<t-icon name="close" size="14px" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Editor Drawer -->
|
||||
<t-drawer
|
||||
@@ -1084,6 +1237,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入结果统计(持久化显示) -->
|
||||
<div v-if="importResult && importResult.display_status === 'open' && !importState.taskId" class="faq-import-result-card">
|
||||
<div class="import-result-content">
|
||||
<div class="import-result-header">
|
||||
<t-icon name="check-circle" size="18px" class="result-icon" />
|
||||
<span class="result-title">最近导入结果</span>
|
||||
<span class="result-time">{{ formatImportTime(importResult.imported_at) }}</span>
|
||||
</div>
|
||||
<div class="import-result-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">导入数据</span>
|
||||
<span class="stat-value">{{ importResult.total_entries }}条</span>
|
||||
</div>
|
||||
<div class="stat-item success">
|
||||
<span class="stat-label">成功</span>
|
||||
<span class="stat-value">{{ importResult.success_count }}条</span>
|
||||
</div>
|
||||
<div v-if="importResult.failed_count > 0" class="stat-item failed">
|
||||
<span class="stat-label">失败</span>
|
||||
<span class="stat-value">{{ importResult.failed_count }}条</span>
|
||||
<t-button
|
||||
v-if="importResult.failed_entries_url"
|
||||
variant="text"
|
||||
theme="primary"
|
||||
size="small"
|
||||
class="download-failed-btn"
|
||||
@click="downloadFailedEntries"
|
||||
>
|
||||
下载原因
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="importResult.skipped_count > 0" class="stat-item skipped">
|
||||
<span class="stat-label">跳过</span>
|
||||
<span class="stat-value">{{ importResult.skipped_count }}条</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-result-footer">
|
||||
<span class="import-mode">{{ importResult.import_mode === 'append' ? '追加模式' : '替换模式' }}</span>
|
||||
<t-button
|
||||
variant="text"
|
||||
theme="default"
|
||||
size="small"
|
||||
class="result-close-btn"
|
||||
@click="closeImportResult"
|
||||
>
|
||||
<t-icon name="close" size="14px" />
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1112,6 +1316,8 @@ import {
|
||||
getKnowledgeBaseById,
|
||||
listKnowledgeBases,
|
||||
getFAQImportProgress,
|
||||
getFAQImportResult,
|
||||
updateFAQImportResultDisplayStatus,
|
||||
} from '@/api/knowledge-base'
|
||||
import * as XLSX from 'xlsx'
|
||||
import Papa from 'papaparse'
|
||||
@@ -1305,6 +1511,20 @@ const importState = reactive({
|
||||
pollingInterval: null as ReturnType<typeof setInterval> | null,
|
||||
})
|
||||
|
||||
// FAQ导入结果状态(持久化的)
|
||||
const importResult = ref<{
|
||||
total_entries: number
|
||||
success_count: number
|
||||
failed_count: number
|
||||
skipped_count: number
|
||||
import_mode: string
|
||||
imported_at: string
|
||||
task_id: string
|
||||
processing_time: number
|
||||
failed_entries_url?: string
|
||||
display_status: string
|
||||
} | null>(null)
|
||||
|
||||
// Search test state
|
||||
const searchDrawerVisible = ref(false)
|
||||
const searching = ref(false)
|
||||
@@ -2238,6 +2458,7 @@ const startPolling = (taskId: string) => {
|
||||
overallFAQTotal.value = 0 // Reset to trigger re-fetch
|
||||
await loadEntries()
|
||||
await loadTags()
|
||||
await loadImportResult() // 加载最新的导入结果统计
|
||||
// 任务完成后,3秒后自动关闭进度条
|
||||
setTimeout(() => {
|
||||
if (importState.taskStatus?.status === 'success') {
|
||||
@@ -2367,6 +2588,46 @@ const restoreImportTask = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载持久化的导入结果统计
|
||||
const loadImportResult = async () => {
|
||||
if (!props.kbId) return
|
||||
try {
|
||||
const res: any = await getFAQImportResult(props.kbId)
|
||||
if (res?.data) {
|
||||
importResult.value = res.data
|
||||
} else {
|
||||
importResult.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load FAQ import result:', error)
|
||||
importResult.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭导入结果统计卡片
|
||||
const closeImportResult = async () => {
|
||||
if (!props.kbId) return
|
||||
try {
|
||||
await updateFAQImportResultDisplayStatus(props.kbId, 'close')
|
||||
if (importResult.value) {
|
||||
importResult.value.display_status = 'close'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to close import result:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化导入时间
|
||||
const formatImportTime = (timeStr?: string) => {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
} catch (e) {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importState.file || !importState.preview.length) {
|
||||
MessagePlugin.warning(t('knowledgeEditor.faqImport.selectFile'))
|
||||
@@ -2827,6 +3088,7 @@ onMounted(async () => {
|
||||
// 如果已有kbId,恢复导入任务状态
|
||||
if (props.kbId) {
|
||||
await restoreImportTask()
|
||||
await loadImportResult() // 加载导入结果
|
||||
}
|
||||
// 主动触发一次选中数量事件,确保左侧菜单能接收到初始状态
|
||||
nextTick(() => {
|
||||
|
||||
@@ -2897,6 +2897,57 @@ func csvEscape(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// saveFAQImportResultToDatabase 保存FAQ导入结果统计到数据库
|
||||
func (s *knowledgeService) saveFAQImportResultToDatabase(ctx context.Context,
|
||||
payload *types.FAQImportPayload, progress *types.FAQImportProgress, originalTotalEntries int) error {
|
||||
|
||||
// 获取FAQ知识库实例
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
|
||||
knowledge, err := s.repo.GetKnowledgeByID(ctx, tenantID, payload.KnowledgeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get FAQ knowledge: %w", err)
|
||||
}
|
||||
|
||||
// 计算跳过的条目数(总数 - 成功 - 失败)
|
||||
skippedCount := originalTotalEntries - progress.SuccessCount - progress.FailedCount
|
||||
if skippedCount < 0 {
|
||||
skippedCount = 0
|
||||
}
|
||||
|
||||
// 创建导入结果统计
|
||||
importResult := &types.FAQImportResult{
|
||||
TotalEntries: originalTotalEntries,
|
||||
SuccessCount: progress.SuccessCount,
|
||||
FailedCount: progress.FailedCount,
|
||||
SkippedCount: skippedCount,
|
||||
ImportMode: payload.Mode,
|
||||
ImportedAt: time.Now(),
|
||||
TaskID: payload.TaskID,
|
||||
ProcessingTime: time.Now().Unix() - progress.CreatedAt, // 处理耗时(秒)
|
||||
DisplayStatus: "open", // 新导入的结果默认显示
|
||||
}
|
||||
|
||||
// 如果有失败条目且提供了下载URL,设置失败URL
|
||||
if progress.FailedCount > 0 && progress.FailedEntriesURL != "" {
|
||||
importResult.FailedEntriesURL = progress.FailedEntriesURL
|
||||
}
|
||||
|
||||
// 设置导入结果到Knowledge的metadata中
|
||||
if err := knowledge.SetLastFAQImportResult(importResult); err != nil {
|
||||
return fmt.Errorf("failed to set FAQ import result: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
if err := s.repo.UpdateKnowledge(ctx, knowledge); err != nil {
|
||||
return fmt.Errorf("failed to update knowledge with import result: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Saved FAQ import result to database: knowledge_id=%s, task_id=%s, total=%d, success=%d, failed=%d, skipped=%d",
|
||||
payload.KnowledgeID, payload.TaskID, originalTotalEntries, progress.SuccessCount, progress.FailedCount, skippedCount)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildFAQFailedEntry 构建 FAQFailedEntry
|
||||
func buildFAQFailedEntry(idx int, reason string, entry *types.FAQEntryPayload) types.FAQFailedEntry {
|
||||
answerAll := false
|
||||
@@ -5967,6 +6018,13 @@ func (s *knowledgeService) finalizeFAQValidation(ctx context.Context, payload *t
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是 dry run 模式,保存导入结果统计到数据库
|
||||
if !payload.DryRun {
|
||||
if err := s.saveFAQImportResultToDatabase(ctx, payload, progress, originalTotalEntries); err != nil {
|
||||
logger.Warnf(ctx, "Failed to save FAQ import result to database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 updateFAQImportProgressStatus 来确保正确清理 running key
|
||||
// 但是需要先保存其他字段,因为 updateFAQImportProgressStatus 不会保存所有字段
|
||||
if err := s.saveFAQImportProgress(ctx, progress); err != nil {
|
||||
@@ -6040,6 +6098,94 @@ func (s *knowledgeService) GetFAQImportProgress(ctx context.Context, taskID stri
|
||||
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 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
|
||||
}
|
||||
|
||||
// UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result
|
||||
func (s *knowledgeService) UpdateLastFAQImportResultDisplayStatus(ctx context.Context, kbID string, displayStatus string) error {
|
||||
// 验证displayStatus参数
|
||||
if displayStatus != "open" && displayStatus != "close" {
|
||||
return werrors.NewBadRequestError("invalid display status, must be 'open' or 'close'")
|
||||
}
|
||||
|
||||
// 获取当前租户ID
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
|
||||
|
||||
// 查找FAQ类型的knowledge
|
||||
knowledgeList, err := s.repo.ListKnowledgeByKnowledgeBaseID(ctx, tenantID, kbID)
|
||||
if err != nil {
|
||||
return 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 faqKnowledge == nil {
|
||||
return werrors.NewNotFoundError("FAQ knowledge not found in this knowledge base")
|
||||
}
|
||||
|
||||
// 解析当前的导入结果
|
||||
result, err := faqKnowledge.GetLastFAQImportResult()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse FAQ import result: %w", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return werrors.NewNotFoundError("no FAQ import result found")
|
||||
}
|
||||
|
||||
// 更新显示状态
|
||||
result.DisplayStatus = displayStatus
|
||||
|
||||
// 保存更新后的结果
|
||||
if err := faqKnowledge.SetLastFAQImportResult(result); err != nil {
|
||||
return fmt.Errorf("failed to set FAQ import result: %w", err)
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
if err := s.repo.UpdateKnowledge(ctx, faqKnowledge); err != nil {
|
||||
return fmt.Errorf("failed to update knowledge: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessKBClone handles Asynq knowledge base clone tasks
|
||||
func (s *knowledgeService) ProcessKBClone(ctx context.Context, t *asynq.Task) error {
|
||||
var payload types.KBClonePayload
|
||||
|
||||
@@ -422,3 +422,82 @@ func (h *FAQHandler) GetImportProgress(c *gin.Context) {
|
||||
"data": progress,
|
||||
})
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// UpdateLastImportResultDisplayStatus godoc
|
||||
// @Summary 更新FAQ最后一次导入结果显示状态
|
||||
// @Description 更新FAQ知识库导入结果统计卡片的显示或隐藏状态
|
||||
// @Tags FAQ管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param request body updateLastFAQImportResultDisplayStatusRequest true "状态更新请求"
|
||||
// @Success 200 {object} map[string]interface{} "更新成功"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 404 {object} errors.AppError "知识库不存在或无导入记录"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /knowledge-bases/{id}/faq/import/last-result/display [put]
|
||||
func (h *FAQHandler) UpdateLastImportResultDisplayStatus(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
kbID := secutils.SanitizeForLog(c.Param("id"))
|
||||
|
||||
var req updateLastFAQImportResultDisplayStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error(ctx, "Failed to bind display status update payload", err)
|
||||
c.Error(errors.NewBadRequestError("请求参数不合法").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.knowledgeService.UpdateLastFAQImportResultDisplayStatus(ctx, kbID, req.DisplayStatus); err != nil {
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,6 +194,9 @@ 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.PUT("/import/last-result/display", handler.UpdateLastImportResultDisplayStatus)
|
||||
}
|
||||
// FAQ import progress route (outside of knowledge-base scope)
|
||||
faqImport := r.Group("/faq/import")
|
||||
|
||||
@@ -320,6 +320,30 @@ type FAQImportMetadata struct {
|
||||
ImportProcessed int `json:"import_processed"`
|
||||
}
|
||||
|
||||
// FAQImportResult 存储FAQ导入完成后的统计结果
|
||||
// 这个信息是持久化的,不跟随进度状态,直到下次导入时被替换
|
||||
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"` // 导入模式:append 或 replace
|
||||
ImportedAt time.Time `json:"imported_at"` // 导入完成时间
|
||||
TaskID string `json:"task_id"` // 导入任务ID
|
||||
|
||||
// 失败详情URL(失败条目较多时提供下载链接)
|
||||
FailedEntriesURL string `json:"failed_entries_url,omitempty"` // 失败条目CSV下载URL
|
||||
|
||||
// 显示控制
|
||||
DisplayStatus string `json:"display_status"` // 显示状态:open 或 close
|
||||
|
||||
// 额外统计信息
|
||||
ProcessingTime int64 `json:"processing_time"` // 处理耗时(毫秒)
|
||||
}
|
||||
|
||||
// ToJSON converts the metadata to JSON type.
|
||||
func (m *FAQImportMetadata) ToJSON() (JSON, error) {
|
||||
if m == nil {
|
||||
@@ -332,6 +356,18 @@ func (m *FAQImportMetadata) ToJSON() (JSON, error) {
|
||||
return JSON(bytes), nil
|
||||
}
|
||||
|
||||
// ToJSON converts the import result to JSON type.
|
||||
func (r *FAQImportResult) ToJSON() (JSON, error) {
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
bytes, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return JSON(bytes), nil
|
||||
}
|
||||
|
||||
// ParseFAQImportMetadata parses FAQ import metadata from Knowledge.
|
||||
func ParseFAQImportMetadata(k *Knowledge) (*FAQImportMetadata, error) {
|
||||
if k == nil || len(k.Metadata) == 0 {
|
||||
|
||||
@@ -126,6 +126,10 @@ 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.
|
||||
// fileTypes: optional list of file extensions to filter by (e.g., ["csv", "xlsx"])
|
||||
SearchKnowledge(ctx context.Context, keyword string, offset, limit int, fileTypes []string) ([]*types.Knowledge, bool, error)
|
||||
|
||||
@@ -93,6 +93,8 @@ type Knowledge struct {
|
||||
StorageSize int64 `json:"storage_size"`
|
||||
// Metadata of the knowledge
|
||||
Metadata JSON `json:"metadata" gorm:"type:json"`
|
||||
// Last FAQ import result (for FAQ type knowledge only)
|
||||
LastFAQImportResult JSON `json:"last_faq_import_result" gorm:"type:json"`
|
||||
// Creation time of the knowledge
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// Last updated time of the knowledge
|
||||
@@ -214,6 +216,32 @@ func (k *Knowledge) SetManualMetadata(meta *ManualKnowledgeMetadata) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLastFAQImportResult sets FAQ import result to the dedicated field.
|
||||
func (k *Knowledge) SetLastFAQImportResult(result *FAQImportResult) error {
|
||||
if result == nil {
|
||||
k.LastFAQImportResult = nil
|
||||
return nil
|
||||
}
|
||||
jsonValue, err := result.ToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.LastFAQImportResult = jsonValue
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLastFAQImportResult parses and returns FAQ import result from the dedicated field.
|
||||
func (k *Knowledge) GetLastFAQImportResult() (*FAQImportResult, error) {
|
||||
if len(k.LastFAQImportResult) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var result FAQImportResult
|
||||
if err := json.Unmarshal(k.LastFAQImportResult, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// IsManual returns true if the knowledge item is manual Markdown knowledge.
|
||||
func (k *Knowledge) IsManual() bool {
|
||||
return k != nil && k.Type == KnowledgeTypeManual
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove last_faq_import_result column from knowledge table
|
||||
|
||||
ALTER TABLE knowledge DROP COLUMN last_faq_import_result;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add last_faq_import_result column to knowledge table
|
||||
-- This field stores the latest FAQ import result for FAQ type knowledge
|
||||
|
||||
ALTER TABLE knowledge ADD COLUMN last_faq_import_result JSON DEFAULT NULL COMMENT 'Last FAQ import result (for FAQ type knowledge only)';
|
||||
Reference in New Issue
Block a user