feat: 增加智能体对话体验配置

- 支持欢迎语、猜你想问和输入提示的编辑、草稿预览与发布态展示

- 补充配置校验、发布快照持久化和发布后回显修复
This commit is contained in:
2026-07-14 21:21:57 +08:00
parent ce8b4fb420
commit 705e0faab6
18 changed files with 1351 additions and 160 deletions

View File

@@ -0,0 +1,93 @@
package tech.easyflow.agent.config;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* {@link AgentInteractionConfigSupport} 单元测试。
*/
public class AgentInteractionConfigSupportTest {
/**
* 验证空配置可以转换为稳定结构。
*/
@Test
public void shouldNormalizeEmptyConfig() {
Map<String, Object> normalized = AgentInteractionConfigSupport.normalize(null);
Assert.assertEquals("", normalized.get("welcomeMessage"));
Assert.assertEquals(List.of(), normalized.get("suggestedQuestions"));
Assert.assertEquals("", normalized.get("inputPlaceholder"));
}
/**
* 验证空问题会被过滤,合法问题会去除首尾空白并保持顺序。
*/
@Test
public void shouldNormalizeSuggestedQuestions() {
Map<String, Object> source = new LinkedHashMap<>();
source.put("welcomeMessage", " 欢迎使用\n智能助手 ");
source.put("suggestedQuestions", List.of(" 第一个问题 ", "", " 第二个问题"));
source.put("inputPlaceholder", " 请输入问题 ");
Map<String, Object> normalized = AgentInteractionConfigSupport.normalize(source);
Assert.assertEquals("欢迎使用\n智能助手", normalized.get("welcomeMessage"));
Assert.assertEquals(List.of("第一个问题", "第二个问题"), normalized.get("suggestedQuestions"));
Assert.assertEquals("请输入问题", normalized.get("inputPlaceholder"));
}
/**
* 验证重复问题会被拒绝。
*/
@Test
public void shouldRejectDuplicateQuestionsAfterTrim() {
Map<String, Object> source = Map.of(
"suggestedQuestions", List.of("如何使用?", " 如何使用? ")
);
assertBusinessException(() -> AgentInteractionConfigSupport.normalize(source));
}
/**
* 验证多行输入提示会被拒绝。
*/
@Test
public void shouldRejectMultilinePlaceholder() {
Map<String, Object> source = Map.of("inputPlaceholder", "第一行\n第二行");
assertBusinessException(() -> AgentInteractionConfigSupport.normalize(source));
}
/**
* 验证问题数量不能超过上限。
*/
@Test
public void shouldRejectTooManyQuestions() {
Map<String, Object> source = Map.of(
"suggestedQuestions",
List.of("问题1", "问题2", "问题3", "问题4", "问题5", "问题6", "问题7")
);
assertBusinessException(() -> AgentInteractionConfigSupport.normalize(source));
}
/**
* 断言操作会抛出业务异常。
*
* @param action 待执行操作
*/
private void assertBusinessException(Runnable action) {
try {
action.run();
Assert.fail("应抛出 BusinessException");
} catch (BusinessException expected) {
Assert.assertNotNull(expected.getMessage());
}
}
}

View File

@@ -1,20 +1,86 @@
package tech.easyflow.agent.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.update.UpdateChain;
import com.mybatisflex.core.util.LambdaGetter;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.ai.enums.PublishStatus;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link AgentApprovalSubjectHandler} 单元测试。
*/
public class AgentApprovalSubjectHandlerTest {
/**
* 审批状态变更必须使用字段级更新,避免覆盖 Agent 草稿配置。
*/
@Test
public void persistResourceStateShouldUseSelectiveUpdate() {
AgentService agentService = mock(AgentService.class);
UpdateChain<Agent> updateChain = prepareUpdateChain(agentService);
AgentApprovalSubjectHandler handler = handler(agentService);
handler.persistResourceState(
BigInteger.valueOf(1001),
PublishStatus.PUBLISH_PENDING,
BigInteger.valueOf(2001)
);
verifySelectiveUpdate(updateChain, BigInteger.valueOf(1001), 2);
verify(agentService, never()).updateById(any(Agent.class));
}
/**
* 发布 Agent 必须使用字段级更新,确保发布后草稿配置仍可回显。
*/
@Test
public void publishResourceShouldUseSelectiveUpdate() {
AgentService agentService = mock(AgentService.class);
UpdateChain<Agent> updateChain = prepareUpdateChain(agentService);
AgentApprovalSubjectHandler handler = handler(agentService);
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("interactionConfigJson", Map.of("welcomeMessage", "你好"));
handler.publishResource(BigInteger.valueOf(1001), snapshot, BigInteger.valueOf(3001));
verifySelectiveUpdate(updateChain, BigInteger.valueOf(1001), 5);
verify(agentService, never()).updateById(any(Agent.class));
}
/**
* Agent 下线必须使用字段级更新,避免覆盖未发布配置。
*/
@Test
public void markResourceOfflineShouldUseSelectiveUpdate() {
AgentService agentService = mock(AgentService.class);
UpdateChain<Agent> updateChain = prepareUpdateChain(agentService);
AgentApprovalSubjectHandler handler = handler(agentService);
handler.markResourceOffline(BigInteger.valueOf(1001));
verifySelectiveUpdate(updateChain, BigInteger.valueOf(1001), 2);
verify(agentService, never()).updateById(any(Agent.class));
}
/**
* 审批删除 Agent 前必须同步清理工具绑定和知识库绑定,避免留下孤儿数据。
*/
@@ -39,6 +105,60 @@ public class AgentApprovalSubjectHandlerTest {
Assert.assertEquals(1, knowledgeRemoveCalls.get());
}
/**
* 创建用于生命周期测试的处理器。
*
* @param agentService Agent 服务
* @return Agent 审批资源处理器
*/
private static AgentApprovalSubjectHandler handler(AgentService agentService) {
return new AgentApprovalSubjectHandler(
null,
new ObjectMapper(),
agentService,
null,
null,
null
);
}
/**
* 准备字段更新对象。
*
* @param agentService Agent 服务
* @return 字段更新对象
*/
@SuppressWarnings("unchecked")
private static UpdateChain<Agent> prepareUpdateChain(AgentService agentService) {
UpdateChain<Agent> updateChain = mock(UpdateChain.class);
when(agentService.updateChain()).thenReturn(updateChain);
return updateChain;
}
/**
* 校验生命周期操作仅通过指定主键执行字段更新。
*
* @param updateChain 字段更新对象
* @param resourceId Agent 主键
* @param fieldCount 更新字段数量
*/
@SuppressWarnings("unchecked")
private static void verifySelectiveUpdate(UpdateChain<Agent> updateChain,
BigInteger resourceId,
int fieldCount) {
verify(updateChain, times(fieldCount)).set(any(LambdaGetter.class), any());
verify(updateChain).eq(any(LambdaGetter.class), eq(resourceId));
verify(updateChain).update();
}
/**
* 创建只统计删除调用的服务代理。
*
* @param type 服务接口类型
* @param removeCalls 删除调用计数器
* @param <T> 服务接口类型
* @return 服务代理
*/
@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, AtomicInteger removeCalls) {
return (T) Proxy.newProxyInstance(