feat: 完成S02桥接能力并接通M09工作流文档解析闭环
- 新增统一文档解析桥接子域,封装 easy-agents 文档解析门面 - 支持工作流开始节点文件上传与素材选择的单文件对象输入 - DocNode 改为文档解析节点,PDF 走统一解析,非 PDF 保持默认读取
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package tech.easyflow.ai.document.service.impl;
|
||||
|
||||
import com.easyagents.document.core.DocumentParseService;
|
||||
import com.easyagents.document.core.model.ParseRequest;
|
||||
import com.easyagents.document.core.model.ParseResponse;
|
||||
import com.easyagents.document.core.model.ParseResult;
|
||||
import com.easyagents.document.core.model.ParseTaskInfo;
|
||||
import com.easyagents.document.core.model.ParseTaskStatus;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
import tech.easyflow.ai.document.support.DocumentSourceLoader;
|
||||
import tech.easyflow.ai.document.support.DocumentParseRequestFactory;
|
||||
import tech.easyflow.ai.document.support.DocumentParseResultMapper;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link DocumentParseBridgeServiceImpl} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseBridgeServiceImplTest {
|
||||
|
||||
/**
|
||||
* 验证同步解析成功透传并返回标准化结果。
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseSuccessfully() {
|
||||
FakeDocumentParseService parseService = new FakeDocumentParseService();
|
||||
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(parseService);
|
||||
|
||||
DocumentParsedResult document = bridgeService.parse(buildSource(), DocumentParseScenario.WORKFLOW_TEXT);
|
||||
|
||||
Assert.assertEquals("# demo", document.getPreferredText());
|
||||
Assert.assertFalse(parseService.lastParseRequest.getReturnMiddleJson());
|
||||
Assert.assertFalse(parseService.lastParseRequest.getReturnImages());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证异步提交、状态查询和结果查询链路可用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldSupportAsyncFlow() {
|
||||
FakeDocumentParseService parseService = new FakeDocumentParseService();
|
||||
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(parseService);
|
||||
|
||||
DocumentParseTaskStatus taskStatus = bridgeService.submit(buildSource(), DocumentParseScenario.KNOWLEDGE_IMPORT);
|
||||
DocumentParseTaskStatus queriedStatus = bridgeService.queryTask("task-1");
|
||||
DocumentParsedResult queriedResult = bridgeService.queryResult("task-1");
|
||||
|
||||
Assert.assertEquals("task-1", taskStatus.getTaskId());
|
||||
Assert.assertEquals("running", queriedStatus.getStatus());
|
||||
Assert.assertEquals("# demo", queriedResult.getPreferredText());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证聚合查询在完成状态下会附带标准化结果。
|
||||
*/
|
||||
@Test
|
||||
public void shouldQueryTaskInfoSuccessfully() {
|
||||
FakeDocumentParseService parseService = new FakeDocumentParseService();
|
||||
parseService.taskStatusValue = "completed";
|
||||
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(parseService);
|
||||
|
||||
DocumentParseTaskInfo taskInfo = bridgeService.queryTaskInfo("task-1");
|
||||
|
||||
Assert.assertEquals("completed", taskInfo.getStatus());
|
||||
Assert.assertNotNull(taskInfo.getResult());
|
||||
Assert.assertEquals("# demo", taskInfo.getResult().getPreferredText());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证缺少底层服务时抛出稳定错误码。
|
||||
*/
|
||||
@Test
|
||||
public void shouldThrowWhenServiceDisabled() {
|
||||
DocumentParseBridgeServiceImpl bridgeService = buildBridgeService(null);
|
||||
|
||||
try {
|
||||
bridgeService.parse(buildSource(), DocumentParseScenario.WORKFLOW_TEXT);
|
||||
Assert.fail("expected DocumentParseBridgeException");
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
Assert.assertEquals("service_not_enabled", e.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentParseBridgeServiceImpl buildBridgeService(DocumentParseService parseService) {
|
||||
return new DocumentParseBridgeServiceImpl(
|
||||
parseService,
|
||||
new DocumentSourceLoader(new InMemoryFileStorageService()),
|
||||
new DocumentParseRequestFactory(),
|
||||
new DocumentParseResultMapper()
|
||||
);
|
||||
}
|
||||
|
||||
private DocumentSourceRef buildSource() {
|
||||
DocumentSourceRef sourceRef = DocumentSourceRef.ofBytes("demo.pdf", "pdf-data".getBytes(StandardCharsets.UTF_8));
|
||||
sourceRef.setContentType("application/pdf");
|
||||
sourceRef.setSize(8L);
|
||||
return sourceRef;
|
||||
}
|
||||
|
||||
private static class InMemoryFileStorageService implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(org.springframework.web.multipart.MultipartFile file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) {
|
||||
return new ByteArrayInputStream("pdf-data".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return 8L;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeDocumentParseService implements DocumentParseService {
|
||||
|
||||
private ParseRequest lastParseRequest;
|
||||
private String taskStatusValue = "running";
|
||||
|
||||
@Override
|
||||
public ParseResponse parse(ParseRequest request) {
|
||||
this.lastParseRequest = request;
|
||||
return buildResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseTaskStatus submit(ParseRequest request) {
|
||||
this.lastParseRequest = request;
|
||||
ParseTaskStatus status = new ParseTaskStatus();
|
||||
status.setTaskId("task-1");
|
||||
status.setStatus("submitted");
|
||||
status.setFileNames(Collections.singletonList("demo.pdf"));
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseTaskStatus queryTask(String taskId) {
|
||||
ParseTaskStatus status = new ParseTaskStatus();
|
||||
status.setTaskId(taskId);
|
||||
status.setStatus(taskStatusValue);
|
||||
status.setFileNames(Collections.singletonList("demo.pdf"));
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseResponse queryResult(String taskId) {
|
||||
return buildResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseTaskInfo queryTaskInfo(String taskId) {
|
||||
ParseTaskInfo taskInfo = ParseTaskInfo.fromStatus(queryTask(taskId));
|
||||
if ("completed".equals(taskStatusValue)) {
|
||||
taskInfo.setResult(buildResponse());
|
||||
}
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
private ParseResponse buildResponse() {
|
||||
ParseResult result = new ParseResult();
|
||||
result.setFileName("demo.pdf");
|
||||
result.setMarkdown("# demo");
|
||||
result.setPlainText("demo");
|
||||
ParseResponse response = new ParseResponse();
|
||||
response.setResults(Collections.singletonList(result));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import com.easyagents.document.core.model.ParseRequest;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.document.model.DocumentParseScenario;
|
||||
|
||||
/**
|
||||
* {@link DocumentParseRequestFactory} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseRequestFactoryTest {
|
||||
|
||||
/**
|
||||
* 验证工作流文本场景只请求最小文本结果。
|
||||
*/
|
||||
@Test
|
||||
public void shouldBuildWorkflowTextScenarioRequest() {
|
||||
DocumentParseRequestFactory factory = new DocumentParseRequestFactory();
|
||||
|
||||
ParseRequest request = factory.build(buildSource(), DocumentParseScenario.WORKFLOW_TEXT);
|
||||
|
||||
Assert.assertNull(request.getParseMethod());
|
||||
Assert.assertNull(request.getFormulaEnabled());
|
||||
Assert.assertNull(request.getTableEnabled());
|
||||
Assert.assertTrue(request.getReturnMarkdown());
|
||||
Assert.assertFalse(request.getReturnMiddleJson());
|
||||
Assert.assertFalse(request.getReturnContentList());
|
||||
Assert.assertFalse(request.getReturnImages());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证知识库导入场景保留结构化工件。
|
||||
*/
|
||||
@Test
|
||||
public void shouldBuildKnowledgeImportScenarioRequest() {
|
||||
DocumentParseRequestFactory factory = new DocumentParseRequestFactory();
|
||||
|
||||
ParseRequest request = factory.build(buildSource(), DocumentParseScenario.KNOWLEDGE_IMPORT);
|
||||
|
||||
Assert.assertTrue(request.getReturnMarkdown());
|
||||
Assert.assertTrue(request.getReturnMiddleJson());
|
||||
Assert.assertTrue(request.getReturnContentList());
|
||||
Assert.assertTrue(request.getReturnImages());
|
||||
}
|
||||
|
||||
private LoadedDocumentSource buildSource() {
|
||||
LoadedDocumentSource source = new LoadedDocumentSource();
|
||||
source.setFileName("demo.pdf");
|
||||
source.setContentType("application/pdf");
|
||||
source.setContentBytes("pdf-data".getBytes());
|
||||
source.setSize(8L);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import com.easyagents.document.core.model.ParseArtifacts;
|
||||
import com.easyagents.document.core.model.ParseResult;
|
||||
import com.easyagents.document.core.model.ParseResponse;
|
||||
import com.easyagents.document.core.model.ParseTaskInfo;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link DocumentParseResultMapper} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentParseResultMapperTest {
|
||||
|
||||
/**
|
||||
* 验证 preferredText 按 markdown 优先、plainText 回退。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreferMarkdown() {
|
||||
DocumentParseResultMapper mapper = new DocumentParseResultMapper();
|
||||
ParseResult result = new ParseResult();
|
||||
result.setFileName("demo.pdf");
|
||||
result.setMarkdown("# title");
|
||||
result.setPlainText("plain");
|
||||
|
||||
DocumentParsedResult mapped = mapper.map(result);
|
||||
|
||||
Assert.assertEquals("# title", mapped.getPreferredText());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证结构化工件被正确映射。
|
||||
*/
|
||||
@Test
|
||||
public void shouldMapArtifacts() {
|
||||
DocumentParseResultMapper mapper = new DocumentParseResultMapper();
|
||||
ParseResult result = new ParseResult();
|
||||
ParseArtifacts artifacts = new ParseArtifacts();
|
||||
artifacts.setMiddleJson(Collections.singletonMap("page", 1));
|
||||
artifacts.setContentList(Collections.singletonList("block"));
|
||||
artifacts.setModelOutput(Collections.singletonMap("raw", "ok"));
|
||||
result.setArtifacts(artifacts);
|
||||
|
||||
DocumentParsedResult mapped = mapper.map(result);
|
||||
|
||||
Assert.assertNotNull(mapped.getArtifacts());
|
||||
Assert.assertEquals(Collections.singletonMap("page", 1), mapped.getArtifacts().getMiddleJson());
|
||||
Assert.assertEquals(Collections.singletonList("block"), mapped.getArtifacts().getContentList());
|
||||
Assert.assertEquals(Collections.singletonMap("raw", "ok"), mapped.getArtifacts().getModelOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务聚合结果被正确映射。
|
||||
*/
|
||||
@Test
|
||||
public void shouldMapTaskInfo() {
|
||||
DocumentParseResultMapper mapper = new DocumentParseResultMapper();
|
||||
ParseTaskInfo taskInfo = new ParseTaskInfo();
|
||||
taskInfo.setTaskId("task-1");
|
||||
taskInfo.setStatus("completed");
|
||||
|
||||
ParseResult result = new ParseResult();
|
||||
result.setFileName("demo.pdf");
|
||||
result.setMarkdown("# title");
|
||||
ParseResponse response = new ParseResponse();
|
||||
response.setResults(Collections.singletonList(result));
|
||||
taskInfo.setResult(response);
|
||||
|
||||
DocumentParseTaskInfo mapped = mapper.map(taskInfo);
|
||||
|
||||
Assert.assertEquals("task-1", mapped.getTaskId());
|
||||
Assert.assertNotNull(mapped.getResult());
|
||||
Assert.assertEquals("# title", mapped.getResult().getPreferredText());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package tech.easyflow.ai.document.support;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.document.exception.DocumentParseBridgeException;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
/**
|
||||
* {@link DocumentSourceLoader} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocumentSourceLoaderTest {
|
||||
|
||||
/**
|
||||
* 验证可从 filePath 正常读取文件内容。
|
||||
*
|
||||
* @throws IOException 测试读取异常
|
||||
*/
|
||||
@Test
|
||||
public void shouldLoadContentFromFilePath() throws IOException {
|
||||
DocumentSourceLoader loader = new DocumentSourceLoader(new FakeFileStorageService("demo-pdf".getBytes(StandardCharsets.UTF_8)));
|
||||
DocumentSourceRef sourceRef = DocumentSourceRef.ofPath("/attachment/test/demo.pdf");
|
||||
|
||||
LoadedDocumentSource loadedSource = loader.load(sourceRef);
|
||||
|
||||
Assert.assertEquals("demo.pdf", loadedSource.getFileName());
|
||||
Assert.assertEquals(8L, loadedSource.getSize().longValue());
|
||||
Assert.assertArrayEquals("demo-pdf".getBytes(StandardCharsets.UTF_8), loadedSource.getContentBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证缺少有效来源时抛出明确异常。
|
||||
*/
|
||||
@Test
|
||||
public void shouldThrowWhenSourceMissing() {
|
||||
DocumentSourceLoader loader = new DocumentSourceLoader(new FakeFileStorageService(new byte[0]));
|
||||
|
||||
try {
|
||||
loader.load(new DocumentSourceRef());
|
||||
Assert.fail("expected DocumentParseBridgeException");
|
||||
} catch (DocumentParseBridgeException e) {
|
||||
Assert.assertEquals("unsupported_source", e.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 filePath 为远端 URL 时不会误走存储读取。
|
||||
*/
|
||||
@Test
|
||||
public void shouldPreferRemoteDownloadWhenFilePathIsRemoteUrl() throws IOException {
|
||||
DocumentSourceLoader loader = new DocumentSourceLoader(new FailingFileStorageService());
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
byte[] body = "demo-pdf".getBytes(StandardCharsets.UTF_8);
|
||||
server.createContext("/demo.pdf", exchange -> {
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
DocumentSourceRef sourceRef = new DocumentSourceRef();
|
||||
sourceRef.setFileName("demo.pdf");
|
||||
sourceRef.setFilePath("http://127.0.0.1:" + server.getAddress().getPort() + "/demo.pdf");
|
||||
|
||||
LoadedDocumentSource loadedSource = loader.load(sourceRef);
|
||||
|
||||
Assert.assertEquals("demo.pdf", loadedSource.getFileName());
|
||||
Assert.assertArrayEquals(body, loadedSource.getContentBytes());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeFileStorageService implements FileStorageService {
|
||||
|
||||
private final byte[] content;
|
||||
|
||||
private FakeFileStorageService(byte[] content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(org.springframework.web.multipart.MultipartFile file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) {
|
||||
return new ByteArrayInputStream(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return content.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailingFileStorageService implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(org.springframework.web.multipart.MultipartFile file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) throws IOException {
|
||||
throw new IOException("should not read remote url from storage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
|
||||
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
|
||||
import tech.easyflow.ai.document.model.DocumentParsedResult;
|
||||
import tech.easyflow.ai.document.model.DocumentSourceRef;
|
||||
import tech.easyflow.ai.document.service.DocumentParseBridgeService;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
/**
|
||||
* {@link DocNodeFileContentExtractor} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-04-14
|
||||
*/
|
||||
public class DocNodeFileContentExtractorTest {
|
||||
|
||||
/**
|
||||
* 验证 PDF 文件会走统一文档解析桥接服务。
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseDocumentBridgeForPdf() {
|
||||
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
bridgeService,
|
||||
new FakeFileStorageService(),
|
||||
new FakeReaderManager("ignored")
|
||||
);
|
||||
|
||||
String content = extractor.extract(buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"));
|
||||
|
||||
Assert.assertEquals("# parsed", content);
|
||||
Assert.assertNotNull(bridgeService.lastSource);
|
||||
Assert.assertEquals("demo.pdf", bridgeService.lastSource.getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非 PDF 文件会继续走默认读取器。
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseDefaultReaderForNonPdf() {
|
||||
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
bridgeService,
|
||||
new FakeFileStorageService(),
|
||||
new FakeReaderManager("plain text")
|
||||
);
|
||||
|
||||
String content = extractor.extract(buildFileValue("demo.docx", "/files/demo.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
|
||||
|
||||
Assert.assertEquals("plain text", content);
|
||||
Assert.assertNull(bridgeService.lastSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证缺少 filePath 时会抛出明确异常。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectMissingFilePath() {
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
new RecordingDocumentParseBridgeService(),
|
||||
new FakeFileStorageService(),
|
||||
new FakeReaderManager("plain text")
|
||||
);
|
||||
|
||||
try {
|
||||
extractor.extract(buildFileValue("demo.pdf", null, "application/pdf"));
|
||||
Assert.fail("expected BusinessException");
|
||||
} catch (BusinessException e) {
|
||||
Assert.assertEquals("文件输入缺少 filePath", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证解析结果为空时不会回退旧 PDF 读取链路。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenPdfParseResultIsEmpty() {
|
||||
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
|
||||
bridgeService.response.setPreferredText(null);
|
||||
bridgeService.response.setMarkdown(null);
|
||||
bridgeService.response.setPlainText(null);
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
bridgeService,
|
||||
new FakeFileStorageService(),
|
||||
new FakeReaderManager("pdf fallback")
|
||||
);
|
||||
|
||||
try {
|
||||
extractor.extract(buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"));
|
||||
Assert.fail("expected BusinessException");
|
||||
} catch (BusinessException e) {
|
||||
Assert.assertEquals("PDF 文档解析结果为空", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证远端素材 URL 的非 PDF 文件不会误走本地存储读取。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReadRemoteUrlForNonPdf() {
|
||||
RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
|
||||
HttpServer server;
|
||||
try {
|
||||
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
byte[] body = "remote text".getBytes(StandardCharsets.UTF_8);
|
||||
server.createContext("/demo.docx", exchange -> {
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
exchange.getResponseBody().write(body);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
|
||||
bridgeService,
|
||||
new FailingFileStorageService(),
|
||||
new ReadingReaderManager()
|
||||
);
|
||||
|
||||
String content = extractor.extract(buildFileValue(
|
||||
"demo.docx",
|
||||
"http://127.0.0.1:" + server.getAddress().getPort() + "/demo.docx",
|
||||
""
|
||||
));
|
||||
|
||||
Assert.assertEquals("remote text", content);
|
||||
Assert.assertNull(bridgeService.lastSource);
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildFileValue(String fileName, String filePath, String contentType) {
|
||||
Map<String, Object> value = new HashMap<String, Object>();
|
||||
value.put("fileName", fileName);
|
||||
value.put("filePath", filePath);
|
||||
value.put("contentType", contentType);
|
||||
value.put("size", 16L);
|
||||
value.put("url", filePath);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static class RecordingDocumentParseBridgeService implements DocumentParseBridgeService {
|
||||
|
||||
private final DocumentParsedResult response = new DocumentParsedResult();
|
||||
private DocumentSourceRef lastSource;
|
||||
|
||||
private RecordingDocumentParseBridgeService() {
|
||||
response.setPreferredText("# parsed");
|
||||
response.setMarkdown("# parsed");
|
||||
response.setPlainText("parsed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentParsedResult parse(DocumentSourceRef source, tech.easyflow.ai.document.model.DocumentParseScenario scenario) {
|
||||
this.lastSource = source;
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentParseTaskStatus submit(DocumentSourceRef source, tech.easyflow.ai.document.model.DocumentParseScenario scenario) {
|
||||
return new DocumentParseTaskStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentParseTaskStatus queryTask(String taskId) {
|
||||
return new DocumentParseTaskStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentParsedResult queryResult(String taskId) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentParseTaskInfo queryTaskInfo(String taskId) {
|
||||
return new DocumentParseTaskInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeFileStorageService implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(org.springframework.web.multipart.MultipartFile file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(File file, String prePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) throws IOException {
|
||||
return new ByteArrayInputStream("doc".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return 3L;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakeReaderManager extends ReaderManager {
|
||||
|
||||
private final String content;
|
||||
|
||||
private FakeReaderManager(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadDocService getReader() {
|
||||
return (fileName, is) -> content;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReadingReaderManager extends ReaderManager {
|
||||
|
||||
@Override
|
||||
public ReadDocService getReader() {
|
||||
return (fileName, is) -> {
|
||||
try {
|
||||
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailingFileStorageService implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(org.springframework.web.multipart.MultipartFile file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(File file, String prePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readStream(String path) throws IOException {
|
||||
throw new IOException("should not read remote url from storage");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String path) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user