fix: 同步工具审批过期状态

- 以条件更新避免过期任务覆盖并发人工审批

- 将过期结果同步到本地或远程运行节点

- 补充跨节点命令与过期任务回归测试
This commit is contained in:
2026-07-23 19:47:23 +08:00
parent df9fe4c2fe
commit 4a8e633083
10 changed files with 296 additions and 7 deletions

View File

@@ -13,5 +13,10 @@ public enum AgentRuntimeCommandAction {
/**
* 拒绝工具执行。
*/
REJECT
REJECT,
/**
* 审批过期并取消工具执行。
*/
EXPIRE
}

View File

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

View File

@@ -91,6 +91,21 @@ public class AgentRuntimeCommandProducer {
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.REJECT, reason, operatorId, userId);
}
/**
* 投递远程审批过期命令。
*
* @param targetNodeId 目标节点 ID
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param reason 过期原因
*/
public void sendExpire(String targetNodeId,
String requestId,
String resumeToken,
String reason) {
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.EXPIRE, reason, null, null);
}
private void sendAndWait(String targetNodeId,
String requestId,
String resumeToken,

View File

@@ -74,6 +74,7 @@ public class AgentRunService {
private static final Logger LOG = LoggerFactory.getLogger(AgentRunService.class);
private static final String ASSISTANT_CODE = "AGENT";
private static final String DRAFT_ASSISTANT_CODE = "AGENT_DRAFT";
private static final String HITL_APPROVAL_EXPIRED_REASON = "审批超时,已自动拒绝";
@Resource
private AgentService agentService;
@@ -338,6 +339,38 @@ public class AgentRunService {
() -> agentHitlPendingService.reject(resumeToken, operatorId, reason));
}
/**
* 将已经持久化为过期状态的审批同步到运行节点。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
*/
public void expireApproval(String requestId, String resumeToken) {
if (agentRunRegistry.containsResumeTarget(requestId, resumeToken)) {
expireApprovalLocal(requestId, resumeToken, HITL_APPROVAL_EXPIRED_REASON);
return;
}
dispatchRemoteRuntimeCommand(requestId, resumeToken, AgentRuntimeCommandAction.EXPIRE,
HITL_APPROVAL_EXPIRED_REASON, null, null);
}
/**
* 在当前运行节点取消已经过期的审批。
*
* <p>数据库记录已由过期任务原子更新,本方法只恢复 runtime 的拒绝分支,
* 避免再次消费持久化 pending。</p>
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param reason 过期原因
*/
public void expireApprovalLocal(String requestId, String resumeToken, String reason) {
String resolvedReason = reason == null || reason.isBlank()
? HITL_APPROVAL_EXPIRED_REASON
: reason;
agentRunRegistry.reject(requestId, resumeToken, null, resolvedReason);
}
private void dispatchRemoteRuntimeCommand(String requestId,
String resumeToken,
AgentRuntimeCommandAction action,
@@ -364,7 +397,16 @@ public class AgentRunService {
agentRuntimeCommandProducer.sendApprove(ownerNodeId, resolvedRequestId, resumeToken, operatorId, userId);
return;
}
agentRuntimeCommandProducer.sendReject(ownerNodeId, resolvedRequestId, resumeToken, reason, operatorId, userId);
if (action == AgentRuntimeCommandAction.REJECT) {
agentRuntimeCommandProducer.sendReject(
ownerNodeId, resolvedRequestId, resumeToken, reason, operatorId, userId);
return;
}
if (action == AgentRuntimeCommandAction.EXPIRE) {
agentRuntimeCommandProducer.sendExpire(ownerNodeId, resolvedRequestId, resumeToken, reason);
return;
}
throw new BusinessException("不支持的 Agent 运行命令");
}
private String resolveRequestIdForRemoteDispatch(String requestId, String resumeToken) {

View File

@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.entity.AgentHitlPending;
import tech.easyflow.agent.runtime.AgentRunService;
import tech.easyflow.common.cache.DistributedScheduledLock;
import java.util.List;
@@ -19,14 +20,18 @@ public class AgentHitlPendingExpirationTask {
private static final int BATCH_SIZE = 100;
private final AgentHitlPendingService pendingService;
private final AgentRunService agentRunService;
/**
* 创建任务。
*
* @param pendingService pending 服务
* @param agentRunService Agent 运行服务
*/
public AgentHitlPendingExpirationTask(AgentHitlPendingService pendingService) {
public AgentHitlPendingExpirationTask(AgentHitlPendingService pendingService,
AgentRunService agentRunService) {
this.pendingService = pendingService;
this.agentRunService = agentRunService;
}
/**
@@ -40,8 +45,29 @@ public class AgentHitlPendingExpirationTask {
if (!expired.isEmpty()) {
LOG.info("Expired Agent HITL pending records, count={}", expired.size());
}
for (AgentHitlPending pending : expired) {
notifyRuntimeExpired(pending);
}
} catch (RuntimeException e) {
LOG.warn("Expire Agent HITL pending records failed, message={}", e.getMessage(), e);
}
}
/**
* 通知审批所属运行节点进入过期取消分支。
*
* @param pending 已过期审批记录
*/
private void notifyRuntimeExpired(AgentHitlPending pending) {
if (pending == null) {
return;
}
try {
agentRunService.expireApproval(pending.getRequestId(), pending.getResumeToken());
} catch (RuntimeException e) {
// 单条运行态已结束或远程节点不可用时继续处理本批其他过期记录。
LOG.warn("Notify expired Agent HITL runtime failed, requestId={}, message={}",
pending.getRequestId(), e.getMessage(), e);
}
}
}

View File

@@ -13,6 +13,7 @@ import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.math.BigInteger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
@@ -147,14 +148,28 @@ public class AgentHitlPendingServiceImpl implements AgentHitlPendingService {
.le("expires_at", new Date())
.limit(Math.max(1, limit)));
Date now = new Date();
List<AgentHitlPending> expired = new ArrayList<>(records.size());
for (AgentHitlPending record : records) {
record.setStatus(AgentHitlPendingStatus.EXPIRED.name());
record.setRejectReason("审批超时,已自动拒绝");
AgentHitlPending update = new AgentHitlPending();
update.setStatus(AgentHitlPendingStatus.EXPIRED.name());
update.setRejectReason("审批超时,已自动拒绝");
update.setConsumedAt(now);
update.setModified(now);
// 用 status=PENDING 作为过期条件,避免定时任务覆盖刚刚完成的人工审批。
int updated = pendingMapper.updateByQuery(update, QueryWrapper.create()
.eq("id", record.getId())
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0));
if (updated <= 0) {
continue;
}
record.setStatus(update.getStatus());
record.setRejectReason(update.getRejectReason());
record.setConsumedAt(now);
record.setModified(now);
pendingMapper.update(record);
expired.add(record);
}
return records;
return expired;
}
private AgentHitlPending consume(String resumeToken,