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,115 @@
package tech.easyflow.agent.config;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Agent 对话体验配置的规范化与校验工具。
*/
public final class AgentInteractionConfigSupport {
/** 欢迎语最大字符数。 */
public static final int MAX_WELCOME_MESSAGE_LENGTH = 300;
/** 猜你想问最大数量。 */
public static final int MAX_SUGGESTED_QUESTION_COUNT = 6;
/** 单条猜你想问最大字符数。 */
public static final int MAX_SUGGESTED_QUESTION_LENGTH = 80;
/** 输入提示最大字符数。 */
public static final int MAX_INPUT_PLACEHOLDER_LENGTH = 40;
private AgentInteractionConfigSupport() {
}
/**
* 规范化并校验对话体验配置。
*
* @param source 原始配置
* @return 仅包含受支持字段的稳定配置
* @throws BusinessException 配置字段类型或内容不合法时抛出
*/
public static Map<String, Object> normalize(Map<String, Object> source) {
Map<String, Object> config = source == null ? Map.of() : source;
String welcomeMessage = optionalString(config.get("welcomeMessage"), "欢迎语");
if (welcomeMessage.length() > MAX_WELCOME_MESSAGE_LENGTH) {
throw new BusinessException("欢迎语不能超过 300 个字符");
}
String inputPlaceholder = optionalString(config.get("inputPlaceholder"), "输入提示");
if (inputPlaceholder.contains("\n") || inputPlaceholder.contains("\r")) {
throw new BusinessException("输入提示仅支持单行文本");
}
if (inputPlaceholder.length() > MAX_INPUT_PLACEHOLDER_LENGTH) {
throw new BusinessException("输入提示不能超过 40 个字符");
}
List<String> suggestedQuestions = normalizeSuggestedQuestions(config.get("suggestedQuestions"));
Map<String, Object> normalized = new LinkedHashMap<>();
normalized.put("welcomeMessage", welcomeMessage);
normalized.put("suggestedQuestions", suggestedQuestions);
normalized.put("inputPlaceholder", inputPlaceholder);
return normalized;
}
/**
* 规范化猜你想问列表,过滤空项并保留原有顺序。
*
* @param value 原始列表值
* @return 规范化后的问题列表
* @throws BusinessException 列表类型、数量、长度或重复性不合法时抛出
*/
private static List<String> normalizeSuggestedQuestions(Object value) {
if (value == null) {
return List.of();
}
if (!(value instanceof Collection<?> values)) {
throw new BusinessException("猜你想问格式不正确");
}
List<String> normalized = new ArrayList<>();
Set<String> uniqueQuestions = new LinkedHashSet<>();
for (Object item : values) {
if (!(item instanceof String question)) {
throw new BusinessException("猜你想问仅支持文本内容");
}
String trimmedQuestion = question.trim();
if (trimmedQuestion.isEmpty()) {
continue;
}
if (trimmedQuestion.length() > MAX_SUGGESTED_QUESTION_LENGTH) {
throw new BusinessException("单条猜你想问不能超过 80 个字符");
}
if (!uniqueQuestions.add(trimmedQuestion)) {
throw new BusinessException("猜你想问不能重复");
}
normalized.add(trimmedQuestion);
}
if (normalized.size() > MAX_SUGGESTED_QUESTION_COUNT) {
throw new BusinessException("猜你想问最多配置 6 条");
}
return normalized;
}
/**
* 将可选字段转换为去除首尾空白的文本。
*
* @param value 原始字段值
* @param fieldName 字段名称
* @return 规范化后的文本
* @throws BusinessException 字段不是文本时抛出
*/
private static String optionalString(Object value, String fieldName) {
if (value == null) {
return "";
}
if (!(value instanceof String text)) {
throw new BusinessException(fieldName + "格式不正确");
}
return text.trim();
}
}

View File

@@ -43,6 +43,9 @@ public class Agent extends DateEntity implements VisibilityResource, Serializabl
private Map<String, Object> memoryConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> executionConfigJson = new LinkedHashMap<>();
/** 对话欢迎语、猜你想问和输入提示配置。 */
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> interactionConfigJson = new LinkedHashMap<>();
private Integer status;
private String visibilityScope;
private String publishStatus;
@@ -95,6 +98,18 @@ public class Agent extends DateEntity implements VisibilityResource, Serializabl
public void setMemoryConfigJson(Map<String, Object> memoryConfigJson) { this.memoryConfigJson = memoryConfigJson == null ? new LinkedHashMap<>() : memoryConfigJson; }
public Map<String, Object> getExecutionConfigJson() { return executionConfigJson; }
public void setExecutionConfigJson(Map<String, Object> executionConfigJson) { this.executionConfigJson = executionConfigJson == null ? new LinkedHashMap<>() : executionConfigJson; }
/**
* 获取对话体验配置。
*
* @return 对话体验配置
*/
public Map<String, Object> getInteractionConfigJson() { return interactionConfigJson; }
/**
* 设置对话体验配置。
*
* @param interactionConfigJson 对话体验配置
*/
public void setInteractionConfigJson(Map<String, Object> interactionConfigJson) { this.interactionConfigJson = interactionConfigJson == null ? new LinkedHashMap<>() : interactionConfigJson; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getVisibilityScope() { return visibilityScope; }

View File

@@ -2,6 +2,7 @@ package tech.easyflow.agent.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
@@ -122,32 +123,33 @@ public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHand
@Override
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(publishStatus.getCode());
agent.setCurrentApprovalInstanceId(currentApprovalInstanceId);
agentService.updateById(agent);
// 生命周期操作仅更新状态字段,避免实体默认空配置覆盖 Agent 草稿配置。
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, publishStatus.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, currentApprovalInstanceId);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
}
@Override
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
agent.setPublishedSnapshotJson(resourceSnapshot);
agent.setPublishedAt(new Date());
agent.setPublishedBy(operatorId);
agent.setCurrentApprovalInstanceId(null);
agentService.updateById(agent);
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, PublishStatus.PUBLISHED.getCode());
updateChain.set(Agent::getPublishedSnapshotJson, resourceSnapshot);
updateChain.set(Agent::getPublishedAt, new Date());
updateChain.set(Agent::getPublishedBy, operatorId);
updateChain.set(Agent::getCurrentApprovalInstanceId, null);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
}
@Override
protected void markResourceOffline(BigInteger resourceId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(PublishStatus.OFFLINE.getCode());
agent.setCurrentApprovalInstanceId(null);
agentService.updateById(agent);
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, PublishStatus.OFFLINE.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, null);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
}
@Override

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.agent.config.AgentInteractionConfigSupport;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
@@ -133,6 +134,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
snapshot.put("promptConfigJson", detail.getPromptConfigJson());
snapshot.put("memoryConfigJson", detail.getMemoryConfigJson());
snapshot.put("executionConfigJson", detail.getExecutionConfigJson());
snapshot.put("interactionConfigJson", detail.getInteractionConfigJson());
snapshot.put("visibilityScope", detail.getVisibilityScope());
snapshot.put("toolBindings", snapshotToolBindings(detail.getToolBindings()));
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail.getKnowledgeBindings()));
@@ -162,6 +164,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
agent.setModelId(toBigInteger(snapshot.get("modelId")));
agent.setCategoryId(toBigInteger(snapshot.get("categoryId")));
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
agent.setPublishedSnapshotJson(snapshot);
agent.setToolBindings(objectMapper.convertValue(snapshot.get("toolBindings"), TOOL_BINDING_LIST_TYPE));
agent.setKnowledgeBindings(objectMapper.convertValue(snapshot.get("knowledgeBindings"), KNOWLEDGE_BINDING_LIST_TYPE));
@@ -191,6 +194,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
throw new BusinessException("Agent 模型不存在");
}
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
}
private void applyDraftDefaults(Agent agent) {
@@ -231,6 +235,7 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
existing.setPromptConfigJson(incoming.getPromptConfigJson());
existing.setMemoryConfigJson(incoming.getMemoryConfigJson());
existing.setExecutionConfigJson(incoming.getExecutionConfigJson());
existing.setInteractionConfigJson(incoming.getInteractionConfigJson());
existing.setStatus(incoming.getStatus() == null ? 1 : incoming.getStatus());
existing.setVisibilityScope(incoming.getVisibilityScope());
existing.setModified(new Date());