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:
jw_mac
2026-06-25 21:03:11 +08:00
committed by lyingbug
parent 5f2ca6c975
commit a14c1d87aa
5 changed files with 168 additions and 5 deletions
+9 -1
View File
@@ -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
});
}
+33 -3
View File
@@ -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 })"
+102
View File
@@ -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",
})
}
+1
View File
@@ -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)