feat(knowledge): add batch reparse capability
This commit introduces the Batch Reparse feature for the Knowledge Base. Users can now select multiple documents and trigger a re-parsing pipeline in batches, streamlining the recovery from parse failures.
This commit is contained in:
@@ -120,7 +120,7 @@ export function updateKnowledgeBase(id: string, data: {
|
||||
};
|
||||
}
|
||||
}) {
|
||||
return put(`/api/v1/knowledge-bases/${id}` , data);
|
||||
return put(`/api/v1/knowledge-bases/${id}`, data);
|
||||
}
|
||||
|
||||
export function rebuildKBIndex(kbId: string) {
|
||||
@@ -481,3 +481,11 @@ export function knowledgeSemanticSearch(data: {
|
||||
}) {
|
||||
return post('/api/v1/knowledge-search', data);
|
||||
}
|
||||
|
||||
export function batchReparseKnowledge(kbId: string, ids: string[], processConfig?: any) {
|
||||
return post(`/api/v1/knowledge/batch-reparse`, {
|
||||
kb_id: kbId,
|
||||
ids,
|
||||
process_config: processConfig
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
reparseKnowledge,
|
||||
cancelKnowledgeParse,
|
||||
batchDeleteKnowledge,
|
||||
batchReparseKnowledge,
|
||||
getKnowledgeSpans,
|
||||
getKnowledgeDetails,
|
||||
} from "@/api/knowledge-base/index";
|
||||
@@ -60,7 +61,7 @@ import { formatFileSize } from '@/utils/files';
|
||||
import { useMarqueeSelect } from '@/hooks/useMarqueeSelect';
|
||||
import type { ParserEngineInfo } from '@/api/system';
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { t, te } = useI18n();
|
||||
const kbId = computed(() => (route.params as any).kbId as string || '');
|
||||
const kbInfo = ref<any>(null);
|
||||
const uploadSourceRef = ref<InstanceType<typeof KbUploadSourceDropdown> | null>(null);
|
||||
@@ -415,6 +416,35 @@ watch(viewMode, (v) => {
|
||||
const selectedIds = ref<Set<string>>(new Set());
|
||||
let lastSelectedIndex = -1;
|
||||
const batchDeleting = ref(false);
|
||||
const batchReparsing = ref(false);
|
||||
|
||||
const confirmBatchReparse = async () => {
|
||||
if (batchReparsing.value || selectedIds.value.size === 0) return;
|
||||
const ids = Array.from(selectedIds.value);
|
||||
batchReparsing.value = true;
|
||||
try {
|
||||
const res: any = await batchReparseKnowledge(kbId.value, ids);
|
||||
if (res?.success) {
|
||||
if (res.partial && res.failed_ids && res.failed_ids.length > 0) {
|
||||
MessagePlugin.warning(`部分重构任务提交成功,失败 ${res.failed_ids.length} 个`);
|
||||
} else {
|
||||
const successMsg = te('knowledgeBase.batchReparseSuccess')
|
||||
? t('knowledgeBase.batchReparseSuccess', { count: ids.length })
|
||||
: `已提交 ${ids.length} 个重构任务`;
|
||||
MessagePlugin.success(successMsg);
|
||||
}
|
||||
clearSelection();
|
||||
batchMode.value = false;
|
||||
loadKnowledgeFiles(kbId.value);
|
||||
} else {
|
||||
MessagePlugin.error(res?.message || '批量重建失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
MessagePlugin.error(e?.message || '批量重建失败');
|
||||
} finally {
|
||||
batchReparsing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedTagId = ref<string>('');
|
||||
const tagList = ref<any[]>([]);
|
||||
@@ -2607,9 +2637,9 @@ async function createNewSession(value: string): Promise<void> {
|
||||
</template>
|
||||
</div>
|
||||
<div class="doc-batch-bar-anchor" v-show="batchMode || selectedIds.size > 0">
|
||||
<DocumentBatchBar :count="selectedIds.size" :loading="batchDeleting"
|
||||
<DocumentBatchBar :count="selectedIds.size" :loading="batchDeleting || batchReparsing"
|
||||
:visible="batchMode || selectedIds.size > 0" @cancel="handleBatchCancel"
|
||||
@delete="confirmBatchDelete" />
|
||||
@delete="confirmBatchDelete" @reparse="confirmBatchReparse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,10 @@ defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(e: 'cancel'): void;
|
||||
(e: 'delete'): void;
|
||||
(e: 'reparse'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, te } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,6 +40,27 @@ const { t } = useI18n();
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="batch-bar-actions">
|
||||
<t-popconfirm
|
||||
theme="warning"
|
||||
:content="te('knowledgeBase.overviewDescReparse') ? t('knowledgeBase.overviewDescReparse') : '确认后现有内容将被清除并重新解析该文档'"
|
||||
:confirm-btn="{ content: te('knowledgeBase.confirmReparse') ? t('knowledgeBase.confirmReparse') : '确认并重新解析', theme: 'warning' }"
|
||||
:cancel-btn="{ content: t('common.cancel') }"
|
||||
placement="top"
|
||||
@confirm="emit('reparse')"
|
||||
>
|
||||
<t-button
|
||||
theme="default"
|
||||
variant="outline"
|
||||
size="small"
|
||||
:disabled="count === 0"
|
||||
:loading="loading"
|
||||
@click.stop
|
||||
>
|
||||
<template #icon><t-icon name="refresh" size="14px" /></template>
|
||||
{{ t('knowledgeBase.rebuildDocument') }}
|
||||
</t-button>
|
||||
</t-popconfirm>
|
||||
|
||||
<t-popconfirm
|
||||
theme="warning"
|
||||
:content="t('knowledgeBase.confirmBatchDeleteDocument', { count })"
|
||||
|
||||
@@ -2228,3 +2228,105 @@ func sliceContains(ss []string, target string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type batchReparseKnowledgeRequest struct {
|
||||
KBID string `json:"kb_id" binding:"required"`
|
||||
IDs []string `json:"ids" binding:"required"`
|
||||
ProcessConfig *types.KnowledgeProcessOverrides `json:"process_config,omitempty"`
|
||||
}
|
||||
|
||||
// BatchReparseKnowledge godoc
|
||||
// @Summary 批量重新解析知识
|
||||
// @Description 按 ID 列表批量重新解析单个知识库下的多个知识条目
|
||||
// @Tags 知识管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body batchReparseKnowledgeRequest true "批量重解析请求"
|
||||
// @Success 200 {object} map[string]interface{} "任务已提交"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 403 {object} errors.AppError "权限不足"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /knowledge/batch-reparse [post]
|
||||
func (h *KnowledgeHandler) BatchReparseKnowledge(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var req batchReparseKnowledgeRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Errorf(ctx, "failed to parse batch reparse knowledge request: %v", err)
|
||||
c.Error(errors.NewBadRequestError("invalid batch reparse knowledge request parameters"))
|
||||
return
|
||||
}
|
||||
|
||||
var ids []string
|
||||
seen := make(map[string]struct{})
|
||||
for _, id := range req.IDs {
|
||||
if id != "" {
|
||||
if _, ok := seen[id]; !ok {
|
||||
ids = append(ids, id)
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
c.Error(errors.NewBadRequestError("no knowledge IDs provided for batch reparse"))
|
||||
return
|
||||
}
|
||||
if len(ids) > 200 {
|
||||
c.Error(errors.NewBadRequestError("the number of IDs exceeds the maximum batch size of 200"))
|
||||
return
|
||||
}
|
||||
|
||||
_, _, effectiveTenantID, permission, err := h.validateKnowledgeBaseAccessWithKBID(c, req.KBID)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
if permission != types.OrgRoleAdmin && permission != types.OrgRoleEditor {
|
||||
c.Error(errors.NewForbiddenError("no permission to reparse knowledge in this kb"))
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, types.TenantIDContextKey, effectiveTenantID)
|
||||
knows, err := h.kgService.GetKnowledgeBatch(ctx, effectiveTenantID, ids)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "failed to get knowledge batch, kb_id: %s, size: %d, err: %v", req.KBID, len(ids), err)
|
||||
c.Error(errors.NewInternalServerError("failed to get knowledge batch"))
|
||||
return
|
||||
}
|
||||
if len(knows) != len(ids) {
|
||||
c.Error(errors.NewBadRequestError("some knowledge entries were not found"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, know := range knows {
|
||||
if know.KnowledgeBaseID != req.KBID {
|
||||
c.Error(errors.NewBadRequestError("some knowledge entries do not belong to the specified knowledge base"))
|
||||
return
|
||||
}
|
||||
}
|
||||
var failedIDs []string
|
||||
for _, id := range ids {
|
||||
_, err := h.kgService.ReparseKnowledge(ctx, id, req.ProcessConfig)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "Failed to reparse knowledge %s: %v", id, err)
|
||||
failedIDs = append(failedIDs, id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedIDs) > 0 {
|
||||
c.JSON(200, gin.H{
|
||||
"success": len(failedIDs) < len(ids),
|
||||
"partial": len(failedIDs) < len(ids),
|
||||
"message": "Batch reparse completed with some failures",
|
||||
"failed_ids": failedIDs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "Batch reparse task submitted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -323,6 +323,7 @@ func RegisterKnowledgeRoutes(r *gin.RouterGroup, handler *handler.KnowledgeHandl
|
||||
// "must own every targeted KB" guard if the requirement
|
||||
// surfaces.
|
||||
k.PUT("/tags", g.Contributor(), handler.UpdateKnowledgeTagBatch)
|
||||
k.POST("/batch-reparse", g.Contributor(), handler.BatchReparseKnowledge)
|
||||
k.GET("/search", g.Viewer(), handler.SearchKnowledge)
|
||||
k.POST("/batch-delete", g.Contributor(), handler.BatchDeleteKnowledge)
|
||||
k.POST("/move", g.Contributor(), handler.MoveKnowledge)
|
||||
|
||||
Reference in New Issue
Block a user