fix: 完成系统向智能体数据链路切换

- 切换工作台、聊天历史、资源候选与公共调用到 Agent

- 加固资源绑定、删除保护及发布运行并发控制

- 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
2026-07-31 14:24:15 +08:00
parent f0aba1eddd
commit f872eac1f9
114 changed files with 5997 additions and 874 deletions

View File

@@ -18,5 +18,10 @@ public enum AgentRuntimeCommandAction {
/**
* 审批过期并取消工具执行。
*/
EXPIRE
EXPIRE,
/**
* 取消指定 Agent 在目标节点上的全部运行。
*/
CANCEL_AGENT
}

View File

@@ -93,6 +93,8 @@ public class AgentRuntimeCommandConsumer implements MQConsumerHandler {
} else if (command.getAction() == AgentRuntimeCommandAction.EXPIRE) {
agentRunService.expireApprovalLocal(
command.getRequestId(), command.getResumeToken(), command.getReason());
} else if (command.getAction() == AgentRuntimeCommandAction.CANCEL_AGENT) {
agentRunService.cancelAgentLocal(command.getAgentId());
} else {
markFailureQuietly(command, new IllegalArgumentException("不支持的 Agent 远程运行命令"));
LOG.warn("跳过不支持的 Agent 远程运行命令: messageId={}, commandId={}, action={}",

View File

@@ -15,6 +15,7 @@ public class AgentRuntimeCommandMessage {
private String reason;
private BigInteger operatorId;
private String userId;
private String agentId;
private String targetNodeId;
private Date occurredAt;
@@ -74,6 +75,24 @@ public class AgentRuntimeCommandMessage {
this.userId = userId;
}
/**
* 获取待取消运行所属的 Agent ID。
*
* @return Agent ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置待取消运行所属的 Agent ID。
*
* @param agentId Agent ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
public String getTargetNodeId() {
return targetNodeId;
}

View File

@@ -69,7 +69,10 @@ public class AgentRuntimeCommandProducer {
String resumeToken,
BigInteger operatorId,
String userId) {
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.APPROVE, null, operatorId, userId);
sendAndWait(
targetNodeId, requestId, resumeToken, null,
AgentRuntimeCommandAction.APPROVE, null, operatorId, userId
);
}
/**
@@ -88,7 +91,10 @@ public class AgentRuntimeCommandProducer {
String reason,
BigInteger operatorId,
String userId) {
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.REJECT, reason, operatorId, userId);
sendAndWait(
targetNodeId, requestId, resumeToken, null,
AgentRuntimeCommandAction.REJECT, reason, operatorId, userId
);
}
/**
@@ -103,12 +109,43 @@ public class AgentRuntimeCommandProducer {
String requestId,
String resumeToken,
String reason) {
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.EXPIRE, reason, null, null);
sendAndWait(
targetNodeId, requestId, resumeToken, null,
AgentRuntimeCommandAction.EXPIRE, reason, null, null
);
}
/**
* 投递远程 Agent 全部运行取消命令。
*
* @param targetNodeId 目标节点 ID
* @param agentId Agent ID
* @param reason 取消原因
*/
public void sendCancelAgent(String targetNodeId, String agentId, String reason) {
sendAndWait(
targetNodeId, null, null, agentId,
AgentRuntimeCommandAction.CANCEL_AGENT, reason, null, null
);
}
/**
* 投递远程运行命令并等待目标节点确认。
*
* @param targetNodeId 目标节点 ID
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param agentId Agent ID
* @param action 命令动作
* @param reason 操作原因
* @param operatorId 操作人 ID
* @param userId 用户 ID
* @throws BusinessException 命令投递、处理或确认失败时抛出
*/
private void sendAndWait(String targetNodeId,
String requestId,
String resumeToken,
String agentId,
AgentRuntimeCommandAction action,
String reason,
BigInteger operatorId,
@@ -120,6 +157,7 @@ public class AgentRuntimeCommandProducer {
command.setCommandId(UUID.randomUUID().toString());
command.setRequestId(requestId);
command.setResumeToken(resumeToken);
command.setAgentId(agentId);
command.setAction(action);
command.setReason(reason);
command.setOperatorId(operatorId);
@@ -135,8 +173,8 @@ public class AgentRuntimeCommandProducer {
try {
message.setBody(objectMapper.writeValueAsString(command));
String recordId = mqProducer.send(message);
LOG.info("Agent 远程运行命令已投递: action={}, requestId={}, targetNodeId={}, recordId={}",
action, requestId, targetNodeId, recordId);
LOG.info("Agent 远程运行命令已投递: action={}, requestId={}, agentId={}, targetNodeId={}, recordId={}",
action, requestId, agentId, targetNodeId, recordId);
AgentRuntimeCommandResult result = resultRegistry.waitForResult(command.getCommandId());
if (!result.isSuccess()) {
throw new BusinessException(result.getMessage());
@@ -146,18 +184,29 @@ public class AgentRuntimeCommandProducer {
} catch (BusinessException e) {
throw e;
} catch (RuntimeException e) {
LOG.error("Agent 远程运行命令投递失败: action={}, requestId={}, targetNodeId={}",
action, requestId, targetNodeId, e);
LOG.error("Agent 远程运行命令投递失败: action={}, requestId={}, agentId={}, targetNodeId={}",
action, requestId, agentId, targetNodeId, e);
throw new BusinessException("Agent 运行节点不可用,请重新发起对话");
} finally {
deleteResultQuietly(command.getCommandId());
}
}
/**
* 构建目标节点命令主题。
*
* @param nodeId 节点 ID
* @return 命令主题
*/
private String commandTopic(String nodeId) {
return properties.getCommandTopicPrefix() + ":" + nodeId;
}
/**
* 清理远程命令确认结果,失败时由 Redis TTL 兜底。
*
* @param commandId 命令 ID
*/
private void deleteResultQuietly(String commandId) {
try {
resultRegistry.deleteResult(commandId);

View File

@@ -7,6 +7,7 @@ public class AgentRuntimeRoute {
private String nodeId;
private String bootId;
private String agentId;
/**
* 获取 owner 节点 ID。
@@ -43,4 +44,22 @@ public class AgentRuntimeRoute {
public void setBootId(String bootId) {
this.bootId = bootId;
}
/**
* 获取运行所属 Agent ID。
*
* @return Agent ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置运行所属 Agent ID。
*
* @param agentId Agent ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
}

View File

@@ -9,6 +9,9 @@ import org.springframework.stereotype.Component;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Agent 运行态 Redis 路由注册表。
@@ -21,6 +24,7 @@ public class AgentRuntimeRouteRegistry {
private static final String REQUEST_ROUTE_PREFIX = "easyflow:agent:runtime:request:";
private static final String TOKEN_ROUTE_PREFIX = "easyflow:agent:runtime:resume-token:";
private static final String NODE_HEARTBEAT_PREFIX = "easyflow:agent:runtime:node:";
private static final String AGENT_RUNS_PREFIX = "easyflow:agent:runtime:agent:";
private final StringRedisTemplate stringRedisTemplate;
private final AgentRuntimeProperties properties;
@@ -47,10 +51,29 @@ public class AgentRuntimeRouteRegistry {
* @param requestId 请求 ID
*/
public void registerRun(String requestId) {
registerRun(requestId, null);
}
/**
* 注册运行请求 owner 节点及所属 Agent。
*
* @param requestId 请求 ID
* @param agentId Agent ID
*/
public void registerRun(String requestId, String agentId) {
if (requestId == null || requestId.isBlank()) {
return;
}
stringRedisTemplate.opsForValue().set(requestKey(requestId), serializeRoute(currentRoute()), properties.getRouteTtl());
stringRedisTemplate.opsForValue().set(
requestKey(requestId),
serializeRoute(currentRoute(agentId)),
properties.getRouteTtl()
);
if (agentId != null && !agentId.isBlank()) {
String key = agentRunsKey(agentId);
stringRedisTemplate.opsForSet().add(key, requestId);
stringRedisTemplate.expire(key, properties.getRouteTtl());
}
}
/**
@@ -107,6 +130,35 @@ public class AgentRuntimeRouteRegistry {
return stringRedisTemplate.opsForValue().get(tokenKey(resumeToken));
}
/**
* 查询指定 Agent 当前活跃运行所在的节点。
*
* @param agentId Agent ID
* @return 去重后的 owner 节点 ID
*/
public Set<String> findOwnerNodesByAgent(String agentId) {
if (agentId == null || agentId.isBlank()) {
return Collections.emptySet();
}
Set<String> requestIds = stringRedisTemplate.opsForSet().members(agentRunsKey(agentId));
if (requestIds == null || requestIds.isEmpty()) {
return Collections.emptySet();
}
Set<String> nodeIds = new LinkedHashSet<>();
for (String requestId : requestIds) {
AgentRuntimeRoute route = findOwnerRoute(requestId);
if (route == null || route.getNodeId() == null || route.getNodeId().isBlank()
|| !agentId.equals(route.getAgentId())
|| route.getBootId() == null
|| !route.getBootId().equals(currentNodeBootId(route.getNodeId()))) {
removeAgentRunIndexQuietly(agentId, requestId);
continue;
}
nodeIds.add(route.getNodeId());
}
return nodeIds;
}
/**
* 删除指定运行请求的路由。
*
@@ -116,7 +168,16 @@ public class AgentRuntimeRouteRegistry {
if (requestId == null || requestId.isBlank()) {
return;
}
AgentRuntimeRoute route = null;
try {
route = findOwnerRoute(requestId);
} catch (RuntimeException exception) {
LOG.warn("读取待清理的 Agent 运行路由失败: requestId={}", requestId, exception);
}
deleteQuietly(requestKey(requestId));
if (route != null && route.getAgentId() != null && !route.getAgentId().isBlank()) {
removeAgentRunIndexQuietly(route.getAgentId(), requestId);
}
}
/**
@@ -184,10 +245,15 @@ public class AgentRuntimeRouteRegistry {
return NODE_HEARTBEAT_PREFIX + nodeId;
}
private AgentRuntimeRoute currentRoute() {
private String agentRunsKey(String agentId) {
return AGENT_RUNS_PREFIX + agentId;
}
private AgentRuntimeRoute currentRoute(String agentId) {
AgentRuntimeRoute route = new AgentRuntimeRoute();
route.setNodeId(properties.getInstanceId());
route.setBootId(properties.getBootId());
route.setAgentId(agentId);
return route;
}
@@ -219,4 +285,19 @@ public class AgentRuntimeRouteRegistry {
LOG.warn("清理 Agent 运行态 Redis 路由失败: key={}", key, e);
}
}
/**
* 从 Agent 反向运行索引中移除请求。
*
* @param agentId Agent ID
* @param requestId 请求 ID
*/
private void removeAgentRunIndexQuietly(String agentId, String requestId) {
try {
stringRedisTemplate.opsForSet().remove(agentRunsKey(agentId), requestId);
} catch (RuntimeException exception) {
LOG.warn("清理 Agent 运行反向索引失败: agentId={}, requestId={}",
agentId, requestId, exception);
}
}
}

View File

@@ -4,12 +4,17 @@ 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.distributed.AgentRuntimeCommandProducer;
import tech.easyflow.agent.distributed.AgentRuntimeRouteRegistry;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.runtime.AgentRunRegistry;
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.publish.AbstractAiResourceLifecycleHandler;
import tech.easyflow.approval.enums.ApprovalResourceType;
@@ -22,6 +27,7 @@ import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Date;
import java.util.Map;
import java.util.Set;
/**
* Agent 审批资源处理器。
@@ -33,6 +39,11 @@ public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHand
private final AgentToolBindingService agentToolBindingService;
private final AgentKnowledgeBindingService agentKnowledgeBindingService;
private final ResourceAccessService resourceAccessService;
private final AgentBindingLockExecutor agentBindingLockExecutor;
private final AgentRunRegistry agentRunRegistry;
private final AgentHitlPendingService agentHitlPendingService;
private final AgentRuntimeRouteRegistry agentRuntimeRouteRegistry;
private final AgentRuntimeCommandProducer agentRuntimeCommandProducer;
/**
* 创建 Agent 审批资源处理器。
@@ -43,18 +54,33 @@ public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHand
* @param agentToolBindingService Agent 工具绑定服务
* @param agentKnowledgeBindingService Agent 知识库绑定服务
* @param resourceAccessService 资源访问服务
* @param agentBindingLockExecutor Agent 配置锁执行器
* @param agentRunRegistry Agent 运行态注册表
* @param agentHitlPendingService Agent 待审批运行服务
* @param agentRuntimeRouteRegistry Agent 分布式运行路由注册表
* @param agentRuntimeCommandProducer Agent 远程运行命令生产者
*/
public AgentApprovalSubjectHandler(ApprovalInstanceService approvalInstanceService,
ObjectMapper objectMapper,
AgentService agentService,
AgentToolBindingService agentToolBindingService,
AgentKnowledgeBindingService agentKnowledgeBindingService,
ResourceAccessService resourceAccessService) {
ResourceAccessService resourceAccessService,
AgentBindingLockExecutor agentBindingLockExecutor,
AgentRunRegistry agentRunRegistry,
AgentHitlPendingService agentHitlPendingService,
AgentRuntimeRouteRegistry agentRuntimeRouteRegistry,
AgentRuntimeCommandProducer agentRuntimeCommandProducer) {
super(approvalInstanceService, objectMapper);
this.agentService = agentService;
this.agentToolBindingService = agentToolBindingService;
this.agentKnowledgeBindingService = agentKnowledgeBindingService;
this.resourceAccessService = resourceAccessService;
this.agentBindingLockExecutor = agentBindingLockExecutor;
this.agentRunRegistry = agentRunRegistry;
this.agentHitlPendingService = agentHitlPendingService;
this.agentRuntimeRouteRegistry = agentRuntimeRouteRegistry;
this.agentRuntimeCommandProducer = agentRuntimeCommandProducer;
}
/**
@@ -123,44 +149,76 @@ public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHand
@Override
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
// 生命周期操作仅更新状态字段,避免实体默认空配置覆盖 Agent 草稿配置。
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, publishStatus.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, currentApprovalInstanceId);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
agentBindingLockExecutor.execute(resourceId, () -> {
// 生命周期操作仅更新状态字段,避免实体默认空配置覆盖 Agent 草稿配置。
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, publishStatus.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, currentApprovalInstanceId);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
return null;
});
}
@Override
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
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();
agentBindingLockExecutor.execute(resourceId, () -> {
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();
return null;
});
}
@Override
protected void markResourceOffline(BigInteger resourceId) {
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, PublishStatus.OFFLINE.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, null);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
agentBindingLockExecutor.execute(resourceId, () -> {
UpdateChain<Agent> updateChain = agentService.updateChain();
updateChain.set(Agent::getPublishStatus, PublishStatus.OFFLINE.getCode());
updateChain.set(Agent::getCurrentApprovalInstanceId, null);
updateChain.eq(Agent::getId, resourceId);
updateChain.update();
cancelActiveRuns(resourceId, "Agent 已下线,待审批运行已取消");
return null;
});
}
@Override
protected void removeResource(BigInteger resourceId) {
agentService.removeById(resourceId);
agentBindingLockExecutor.execute(resourceId, () -> {
cancelActiveRuns(resourceId, "Agent 已删除,待审批运行已取消");
agentToolBindingService.remove(
QueryWrapper.create().eq(AgentToolBinding::getAgentId, resourceId));
agentKnowledgeBindingService.remove(
QueryWrapper.create().eq(AgentKnowledgeBinding::getAgentId, resourceId));
agentService.removeById(resourceId);
return null;
});
}
@Override
protected void beforeRemove(BigInteger resourceId) {
agentToolBindingService.remove(QueryWrapper.create().eq(AgentToolBinding::getAgentId, resourceId));
agentKnowledgeBindingService.remove(QueryWrapper.create().eq(AgentKnowledgeBinding::getAgentId, resourceId));
/**
* 取消指定 Agent 的集群运行态和持久化待审批请求。
*
* @param resourceId Agent ID
* @param reason 取消原因
*/
private void cancelActiveRuns(BigInteger resourceId, String reason) {
String agentId = resourceId.toString();
Set<String> ownerNodeIds = agentRuntimeRouteRegistry.findOwnerNodesByAgent(agentId);
agentHitlPendingService.cancelByAgentId(resourceId, reason);
agentRunRegistry.cancelAgent(agentId);
String currentNodeId = agentRuntimeRouteRegistry.currentNodeId();
for (String ownerNodeId : ownerNodeIds) {
if (ownerNodeId == null || ownerNodeId.isBlank() || ownerNodeId.equals(currentNodeId)) {
continue;
}
agentRuntimeCommandProducer.sendCancelAgent(ownerNodeId, agentId, reason);
}
}
@Override

View File

@@ -5,6 +5,7 @@ import tech.easyflow.ai.publish.AiResourceLifecycleService;
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.approval.enums.ApprovalActionType;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -57,15 +58,27 @@ public class AgentPublishAppService {
return submit(id, ApprovalActionType.DELETE);
}
/**
* 提交 Agent 生命周期审批。
*
* @param id Agent ID
* @param actionType 审批动作
* @return 审批动作结果
* @throws BusinessException 资源 ID 或登录信息无效时抛出
*/
private ApprovalActionResult submit(BigInteger id, ApprovalActionType actionType) {
if (id == null) {
throw new BusinessException("Agent 审批时资源ID不能为空");
}
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return aiResourceLifecycleService.submitAction(
ApprovalResourceType.AGENT.getCode(),
id,
actionType.getCode(),
SaTokenUtil.getLoginAccount().getId()
account.getId()
);
}
}

View File

@@ -16,6 +16,7 @@ import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -71,7 +72,10 @@ public class AgentRunRegistry {
}
owners.put(context.requestId(), context.owner());
if (routeRegistry != null) {
routeRegistry.registerRun(context.requestId());
routeRegistry.registerRun(
context.requestId(),
context.owner() == null ? null : context.owner().agentId()
);
}
}
@@ -132,6 +136,28 @@ public class AgentRunRegistry {
remove(requestId);
}
/**
* 取消当前节点上指定 Agent 的全部活跃运行。
*
* @param agentId Agent ID
*/
public void cancelAgent(String agentId) {
if (agentId == null || agentId.isBlank()) {
return;
}
for (Map.Entry<String, RunOwner> entry : new ArrayList<>(owners.entrySet())) {
RunOwner owner = entry.getValue();
if (owner == null || !agentId.equals(owner.agentId())) {
continue;
}
AgentRunContext context = runs.get(entry.getKey());
if (context != null) {
context.cancelAndComplete();
}
remove(entry.getKey());
}
}
/**
* 记录等待审批的恢复令牌。
*

View File

@@ -39,6 +39,7 @@ import tech.easyflow.agent.runtime.document.AgentDocumentContext;
import tech.easyflow.agent.runtime.document.AgentDocumentContextSelector;
import tech.easyflow.agent.runtime.document.AgentDocumentService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.PluginItem;
@@ -99,6 +100,10 @@ public class AgentRunService {
@Resource
private AgentRunRegistry agentRunRegistry;
@Resource
private AgentBindingLockExecutor agentBindingLockExecutor;
@Resource
private AgentRunStartGuard agentRunStartGuard;
@Resource
private AgentRuntimeRouteRegistry agentRuntimeRouteRegistry;
@Resource
private AgentRuntimeCommandProducer agentRuntimeCommandProducer;
@@ -183,6 +188,75 @@ public class AgentRunService {
ASSISTANT_CODE, chatContext, true, easyFlowAgentSessionStore);
}
/**
* 通过公共 API 启动已发布 Agent 的纯文本聊天。
*
* @param chatRequest 聊天请求
* @param apiAccount API Key 对应的隔离调用身份
* @return SSE Emitter
*/
public SseEmitter chatPublic(AgentChatRequest chatRequest, LoginAccount apiAccount) {
validateChatRequest(chatRequest);
if (apiAccount == null || apiAccount.getId() == null) {
throw new BusinessException("API 调用身份不能为空");
}
if (chatRequest.getImageUploadIds() != null && !chatRequest.getImageUploadIds().isEmpty()) {
throw new BusinessException("公共 Agent API 暂不支持图片附件");
}
if (chatRequest.getDocumentUploadIds() != null && !chatRequest.getDocumentUploadIds().isEmpty()) {
throw new BusinessException("公共 Agent API 暂不支持文档附件");
}
if (chatRequest.getCapabilities() != null && !chatRequest.getCapabilities().isEmpty()) {
throw new BusinessException("公共 Agent API 暂不支持临时能力");
}
Agent liveAgent = agentService.getById(chatRequest.getAgentId());
if (liveAgent == null || !Objects.equals(liveAgent.getTenantId(), apiAccount.getTenantId())) {
throw new BusinessException("Agent 不存在或不可用");
}
assertAgentRunnable(liveAgent);
BigInteger sessionId = chatRequest.getSessionId() == null
? BigInteger.valueOf(new SnowFlakeIDKeyGenerator().nextId())
: chatRequest.getSessionId();
ChatSessionSummary existingSession =
resolveExistingSession(apiAccount, sessionId, chatRequest.getAgentId());
Agent agent = agentService.getPublishedView(chatRequest.getAgentId());
assertPublicHitlUnsupported(agent);
String requestId = UUID.randomUUID().toString();
String traceId = UUID.randomUUID().toString();
String titlePrompt = effectivePrompt(chatRequest.getPrompt(), false, false);
ChatRuntimeContext chatContext = buildChatRuntimeContext(
agent, sessionId, titlePrompt, apiAccount, ASSISTANT_CODE, ChatChannel.PUBLIC_API, true);
applyFormalSessionTitle(chatContext, titlePrompt, existingSession);
return run(agent, chatRequest.getPrompt(), Collections.emptyList(), Collections.emptyList(),
apiAccount, requestId, traceId, sessionId.toString(),
ASSISTANT_CODE, chatContext, true, easyFlowAgentSessionStore);
}
/**
* 公共 API 当前没有审批恢复入口,因此拒绝包含 HITL 工具的 Agent。
*
* @param agent 已发布 Agent 运行视图
*/
private void assertPublicHitlUnsupported(Agent agent) {
if (agent == null || agent.getToolBindings() == null) {
return;
}
for (AgentToolBinding binding : agent.getToolBindings()) {
if (binding == null || !Boolean.TRUE.equals(binding.getEnabled())) {
continue;
}
if (Boolean.TRUE.equals(binding.getHitlEnabled())) {
throw new BusinessException("公共 Agent API 暂不支持需要执行确认的工具");
}
Object approvalRequired = binding.getResourceSnapshot() == null
? null : binding.getResourceSnapshot().get("approvalRequired");
if ("MCP".equalsIgnoreCase(binding.getToolType())
&& Boolean.parseBoolean(String.valueOf(approvalRequired))) {
throw new BusinessException("公共 Agent API 暂不支持需要执行确认的 MCP");
}
}
}
/**
* 启动 Agent 草稿态纯文本试用。
*
@@ -615,6 +689,23 @@ public class AgentRunService {
return resolvedRequestId;
}
/**
* 在 Agent 生命周期锁内启动正式运行,避免与下线或删除并发穿透。
*
* @param agent Agent 运行视图
* @param userMessage 用户消息
* @param documentContext 文档上下文
* @param account 当前账号
* @param requestId 请求 ID
* @param traceId 链路 ID
* @param runtimeSessionId 运行会话 ID
* @param assistantCode 助手类型
* @param chatContext 聊天上下文
* @param chatSseEmitter SSE 发射器
* @param persistChatlog 是否持久化聊天日志
* @param runtimeSessionStore 运行会话存储
* @param initialLockHandle 会话运行锁
*/
private void startRuntime(Agent agent,
AgentMessage userMessage,
AgentDocumentContext documentContext,
@@ -628,6 +719,73 @@ public class AgentRunService {
boolean persistChatlog,
AgentSessionStore runtimeSessionStore,
AgentRunLock.Handle initialLockHandle) {
if (!persistChatlog || agent == null || agent.getId() == null) {
startRuntimeLocked(
agent, userMessage, documentContext, account, requestId, traceId,
runtimeSessionId, assistantCode, chatContext, chatSseEmitter,
persistChatlog, runtimeSessionStore, initialLockHandle
);
return;
}
try {
agentBindingLockExecutor.execute(agent.getId(), () -> {
agentRunStartGuard.assertRunnable(agent.getId());
startRuntimeLocked(
agent, userMessage, documentContext, account, requestId, traceId,
runtimeSessionId, assistantCode, chatContext, chatSseEmitter,
true, runtimeSessionStore, initialLockHandle
);
return null;
});
} catch (Exception exception) {
AgentRunRegistry.AgentRunContext runContext = agentRunRegistry.get(requestId);
if (runContext != null) {
runContext.cancel();
agentRunRegistry.remove(requestId);
} else if (initialLockHandle != null) {
initialLockHandle.release();
}
handleRuntimeError(
exception,
requestId,
chatSseEmitter,
chatContext,
new AtomicBoolean(false),
true
);
}
}
/**
* 初始化、注册并订阅单次 Agent 运行。
*
* @param agent Agent 运行视图
* @param userMessage 用户消息
* @param documentContext 文档上下文
* @param account 当前账号
* @param requestId 请求 ID
* @param traceId 链路 ID
* @param runtimeSessionId 运行会话 ID
* @param assistantCode 助手类型
* @param chatContext 聊天上下文
* @param chatSseEmitter SSE 发射器
* @param persistChatlog 是否持久化聊天日志
* @param runtimeSessionStore 运行会话存储
* @param initialLockHandle 会话运行锁
*/
private void startRuntimeLocked(Agent agent,
AgentMessage userMessage,
AgentDocumentContext documentContext,
LoginAccount account,
String requestId,
String traceId,
String runtimeSessionId,
String assistantCode,
ChatRuntimeContext chatContext,
ChatSseEmitter chatSseEmitter,
boolean persistChatlog,
AgentSessionStore runtimeSessionStore,
AgentRunLock.Handle initialLockHandle) {
AtomicBoolean finished = new AtomicBoolean(false);
StringBuilder answer = new StringBuilder();
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
@@ -705,6 +863,18 @@ public class AgentRunService {
}
}
/**
* 取消当前节点上指定 Agent 的全部运行。
*
* @param agentId Agent ID
*/
public void cancelAgentLocal(String agentId) {
if (agentId == null || agentId.isBlank()) {
throw new BusinessException("Agent ID 不能为空");
}
agentRunRegistry.cancelAgent(agentId);
}
private void bindAgentSession(Agent agent, String runtimeSessionId, ChatRuntimeContext chatContext) {
if (easyFlowAgentSessionStore == null || runtimeSessionId == null || runtimeSessionId.isBlank()) {
return;
@@ -1268,8 +1438,31 @@ public class AgentRunService {
String prompt,
LoginAccount account,
String assistantCode) {
return buildChatRuntimeContext(
agent, sessionId, prompt, account, assistantCode, ChatChannel.ADMIN, false);
}
/**
* 构建指定渠道的 Agent 聊天上下文。
*
* @param agent Agent 运行视图
* @param sessionId 会话 ID
* @param prompt 用户输入
* @param account 调用身份
* @param assistantCode 助手类型编码
* @param channel 调用渠道
* @param anonymous 是否匿名调用
* @return 聊天运行上下文
*/
private ChatRuntimeContext buildChatRuntimeContext(Agent agent,
BigInteger sessionId,
String prompt,
LoginAccount account,
String assistantCode,
ChatChannel channel,
boolean anonymous) {
ChatRuntimeContext context = new ChatRuntimeContext();
context.setChannel(ChatChannel.ADMIN);
context.setChannel(channel);
context.setSessionId(sessionId);
context.setTenantId(account.getTenantId());
context.setDeptId(account.getDeptId());
@@ -1280,6 +1473,7 @@ public class AgentRunService {
context.setAssistantCode(assistantCode);
context.setAssistantName(agent.getName());
context.setSessionTitle(toSessionTitle(prompt));
context.setAnonymous(anonymous);
return context;
}
@@ -1491,10 +1685,23 @@ public class AgentRunService {
}
}
/**
* 获取当前登录账号。
*
* @return 当前登录账号
* @throws BusinessException 登录信息失效时抛出
*/
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null || account.getTenantId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
} catch (Exception e) {
if (e instanceof BusinessException businessException) {
throw businessException;
}
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}

View File

@@ -0,0 +1,48 @@
package tech.easyflow.agent.runtime;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
/**
* 在正式 Agent 运行注册前通过数据库行锁确认最新生命周期状态。
*/
@Service
public class AgentRunStartGuard {
private final AgentService agentService;
/**
* 创建 Agent 运行启动守卫。
*
* @param agentService Agent 服务
*/
public AgentRunStartGuard(AgentService agentService) {
this.agentService = agentService;
}
/**
* 锁定 Agent 行并确认当前仍可启动正式运行。
*
* @param agentId Agent ID
* @throws BusinessException Agent 已下线、删除或不可用时抛出
*/
@Transactional(rollbackFor = Exception.class)
public void assertRunnable(BigInteger agentId) {
Agent agent = agentService.getOne(QueryWrapper.create()
.select(Agent::getId, Agent::getStatus, Agent::getPublishStatus)
.eq(Agent::getId, agentId)
.forUpdate());
if (agent == null
|| !Integer.valueOf(1).equals(agent.getStatus())
|| PublishStatus.from(agent.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("当前 Agent 已下线或不可继续会话");
}
}
}

View File

@@ -48,6 +48,14 @@ public interface AgentHitlPendingService {
*/
void cancelByRequestId(String requestId, String reason);
/**
* 取消指定 Agent 的全部待审批运行。
*
* @param agentId Agent ID
* @param reason 取消原因
*/
void cancelByAgentId(BigInteger agentId, String reason);
/**
* 删除指定聊天会话的 pending。
*

View File

@@ -105,6 +105,34 @@ public class AgentHitlPendingServiceImpl implements AgentHitlPendingService {
}
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelByAgentId(BigInteger agentId, String reason) {
if (agentId == null) {
return;
}
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
.eq("agent_id", agentId)
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0)
.forUpdate());
Date now = new Date();
for (AgentHitlPending record : records) {
AgentHitlPending update = new AgentHitlPending();
update.setStatus(AgentHitlPendingStatus.CANCELLED.name());
update.setRejectReason(reason);
update.setConsumedAt(now);
update.setModified(now);
pendingMapper.updateByQuery(update, QueryWrapper.create()
.eq("id", record.getId())
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0));
}
}
@Override
public void deleteByChatSessionId(BigInteger chatSessionId) {
if (chatSessionId == null) {

View File

@@ -0,0 +1,79 @@
package tech.easyflow.agent.security;
import com.mybatisflex.core.query.QueryCondition;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysDeptService;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Set;
import static tech.easyflow.agent.entity.table.AgentTableDef.AGENT;
/**
* 将 Agent 的租户、分类、归属人与可见范围转换为数据库查询条件。
*/
@Component
public class AgentVisibilityQueryHelper {
private final CategoryPermissionService categoryPermissionService;
private final SysDeptService sysDeptService;
/**
* 创建 Agent 可见性查询助手。
*
* @param categoryPermissionService 分类权限服务
* @param sysDeptService 部门服务
*/
public AgentVisibilityQueryHelper(CategoryPermissionService categoryPermissionService,
SysDeptService sysDeptService) {
this.categoryPermissionService = categoryPermissionService;
this.sysDeptService = sysDeptService;
}
/**
* 将当前账号可读 Agent 范围追加到查询条件。
*
* @param queryWrapper Agent 查询条件
*/
public void applyReadableAccess(QueryWrapper queryWrapper) {
LoginAccount account = SaTokenUtil.getLoginAccount();
BigInteger accountId = account == null ? null : account.getId();
BigInteger tenantId = account == null ? null : account.getTenantId();
if (accountId == null || tenantId == null) {
queryWrapper.and(AGENT.ID.eq(BigInteger.valueOf(-1)));
return;
}
// 项目没有启用全局租户过滤器,超级管理员也必须限制在当前租户。
queryWrapper.and(AGENT.TENANT_ID.eq(tenantId));
RoleCategoryAccessSnapshot access =
categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
if (access.isSuperAdmin()) {
return;
}
QueryCondition owner = AGENT.CREATED_BY.eq(accountId);
if (access.isRestricted() && access.getCategoryIds().isEmpty()) {
queryWrapper.and(owner);
return;
}
Set<BigInteger> readableDeptIds = account.getDeptId() == null
? Collections.emptySet()
: sysDeptService.getSelfAndAncestorDeptIds(account.getDeptId());
QueryCondition visible = AGENT.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
if (!readableDeptIds.isEmpty()) {
visible = visible.or(AGENT.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
.and(AGENT.DEPT_ID.in(readableDeptIds)));
}
if (access.isRestricted()) {
visible = AGENT.CATEGORY_ID.in(access.getCategoryIds()).and(visible);
}
queryWrapper.and(owner.or(visible));
}
}

View File

@@ -0,0 +1,236 @@
package tech.easyflow.agent.service;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentCategory;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Objects;
/**
* Agent 依赖资源的租户、权限和可用性校验服务。
*/
@Service
public class AgentDependencyAccessService {
private final ModelService modelService;
private final WorkflowService workflowService;
private final PluginItemService pluginItemService;
private final PluginMapper pluginMapper;
private final PluginVisibilityService pluginVisibilityService;
private final McpService mcpService;
private final DocumentCollectionService documentCollectionService;
private final AgentCategoryService agentCategoryService;
private final CategoryPermissionService categoryPermissionService;
private final ResourceAccessService resourceAccessService;
/**
* 创建 Agent 依赖资源校验服务。
*
* @param modelService 模型服务
* @param workflowService 工作流服务
* @param pluginItemService 插件工具服务
* @param pluginMapper 插件 Mapper
* @param pluginVisibilityService 插件可见性服务
* @param mcpService MCP 服务
* @param documentCollectionService 知识库服务
* @param agentCategoryService Agent 分类服务
* @param categoryPermissionService 分类权限服务
* @param resourceAccessService 资源权限服务
*/
public AgentDependencyAccessService(ModelService modelService,
WorkflowService workflowService,
PluginItemService pluginItemService,
PluginMapper pluginMapper,
PluginVisibilityService pluginVisibilityService,
McpService mcpService,
DocumentCollectionService documentCollectionService,
AgentCategoryService agentCategoryService,
CategoryPermissionService categoryPermissionService,
ResourceAccessService resourceAccessService) {
this.modelService = modelService;
this.workflowService = workflowService;
this.pluginItemService = pluginItemService;
this.pluginMapper = pluginMapper;
this.pluginVisibilityService = pluginVisibilityService;
this.mcpService = mcpService;
this.documentCollectionService = documentCollectionService;
this.agentCategoryService = agentCategoryService;
this.categoryPermissionService = categoryPermissionService;
this.resourceAccessService = resourceAccessService;
}
/**
* 校验 Agent 模型并锁定模型行。
*
* @param agent Agent
* @param modelId 模型 ID
* @return 模型
*/
public Model requireModel(Agent agent, BigInteger modelId) {
if (modelId == null) {
throw new BusinessException("Agent 模型不能为空");
}
Model model = modelService.getOne(QueryWrapper.create()
.eq(Model::getId, modelId)
.forUpdate());
if (model == null) {
throw new BusinessException("Agent 模型不存在");
}
assertSameTenant(agent, model.getTenantId(), "无权限使用该模型");
if (!Model.MODEL_TYPES[0].equals(model.getModelType())) {
throw new BusinessException("Agent 仅支持聊天模型");
}
return model;
}
/**
* 校验并锁定工作流。
*
* @param agent Agent
* @param workflowId 工作流 ID
* @return 已发布工作流
*/
public Workflow requireWorkflow(Agent agent, BigInteger workflowId) {
Workflow workflow = workflowService.getOne(QueryWrapper.create()
.eq(Workflow::getId, workflowId)
.forUpdate());
if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("绑定工作流不存在或未发布");
}
assertSameTenant(agent, workflow.getTenantId(), "无权限绑定该工作流");
resourceAccessService.assertAccess(
CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限绑定该工作流");
return workflow;
}
/**
* 校验插件工具及其所属插件,并按父子顺序锁定资源行。
*
* @param agent Agent
* @param pluginItemId 插件工具 ID
* @return 插件工具
*/
public PluginItem requirePluginItem(Agent agent, BigInteger pluginItemId) {
PluginItem current = pluginItemService.getById(pluginItemId);
if (current == null || current.getPluginId() == null) {
throw new BusinessException("绑定插件不存在");
}
Plugin plugin = pluginMapper.selectOneByQuery(QueryWrapper.create()
.eq(Plugin::getId, current.getPluginId())
.forUpdate());
PluginItem pluginItem = pluginItemService.getOne(QueryWrapper.create()
.eq(PluginItem::getId, pluginItemId)
.forUpdate());
if (plugin == null || pluginItem == null || !Objects.equals(plugin.getId(), pluginItem.getPluginId())) {
throw new BusinessException("绑定插件不存在");
}
if (!Integer.valueOf(1).equals(pluginItem.getStatus())) {
throw new BusinessException("绑定插件未启用");
}
assertSameTenant(agent, plugin.getTenantId(), "无权限绑定该插件");
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定该插件");
return pluginItem;
}
/**
* 校验并锁定 MCP。
*
* @param agent Agent
* @param mcpId MCP ID
* @return MCP
*/
public Mcp requireMcp(Agent agent, BigInteger mcpId) {
Mcp mcp = mcpService.getOne(QueryWrapper.create()
.eq(Mcp::getId, mcpId)
.forUpdate());
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
throw new BusinessException("绑定 MCP 不存在或未启用");
}
assertSameTenant(agent, mcp.getTenantId(), "无权限绑定该 MCP");
return mcp;
}
/**
* 校验并锁定知识库。
*
* @param agent Agent
* @param knowledgeId 知识库 ID
* @return 已发布知识库
*/
public DocumentCollection requireKnowledge(Agent agent, BigInteger knowledgeId) {
DocumentCollection knowledge = documentCollectionService.getOne(QueryWrapper.create()
.eq(DocumentCollection::getId, knowledgeId)
.forUpdate());
if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("绑定知识库不存在或未发布");
}
assertSameTenant(agent, knowledge.getTenantId(), "无权限绑定该知识库");
resourceAccessService.assertAccess(
CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限绑定该知识库");
return knowledge;
}
/**
* 校验 Agent 分类属于当前租户并在当前账号授权范围内。
*
* @param agent Agent
*/
public void validateCategory(Agent agent) {
if (agent == null || agent.getCategoryId() == null) {
return;
}
AgentCategory category = agentCategoryService.getById(agent.getCategoryId());
if (category == null || !Integer.valueOf(1).equals(category.getStatus())) {
throw new BusinessException("Agent 分类不存在或未启用");
}
assertSameTenant(agent, category.getTenantId(), "无权限使用该 Agent 分类");
RoleCategoryAccessSnapshot access =
categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
if (access.isRestricted() && !access.getCategoryIds().contains(agent.getCategoryId())) {
throw new BusinessException("无权限使用该 Agent 分类");
}
}
/**
* 校验资源与 Agent 属于同一租户。
*
* @param agent Agent
* @param resourceTenantId 资源租户 ID
* @param message 拒绝消息
*/
private void assertSameTenant(Agent agent, Object resourceTenantId, String message) {
BigInteger agentTenantId = agent == null ? null : agent.getTenantId();
if (agentTenantId == null) {
LoginAccount account = SaTokenUtil.getLoginAccount();
agentTenantId = account == null ? null : account.getTenantId();
}
if (agentTenantId == null || resourceTenantId == null
|| !agentTenantId.toString().equals(String.valueOf(resourceTenantId))) {
throw new BusinessException(message);
}
}
}

View File

@@ -0,0 +1,378 @@
package tech.easyflow.agent.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
import tech.easyflow.agent.vo.AgentOptionView;
import tech.easyflow.agent.vo.AgentResourceOptionsView;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Agent 与设计器依赖资源的安全选项查询服务。
*/
@Service
public class AgentOptionQueryService {
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
};
private final AgentService agentService;
private final ModelService modelService;
private final DocumentCollectionService documentCollectionService;
private final WorkflowService workflowService;
private final PluginService pluginService;
private final PluginItemService pluginItemService;
private final PluginVisibilityService pluginVisibilityService;
private final McpService mcpService;
private final AgentVisibilityQueryHelper agentVisibilityQueryHelper;
private final ResourceAccessService resourceAccessService;
private final ObjectMapper objectMapper;
/**
* 创建 Agent 安全选项查询服务。
*
* @param agentService Agent 服务
* @param modelService 模型服务
* @param documentCollectionService 知识库服务
* @param workflowService 工作流服务
* @param pluginService 插件服务
* @param pluginItemService 插件工具服务
* @param pluginVisibilityService 插件可见性服务
* @param mcpService MCP 服务
* @param agentVisibilityQueryHelper Agent 可见性查询助手
* @param resourceAccessService 资源访问服务
* @param objectMapper JSON 映射器
*/
public AgentOptionQueryService(AgentService agentService,
ModelService modelService,
DocumentCollectionService documentCollectionService,
WorkflowService workflowService,
PluginService pluginService,
PluginItemService pluginItemService,
PluginVisibilityService pluginVisibilityService,
McpService mcpService,
AgentVisibilityQueryHelper agentVisibilityQueryHelper,
ResourceAccessService resourceAccessService,
ObjectMapper objectMapper) {
this.agentService = agentService;
this.modelService = modelService;
this.documentCollectionService = documentCollectionService;
this.workflowService = workflowService;
this.pluginService = pluginService;
this.pluginItemService = pluginItemService;
this.pluginVisibilityService = pluginVisibilityService;
this.mcpService = mcpService;
this.agentVisibilityQueryHelper = agentVisibilityQueryHelper;
this.resourceAccessService = resourceAccessService;
this.objectMapper = objectMapper;
}
/**
* 查询当前账号可见的 Agent 安全选项。
*
* @param publishedOnly 是否仅查询已发布 Agent
* @return Agent 选项
*/
public List<AgentOptionView> listAgentOptions(boolean publishedOnly) {
LoginAccount account = requireAccount();
QueryWrapper wrapper = QueryWrapper.create();
agentVisibilityQueryHelper.applyReadableAccess(wrapper);
wrapper.orderBy(Agent::getModified, false);
if (publishedOnly) {
wrapper.eq(Agent::getPublishStatus, PublishStatus.PUBLISHED.getCode())
.eq(Agent::getStatus, 1);
}
List<AgentOptionView> result = new ArrayList<>();
ResourceAction action = publishedOnly ? ResourceAction.USE : ResourceAction.READ;
for (Agent agent : agentService.list(wrapper)) {
if (!resourceAccessService.canAccess(CategoryResourceType.AGENT, agent, action)) {
continue;
}
result.add(toAgentOption(agent, publishedOnly));
}
return result;
}
/**
* 查询 Agent 设计器所需的安全资源选项。
*
* @return 资源选项集合
*/
public AgentResourceOptionsView listDesignerResourceOptions() {
LoginAccount account = requireAccount();
return new AgentResourceOptionsView(
listModelOptions(account),
listKnowledgeOptions(account),
listWorkflowOptions(account),
listPluginToolOptions(account),
listMcpOptions(account)
);
}
/**
* 查询当前账号可用于 Agent 会话的知识库安全选项。
*
* @return 知识库选项
*/
public List<AgentResourceOptionsView.ResourceOption> listKnowledgeOptions() {
return listKnowledgeOptions(requireAccount());
}
/**
* 查询指定 MCP 的安全工具列表。
*
* @param mcpId MCP ID
* @return MCP 工具选项
*/
public List<AgentResourceOptionsView.McpToolOption> listMcpTools(BigInteger mcpId) {
LoginAccount account = requireAccount();
Mcp mcp = mcpService.getOne(QueryWrapper.create()
.eq(Mcp::getId, mcpId)
.eq(Mcp::getTenantId, account.getTenantId())
.eq(Mcp::getStatus, true));
if (mcp == null) {
throw new BusinessException("MCP 不存在或不可用");
}
Mcp detail = mcpService.getMcpTools(mcpId.toString());
if (detail == null || detail.getTools() == null) {
return Collections.emptyList();
}
List<AgentResourceOptionsView.McpToolOption> result = new ArrayList<>();
for (Object tool : detail.getTools()) {
Map<String, Object> value = objectMapper.convertValue(tool, MAP_TYPE);
result.add(new AgentResourceOptionsView.McpToolOption(
text(value.get("name")),
text(value.get("description"))
));
}
return result;
}
/**
* 查询当前租户可用于 Agent 的模型选项。
*
* @param account 当前登录账号
* @return 模型选项
*/
private List<AgentResourceOptionsView.ModelOption> listModelOptions(LoginAccount account) {
Model query = new Model();
query.setTenantId(account.getTenantId());
query.setModelType(Model.MODEL_TYPES[0]);
return modelService.listSelectableModels(query, false, "id", "desc").stream()
.filter(model -> Objects.equals(model.getTenantId(), account.getTenantId()))
.map(model -> new AgentResourceOptionsView.ModelOption(
model.getId(),
model.getTitle(),
model.getContextWindowTokens(),
model.getMaxOutputTokens()
))
.toList();
}
/**
* 查询当前账号可使用的已发布知识库。
*
* @param account 当前登录账号
* @return 知识库选项
*/
private List<AgentResourceOptionsView.ResourceOption> listKnowledgeOptions(LoginAccount account) {
return documentCollectionService.list(QueryWrapper.create()
.eq(DocumentCollection::getTenantId, account.getTenantId())
.eq(DocumentCollection::getPublishStatus, PublishStatus.PUBLISHED.getCode())
.orderBy(DocumentCollection::getModified, false))
.stream()
.filter(item -> resourceAccessService.canAccess(
CategoryResourceType.KNOWLEDGE, item, ResourceAction.USE))
.map(item -> new AgentResourceOptionsView.ResourceOption(
item.getId(), item.getTitle(), item.getDescription(), null))
.toList();
}
/**
* 查询当前账号可使用的已发布工作流。
*
* @param account 当前登录账号
* @return 工作流选项
*/
private List<AgentResourceOptionsView.ResourceOption> listWorkflowOptions(LoginAccount account) {
return workflowService.list(QueryWrapper.create()
.eq(Workflow::getTenantId, account.getTenantId())
.eq(Workflow::getPublishStatus, PublishStatus.PUBLISHED.getCode())
.eq(Workflow::getStatus, 1)
.orderBy(Workflow::getModified, false))
.stream()
.filter(item -> resourceAccessService.canAccess(
CategoryResourceType.WORKFLOW, item, ResourceAction.USE))
.map(item -> new AgentResourceOptionsView.ResourceOption(
item.getId(), item.getTitle(), item.getDescription(), item.getEnglishName()))
.toList();
}
/**
* 查询当前账号可使用的插件工具。
*
* @param account 当前登录账号
* @return 插件工具选项
*/
private List<AgentResourceOptionsView.PluginToolOption> listPluginToolOptions(LoginAccount account) {
List<Plugin> plugins = pluginService.list(QueryWrapper.create()
.eq(Plugin::getTenantId, account.getTenantId()))
.stream()
.filter(plugin -> pluginVisibilityService.canAccessPlugin(
plugin.getCreatedBy(), plugin.getId()))
.toList();
if (plugins.isEmpty()) {
return Collections.emptyList();
}
Map<BigInteger, String> pluginNames = new LinkedHashMap<>();
plugins.forEach(plugin -> pluginNames.put(plugin.getId(), plugin.getName()));
return pluginItemService.list(QueryWrapper.create()
.in(PluginItem::getPluginId, pluginNames.keySet())
.eq(PluginItem::getStatus, 1)
.orderBy(PluginItem::getId, false))
.stream()
.map(item -> new AgentResourceOptionsView.PluginToolOption(
item.getId(),
item.getName(),
item.getDescription(),
item.getEnglishName(),
pluginNames.get(item.getPluginId())
))
.toList();
}
/**
* 查询当前租户可使用的 MCP。
*
* @param account 当前登录账号
* @return MCP 选项
*/
private List<AgentResourceOptionsView.McpOption> listMcpOptions(LoginAccount account) {
return mcpService.list(QueryWrapper.create()
.eq(Mcp::getTenantId, account.getTenantId())
.eq(Mcp::getStatus, true)
.orderBy(Mcp::getModified, false))
.stream()
.map(item -> new AgentResourceOptionsView.McpOption(
item.getId(),
item.getTitle(),
item.getDescription(),
item.getApprovalRequired()
))
.toList();
}
/**
* 将 Agent 转换为不包含运行配置的安全选项。
*
* @param agent Agent 数据
* @param publishedOnly 是否读取发布快照中的展示信息
* @return Agent 安全选项
*/
private AgentOptionView toAgentOption(Agent agent, boolean publishedOnly) {
Map<String, Object> snapshot = publishedOnly ? agent.getPublishedSnapshotJson() : Map.of();
Map<String, Object> basic = snapshot == null ? Map.of() : map(snapshot.get("basicSummary"));
Map<String, Object> model = snapshot == null ? Map.of() : map(snapshot.get("modelSummary"));
Map<String, Object> interaction = publishedOnly
? map(snapshot == null ? null : snapshot.get("interactionConfigJson"))
: agent.getInteractionConfigJson();
return new AgentOptionView(
agent.getId(),
firstText(text(basic.get("name")), agent.getName()),
firstText(text(basic.get("description")), agent.getDescription()),
firstText(text(basic.get("avatar")), agent.getAvatar()),
interaction,
publishedOnly ? booleanValue(model.get("supportImage")) : null
);
}
/**
* 将对象安全转换为字符串键 Map。
*
* @param value 待转换值
* @return Map非 Map 值返回空 Map
*/
private Map<String, Object> map(Object value) {
if (!(value instanceof Map<?, ?> raw)) {
return Collections.emptyMap();
}
Map<String, Object> result = new LinkedHashMap<>();
raw.forEach((key, item) -> result.put(String.valueOf(key), item));
return result;
}
/**
* 转换可空布尔值。
*
* @param value 原始值
* @return 布尔值或 null
*/
private Boolean booleanValue(Object value) {
return value == null ? null : Boolean.parseBoolean(String.valueOf(value));
}
/**
* 返回首个非空文本。
*
* @param value 首选文本
* @param fallback 备用文本
* @return 最终文本
*/
private String firstText(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
/**
* 将对象转换为可空文本。
*
* @param value 原始值
* @return 文本或 null
*/
private String text(Object value) {
return value == null ? null : String.valueOf(value);
}
/**
* 获取带租户信息的当前登录账号。
*
* @return 当前登录账号
* @throws BusinessException 登录状态无效时抛出
*/
private LoginAccount requireAccount() {
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getTenantId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
}
}

View File

@@ -8,11 +8,10 @@ import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.mapper.AgentKnowledgeBindingMapper;
import tech.easyflow.agent.mapper.AgentMapper;
import tech.easyflow.agent.service.AgentDependencyAccessService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -22,9 +21,13 @@ import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Agent 知识库绑定服务实现。
@@ -38,9 +41,11 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
@Resource
private AgentMapper agentMapper;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private AgentBindingLockExecutor agentBindingLockExecutor;
@Resource
private AgentDependencyAccessService agentDependencyAccessService;
/**
* {@inheritDoc}
@@ -48,19 +53,21 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
@Override
@Transactional(rollbackFor = Exception.class)
public List<AgentKnowledgeBinding> replaceBindings(BigInteger agentId, List<AgentKnowledgeBinding> bindings) {
Agent agent = requireAgent(agentId);
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
remove(QueryWrapper.create().where("agent_id = ?", agentId));
if (bindings == null || bindings.isEmpty()) {
return Collections.emptyList();
}
for (int i = 0; i < bindings.size(); i++) {
AgentKnowledgeBinding binding = bindings.get(i);
validateBinding(binding);
applyBindingDefaults(agent, binding, i);
}
saveBatch(bindings);
return listEnabled(agentId);
return agentBindingLockExecutor.execute(agentId, () -> {
Agent agent = requireAgentForUpdate(agentId);
resourceAccessService.assertAccess(
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
validateBindings(agent, bindings);
remove(QueryWrapper.create().where("agent_id = ?", agentId));
if (bindings == null || bindings.isEmpty()) {
return Collections.emptyList();
}
for (int i = 0; i < bindings.size(); i++) {
applyBindingDefaults(agent, bindings.get(i), i);
}
saveBatch(bindings);
return listEnabled(agentId);
});
}
/**
@@ -74,26 +81,70 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
.orderBy("sort_no asc, id asc"));
}
private Agent requireAgent(BigInteger agentId) {
Agent agent = agentMapper.selectOneById(agentId);
/**
* 锁定并加载待修改的 Agent。
*
* @param agentId Agent ID
* @return 已锁定 Agent
* @throws BusinessException Agent 不存在时抛出
*/
private Agent requireAgentForUpdate(BigInteger agentId) {
Agent agent = agentMapper.selectOneByQuery(QueryWrapper.create()
.eq(Agent::getId, agentId)
.forUpdate());
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateBinding(AgentKnowledgeBinding binding) {
/**
* 校验知识库绑定并锁定目标知识库到当前事务结束。
*
* @param agent 当前 Agent
* @param binding 知识库绑定
* @throws BusinessException 绑定参数无效或知识库不可用时抛出
*/
private void validateBinding(Agent agent, AgentKnowledgeBinding binding) {
if (binding == null || binding.getKnowledgeId() == null) {
throw new BusinessException("知识库绑定参数不完整");
}
DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("绑定知识库不存在或未发布");
}
KnowledgeRetrievalModes.parse(binding.getRetrievalMode());
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限绑定该知识库");
agentDependencyAccessService.requireKnowledge(agent, binding.getKnowledgeId());
}
/**
* 按知识库 ID 的稳定顺序校验绑定并锁定关联资源。
*
* @param agent 当前 Agent
* @param bindings 知识库绑定
*/
private void validateBindings(Agent agent, List<AgentKnowledgeBinding> bindings) {
if (bindings == null || bindings.isEmpty()) {
return;
}
Set<BigInteger> knowledgeIds = new LinkedHashSet<>();
for (AgentKnowledgeBinding binding : bindings) {
if (binding != null && binding.getKnowledgeId() != null
&& !knowledgeIds.add(binding.getKnowledgeId())) {
throw new BusinessException("同一知识库不能重复绑定");
}
}
List<AgentKnowledgeBinding> validationOrder = new ArrayList<>(bindings);
validationOrder.sort(Comparator.comparing(binding ->
binding == null || binding.getKnowledgeId() == null
? BigInteger.ZERO
: binding.getKnowledgeId()));
validationOrder.forEach(binding -> validateBinding(agent, binding));
}
/**
* 写入知识库绑定的归属、审计与排序默认值。
*
* @param agent 当前 Agent
* @param binding 知识库绑定
* @param index 绑定顺序
*/
private void applyBindingDefaults(Agent agent, AgentKnowledgeBinding binding, int index) {
LoginAccount account = requireCurrentLoginAccount();
Date now = new Date();
@@ -113,10 +164,23 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
binding.setModifiedBy(account.getId());
}
/**
* 获取当前登录账号。
*
* @return 当前登录账号
* @throws BusinessException 登录信息失效时抛出
*/
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
} catch (Exception e) {
if (e instanceof BusinessException businessException) {
throw businessException;
}
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}

View File

@@ -0,0 +1,444 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.enums.AgentToolType;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.ai.service.AgentResourceBindingProvider;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Agent 对共享 AI 资源的绑定查询与解绑实现。
*/
@Service
public class AgentResourceBindingProviderImpl implements AgentResourceBindingProvider {
private final AgentService agentService;
private final AgentToolBindingService agentToolBindingService;
private final AgentKnowledgeBindingService agentKnowledgeBindingService;
private final AgentBindingLockExecutor agentBindingLockExecutor;
/**
* 创建 Agent 资源绑定提供者。
*
* @param agentService Agent 服务
* @param agentToolBindingService Agent 工具绑定服务
* @param agentKnowledgeBindingService Agent 知识库绑定服务
* @param agentBindingLockExecutor Agent 绑定锁执行器
*/
public AgentResourceBindingProviderImpl(AgentService agentService,
AgentToolBindingService agentToolBindingService,
AgentKnowledgeBindingService agentKnowledgeBindingService,
AgentBindingLockExecutor agentBindingLockExecutor) {
this.agentService = agentService;
this.agentToolBindingService = agentToolBindingService;
this.agentKnowledgeBindingService = agentKnowledgeBindingService;
this.agentBindingLockExecutor = agentBindingLockExecutor;
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId) {
return listAgents(collectToolResourceAgentIds(AgentToolType.WORKFLOW, workflowId, false));
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId) {
return listAgents(collectKnowledgeAgentIds(knowledgeId, false));
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByPluginItemId(BigInteger pluginItemId) {
return listAgents(collectToolResourceAgentIds(AgentToolType.PLUGIN, pluginItemId, false));
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByMcpId(BigInteger mcpId) {
return listAgents(collectToolResourceAgentIds(AgentToolType.MCP, mcpId, false));
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByModelId(BigInteger modelId) {
Set<BigInteger> agentIds = collectAgentIdsFromAgents(agentService.list(QueryWrapper.create()
.select(Agent::getId)
.eq(Agent::getModelId, modelId)));
for (Agent agent : listPublishedSnapshotAgents()) {
if (sameId(agent.getPublishedSnapshotJson().get("modelId"), modelId)) {
agentIds.add(agent.getId());
}
}
return listAgents(agentIds);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void unbindWorkflow(BigInteger workflowId) {
Set<BigInteger> agentIds = collectToolResourceAgentIds(
AgentToolType.WORKFLOW, workflowId, true);
for (BigInteger agentId : sortedAgentIds(agentIds)) {
agentBindingLockExecutor.execute(agentId, () -> {
agentToolBindingService.remove(QueryWrapper.create()
.eq(AgentToolBinding::getAgentId, agentId)
.eq(AgentToolBinding::getToolType, AgentToolType.WORKFLOW.name())
.eq(AgentToolBinding::getTargetId, workflowId));
trimPublishedSnapshot(
agentId,
workflowId,
AgentToolType.WORKFLOW.name(),
"toolBindings",
"toolSummaries"
);
return null;
});
}
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void unbindKnowledge(BigInteger knowledgeId) {
Set<BigInteger> agentIds = collectKnowledgeAgentIds(knowledgeId, true);
for (BigInteger agentId : sortedAgentIds(agentIds)) {
agentBindingLockExecutor.execute(agentId, () -> {
agentKnowledgeBindingService.remove(QueryWrapper.create()
.eq(AgentKnowledgeBinding::getAgentId, agentId)
.eq(AgentKnowledgeBinding::getKnowledgeId, knowledgeId));
trimPublishedSnapshot(
agentId,
knowledgeId,
null,
"knowledgeBindings",
"knowledgeSummaries"
);
return null;
});
}
}
/**
* 查询 Agent 摘要并保持绑定中首次出现的顺序。
*
* @param agentIds Agent ID 集合
* @return Agent 摘要列表
*/
private List<OfflineImpactBindingVo> listAgents(Set<BigInteger> agentIds) {
if (agentIds.isEmpty()) {
return Collections.emptyList();
}
Map<BigInteger, Agent> agentMap = new LinkedHashMap<>();
for (Agent agent : agentService.listByIds(agentIds)) {
agentMap.put(agent.getId(), agent);
}
List<OfflineImpactBindingVo> result = new ArrayList<>(agentIds.size());
for (BigInteger agentId : agentIds) {
Agent agent = agentMap.get(agentId);
OfflineImpactBindingVo item = new OfflineImpactBindingVo();
item.setId(agentId);
item.setTitle(agent == null ? "已删除智能体(悬空绑定)" : agent.getName());
result.add(item);
}
return result;
}
/**
* 收集草稿绑定和已发布快照中引用指定工具资源的 Agent ID。
*
* @param toolType 工具类型
* @param resourceId 资源 ID
* @param lockBindings 是否锁定实时绑定行
* @return Agent ID 集合
*/
private Set<BigInteger> collectToolResourceAgentIds(AgentToolType toolType,
BigInteger resourceId,
boolean lockBindings) {
QueryWrapper wrapper = QueryWrapper.create()
.eq(AgentToolBinding::getToolType, toolType.name())
.eq(AgentToolBinding::getTargetId, resourceId);
if (lockBindings) {
wrapper.forUpdate();
}
Set<BigInteger> agentIds = collectAgentIdsFromToolBindings(
agentToolBindingService.list(wrapper));
for (Agent agent : listPublishedSnapshotAgents()) {
if (snapshotContainsToolResource(agent.getPublishedSnapshotJson(), toolType, resourceId)) {
agentIds.add(agent.getId());
}
}
return agentIds;
}
/**
* 收集草稿绑定和已发布快照中引用指定知识库的 Agent ID。
*
* @param knowledgeId 知识库 ID
* @param lockBindings 是否锁定实时绑定行
* @return Agent ID 集合
*/
private Set<BigInteger> collectKnowledgeAgentIds(BigInteger knowledgeId, boolean lockBindings) {
QueryWrapper wrapper = QueryWrapper.create()
.eq(AgentKnowledgeBinding::getKnowledgeId, knowledgeId);
if (lockBindings) {
wrapper.forUpdate();
}
Set<BigInteger> agentIds = collectAgentIdsFromKnowledgeBindings(
agentKnowledgeBindingService.list(wrapper));
for (Agent agent : listPublishedSnapshotAgents()) {
if (snapshotContainsKnowledge(agent.getPublishedSnapshotJson(), knowledgeId)) {
agentIds.add(agent.getId());
}
}
return agentIds;
}
/**
* 查询带发布快照的 Agent 最小字段。
*
* @return Agent 发布快照记录
*/
private List<Agent> listPublishedSnapshotAgents() {
return agentService.list(QueryWrapper.create()
.select(
Agent::getId,
Agent::getName,
Agent::getModelId,
Agent::getPublishedSnapshotJson
)
.isNotNull(Agent::getPublishedSnapshotJson));
}
/**
* 判断发布快照是否引用指定工具资源。
*
* @param snapshot 发布快照
* @param toolType 工具类型
* @param resourceId 资源 ID
* @return 是否引用
*/
private boolean snapshotContainsToolResource(Map<String, Object> snapshot,
AgentToolType toolType,
BigInteger resourceId) {
return snapshotListContains(snapshot, "toolBindings", resourceId, toolType.name())
|| snapshotListContains(snapshot, "toolSummaries", resourceId, toolType.name());
}
/**
* 判断发布快照是否引用指定知识库。
*
* @param snapshot 发布快照
* @param knowledgeId 知识库 ID
* @return 是否引用
*/
private boolean snapshotContainsKnowledge(Map<String, Object> snapshot, BigInteger knowledgeId) {
return snapshotListContains(snapshot, "knowledgeBindings", knowledgeId, null)
|| snapshotListContains(snapshot, "knowledgeSummaries", knowledgeId, null);
}
/**
* 判断快照列表中是否存在指定资源。
*
* @param snapshot 发布快照
* @param key 列表字段
* @param resourceId 资源 ID
* @param toolType 工具类型;知识库为空
* @return 是否存在
*/
private boolean snapshotListContains(Map<String, Object> snapshot,
String key,
BigInteger resourceId,
String toolType) {
if (snapshot == null || snapshot.isEmpty()) {
return false;
}
Object value = snapshot.get(key);
if (!(value instanceof List<?> items)) {
return false;
}
return items.stream().anyMatch(item -> matchesResourceBinding(item, resourceId, toolType));
}
/**
* 收集工具绑定中的 Agent ID。
*
* @param bindings 工具绑定
* @return 去重后的 Agent ID
*/
private Set<BigInteger> collectAgentIdsFromToolBindings(List<AgentToolBinding> bindings) {
Set<BigInteger> result = new LinkedHashSet<>();
if (bindings == null) {
return result;
}
for (AgentToolBinding binding : bindings) {
if (binding != null && binding.getAgentId() != null) {
result.add(binding.getAgentId());
}
}
return result;
}
/**
* 收集知识库绑定中的 Agent ID。
*
* @param bindings 知识库绑定
* @return 去重后的 Agent ID
*/
private Set<BigInteger> collectAgentIdsFromKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
Set<BigInteger> result = new LinkedHashSet<>();
if (bindings == null) {
return result;
}
for (AgentKnowledgeBinding binding : bindings) {
if (binding != null && binding.getAgentId() != null) {
result.add(binding.getAgentId());
}
}
return result;
}
/**
* 收集 Agent 实体中的 ID。
*
* @param agents Agent 列表
* @return Agent ID 集合
*/
private Set<BigInteger> collectAgentIdsFromAgents(List<Agent> agents) {
Set<BigInteger> result = new LinkedHashSet<>();
if (agents == null) {
return result;
}
for (Agent agent : agents) {
if (agent != null && agent.getId() != null) {
result.add(agent.getId());
}
}
return result;
}
/**
* 按 Agent ID 升序返回锁定顺序,避免并发批量解绑以相反顺序持锁。
*
* @param agentIds Agent ID 集合
* @return 稳定排序后的 Agent ID
*/
private List<BigInteger> sortedAgentIds(Set<BigInteger> agentIds) {
if (agentIds == null || agentIds.isEmpty()) {
return Collections.emptyList();
}
return agentIds.stream()
.sorted(Comparator.naturalOrder())
.toList();
}
/**
* 从 Agent 发布快照中移除指定资源绑定。
*
* @param agentId Agent ID
* @param resourceId 资源 ID
* @param toolType 工具类型;知识库绑定时为空
* @param bindingsKeys 快照绑定字段
*/
private void trimPublishedSnapshot(BigInteger agentId,
BigInteger resourceId,
String toolType,
String... bindingsKeys) {
Agent agent = agentService.getOne(QueryWrapper.create()
.eq(Agent::getId, agentId)
.forUpdate());
if (agent == null || agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
return;
}
Map<String, Object> snapshot = new LinkedHashMap<>(agent.getPublishedSnapshotJson());
boolean changed = false;
for (String bindingsKey : bindingsKeys) {
Object rawBindings = snapshot.get(bindingsKey);
if (!(rawBindings instanceof List<?> bindings)) {
continue;
}
List<Object> filtered = new ArrayList<>(bindings.size());
for (Object item : bindings) {
if (matchesResourceBinding(item, resourceId, toolType)) {
changed = true;
continue;
}
filtered.add(item);
}
snapshot.put(bindingsKey, filtered);
}
if (!changed) {
return;
}
// 仅更新发布快照,避免并发草稿编辑被旧实体中的其他字段覆盖。
agentService.updateChain()
.set(Agent::getPublishedSnapshotJson, snapshot)
.eq(Agent::getId, agentId)
.update();
}
/**
* 判断快照项是否指向指定资源。
*
* @param item 快照项
* @param resourceId 资源 ID
* @param toolType 工具类型;知识库绑定时为空
* @return 是否匹配
*/
private boolean matchesResourceBinding(Object item, BigInteger resourceId, String toolType) {
if (!(item instanceof Map<?, ?> binding)) {
return false;
}
Object currentId = toolType == null ? binding.get("knowledgeId") : binding.get("targetId");
if (!Objects.equals(String.valueOf(currentId), String.valueOf(resourceId))) {
return false;
}
return toolType == null || toolType.equalsIgnoreCase(String.valueOf(binding.get("toolType")));
}
/**
* 比较快照中的资源 ID 与数据库 ID。
*
* @param snapshotId 快照 ID
* @param resourceId 数据库 ID
* @return 是否相同
*/
private boolean sameId(Object snapshotId, BigInteger resourceId) {
return snapshotId != null
&& resourceId != null
&& Objects.equals(String.valueOf(snapshotId), resourceId.toString());
}
}

View File

@@ -2,6 +2,7 @@ package tech.easyflow.agent.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -10,9 +11,12 @@ import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.mapper.AgentMapper;
import tech.easyflow.agent.runtime.AgentRuntimeCompiler;
import tech.easyflow.agent.service.AgentDependencyAccessService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.*;
@@ -58,6 +62,12 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
private ResourceAccessService resourceAccessService;
@Resource
private ObjectMapper objectMapper;
@Resource
private AgentDependencyAccessService agentDependencyAccessService;
@Resource
private AgentBindingLockExecutor agentBindingLockExecutor;
@Resource
private AgentRuntimeCompiler agentRuntimeCompiler;
/**
* {@inheritDoc}
@@ -77,8 +87,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
@Override
@Transactional(rollbackFor = Exception.class)
public Agent saveDraft(Agent agent) {
validateDraft(agent);
applyDraftDefaults(agent);
validateDraft(agent);
save(agent);
return getDetail(agent.getId());
}
@@ -92,12 +102,16 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
if (agent == null || agent.getId() == null) {
throw new BusinessException("Agent ID 不能为空");
}
Agent existing = requireAgent(agent.getId());
resourceAccessService.assertAccess(CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
validateDraft(agent);
applyDraftUpdate(existing, agent);
updateById(existing);
return getDetail(existing.getId());
return agentBindingLockExecutor.execute(agent.getId(), () -> {
Agent existing = requireAgentForUpdate(agent.getId());
resourceAccessService.assertAccess(
CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
agent.setTenantId(existing.getTenantId());
validateDraft(agent);
applyDraftUpdate(existing, agent);
updateById(existing);
return getDetail(existing.getId());
});
}
/**
@@ -117,8 +131,27 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> buildPublishSnapshot(Agent agent) {
Agent detail = getDetail(agent.getId());
if (agent == null || agent.getId() == null) {
throw new BusinessException("Agent ID 不能为空");
}
return agentBindingLockExecutor.execute(agent.getId(), () -> buildPublishSnapshotLocked(agent.getId()));
}
/**
* 在 Agent 锁和数据库行锁内构建并校验发布快照。
*
* @param agentId Agent ID
* @return 发布快照
*/
private Map<String, Object> buildPublishSnapshotLocked(BigInteger agentId) {
Agent detail = requireAgentForUpdate(agentId);
resourceAccessService.assertAccess(
CategoryResourceType.AGENT, detail, ResourceAction.MANAGE, "无权限管理该 Agent");
detail.setToolBindings(agentToolBindingService.listEnabled(agentId));
detail.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(agentId));
validateDraft(detail);
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("id", detail.getId());
snapshot.put("tenantId", detail.getTenantId());
@@ -136,8 +169,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
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()));
snapshot.put("toolBindings", snapshotToolBindings(detail, detail.getToolBindings()));
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail, detail.getKnowledgeBindings()));
snapshot.put("basicSummary", basicSummary(detail));
snapshot.put("modelSummary", modelSummary(detail.getModelId()));
snapshot.put("parameterSummary", parameterSummary(detail));
@@ -145,6 +178,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
snapshot.put("toolSummaries", toolSummaries(detail.getToolBindings()));
snapshot.put("knowledgeSummaries", knowledgeSummaries(detail.getKnowledgeBindings()));
snapshot.put("snapshotAt", new Date());
// 发布前完整编译一次,提前暴露工具运行名冲突和运行定义错误。
agentRuntimeCompiler.compile(fromSnapshot(snapshot));
return snapshot;
}
@@ -179,6 +214,22 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
return agent;
}
/**
* 查询并锁定 Agent 数据行。
*
* @param id Agent ID
* @return Agent
*/
private Agent requireAgentForUpdate(BigInteger id) {
Agent agent = getOne(QueryWrapper.create()
.eq(Agent::getId, id)
.forUpdate());
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateDraft(Agent agent) {
if (agent == null) {
throw new BusinessException("Agent 不能为空");
@@ -186,13 +237,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
if (agent.getName() == null || agent.getName().isBlank()) {
throw new BusinessException("Agent 名称不能为空");
}
if (agent.getModelId() == null) {
throw new BusinessException("Agent 模型不能为空");
}
Model model = modelService.getModelInstance(agent.getModelId());
if (model == null) {
throw new BusinessException("Agent 模型不存在");
}
agentDependencyAccessService.requireModel(agent, agent.getModelId());
agentDependencyAccessService.validateCategory(agent);
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
agent.setExecutionConfigJson(normalizeExecutionConfig(agent.getExecutionConfigJson()));
@@ -334,57 +380,47 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
return summary;
}
private List<AgentToolBinding> snapshotToolBindings(List<AgentToolBinding> bindings) {
private List<AgentToolBinding> snapshotToolBindings(Agent agent, List<AgentToolBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(binding -> {
AgentToolBinding snapshot = objectMapper.convertValue(binding, AgentToolBinding.class);
snapshot.setResourceSnapshot(toolResourceSnapshot(agent, binding));
snapshot.setResourceSummary(toolSummary(binding));
snapshot.setResourceSnapshot(toolResourceSnapshot(binding));
return snapshot;
}).toList();
}
private Map<String, Object> toolResourceSnapshot(AgentToolBinding binding) {
private Map<String, Object> toolResourceSnapshot(Agent agent, AgentToolBinding binding) {
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
Workflow workflow = workflowService.getPublishedById(binding.getTargetId());
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
throw new BusinessException("绑定工作流不存在或未发布");
}
Workflow workflow = agentDependencyAccessService.requireWorkflow(agent, binding.getTargetId());
return objectMapper.convertValue(workflow, new TypeReference<Map<String, Object>>() {});
}
if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
if (pluginItem == null) {
throw new BusinessException("绑定插件不存在");
}
PluginItem pluginItem = agentDependencyAccessService.requirePluginItem(agent, binding.getTargetId());
return objectMapper.convertValue(pluginItem, new TypeReference<Map<String, Object>>() {});
}
Mcp mcp = mcpService.getById(binding.getTargetId());
if (mcp == null) {
throw new BusinessException("绑定 MCP 不存在");
}
Mcp mcp = agentDependencyAccessService.requireMcp(agent, binding.getTargetId());
return objectMapper.convertValue(mcp, new TypeReference<Map<String, Object>>() {});
}
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(
Agent agent, List<AgentKnowledgeBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(binding -> {
AgentKnowledgeBinding snapshot = objectMapper.convertValue(binding, AgentKnowledgeBinding.class);
snapshot.setResourceSnapshot(knowledgeResourceSnapshot(agent, binding));
snapshot.setResourceSummary(knowledgeSummary(binding));
snapshot.setResourceSnapshot(knowledgeResourceSnapshot(binding));
return snapshot;
}).toList();
}
private Map<String, Object> knowledgeResourceSnapshot(AgentKnowledgeBinding binding) {
DocumentCollection knowledge = documentCollectionService.getPublishedById(binding.getKnowledgeId());
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
throw new BusinessException("绑定知识库不存在或未发布");
}
private Map<String, Object> knowledgeResourceSnapshot(Agent agent, AgentKnowledgeBinding binding) {
DocumentCollection knowledge =
agentDependencyAccessService.requireKnowledge(agent, binding.getKnowledgeId());
return objectMapper.convertValue(knowledge, new TypeReference<Map<String, Object>>() {});
}
@@ -408,10 +444,23 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
return summary;
}
/**
* 获取当前登录账号。
*
* @return 当前登录账号
* @throws BusinessException 登录信息失效时抛出
*/
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null || account.getTenantId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
} catch (Exception e) {
if (e instanceof BusinessException businessException) {
throw businessException;
}
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}

View File

@@ -9,14 +9,9 @@ import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.enums.AgentToolType;
import tech.easyflow.agent.mapper.AgentMapper;
import tech.easyflow.agent.mapper.AgentToolBindingMapper;
import tech.easyflow.agent.service.AgentDependencyAccessService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -26,9 +21,13 @@ import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Agent 工具绑定服务实现。
@@ -40,13 +39,11 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
@Resource
private AgentMapper agentMapper;
@Resource
private WorkflowService workflowService;
@Resource
private PluginItemService pluginItemService;
@Resource
private McpService mcpService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private AgentBindingLockExecutor agentBindingLockExecutor;
@Resource
private AgentDependencyAccessService agentDependencyAccessService;
/**
* {@inheritDoc}
@@ -54,19 +51,21 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
@Override
@Transactional(rollbackFor = Exception.class)
public List<AgentToolBinding> replaceBindings(BigInteger agentId, List<AgentToolBinding> bindings) {
Agent agent = requireAgent(agentId);
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
remove(QueryWrapper.create().where("agent_id = ?", agentId));
if (bindings == null || bindings.isEmpty()) {
return Collections.emptyList();
}
for (int i = 0; i < bindings.size(); i++) {
AgentToolBinding binding = bindings.get(i);
validateBinding(binding);
applyBindingDefaults(agent, binding, i);
}
saveBatch(bindings);
return listEnabled(agentId);
return agentBindingLockExecutor.execute(agentId, () -> {
Agent agent = requireAgentForUpdate(agentId);
resourceAccessService.assertAccess(
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
validateBindings(agent, bindings);
remove(QueryWrapper.create().where("agent_id = ?", agentId));
if (bindings == null || bindings.isEmpty()) {
return Collections.emptyList();
}
for (int i = 0; i < bindings.size(); i++) {
applyBindingDefaults(agent, bindings.get(i), i);
}
saveBatch(bindings);
return listEnabled(agentId);
});
}
/**
@@ -80,40 +79,89 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
.orderBy("sort_no asc, id asc"));
}
private Agent requireAgent(BigInteger agentId) {
Agent agent = agentMapper.selectOneById(agentId);
/**
* 锁定并加载待修改的 Agent。
*
* @param agentId Agent ID
* @return 已锁定 Agent
* @throws BusinessException Agent 不存在时抛出
*/
private Agent requireAgentForUpdate(BigInteger agentId) {
Agent agent = agentMapper.selectOneByQuery(QueryWrapper.create()
.eq(Agent::getId, agentId)
.forUpdate());
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateBinding(AgentToolBinding binding) {
/**
* 校验单个工具绑定,并锁定目标资源到当前事务结束。
*
* @param agent 当前 Agent
* @param binding 工具绑定
* @throws BusinessException 绑定参数无效或目标资源不可用时抛出
*/
private void validateBinding(Agent agent, AgentToolBinding binding) {
if (binding == null || binding.getTargetId() == null || binding.getToolType() == null) {
throw new BusinessException("工具绑定参数不完整");
}
AgentToolType type = AgentToolType.from(binding.getToolType());
if (type == AgentToolType.WORKFLOW) {
Workflow workflow = workflowService.getById(binding.getTargetId());
if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("绑定工作流不存在或未发布");
}
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限绑定该工作流");
agentDependencyAccessService.requireWorkflow(agent, binding.getTargetId());
return;
}
if (type == AgentToolType.PLUGIN) {
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
if (pluginItem == null || pluginItem.getStatus() == null || pluginItem.getStatus() != 1) {
throw new BusinessException("绑定插件不存在或未启用");
}
agentDependencyAccessService.requirePluginItem(agent, binding.getTargetId());
return;
}
Mcp mcp = mcpService.getById(binding.getTargetId());
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
throw new BusinessException("绑定 MCP 不存在或未启用");
}
agentDependencyAccessService.requireMcp(agent, binding.getTargetId());
}
/**
* 按稳定顺序校验绑定并锁定关联资源,降低并发替换产生数据库死锁的概率。
*
* @param agent 当前 Agent
* @param bindings 工具绑定
*/
private void validateBindings(Agent agent, List<AgentToolBinding> bindings) {
if (bindings == null || bindings.isEmpty()) {
return;
}
Set<String> resourceKeys = new LinkedHashSet<>();
Set<String> declaredToolNames = new LinkedHashSet<>();
for (AgentToolBinding binding : bindings) {
if (binding == null || binding.getToolType() == null || binding.getTargetId() == null) {
continue;
}
String resourceKey = AgentToolType.from(binding.getToolType()).name() + ":" + binding.getTargetId();
if (!resourceKeys.add(resourceKey)) {
throw new BusinessException("同一工具资源不能重复绑定");
}
String toolName = binding.getToolName();
if (toolName != null && !toolName.isBlank() && !declaredToolNames.add(toolName.trim())) {
throw new BusinessException("Agent 工具运行名冲突:" + toolName.trim() + ",请调整工具名称");
}
}
List<AgentToolBinding> validationOrder = new ArrayList<>(bindings);
validationOrder.sort(Comparator
.comparing((AgentToolBinding binding) ->
binding == null || binding.getToolType() == null ? "" : binding.getToolType())
.thenComparing(binding ->
binding == null || binding.getTargetId() == null
? BigInteger.ZERO
: binding.getTargetId()));
validationOrder.forEach(binding -> validateBinding(agent, binding));
}
/**
* 写入工具绑定的归属、审计与排序默认值。
*
* @param agent 当前 Agent
* @param binding 工具绑定
* @param index 绑定顺序
*/
private void applyBindingDefaults(Agent agent, AgentToolBinding binding, int index) {
LoginAccount account = requireCurrentLoginAccount();
Date now = new Date();
@@ -129,10 +177,23 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
binding.setModifiedBy(account.getId());
}
/**
* 获取当前登录账号。
*
* @return 当前登录账号
* @throws BusinessException 登录信息失效时抛出
*/
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getId() == null) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
return account;
} catch (Exception e) {
if (e instanceof BusinessException businessException) {
throw businessException;
}
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}

View File

@@ -0,0 +1,198 @@
package tech.easyflow.agent.support;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import tech.easyflow.common.cache.RedisLockExecutor;
import java.math.BigInteger;
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
/**
* 在同一 Agent 维度串行执行绑定变更,并将锁持有到数据库事务结束。
*/
@Component
public class AgentBindingLockExecutor {
private static final Logger LOG = LoggerFactory.getLogger(AgentBindingLockExecutor.class);
private static final String LOCK_KEY_PREFIX = "easyflow:lock:agent:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(30);
private final RedisLockExecutor redisLockExecutor;
private final ScheduledExecutorService renewExecutor;
/**
* 创建 Agent 绑定锁执行器。
*
* @param redisLockExecutor Redis 分布式锁执行器
*/
@Autowired
public AgentBindingLockExecutor(RedisLockExecutor redisLockExecutor) {
this(
redisLockExecutor,
Executors.newSingleThreadScheduledExecutor(new AgentBindingLockRenewThreadFactory())
);
}
/**
* 创建使用指定续期执行器的 Agent 绑定锁执行器。
*
* @param redisLockExecutor Redis 分布式锁执行器
* @param renewExecutor 锁续期执行器
*/
AgentBindingLockExecutor(RedisLockExecutor redisLockExecutor,
ScheduledExecutorService renewExecutor) {
this.redisLockExecutor = redisLockExecutor;
this.renewExecutor = renewExecutor;
}
/**
* 在 Agent 绑定锁内执行任务。
*
* <p>存在活动事务时,锁会在事务完成后释放,避免提交前出现并发写入窗口。</p>
*
* @param agentId Agent ID
* @param task 绑定变更任务
* @param <T> 返回类型
* @return 任务结果
*/
public <T> T execute(BigInteger agentId, Supplier<T> task) {
if (agentId == null) {
throw new IllegalArgumentException("agentId 不能为空");
}
String lockKey = LOCK_KEY_PREFIX + agentId;
if (TransactionSynchronizationManager.hasResource(lockKey)) {
return task.get();
}
RedisLockExecutor.LockHandle lockHandle = redisLockExecutor.acquire(
lockKey,
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT
);
AtomicBoolean leaseValid = new AtomicBoolean(true);
ScheduledFuture<?> renewTask = scheduleRenew(agentId, lockHandle, leaseValid);
Runnable releaseAction = () -> {
renewTask.cancel(false);
lockHandle.release();
};
boolean releaseAfterTransaction = false;
try {
releaseAfterTransaction =
registerTransactionRelease(lockKey, releaseAction, leaseValid);
T result = task.get();
if (!leaseValid.get()) {
throw new IllegalStateException("Agent 绑定锁已失效,当前操作已取消");
}
return result;
} finally {
if (!releaseAfterTransaction) {
releaseAction.run();
}
}
}
/**
* 注册事务完成后的锁释放动作。
*
* @param lockKey 锁键
* @param releaseAction 锁释放动作
* @param leaseValid 锁租期有效标记
* @return 已注册事务回调时返回 {@code true}
*/
private boolean registerTransactionRelease(String lockKey,
Runnable releaseAction,
AtomicBoolean leaseValid) {
if (!TransactionSynchronizationManager.isActualTransactionActive()
|| !TransactionSynchronizationManager.isSynchronizationActive()) {
return false;
}
TransactionSynchronizationManager.bindResource(lockKey, Boolean.TRUE);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
if (!leaseValid.get()) {
throw new IllegalStateException("Agent 绑定锁已失效,事务禁止提交");
}
}
@Override
public void afterCompletion(int status) {
try {
TransactionSynchronizationManager.unbindResourceIfPossible(lockKey);
} finally {
releaseAction.run();
}
}
});
return true;
}
/**
* 定期续期 Agent 绑定锁,保证长事务中的早期锁不会在提交前过期。
*
* @param agentId Agent ID
* @param lockHandle 锁句柄
* @param leaseValid 锁租期有效标记
* @return 续期任务
*/
private ScheduledFuture<?> scheduleRenew(BigInteger agentId,
RedisLockExecutor.LockHandle lockHandle,
AtomicBoolean leaseValid) {
long renewIntervalMillis = Math.max(LOCK_LEASE_TIMEOUT.toMillis() / 3L, 1000L);
return renewExecutor.scheduleWithFixedDelay(() -> {
try {
if (lockHandle.renew()) {
return;
}
leaseValid.set(false);
LOG.warn("Agent 绑定锁续期失败agentId={}", agentId);
} catch (RuntimeException exception) {
leaseValid.set(false);
LOG.error("Agent 绑定锁续期异常agentId={}", agentId, exception);
}
}, renewIntervalMillis, renewIntervalMillis, TimeUnit.MILLISECONDS);
}
/**
* 关闭锁续期线程。
*/
@PreDestroy
public void destroy() {
renewExecutor.shutdownNow();
}
/**
* Agent 绑定锁续期线程工厂。
*/
private static final class AgentBindingLockRenewThreadFactory implements ThreadFactory {
private final AtomicInteger index = new AtomicInteger(1);
/**
* 创建守护续期线程。
*
* @param runnable 续期任务
* @return 续期线程
*/
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setName("agent-binding-lock-renew-" + index.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
}

View File

@@ -0,0 +1,24 @@
package tech.easyflow.agent.vo;
import java.math.BigInteger;
import java.util.Map;
/**
* Agent 选择项,只包含聊天和筛选所需的安全字段。
*
* @param id Agent ID
* @param name Agent 名称
* @param description Agent 描述
* @param avatar Agent 头像
* @param interactionConfigJson 对话交互配置
* @param supportImage 模型是否支持图片
*/
public record AgentOptionView(
BigInteger id,
String name,
String description,
String avatar,
Map<String, Object> interactionConfigJson,
Boolean supportImage
) {
}

View File

@@ -0,0 +1,97 @@
package tech.easyflow.agent.vo;
import java.math.BigInteger;
import java.util.List;
/**
* Agent 设计器依赖资源的安全选择项集合。
*
* @param models 模型选项
* @param knowledges 知识库选项
* @param workflows 工作流选项
* @param pluginTools 插件工具选项
* @param mcps MCP 选项
*/
public record AgentResourceOptionsView(
List<ModelOption> models,
List<ResourceOption> knowledges,
List<ResourceOption> workflows,
List<PluginToolOption> pluginTools,
List<McpOption> mcps
) {
/**
* 模型安全选择项。
*
* @param id 模型 ID
* @param title 展示名称
* @param contextWindowTokens 上下文窗口
* @param maxOutputTokens 最大输出 Token
*/
public record ModelOption(
BigInteger id,
String title,
Long contextWindowTokens,
Long maxOutputTokens
) {
}
/**
* 通用可发布资源选择项。
*
* @param id 资源 ID
* @param title 标题
* @param description 描述
* @param englishName 英文运行名
*/
public record ResourceOption(
BigInteger id,
String title,
String description,
String englishName
) {
}
/**
* 插件工具安全选择项。
*
* @param id 插件工具 ID
* @param name 工具名称
* @param description 工具描述
* @param englishName 英文运行名
* @param pluginName 所属插件名称
*/
public record PluginToolOption(
BigInteger id,
String name,
String description,
String englishName,
String pluginName
) {
}
/**
* MCP 安全选择项。
*
* @param id MCP ID
* @param title MCP 标题
* @param description MCP 描述
* @param approvalRequired 是否默认要求执行确认
*/
public record McpOption(
BigInteger id,
String title,
String description,
Boolean approvalRequired
) {
}
/**
* MCP 工具安全展示项。
*
* @param name 工具名称
* @param description 工具描述
*/
public record McpToolOption(String name, String description) {
}
}

View File

@@ -113,6 +113,34 @@ public class AgentRuntimeCommandConsumerTest {
Assert.assertEquals("cmd-expire", resultRegistry.lastSuccessCommandId);
}
/**
* 验证 Agent 集群取消命令只取消目标节点的对应 Agent 运行。
*
* @throws Exception 消息序列化异常
*/
@Test
public void consumerShouldHandleCancelAgentCommand() throws Exception {
AgentRuntimeProperties properties = new AgentRuntimeProperties();
properties.setInstanceId("node-a");
RecordingAgentRunService service = new RecordingAgentRunService();
RecordingCommandResultRegistry resultRegistry = new RecordingCommandResultRegistry();
AgentRuntimeCommandConsumer consumer = new AgentRuntimeCommandConsumer(
new ObjectMapper(),
properties,
new MQProperties(),
service,
resultRegistry
);
AgentRuntimeCommandMessage command = command("cmd-cancel", "node-a");
command.setAction(AgentRuntimeCommandAction.CANCEL_AGENT);
command.setAgentId("1001");
consumer.handle(List.of(message(command)));
Assert.assertEquals("1001", service.lastCancelledAgentId);
Assert.assertEquals("cmd-cancel", resultRegistry.lastSuccessCommandId);
}
private AgentRuntimeCommandMessage command(String commandId, String targetNodeId) {
AgentRuntimeCommandMessage command = new AgentRuntimeCommandMessage();
command.setCommandId(commandId);
@@ -138,6 +166,7 @@ public class AgentRuntimeCommandConsumerTest {
private int expireCount;
private String lastRequestId;
private String lastReason;
private String lastCancelledAgentId;
@Override
public void approveRuntimeLocal(String requestId, String resumeToken, BigInteger operatorId, String userId) {
@@ -151,6 +180,11 @@ public class AgentRuntimeCommandConsumerTest {
lastRequestId = requestId;
lastReason = reason;
}
@Override
public void cancelAgentLocal(String agentId) {
lastCancelledAgentId = agentId;
}
}
private static class RecordingCommandResultRegistry extends AgentRuntimeCommandResultRegistry {

View File

@@ -6,11 +6,13 @@ import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.agent.distributed.AgentRuntimeRouteRegistry;
import java.time.Duration;
import java.util.Set;
/**
* {@link AgentRuntimeRouteRegistry} 回归测试。
@@ -40,12 +42,49 @@ public class AgentRuntimeRouteRegistryTest {
"easyflow:agent:runtime:resume-token:token-1", "request-1", Duration.ofHours(24));
}
/**
* 验证正式运行会写入 Agent 反向索引,并可解析全部 owner 节点。
*/
@Test
public void agentRunIndexShouldTrackOwnerNodes() {
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
@SuppressWarnings("unchecked")
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
@SuppressWarnings("unchecked")
SetOperations<String, String> setOperations = Mockito.mock(SetOperations.class);
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
Mockito.when(redisTemplate.opsForSet()).thenReturn(setOperations);
Mockito.when(setOperations.members("easyflow:agent:runtime:agent:1001"))
.thenReturn(Set.of("request-1", "request-2"));
Mockito.when(valueOperations.get("easyflow:agent:runtime:request:request-1"))
.thenReturn("{\"nodeId\":\"node-a\",\"bootId\":\"boot-a\",\"agentId\":\"1001\"}");
Mockito.when(valueOperations.get("easyflow:agent:runtime:request:request-2"))
.thenReturn("{\"nodeId\":\"node-b\",\"bootId\":\"boot-b\",\"agentId\":\"1001\"}");
Mockito.when(valueOperations.get("easyflow:agent:runtime:node:node-a")).thenReturn("boot-a");
Mockito.when(valueOperations.get("easyflow:agent:runtime:node:node-b")).thenReturn("boot-b");
AgentRuntimeRouteRegistry registry = registry(redisTemplate, properties("node-a"));
registry.registerRun("request-1", "1001");
Mockito.verify(valueOperations).set(
ArgumentMatchers.eq("easyflow:agent:runtime:request:request-1"),
ArgumentMatchers.contains("\"agentId\":\"1001\""),
ArgumentMatchers.eq(Duration.ofHours(24))
);
Mockito.verify(setOperations).add("easyflow:agent:runtime:agent:1001", "request-1");
Mockito.verify(redisTemplate).expire("easyflow:agent:runtime:agent:1001", Duration.ofHours(24));
Assert.assertEquals(Set.of("node-a", "node-b"), registry.findOwnerNodesByAgent("1001"));
}
/**
* 验证运行结束时清理 Redis 路由。
*/
@Test
public void removeShouldDeleteRunAndTokenRoutes() {
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
@SuppressWarnings("unchecked")
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
AgentRuntimeRouteRegistry registry = registry(redisTemplate, properties("node-a"));
registry.removeRun("request-1");

View File

@@ -5,17 +5,24 @@ import com.mybatisflex.core.update.UpdateChain;
import com.mybatisflex.core.util.LambdaGetter;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.agent.distributed.AgentRuntimeCommandProducer;
import tech.easyflow.agent.distributed.AgentRuntimeRouteRegistry;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.runtime.AgentRunRegistry;
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
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.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -82,27 +89,50 @@ public class AgentApprovalSubjectHandlerTest {
}
/**
* 审批删除 Agent 必须同步清理工具绑定和知识库绑定,避免留下孤儿数据。
* 审批删除 Agent 必须在同一配置锁内取消运行并清理关联数据。
*/
@Test
public void beforeRemoveShouldCleanAgentBindings() {
public void removeResourceShouldCancelRunsAndCleanBindings() {
AtomicInteger toolRemoveCalls = new AtomicInteger();
AtomicInteger knowledgeRemoveCalls = new AtomicInteger();
AgentToolBindingService toolBindingService = proxy(AgentToolBindingService.class, toolRemoveCalls);
AgentKnowledgeBindingService knowledgeBindingService = proxy(AgentKnowledgeBindingService.class, knowledgeRemoveCalls);
AgentService agentService = mock(AgentService.class);
AgentRunRegistry runRegistry = mock(AgentRunRegistry.class);
AgentHitlPendingService pendingService = mock(AgentHitlPendingService.class);
AgentRuntimeRouteRegistry routeRegistry = mock(AgentRuntimeRouteRegistry.class);
AgentRuntimeCommandProducer commandProducer = mock(AgentRuntimeCommandProducer.class);
when(routeRegistry.findOwnerNodesByAgent("1001")).thenReturn(Set.of("node-a", "node-b"));
when(routeRegistry.currentNodeId()).thenReturn("node-a");
AgentApprovalSubjectHandler handler = new AgentApprovalSubjectHandler(
null,
new ObjectMapper(),
null,
agentService,
toolBindingService,
knowledgeBindingService,
null
null,
immediateLockExecutor(),
runRegistry,
pendingService,
routeRegistry,
commandProducer
);
handler.beforeRemove(BigInteger.valueOf(1001));
handler.removeResource(BigInteger.valueOf(1001));
Assert.assertEquals(1, toolRemoveCalls.get());
Assert.assertEquals(1, knowledgeRemoveCalls.get());
verify(pendingService).cancelByAgentId(
BigInteger.valueOf(1001),
"Agent 已删除,待审批运行已取消"
);
verify(runRegistry).cancelAgent("1001");
verify(commandProducer).sendCancelAgent(
"node-b",
"1001",
"Agent 已删除,待审批运行已取消"
);
verify(agentService).removeById(BigInteger.valueOf(1001));
}
/**
@@ -112,16 +142,36 @@ public class AgentApprovalSubjectHandlerTest {
* @return Agent 审批资源处理器
*/
private static AgentApprovalSubjectHandler handler(AgentService agentService) {
AgentRuntimeRouteRegistry routeRegistry = mock(AgentRuntimeRouteRegistry.class);
when(routeRegistry.findOwnerNodesByAgent("1001")).thenReturn(Set.of());
return new AgentApprovalSubjectHandler(
null,
new ObjectMapper(),
agentService,
null,
null,
null
null,
immediateLockExecutor(),
mock(AgentRunRegistry.class),
mock(AgentHitlPendingService.class),
routeRegistry,
mock(AgentRuntimeCommandProducer.class)
);
}
/**
* 创建同步执行任务的 Agent 配置锁测试桩。
*
* @return Agent 配置锁执行器
*/
@SuppressWarnings("unchecked")
private static AgentBindingLockExecutor immediateLockExecutor() {
AgentBindingLockExecutor executor = mock(AgentBindingLockExecutor.class);
when(executor.execute(any(BigInteger.class), any(Supplier.class)))
.thenAnswer(invocation -> ((Supplier<?>) invocation.getArgument(1)).get());
return executor;
}
/**
* 准备字段更新对象。
*

View File

@@ -1246,6 +1246,11 @@ public class AgentRunServiceDraftAndHitlTest {
cancelByRequestIdCount++;
}
@Override
public void cancelByAgentId(BigInteger agentId, String reason) {
// 测试桩无需处理。
}
@Override
public void deleteByChatSessionId(BigInteger chatSessionId) {
// 测试桩无需处理。

View File

@@ -0,0 +1,108 @@
package tech.easyflow.agent.runtime;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.List;
/**
* {@link AgentRunService} 公共 API 租户边界测试。
*/
public class AgentRunServicePublicTest {
/**
* 验证 API Key 不能运行其他租户的 Agent。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void chatPublicShouldRejectCrossTenantAgent() throws Exception {
BigInteger agentId = BigInteger.valueOf(1001);
Agent agent = new Agent();
agent.setId(agentId);
agent.setTenantId(BigInteger.valueOf(2001));
AgentService agentService = Mockito.mock(AgentService.class);
Mockito.when(agentService.getById(agentId)).thenReturn(agent);
AgentRunService service = new AgentRunService();
setField(service, "agentService", agentService);
AgentChatRequest request = new AgentChatRequest();
request.setAgentId(agentId);
request.setPrompt("hello");
LoginAccount account = new LoginAccount();
account.setId(BigInteger.valueOf(3001));
account.setTenantId(BigInteger.valueOf(2002));
BusinessException exception = Assert.assertThrows(
BusinessException.class,
() -> service.chatPublic(request, account)
);
Assert.assertEquals("Agent 不存在或不可用", exception.getMessage());
Mockito.verify(agentService, Mockito.never()).getPublishedView(agentId);
}
/**
* 公共 API 没有恢复入口时必须拒绝包含 HITL 工具的 Agent。
*
* @throws Exception 注入测试依赖失败
*/
@Test
public void chatPublicShouldRejectHitlTool() throws Exception {
BigInteger agentId = BigInteger.valueOf(1001);
BigInteger tenantId = BigInteger.valueOf(2001);
Agent liveAgent = new Agent();
liveAgent.setId(agentId);
liveAgent.setTenantId(tenantId);
liveAgent.setStatus(1);
liveAgent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
AgentToolBinding binding = new AgentToolBinding();
binding.setEnabled(true);
binding.setHitlEnabled(true);
Agent publishedAgent = new Agent();
publishedAgent.setId(agentId);
publishedAgent.setToolBindings(List.of(binding));
AgentService agentService = Mockito.mock(AgentService.class);
Mockito.when(agentService.getById(agentId)).thenReturn(liveAgent);
Mockito.when(agentService.getPublishedView(agentId)).thenReturn(publishedAgent);
AgentRunService service = new AgentRunService();
setField(service, "agentService", agentService);
setField(service, "chatSessionQueryService", Mockito.mock(ChatSessionQueryService.class));
AgentChatRequest request = new AgentChatRequest();
request.setAgentId(agentId);
request.setPrompt("hello");
LoginAccount account = new LoginAccount();
account.setId(BigInteger.valueOf(3001));
account.setTenantId(tenantId);
BusinessException exception = Assert.assertThrows(
BusinessException.class,
() -> service.chatPublic(request, account)
);
Assert.assertEquals("公共 Agent API 暂不支持需要执行确认的工具", exception.getMessage());
}
/**
* 写入被测对象私有字段。
*
* @param target 被测对象
* @param fieldName 字段名
* @param value 字段值
* @throws Exception 字段不存在或不可访问
*/
private void setField(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}

View File

@@ -0,0 +1,71 @@
package tech.easyflow.agent.runtime;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Test;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* {@link AgentRunStartGuard} 单元测试。
*/
public class AgentRunStartGuardTest {
/**
* 已启用且已发布的 Agent 允许启动正式运行。
*/
@Test
public void publishedAgentShouldBeRunnable() {
AgentService agentService = mock(AgentService.class);
Agent agent = agent(1, PublishStatus.PUBLISHED);
when(agentService.getOne(any(QueryWrapper.class))).thenReturn(agent);
new AgentRunStartGuard(agentService).assertRunnable(BigInteger.valueOf(1001));
}
/**
* 已下线 Agent 必须拒绝新运行。
*/
@Test(expected = BusinessException.class)
public void offlineAgentShouldBeRejected() {
AgentService agentService = mock(AgentService.class);
when(agentService.getOne(any(QueryWrapper.class)))
.thenReturn(agent(1, PublishStatus.OFFLINE));
new AgentRunStartGuard(agentService).assertRunnable(BigInteger.valueOf(1001));
}
/**
* 已禁用 Agent 必须拒绝新运行。
*/
@Test(expected = BusinessException.class)
public void disabledAgentShouldBeRejected() {
AgentService agentService = mock(AgentService.class);
when(agentService.getOne(any(QueryWrapper.class)))
.thenReturn(agent(0, PublishStatus.PUBLISHED));
new AgentRunStartGuard(agentService).assertRunnable(BigInteger.valueOf(1001));
}
/**
* 创建测试 Agent。
*
* @param status 启用状态
* @param publishStatus 发布状态
* @return 测试 Agent
*/
private static Agent agent(Integer status, PublishStatus publishStatus) {
Agent agent = new Agent();
agent.setId(BigInteger.valueOf(1001));
agent.setStatus(status);
agent.setPublishStatus(publishStatus.getCode());
return agent;
}
}

View File

@@ -0,0 +1,217 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentCategoryService;
import tech.easyflow.agent.service.AgentDependencyAccessService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Locale;
/**
* Agent 绑定资源状态锁测试。
*/
public class AgentBindingValidationLockTest {
/**
* 验证工作流绑定使用锁定读校验最新发布状态。
*
* @throws Exception 反射调用失败
*/
@Test
public void workflowBindingShouldValidateWithForUpdate() {
Workflow workflow = new Workflow();
workflow.setPublishStatus(PublishStatus.PUBLISHED.getCode());
workflow.setTenantId(BigInteger.ONE);
WorkflowService workflowService = Mockito.mock(WorkflowService.class);
Mockito.when(workflowService.getOne(Mockito.any(QueryWrapper.class))).thenReturn(workflow);
AgentDependencyAccessService service = createService(
workflowService,
Mockito.mock(PluginItemService.class),
Mockito.mock(PluginMapper.class),
Mockito.mock(PluginVisibilityService.class),
Mockito.mock(McpService.class),
Mockito.mock(DocumentCollectionService.class),
Mockito.mock(ResourceAccessService.class)
);
service.requireWorkflow(agent(), BigInteger.valueOf(1001));
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
Mockito.verify(workflowService).getOne(queryCaptor.capture());
Assert.assertTrue(queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE"));
}
/**
* 验证知识库绑定使用锁定读校验最新发布状态。
*
* @throws Exception 反射调用失败
*/
@Test
public void knowledgeBindingShouldValidateWithForUpdate() {
DocumentCollection knowledge = new DocumentCollection();
knowledge.setPublishStatus(PublishStatus.PUBLISHED.getCode());
knowledge.setTenantId(BigInteger.ONE);
DocumentCollectionService knowledgeService = Mockito.mock(DocumentCollectionService.class);
Mockito.when(knowledgeService.getOne(Mockito.any(QueryWrapper.class))).thenReturn(knowledge);
AgentDependencyAccessService service = createService(
Mockito.mock(WorkflowService.class),
Mockito.mock(PluginItemService.class),
Mockito.mock(PluginMapper.class),
Mockito.mock(PluginVisibilityService.class),
Mockito.mock(McpService.class),
knowledgeService,
Mockito.mock(ResourceAccessService.class)
);
service.requireKnowledge(agent(), BigInteger.valueOf(2001));
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
Mockito.verify(knowledgeService).getOne(queryCaptor.capture());
Assert.assertTrue(queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE"));
}
/**
* 验证插件绑定使用锁定读校验最新启用状态。
*
* @throws Exception 反射调用失败
*/
@Test
public void pluginBindingShouldValidateWithForUpdate() {
BigInteger pluginId = BigInteger.valueOf(30);
BigInteger pluginItemId = BigInteger.valueOf(3001);
PluginItem pluginItem = pluginItem(pluginId);
PluginItemService pluginItemService = Mockito.mock(PluginItemService.class);
Mockito.when(pluginItemService.getById(pluginItemId)).thenReturn(pluginItem);
Mockito.when(pluginItemService.getOne(Mockito.any(QueryWrapper.class))).thenReturn(pluginItem);
Plugin plugin = new Plugin();
plugin.setId(pluginId);
plugin.setTenantId(1L);
PluginMapper pluginMapper = Mockito.mock(PluginMapper.class);
Mockito.when(pluginMapper.selectOneByQuery(Mockito.any(QueryWrapper.class))).thenReturn(plugin);
AgentDependencyAccessService service = createService(
Mockito.mock(WorkflowService.class),
pluginItemService,
pluginMapper,
Mockito.mock(PluginVisibilityService.class),
Mockito.mock(McpService.class),
Mockito.mock(DocumentCollectionService.class),
Mockito.mock(ResourceAccessService.class)
);
service.requirePluginItem(agent(), pluginItemId);
ArgumentCaptor<QueryWrapper> pluginQueryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
Mockito.verify(pluginMapper).selectOneByQuery(pluginQueryCaptor.capture());
Assert.assertTrue(pluginQueryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE"));
ArgumentCaptor<QueryWrapper> itemQueryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
Mockito.verify(pluginItemService).getOne(itemQueryCaptor.capture());
Assert.assertTrue(itemQueryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE"));
}
/**
* 验证 MCP 绑定使用锁定读校验最新启用状态。
*
* @throws Exception 反射调用失败
*/
@Test
public void mcpBindingShouldValidateWithForUpdate() {
Mcp mcp = new Mcp();
mcp.setStatus(true);
mcp.setTenantId(BigInteger.ONE);
McpService mcpService = Mockito.mock(McpService.class);
Mockito.when(mcpService.getOne(Mockito.any(QueryWrapper.class))).thenReturn(mcp);
AgentDependencyAccessService service = createService(
Mockito.mock(WorkflowService.class),
Mockito.mock(PluginItemService.class),
Mockito.mock(PluginMapper.class),
Mockito.mock(PluginVisibilityService.class),
mcpService,
Mockito.mock(DocumentCollectionService.class),
Mockito.mock(ResourceAccessService.class)
);
service.requireMcp(agent(), BigInteger.valueOf(4001));
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
Mockito.verify(mcpService).getOne(queryCaptor.capture());
Assert.assertTrue(queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE"));
}
/**
* 创建依赖资源校验服务。
*
* @param workflowService 工作流服务
* @param pluginItemService 插件工具服务
* @param pluginMapper 插件 Mapper
* @param pluginVisibilityService 插件可见性服务
* @param mcpService MCP 服务
* @param documentCollectionService 知识库服务
* @param resourceAccessService 资源权限服务
* @return 依赖资源校验服务
*/
private AgentDependencyAccessService createService(
WorkflowService workflowService,
PluginItemService pluginItemService,
PluginMapper pluginMapper,
PluginVisibilityService pluginVisibilityService,
McpService mcpService,
DocumentCollectionService documentCollectionService,
ResourceAccessService resourceAccessService) {
return new AgentDependencyAccessService(
Mockito.mock(ModelService.class),
workflowService,
pluginItemService,
pluginMapper,
pluginVisibilityService,
mcpService,
documentCollectionService,
Mockito.mock(AgentCategoryService.class),
Mockito.mock(CategoryPermissionService.class),
resourceAccessService
);
}
/**
* 创建同租户 Agent。
*
* @return Agent
*/
private Agent agent() {
Agent agent = new Agent();
agent.setTenantId(BigInteger.ONE);
return agent;
}
/**
* 创建启用的插件工具。
*
* @param pluginId 插件 ID
* @return 插件工具
*/
private PluginItem pluginItem(BigInteger pluginId) {
PluginItem pluginItem = new PluginItem();
pluginItem.setPluginId(pluginId);
pluginItem.setStatus(1);
return pluginItem;
}
}

View File

@@ -0,0 +1,118 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.enums.AgentToolType;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.agent.support.AgentBindingLockExecutor;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;
import java.util.function.Supplier;
/**
* {@link AgentResourceBindingProviderImpl} 批量解绑锁顺序测试。
*/
public class AgentResourceBindingProviderImplTest {
/**
* 验证工作流批量解绑按 Agent ID 升序获取绑定锁。
*/
@Test
public void unbindWorkflowShouldAcquireAgentLocksInAscendingOrder() {
AgentService agentService = Mockito.mock(AgentService.class);
AgentToolBindingService toolBindingService = Mockito.mock(AgentToolBindingService.class);
AgentKnowledgeBindingService knowledgeBindingService =
Mockito.mock(AgentKnowledgeBindingService.class);
AgentBindingLockExecutor lockExecutor = Mockito.mock(AgentBindingLockExecutor.class);
Mockito.when(toolBindingService.list(Mockito.any(QueryWrapper.class)))
.thenReturn(List.of(
workflowBinding(3),
workflowBinding(1),
workflowBinding(2),
workflowBinding(1)
));
List<BigInteger> lockOrder = new ArrayList<>();
Mockito.doAnswer(invocation -> {
lockOrder.add(invocation.getArgument(0));
Supplier<?> task = invocation.getArgument(1);
return task.get();
}).when(lockExecutor).execute(Mockito.any(BigInteger.class), Mockito.any());
AgentResourceBindingProviderImpl provider = new AgentResourceBindingProviderImpl(
agentService,
toolBindingService,
knowledgeBindingService,
lockExecutor
);
provider.unbindWorkflow(BigInteger.TEN);
Assert.assertEquals(
List.of(BigInteger.ONE, BigInteger.TWO, BigInteger.valueOf(3)),
lockOrder
);
}
/**
* 已发布快照中的引用必须参与资源删除影响检查。
*/
@Test
public void listAgentsByWorkflowIdShouldIncludeSnapshotOnlyReference() {
AgentService agentService = Mockito.mock(AgentService.class);
AgentToolBindingService toolBindingService = Mockito.mock(AgentToolBindingService.class);
AgentKnowledgeBindingService knowledgeBindingService =
Mockito.mock(AgentKnowledgeBindingService.class);
AgentBindingLockExecutor lockExecutor = Mockito.mock(AgentBindingLockExecutor.class);
BigInteger agentId = BigInteger.valueOf(7);
Agent agent = new Agent();
agent.setId(agentId);
agent.setName("已发布智能体");
agent.setPublishedSnapshotJson(Map.of(
"toolBindings",
List.of(Map.of(
"toolType", AgentToolType.WORKFLOW.name(),
"targetId", BigInteger.TEN
))
));
Mockito.when(toolBindingService.list(Mockito.any(QueryWrapper.class)))
.thenReturn(List.of());
Mockito.when(agentService.list(Mockito.any(QueryWrapper.class)))
.thenReturn(List.of(agent));
Mockito.when(agentService.listByIds(Mockito.anyCollection()))
.thenReturn(List.of(agent));
AgentResourceBindingProviderImpl provider = new AgentResourceBindingProviderImpl(
agentService,
toolBindingService,
knowledgeBindingService,
lockExecutor
);
var result = provider.listAgentsByWorkflowId(BigInteger.TEN);
Assert.assertEquals(1, result.size());
Assert.assertEquals(agentId, result.get(0).getId());
Assert.assertEquals("已发布智能体", result.get(0).getTitle());
}
/**
* 构造工作流工具绑定。
*
* @param agentId Agent ID
* @return 工具绑定
*/
private static AgentToolBinding workflowBinding(long agentId) {
AgentToolBinding binding = new AgentToolBinding();
binding.setAgentId(BigInteger.valueOf(agentId));
binding.setToolType(AgentToolType.WORKFLOW.name());
binding.setTargetId(BigInteger.TEN);
return binding;
}
}

View File

@@ -0,0 +1,172 @@
package tech.easyflow.agent.support;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import tech.easyflow.common.cache.RedisLockExecutor;
import java.math.BigInteger;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* {@link AgentBindingLockExecutor} 事务锁生命周期测试。
*/
public class AgentBindingLockExecutorTest {
/**
* 验证活动事务内的绑定锁延迟到事务完成后释放。
*/
@Test
public void executeShouldReleaseLockAfterTransactionCompletion() {
RedisLockExecutor redisLockExecutor = Mockito.mock(RedisLockExecutor.class);
RedisLockExecutor.LockHandle lockHandle = Mockito.mock(RedisLockExecutor.LockHandle.class);
ScheduledExecutorService renewExecutor = Mockito.mock(ScheduledExecutorService.class);
ScheduledFuture<?> renewTask = Mockito.mock(ScheduledFuture.class);
Mockito.when(redisLockExecutor.acquire(
Mockito.anyString(),
Mockito.any(Duration.class),
Mockito.any(Duration.class)
)).thenReturn(lockHandle);
Mockito.doReturn(renewTask).when(renewExecutor).scheduleWithFixedDelay(
Mockito.any(Runnable.class),
Mockito.anyLong(),
Mockito.anyLong(),
Mockito.eq(TimeUnit.MILLISECONDS)
);
Mockito.when(lockHandle.renew()).thenReturn(true);
AgentBindingLockExecutor executor = new AgentBindingLockExecutor(redisLockExecutor, renewExecutor);
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(true);
try {
String result = executor.execute(BigInteger.ONE, () -> "ok");
Assert.assertEquals("ok", result);
Mockito.verify(lockHandle, Mockito.never()).release();
Mockito.verify(renewTask, Mockito.never()).cancel(false);
ArgumentCaptor<Runnable> renewCaptor = ArgumentCaptor.forClass(Runnable.class);
Mockito.verify(renewExecutor).scheduleWithFixedDelay(
renewCaptor.capture(),
Mockito.anyLong(),
Mockito.anyLong(),
Mockito.eq(TimeUnit.MILLISECONDS)
);
renewCaptor.getValue().run();
Mockito.verify(lockHandle).renew();
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
Assert.assertEquals(1, synchronizations.size());
synchronizations.get(0).afterCompletion(TransactionSynchronization.STATUS_COMMITTED);
Mockito.verify(renewTask).cancel(false);
Mockito.verify(lockHandle).release();
} finally {
TransactionSynchronizationManager.setActualTransactionActive(false);
TransactionSynchronizationManager.clearSynchronization();
executor.destroy();
}
Mockito.verify(renewExecutor).shutdownNow();
}
/**
* 同一事务内重复进入相同 Agent 锁时只能获取一次 Redis 锁。
*/
@Test
public void executeShouldReuseSameAgentLockWithinTransaction() {
RedisLockExecutor redisLockExecutor = Mockito.mock(RedisLockExecutor.class);
RedisLockExecutor.LockHandle lockHandle = Mockito.mock(RedisLockExecutor.LockHandle.class);
ScheduledExecutorService renewExecutor = Mockito.mock(ScheduledExecutorService.class);
ScheduledFuture<?> renewTask = Mockito.mock(ScheduledFuture.class);
Mockito.when(redisLockExecutor.acquire(
Mockito.anyString(),
Mockito.any(Duration.class),
Mockito.any(Duration.class)
)).thenReturn(lockHandle);
Mockito.doReturn(renewTask).when(renewExecutor).scheduleWithFixedDelay(
Mockito.any(Runnable.class),
Mockito.anyLong(),
Mockito.anyLong(),
Mockito.eq(TimeUnit.MILLISECONDS)
);
AgentBindingLockExecutor executor = new AgentBindingLockExecutor(redisLockExecutor, renewExecutor);
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(true);
try {
String result = executor.execute(
BigInteger.ONE,
() -> executor.execute(BigInteger.ONE, () -> "nested")
);
Assert.assertEquals("nested", result);
Mockito.verify(redisLockExecutor, Mockito.times(1)).acquire(
Mockito.anyString(),
Mockito.any(Duration.class),
Mockito.any(Duration.class)
);
List<TransactionSynchronization> synchronizations =
TransactionSynchronizationManager.getSynchronizations();
Assert.assertEquals(1, synchronizations.size());
synchronizations.get(0).afterCompletion(TransactionSynchronization.STATUS_COMMITTED);
} finally {
TransactionSynchronizationManager.unbindResourceIfPossible(
"easyflow:lock:agent:binding:" + BigInteger.ONE
);
TransactionSynchronizationManager.setActualTransactionActive(false);
TransactionSynchronizationManager.clearSynchronization();
executor.destroy();
}
}
/**
* 锁续期失败后事务提交必须被阻止。
*/
@Test
public void renewalFailureShouldPreventTransactionCommit() {
RedisLockExecutor redisLockExecutor = Mockito.mock(RedisLockExecutor.class);
RedisLockExecutor.LockHandle lockHandle = Mockito.mock(RedisLockExecutor.LockHandle.class);
ScheduledExecutorService renewExecutor = Mockito.mock(ScheduledExecutorService.class);
ScheduledFuture<?> renewTask = Mockito.mock(ScheduledFuture.class);
Mockito.when(redisLockExecutor.acquire(
Mockito.anyString(),
Mockito.any(Duration.class),
Mockito.any(Duration.class)
)).thenReturn(lockHandle);
ArgumentCaptor<Runnable> renewCaptor = ArgumentCaptor.forClass(Runnable.class);
Mockito.doReturn(renewTask).when(renewExecutor).scheduleWithFixedDelay(
renewCaptor.capture(),
Mockito.anyLong(),
Mockito.anyLong(),
Mockito.eq(TimeUnit.MILLISECONDS)
);
Mockito.when(lockHandle.renew()).thenReturn(false);
AgentBindingLockExecutor executor = new AgentBindingLockExecutor(redisLockExecutor, renewExecutor);
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(true);
try {
executor.execute(BigInteger.ONE, () -> "ok");
renewCaptor.getValue().run();
TransactionSynchronization synchronization =
TransactionSynchronizationManager.getSynchronizations().get(0);
IllegalStateException exception = Assert.assertThrows(
IllegalStateException.class,
() -> synchronization.beforeCommit(false)
);
Assert.assertEquals("Agent 绑定锁已失效,事务禁止提交", exception.getMessage());
synchronization.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK);
} finally {
TransactionSynchronizationManager.unbindResourceIfPossible(
"easyflow:lock:agent:binding:" + BigInteger.ONE
);
TransactionSynchronizationManager.setActualTransactionActive(false);
TransactionSynchronizationManager.clearSynchronization();
executor.destroy();
}
}
}