feat: 完善知识库批量导入与公共 API

- 新增批量异步导入、状态查询、失败重试与中断恢复链路

- 拆分知识库读取、导入、维护权限并完善 Public API 契约

- 补充数据库迁移、管理端交互、接口说明与相关测试
This commit is contained in:
2026-08-03 11:13:48 +08:00
parent 6df3dd9981
commit 51dbfd41b6
99 changed files with 16274 additions and 480 deletions

View File

@@ -0,0 +1,64 @@
package tech.easyflow.publicapi.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.documentimport.PublicDocumentImportDtos;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.publicapi.interceptor.PublicApiInterceptor;
import tech.easyflow.system.entity.SysApiKey;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
/**
* {@link PublicKnowledgeDocumentImportController} 请求边界测试。
*
* @author Codex
* @since 2026-08-02
*/
public class PublicKnowledgeDocumentImportControllerTest {
/**
* 验证重试请求缺少 taskId 时返回明确参数错误。
*/
@Test
public void retryShouldRejectMissingTaskId() {
SysApiKey token = new SysApiKey();
token.setId(BigInteger.ONE);
HttpServletRequest servletRequest = (HttpServletRequest) Proxy.newProxyInstance(
HttpServletRequest.class.getClassLoader(),
new Class<?>[]{HttpServletRequest.class},
(instance, method, args) -> {
if ("getAttribute".equals(method.getName())) {
Assert.assertEquals(
PublicApiInterceptor.AUTHENTICATED_API_KEY_ATTRIBUTE,
args[0]
);
return token;
}
throw new AssertionError(
"测试路径不应调用 HttpServletRequest." + method.getName()
);
}
);
PublicKnowledgeDocumentImportController controller =
new PublicKnowledgeDocumentImportController(
null,
null,
null,
null,
null
);
PublicDocumentImportDtos.RetryRequest request =
new PublicDocumentImportDtos.RetryRequest();
try {
controller.retry("test-key", request, servletRequest);
Assert.fail("Expected missing taskId rejection");
} catch (BusinessException expected) {
Assert.assertEquals(400, expected.getHttpStatus());
Assert.assertTrue(expected.getMessage().contains("taskId 不能为空"));
}
}
}

View File

@@ -0,0 +1,192 @@
package tech.easyflow.publicapi.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.web.bind.annotation.RequestParam;
import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.publicapi.dto.PublicKnowledgeSearchResultItem;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.math.BigInteger;
import java.util.List;
/**
* {@link PublicKnowledgeShareController} 公开接口参数契约测试。
*/
public class PublicKnowledgeShareControllerContractTest {
/**
* 验证文档删除接口的 knowledgeId 和 id 均从 JSON 请求体读取。
*
* @throws Exception 反射失败
*/
@Test
public void documentRemoveShouldReadIdentifiersFromJsonBody()
throws Exception {
Method method = PublicKnowledgeShareController.class.getDeclaredMethod(
"removeDocument",
String.class,
BigInteger.class,
String.class,
HttpServletRequest.class
);
Parameter knowledgeId = method.getParameters()[1];
Parameter documentId = method.getParameters()[2];
assertRequiredJsonField(knowledgeId, "knowledgeId");
assertRequiredJsonField(documentId, "id");
Assert.assertNull(knowledgeId.getAnnotation(RequestParam.class));
}
/**
* 验证文档分块更新接口从 JSON 请求体读取 knowledgeId。
*
* @throws Exception 反射失败
*/
@Test
public void documentChunkUpdateShouldReadKnowledgeIdFromJsonBody()
throws Exception {
Method method = PublicKnowledgeShareController.class.getDeclaredMethod(
"updateDocumentChunk",
String.class,
BigInteger.class,
DocumentChunk.class,
HttpServletRequest.class
);
Parameter knowledgeId = method.getParameters()[1];
Parameter chunk = method.getParameters()[2];
assertRequiredJsonField(knowledgeId, "knowledgeId");
Assert.assertNull(knowledgeId.getAnnotation(RequestParam.class));
JsonBody chunkBody = chunk.getAnnotation(JsonBody.class);
Assert.assertNotNull(chunkBody);
Assert.assertEquals("", chunkBody.value());
}
/**
* 验证文档分块删除接口从 JSON 请求体读取 knowledgeId 和 id。
*
* @throws Exception 反射失败
*/
@Test
public void documentChunkRemoveShouldReadIdentifiersFromJsonBody()
throws Exception {
Method method = PublicKnowledgeShareController.class.getDeclaredMethod(
"removeDocumentChunk",
String.class,
BigInteger.class,
BigInteger.class,
HttpServletRequest.class
);
Parameter knowledgeId = method.getParameters()[1];
Parameter chunkId = method.getParameters()[2];
assertRequiredJsonField(knowledgeId, "knowledgeId");
assertRequiredJsonField(chunkId, "id");
Assert.assertNull(knowledgeId.getAnnotation(RequestParam.class));
}
/**
* 验证文档检索结果包含来源文档信息且不混入 FAQ 字段。
*
* @throws Exception 测试依赖注入失败
*/
@Test
public void searchShouldExposeDocumentIdentity() throws Exception {
BigInteger documentId = BigInteger.valueOf(2002);
com.easyagents.core.document.Document hit =
com.easyagents.core.document.Document.of("命中文本");
hit.addMetadata("resultType", DocumentCollection.TYPE_DOCUMENT);
hit.addMetadata("documentId", documentId);
hit.addMetadata("sourceFileName", "manual.pdf");
List<PublicKnowledgeSearchResultItem> results =
mapSearchResult(hit);
Assert.assertEquals(1, results.size());
PublicKnowledgeSearchResultItem item = results.get(0);
Assert.assertEquals(DocumentCollection.TYPE_DOCUMENT, item.getResultType());
Assert.assertEquals(documentId, item.getDocumentId());
Assert.assertEquals("manual.pdf", item.getDocumentName());
Assert.assertEquals("manual.pdf", item.getSourceFileName());
Assert.assertNull(item.getFaqId());
Assert.assertNull(item.getQuestion());
String json = new ObjectMapper().writeValueAsString(item);
Assert.assertFalse(json.contains("\"faqId\""));
Assert.assertFalse(json.contains("\"question\""));
}
/**
* 验证 FAQ 检索结果包含 FAQ 信息且不混入文档字段。
*
* @throws Exception 测试依赖注入失败
*/
@Test
public void searchShouldExposeFaqIdentity() throws Exception {
BigInteger faqId = BigInteger.valueOf(3003);
BigInteger categoryId = BigInteger.valueOf(4004);
com.easyagents.core.document.Document hit =
com.easyagents.core.document.Document.of("FAQ 命中文本");
hit.addMetadata("resultType", DocumentCollection.TYPE_FAQ);
hit.addMetadata("faqId", faqId);
hit.addMetadata("question", "如何申请账号?");
hit.addMetadata("answerText", "请联系管理员。");
hit.addMetadata("categoryId", categoryId);
List<PublicKnowledgeSearchResultItem> results =
mapSearchResult(hit);
Assert.assertEquals(1, results.size());
PublicKnowledgeSearchResultItem item = results.get(0);
Assert.assertEquals(DocumentCollection.TYPE_FAQ, item.getResultType());
Assert.assertEquals(faqId, item.getFaqId());
Assert.assertEquals("如何申请账号?", item.getQuestion());
Assert.assertEquals("请联系管理员。", item.getAnswerText());
Assert.assertEquals(categoryId, item.getCategoryId());
Assert.assertNull(item.getDocumentId());
Assert.assertNull(item.getDocumentName());
String json = new ObjectMapper().writeValueAsString(item);
Assert.assertFalse(json.contains("\"documentId\""));
Assert.assertFalse(json.contains("\"documentName\""));
Assert.assertFalse(json.contains("\"sourceFileName\""));
}
/**
* 调用公开检索结果映射。
*
* @param hit 检索命中
* @return 公开检索结果
* @throws Exception 反射调用失败
*/
@SuppressWarnings("unchecked")
private List<PublicKnowledgeSearchResultItem> mapSearchResult(
com.easyagents.core.document.Document hit
) throws Exception {
Method method = PublicKnowledgeShareController.class
.getDeclaredMethod(
"toKnowledgeSearchResult",
List.class
);
method.setAccessible(true);
return (List<PublicKnowledgeSearchResultItem>) method.invoke(
new PublicKnowledgeShareController(),
List.of(hit)
);
}
/**
* 断言参数绑定到指定的必填 JSON 字段。
*
* @param parameter 方法参数
* @param field JSON 字段名
*/
private void assertRequiredJsonField(Parameter parameter, String field) {
JsonBody jsonBody = parameter.getAnnotation(JsonBody.class);
Assert.assertNotNull(jsonBody);
Assert.assertEquals(field, jsonBody.value());
Assert.assertTrue(jsonBody.required());
}
}