feat: 支持工作流文档节点解析 Office 新格式

- DocNode 对 pdf/docx/pptx/xlsx 统一走桥接同步解析

- 修复 DOCX 误回退到 PDF 解析服务的问题并补齐回归测试
This commit is contained in:
2026-04-18 19:52:45 +08:00
parent 4130381658
commit 8546d927bc
6 changed files with 211 additions and 32 deletions

View File

@@ -101,7 +101,7 @@ public class DocumentParseBridgeServiceImplTest {
@Test
public void shouldRoutePptxToDedicatedService() {
FakePptxDocumentParseService pptxService = new FakePptxDocumentParseService();
FakePdfDocumentParseService defaultService = new FakePdfDocumentParseService();
FakeDefaultDocumentParseService defaultService = new FakeDefaultDocumentParseService();
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(null, pptxService, null, defaultService);
DocumentParsedResult result = bridgeService.parse(buildSource("slides.pptx",
@@ -112,6 +112,61 @@ public class DocumentParseBridgeServiceImplTest {
Assert.assertEquals(0, defaultService.parseCallCount);
}
/**
* 验证 DOCX 会路由到默认桥接服务,而不是误走 PDF 服务。
*/
@Test
public void shouldRouteDocxToDefaultBridgeService() {
FakePdfDocumentParseService pdfService = new FakePdfDocumentParseService();
FakeDefaultDocumentParseService defaultService = new FakeDefaultDocumentParseService();
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(pdfService, null, null, defaultService);
DocumentParsedResult result = bridgeService.parse(buildSource("demo.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"), DocumentParseScenario.WORKFLOW_TEXT);
Assert.assertEquals("# docx", result.getPreferredText());
Assert.assertEquals(1, defaultService.parseCallCount);
Assert.assertEquals(0, pdfService.parseCallCount);
}
/**
* 验证 DOCX 在缺少默认桥接服务时会明确失败,而不是退到 PDF 服务。
*/
@Test
public void shouldThrowWhenDocxBridgeServiceDisabled() {
FakePdfDocumentParseService pdfService = new FakePdfDocumentParseService();
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(pdfService, null, null, null);
try {
bridgeService.parse(buildSource("demo.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"), DocumentParseScenario.WORKFLOW_TEXT);
Assert.fail("expected DocumentParseBridgeException");
} catch (DocumentParseBridgeException e) {
Assert.assertEquals("service_not_enabled", e.getCode());
Assert.assertTrue(e.getMessage().contains("DOCX"));
}
Assert.assertEquals(0, pdfService.parseCallCount);
}
/**
* 验证 XLSX 在缺少专用桥接服务时会明确失败。
*/
@Test
public void shouldThrowWhenXlsxBridgeServiceDisabled() {
FakeDefaultDocumentParseService defaultService = new FakeDefaultDocumentParseService();
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(null, null, null, defaultService);
try {
bridgeService.parse(buildSource("table.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), DocumentParseScenario.WORKFLOW_TEXT);
Assert.fail("expected DocumentParseBridgeException");
} catch (DocumentParseBridgeException e) {
Assert.assertEquals("service_not_enabled", e.getCode());
Assert.assertTrue(e.getMessage().contains("XLSX"));
}
Assert.assertEquals(0, defaultService.parseCallCount);
}
private DocumentParseBridgeServiceImpl buildBridgeService(PdfDocumentParseService pdfDocumentParseService,
PptxDocumentParseService pptxDocumentParseService,
XlsxDocumentParseService xlsxDocumentParseService,
@@ -217,6 +272,45 @@ public class DocumentParseBridgeServiceImplTest {
}
}
private static class FakeDefaultDocumentParseService implements DocumentParseService<ParseRequest> {
private ParseRequest lastParseRequest;
private int parseCallCount;
@Override
public ParseResponse parse(ParseRequest request) {
parseCallCount++;
lastParseRequest = request;
ParseResult result = new ParseResult();
result.setFileName("demo.docx");
result.setMarkdown("# docx");
result.setPlainText("docx");
ParseResponse response = new ParseResponse();
response.setResults(Collections.singletonList(result));
return response;
}
@Override
public ParseTaskStatus submit(ParseRequest request) {
throw new UnsupportedOperationException();
}
@Override
public ParseTaskStatus queryTask(String taskId) {
throw new UnsupportedOperationException();
}
@Override
public ParseResponse queryResult(String taskId) {
throw new UnsupportedOperationException();
}
@Override
public ParseTaskInfo queryTaskInfo(String taskId) {
throw new UnsupportedOperationException();
}
}
private static class FakePptxDocumentParseService implements PptxDocumentParseService {
private int parseCallCount;

View File

@@ -48,10 +48,75 @@ public class DocNodeFileContentExtractorTest {
}
/**
* 验证非 PDF 文件会继续走默认读取器
* 验证 DOCX 文件会走统一文档解析桥接服务
*/
@Test
public void shouldUseDefaultReaderForNonPdf() {
public void shouldUseDocumentBridgeForDocx() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
bridgeService,
new FakeFileStorageService(),
new FakeReaderManager("ignored")
);
String content = extractor.extract(buildFileValue("demo.docx", "/files/demo.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
Assert.assertEquals("# parsed", content);
Assert.assertNotNull(bridgeService.lastSource);
Assert.assertEquals("demo.docx", bridgeService.lastSource.getFileName());
}
/**
* 验证 PPTX 文件会走统一文档解析桥接服务。
*/
@Test
public void shouldUseDocumentBridgeForPptx() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
bridgeService,
new FakeFileStorageService(),
new FakeReaderManager("ignored")
);
String content = extractor.extract(buildFileValue(
"slides.pptx",
"/files/slides.pptx",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
));
Assert.assertEquals("# parsed", content);
Assert.assertNotNull(bridgeService.lastSource);
Assert.assertEquals("slides.pptx", bridgeService.lastSource.getFileName());
}
/**
* 验证 XLSX 文件会走统一文档解析桥接服务。
*/
@Test
public void shouldUseDocumentBridgeForXlsx() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
bridgeService,
new FakeFileStorageService(),
new FakeReaderManager("ignored")
);
String content = extractor.extract(buildFileValue(
"table.xlsx",
"/files/table.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
));
Assert.assertEquals("# parsed", content);
Assert.assertNotNull(bridgeService.lastSource);
Assert.assertEquals("table.xlsx", bridgeService.lastSource.getFileName());
}
/**
* 验证非桥接类型文件会继续走默认读取器。
*/
@Test
public void shouldUseDefaultReaderForUnsupportedType() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
bridgeService,
@@ -59,7 +124,7 @@ public class DocNodeFileContentExtractorTest {
new FakeReaderManager("plain text")
);
String content = extractor.extract(buildFileValue("demo.docx", "/files/demo.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
String content = extractor.extract(buildFileValue("note.txt", "/files/note.txt", "text/plain"));
Assert.assertEquals("plain text", content);
Assert.assertNull(bridgeService.lastSource);
@@ -85,10 +150,10 @@ public class DocNodeFileContentExtractorTest {
}
/**
* 验证解析结果为空时不会回退旧 PDF 读取链路。
* 验证桥接解析结果为空时不会回退旧读取链路。
*/
@Test
public void shouldFailWhenPdfParseResultIsEmpty() {
public void shouldFailWhenBridgeParseResultIsEmpty() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
bridgeService.response.setPreferredText(null);
bridgeService.response.setMarkdown(null);
@@ -103,15 +168,15 @@ public class DocNodeFileContentExtractorTest {
extractor.extract(buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"));
Assert.fail("expected BusinessException");
} catch (BusinessException e) {
Assert.assertEquals("PDF 文档解析结果为空", e.getMessage());
Assert.assertEquals("文档解析结果为空", e.getMessage());
}
}
/**
* 验证远端素材 URL 的非 PDF 文件不会误走本地存储读取。
* 验证远端素材 URL 的非桥接文件不会误走本地存储读取。
*/
@Test
public void shouldReadRemoteUrlForNonPdf() {
public void shouldReadRemoteUrlForUnsupportedType() {
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
HttpServer server;
try {
@@ -120,7 +185,7 @@ public class DocNodeFileContentExtractorTest {
throw new RuntimeException(e);
}
byte[] body = "remote text".getBytes(StandardCharsets.UTF_8);
server.createContext("/demo.docx", exchange -> {
server.createContext("/note.txt", exchange -> {
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
@@ -134,9 +199,9 @@ public class DocNodeFileContentExtractorTest {
);
String content = extractor.extract(buildFileValue(
"demo.docx",
"http://127.0.0.1:" + server.getAddress().getPort() + "/demo.docx",
""
"note.txt",
"http://127.0.0.1:" + server.getAddress().getPort() + "/note.txt",
"text/plain"
));
Assert.assertEquals("remote text", content);