feat: 添加FAQ导入结果持久化展示功能

This commit is contained in:
wizardchen
2026-01-14 17:09:19 +08:00
parent 042d860329
commit 9215c82d3b
10 changed files with 575 additions and 0 deletions
+10
View File
@@ -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(() => {
+146
View File
@@ -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
+79
View File
@@ -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,
})
}
+3
View File
@@ -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")
+36
View File
@@ -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 {
+4
View File
@@ -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)
+28
View File
@@ -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)';