feat: 完成管理端聊天工作台收口

- 新增管理端聊天工作台与会话级额外知识库持久化

- 补齐发布态聊天、历史会话只读判断与答案版本切换

- 新增 chat_round 热数据与主线消息读取支撑
This commit is contained in:
2026-05-14 20:22:46 +08:00
parent 2ad8935a61
commit 47c2bad839
63 changed files with 8609 additions and 136 deletions

View File

@@ -121,11 +121,12 @@ public class BotServiceImplTest {
List.class,
Bot.class,
boolean.class,
ChatTimeToolAvailabilityContext.class
ChatTimeToolAvailabilityContext.class,
Set.class
);
method.setAccessible(true);
method.invoke(service, functionList, runtimeBot, false, buildContext(12, 3));
method.invoke(service, functionList, runtimeBot, false, buildContext(12, 3), new LinkedHashSet<>());
Assert.assertEquals(1, functionList.size());
DocumentCollectionTool tool = (DocumentCollectionTool) functionList.get(0);
@@ -133,6 +134,102 @@ public class BotServiceImplTest {
Assert.assertEquals(RetrievalMode.KEYWORD, tool.getRetrievalMode());
}
/**
* 额外知识库应按用户选择顺序去重,并限制最多 3 个。
*
* @throws Exception 反射调用异常
*/
@Test
@SuppressWarnings("unchecked")
public void sanitizeExtraKnowledgeIdsShouldDeduplicateAndKeepOrder() throws Exception {
BotServiceImpl service = new BotServiceImpl();
Method method = BotServiceImpl.class.getDeclaredMethod("sanitizeExtraKnowledgeIds", List.class);
method.setAccessible(true);
List<BigInteger> result = (List<BigInteger>) method.invoke(
service,
List.of(
BigInteger.valueOf(3),
BigInteger.valueOf(1),
BigInteger.valueOf(3),
BigInteger.valueOf(2)
)
);
Assert.assertEquals(
List.of(BigInteger.valueOf(3), BigInteger.ONE, BigInteger.valueOf(2)),
result
);
}
/**
* 额外知识库超过 3 个时应直接拒绝请求。
*
* @throws Exception 反射调用异常
*/
@Test
public void sanitizeExtraKnowledgeIdsShouldRejectWhenMoreThanThree() throws Exception {
BotServiceImpl service = new BotServiceImpl();
Method method = BotServiceImpl.class.getDeclaredMethod("sanitizeExtraKnowledgeIds", List.class);
method.setAccessible(true);
try {
method.invoke(
service,
List.of(
BigInteger.ONE,
BigInteger.valueOf(2),
BigInteger.valueOf(3),
BigInteger.valueOf(4)
)
);
Assert.fail("expected BusinessException");
} catch (java.lang.reflect.InvocationTargetException exception) {
Assert.assertTrue(exception.getTargetException() instanceof tech.easyflow.common.web.exceptions.BusinessException);
Assert.assertEquals("额外知识库最多选择 3 个", exception.getTargetException().getMessage());
}
}
/**
* 额外知识库工具应强制使用发布态视图,并跳过重复选择。
*
* @throws Exception 反射注入异常
*/
@Test
public void appendExtraKnowledgeToolsShouldUsePublishedKnowledgeAndDeduplicate() throws Exception {
BotServiceImpl service = new BotServiceImpl();
injectAvailabilityService(service, buildAvailabilityService(
new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
Collections.emptySet()
));
DocumentCollection draftKnowledge = buildKnowledge(101, 11, 21, "PUBLIC");
draftKnowledge.setPublishStatus("PUBLISHED");
draftKnowledge.setTitle("draft-title");
draftKnowledge.setDescription("draft-desc");
DocumentCollection publishedKnowledge = buildKnowledge(101, 11, 21, "PUBLIC");
publishedKnowledge.setPublishStatus("PUBLISHED");
publishedKnowledge.setTitle("published-title");
publishedKnowledge.setDescription("published-desc");
injectDocumentCollectionService(service, mockDocumentCollectionServiceForExtra(draftKnowledge, publishedKnowledge));
List<Tool> tools = new ArrayList<>();
service.appendExtraKnowledgeTools(
tools,
List.of(BigInteger.valueOf(101), BigInteger.valueOf(101)),
false,
buildContext(12, 3),
new LinkedHashSet<>()
);
Assert.assertEquals(1, tools.size());
DocumentCollectionTool tool = (DocumentCollectionTool) tools.get(0);
Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
Assert.assertEquals("published-desc", tool.getDescription());
}
private void injectAvailabilityService(BotServiceImpl service, ChatTimeToolAvailabilityService availabilityService) throws Exception {
Field field = BotServiceImpl.class.getDeclaredField("chatTimeToolAvailabilityService");
field.setAccessible(true);
@@ -189,6 +286,7 @@ public class BotServiceImplTest {
knowledge.setCreatedBy(BigInteger.valueOf(createdBy));
knowledge.setCategoryId(BigInteger.valueOf(categoryId));
knowledge.setVisibilityScope(visibilityScope);
knowledge.setPublishStatus("PUBLISHED");
return knowledge;
}
@@ -216,6 +314,26 @@ public class BotServiceImplTest {
);
}
private tech.easyflow.ai.service.DocumentCollectionService mockDocumentCollectionServiceForExtra(DocumentCollection draftCollection,
DocumentCollection publishedCollection) {
return (tech.easyflow.ai.service.DocumentCollectionService) Proxy.newProxyInstance(
tech.easyflow.ai.service.DocumentCollectionService.class.getClassLoader(),
new Class<?>[]{tech.easyflow.ai.service.DocumentCollectionService.class},
(proxy, method, args) -> {
if ("getById".equals(method.getName())) {
return draftCollection;
}
if ("toPublishedView".equals(method.getName())) {
return publishedCollection;
}
if ("getPublishedById".equals(method.getName())) {
return publishedCollection;
}
return defaultValue(method.getReturnType());
}
);
}
private CategoryPermissionService mockCategoryPermissionService(RoleCategoryAccessSnapshot accessSnapshot) {
return (CategoryPermissionService) Proxy.newProxyInstance(
CategoryPermissionService.class.getClassLoader(),