feat(knowledge): S6 摄入轮询键 documentId→batch(Dify 按 batch 轮询 indexing-status)

为 muse_knowledge_processing_task 加 runtime_batch_id 列并落库 Dify 上传返回的索引批次(从 uploadResult.summary 的 batch 键抽取)。poll worker 取 runtimeBatchId 非空优先、否则回退 ragflowDocumentId 作轮询键,RAGFlow 无 batch 路径逐字不变;skip gate 放宽为 documentId 与 batch 都空才跳过。

DDL 采用新迁移 V37 ALTER TABLE ADD COLUMN,非回编 V14 建表语句——沿用 sql/muse append-only 约定(V24/V27/V30/V31 同法),避免破坏已应用库(dev/live)的 Flyway 校验和。

验证:mvn -pl muse-module-knowledge-server -am test 目标两类全绿(PollWorkerTest 9/0/0/0、DocumentServiceTest 17/0/0/0,聚合 Tests run 26 Failures 0 Errors 0 Skipped 0,BUILD SUCCESS)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-08 03:45:46 -07:00
parent a8f5e912f5
commit 13bd910c8f
7 changed files with 77 additions and 5 deletions

View File

@ -292,8 +292,11 @@ public class MuseKnowledgeDocumentService {
uploadResult.externalId(), parseResult);
return new UploadOutcome(ragflowContext, "failed");
}
// Dify 摄入把索引批次 batch 透出到 uploadResult.summary().get("batch")是其 indexing-status 轮询主键
// RAGFlow 无此字段 runtimeBatchId null轮询回退 documentId行为逐字不变
String runtimeBatchId = uploadResult.summary() == null ? null : (String) uploadResult.summary().get("batch");
processingTaskService.markRagflowParseAccepted(ragflowContext.version().getId(), ragflowContext.task().getTaskId(),
ragflowContext.ragflowDatasetId(), uploadResult.externalId(), parseResult);
ragflowContext.ragflowDatasetId(), uploadResult.externalId(), runtimeBatchId, parseResult);
return new UploadOutcome(ragflowContext, "processing");
}

View File

@ -76,8 +76,11 @@ public class MuseKnowledgeParseStatusPollWorker {
int advanced = 0;
for (MuseKnowledgeProcessingTaskDO task : tasks) {
// 缺租户/外部 id 的任务无法安全轮询跳过避免空指针与跨租户写串
// 轮询键防御性放宽ragflowDocumentId runtimeBatchId 都空才跳过Dify 场景两者都会有值
// RAGFlow 只有 documentId任一非空即可据以轮询
if (task.getTenantId() == null || !StringUtils.hasText(task.getRagflowDatasetId())
|| !StringUtils.hasText(task.getRagflowDocumentId())) {
|| (!StringUtils.hasText(task.getRagflowDocumentId())
&& !StringUtils.hasText(task.getRuntimeBatchId()))) {
continue;
}
advanced += TenantUtils.execute(task.getTenantId(), () -> pollOne(task));
@ -87,10 +90,14 @@ public class MuseKnowledgeParseStatusPollWorker {
private int pollOne(MuseKnowledgeProcessingTaskDO task) {
KnowledgeRuntimeClient.RuntimeResult result;
// 轮询主键择键Dify 上传落 runtimeBatchIdindexing-status batch 优先用之
// RAGFlow batch 回退 ragflowDocumentId保持原逐字行为skip gate 已保证两者至少一非空pollKey 不为 null
String pollKey = StringUtils.hasText(task.getRuntimeBatchId())
? task.getRuntimeBatchId() : task.getRagflowDocumentId();
try {
result = ragFlowClient.pollDocumentStatuses(new KnowledgeRuntimeClient.PollDocumentStatusesCommand(
task.getTenantId(), task.getOwnerUserId(), task.getKbId(), task.getRagflowDatasetId(),
List.of(task.getRagflowDocumentId()), task.getTaskId(),
List.of(pollKey), task.getTaskId(),
pollCorrelationId(task), pollRequestHash(task), 1));
} catch (RuntimeException ex) {
// RAGFlow 临时不可达保持 parsing下轮重试不误判失败

View File

@ -20,6 +20,7 @@ import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
@ -178,7 +179,8 @@ public class MuseKnowledgeProcessingTaskService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void markRagflowParseAccepted(Long documentVersionId, String taskId, String ragflowDatasetId,
String ragflowDocumentId, KnowledgeRuntimeClient.RuntimeResult result) {
String ragflowDocumentId, String runtimeBatchId,
KnowledgeRuntimeClient.RuntimeResult result) {
MuseKnowledgeProcessingTaskDO task = processingTaskMapper.selectByTaskId(taskId);
if (task != null) {
task.setStatus("parsing");
@ -187,6 +189,11 @@ public class MuseKnowledgeProcessingTaskService {
// 新建 dataset 的上传链路必须把外部 datasetId 同步固化到 task后续轮询补偿和 live 取证都依赖这条事实
task.setRagflowDatasetId(ragflowDatasetId);
task.setRagflowDocumentId(ragflowDocumentId);
// Dify 上传返回的索引批次是其 indexing-status 轮询主键必须固化RAGFlow batch runtimeBatchId 为空
// hasText 判定避免把已落值覆盖成 nullpoll worker 据此在 batch / documentId 间择键
if (StringUtils.hasText(runtimeBatchId)) {
task.setRuntimeBatchId(runtimeBatchId);
}
task.setRetryable(false);
task.setProgressMessage("RAGFlow parse accepted; polling required");
task.setResultSummary(JsonUtils.toJsonString(Map.of("ragflow", safeSummary(result))));

View File

@ -42,6 +42,12 @@ public class MuseKnowledgeProcessingTaskDO extends TenantBaseDO {
private String status;
private String ragflowDatasetId;
private String ragflowDocumentId;
/**
* 运行时索引批次 idDify Datasets 摄入返回的顶层 batch是其 indexing-status 轮询主键
* GET /v1/datasets/{id}/documents/{batch}/indexing-statusRAGFlow 上传不产生 batch留空
* 轮询回退到 ragflowDocumentId行为逐字不变命名沿用 provider 无关口径避免与 ragflowDocumentId 混淆
*/
private String runtimeBatchId;
/** RAGFlow document run/progress 轮询得到的解析进度。 */
private Integer parseProgress;
private String progressMessage;

View File

@ -323,11 +323,12 @@ class MuseKnowledgeDocumentServiceTest extends BaseMockitoUnitTest {
});
when(processingTaskMapper.selectByTaskId("knowledge-processing-6001")).thenReturn(processingTask(6001L));
when(processingTaskMapper.insert(any(MuseKnowledgeProcessingTaskDO.class))).thenAnswer(invocation -> 1);
// 模拟 Dify 上传把索引批次 batch 透出到 summary验证其被抽取并落库到 task.runtimeBatchIdRAGFlow 无此字段
when(ragFlowClient.uploadDocuments(any())).thenReturn(new KnowledgeRuntimeClient.RuntimeResult(
"rag-doc-1", KnowledgeRuntimeClient.Operation.UPLOAD_DOCUMENTS,
"uploadGlobalKBDocument:cmd-upload-file-success", "hash-file-success", 1, 12L,
KnowledgeRuntimeClient.Status.SUCCEEDED, null, null,
"{\"status\":\"succeeded\"}", List.of(), java.util.Map.of()));
"{\"status\":\"succeeded\"}", List.of(), java.util.Map.of("batch", "dify-batch-1")));
when(ragFlowClient.startParseDocuments(any())).thenReturn(new KnowledgeRuntimeClient.RuntimeResult(
null, KnowledgeRuntimeClient.Operation.START_PARSE_DOCUMENTS,
"uploadGlobalKBDocument:cmd-upload-file-success", "hash-file-success", 1, 8L,
@ -380,6 +381,7 @@ class MuseKnowledgeDocumentServiceTest extends BaseMockitoUnitTest {
"parsing".equals(task.getStatus())
&& "rag-dataset-1".equals(task.getRagflowDatasetId())
&& "rag-doc-1".equals(task.getRagflowDocumentId())
&& "dify-batch-1".equals(task.getRuntimeBatchId())
&& "processing".equals(task.getResultSummary()) == false));
}

View File

@ -9,6 +9,7 @@ import cn.iocoder.muse.module.knowledge.application.muse.facade.KnowledgeRuntime
import cn.iocoder.muse.module.knowledge.dal.dataobject.muse.MuseKnowledgeProcessingTaskDO;
import cn.iocoder.muse.module.knowledge.dal.mysql.muse.MuseKnowledgeProcessingTaskMapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import java.util.List;
@ -129,6 +130,7 @@ class MuseKnowledgeParseStatusPollWorkerTest extends BaseMockitoUnitTest {
@Test
void should_skip_whenMissingRagflowIds() {
// documentId runtimeBatchId 都空 无可用轮询键 跳过
MuseKnowledgeProcessingTaskDO t = parsingTask();
t.setRagflowDocumentId(null);
when(processingTaskMapper.selectParsingForPoll(anyInt())).thenReturn(List.of(t));
@ -138,4 +140,37 @@ class MuseKnowledgeParseStatusPollWorkerTest extends BaseMockitoUnitTest {
assertEquals(0, advanced);
verifyNoInteractions(ragFlowClient);
}
@Test
void should_pollByRuntimeBatchId_whenDifyBatchPresent() {
// Dify 场景上传落 runtimeBatchId=索引 batch ragflowDocumentId=document.id轮询主键取 batch
MuseKnowledgeProcessingTaskDO t = parsingTask();
t.setRuntimeBatchId("dify-batch-9");
when(processingTaskMapper.selectParsingForPoll(anyInt())).thenReturn(List.of(t));
when(ragFlowClient.pollDocumentStatuses(any())).thenReturn(
poll(Status.SUCCEEDED, new DocumentStatus("doc-1", "processing", "RUNNING", 50, "running")));
worker(true).pollOnce();
ArgumentCaptor<KnowledgeRuntimeClient.PollDocumentStatusesCommand> captor =
ArgumentCaptor.forClass(KnowledgeRuntimeClient.PollDocumentStatusesCommand.class);
verify(ragFlowClient).pollDocumentStatuses(captor.capture());
assertEquals(List.of("dify-batch-9"), captor.getValue().ragflowDocumentIds());
}
@Test
void should_pollByDocumentId_whenRuntimeBatchIdAbsent() {
// RAGFlow 场景 batchruntimeBatchId 留空 轮询回退 ragflowDocumentId逐字不变
MuseKnowledgeProcessingTaskDO t = parsingTask();
when(processingTaskMapper.selectParsingForPoll(anyInt())).thenReturn(List.of(t));
when(ragFlowClient.pollDocumentStatuses(any())).thenReturn(
poll(Status.SUCCEEDED, new DocumentStatus("doc-1", "processing", "RUNNING", 50, "running")));
worker(true).pollOnce();
ArgumentCaptor<KnowledgeRuntimeClient.PollDocumentStatusesCommand> captor =
ArgumentCaptor.forClass(KnowledgeRuntimeClient.PollDocumentStatusesCommand.class);
verify(ragFlowClient).pollDocumentStatuses(captor.capture());
assertEquals(List.of("doc-1"), captor.getValue().ragflowDocumentIds());
}
}

View File

@ -0,0 +1,12 @@
-- S6 知识摄入轮询键从 documentId 泛化到 batch为 Dify Datasets 索引状态轮询补运行时批次列。
--
-- 背景KnowledgeRuntimeClient 是 provider 无关端口muse.knowledge.runtime-provider 切换 ragflow / dify
-- Dify uploadDocuments 返回 document.id落 ragflow_document_id+ 顶层 batch其索引状态轮询打
-- GET /v1/datasets/{id}/documents/{batch}/indexing-status轮询主键=batchRAGFlow 仍按 documentId 轮询、
-- 不产生 batch。此前摄入 caller 未持久化 batchpoll worker 只能传 documentId → Dify 轮询打错端点必坏。
-- 本迁移新增可空列承载 Dify 批次Dify 上传落 batch → 按 batch 轮询RAGFlow 留空 → 回退 documentId、行为逐字不变。
--
-- 迁移方式:沿用本目录 append-only 约定V24/V27/V30/V31 均以新迁移 ALTER 既有表),不回编 V14 建表语句,
-- 避免破坏已应用库dev / live的 Flyway 校验和。
ALTER TABLE muse_knowledge_processing_task
ADD COLUMN runtime_batch_id VARCHAR(64);