feat: 全新智能体功能

- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
This commit is contained in:
2026-05-25 11:42:48 +08:00
parent 6c3d98eaac
commit 72df00f25b
168 changed files with 22045 additions and 400 deletions

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-modules</artifactId>
<version>${revision}</version>
</parent>
<name>easyflow-module-agent</name>
<artifactId>easyflow-module-agent</artifactId>
<dependencies>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-ai</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-chatlog</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-approval</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-system</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-chat-protocol</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-cache</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-web</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.easyagents</groupId>
<artifactId>easy-agents-agent-runtime</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,14 @@
package tech.easyflow.agent.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* Agent 模块自动配置。
*/
@AutoConfiguration
@MapperScan("tech.easyflow.agent.mapper")
@EnableConfigurationProperties(AgentRuntimeProperties.class)
public class AgentModuleConfig {
}

View File

@@ -0,0 +1,127 @@
package tech.easyflow.agent.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/**
* Agent 运行态生产化配置。
*/
@ConfigurationProperties(prefix = "easyflow.agent.runtime")
public class AgentRuntimeProperties {
/**
* Redis 热态 session 缓存 TTL。
*/
private Duration sessionCacheTtl = Duration.ofHours(24);
/**
* HITL pending 默认过期时间。
*/
private Duration hitlPendingTimeout = Duration.ofMinutes(30);
/**
* 运行锁等待时间。
*/
private Duration lockWaitTimeout = Duration.ofSeconds(2);
/**
* 运行锁租约时间。
*/
private Duration lockLeaseTimeout = Duration.ofMinutes(5);
/**
* 运行锁续期间隔。
*/
private Duration lockRenewInterval = Duration.ofMinutes(1);
/**
* 获取 Redis 热态 session 缓存 TTL。
*
* @return 缓存 TTL
*/
public Duration getSessionCacheTtl() {
return sessionCacheTtl;
}
/**
* 设置 Redis 热态 session 缓存 TTL。
*
* @param sessionCacheTtl 缓存 TTL
*/
public void setSessionCacheTtl(Duration sessionCacheTtl) {
this.sessionCacheTtl = sessionCacheTtl == null ? Duration.ofHours(24) : sessionCacheTtl;
}
/**
* 获取 HITL pending 默认过期时间。
*
* @return 过期时间
*/
public Duration getHitlPendingTimeout() {
return hitlPendingTimeout;
}
/**
* 设置 HITL pending 默认过期时间。
*
* @param hitlPendingTimeout 过期时间
*/
public void setHitlPendingTimeout(Duration hitlPendingTimeout) {
this.hitlPendingTimeout = hitlPendingTimeout == null ? Duration.ofMinutes(30) : hitlPendingTimeout;
}
/**
* 获取运行锁等待时间。
*
* @return 等待时间
*/
public Duration getLockWaitTimeout() {
return lockWaitTimeout;
}
/**
* 设置运行锁等待时间。
*
* @param lockWaitTimeout 等待时间
*/
public void setLockWaitTimeout(Duration lockWaitTimeout) {
this.lockWaitTimeout = lockWaitTimeout == null ? Duration.ofSeconds(2) : lockWaitTimeout;
}
/**
* 获取运行锁租约时间。
*
* @return 租约时间
*/
public Duration getLockLeaseTimeout() {
return lockLeaseTimeout;
}
/**
* 设置运行锁租约时间。
*
* @param lockLeaseTimeout 租约时间
*/
public void setLockLeaseTimeout(Duration lockLeaseTimeout) {
this.lockLeaseTimeout = lockLeaseTimeout == null ? Duration.ofMinutes(5) : lockLeaseTimeout;
}
/**
* 获取运行锁续期间隔。
*
* @return 续期间隔
*/
public Duration getLockRenewInterval() {
return lockRenewInterval;
}
/**
* 设置运行锁续期间隔。
*
* @param lockRenewInterval 续期间隔
*/
public void setLockRenewInterval(Duration lockRenewInterval) {
this.lockRenewInterval = lockRenewInterval == null ? Duration.ofMinutes(1) : lockRenewInterval;
}
}

View File

@@ -0,0 +1,132 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import tech.easyflow.common.entity.DateEntity;
import tech.easyflow.system.permission.resource.VisibilityResource;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Agent 主实体。
*/
@Table("tb_agent")
public class Agent extends DateEntity implements VisibilityResource, Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger deptId;
private String name;
private String description;
private String avatar;
private BigInteger categoryId;
private BigInteger modelId;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> modelConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> generationConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> promptConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> memoryConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> executionConfigJson = new LinkedHashMap<>();
private Integer status;
private String visibilityScope;
private String publishStatus;
private BigInteger currentApprovalInstanceId;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> publishedSnapshotJson = new LinkedHashMap<>();
private Date publishedAt;
private BigInteger publishedBy;
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
@Column(ignore = true)
private Boolean approvalPending;
@Column(ignore = true)
private String currentApprovalActionType;
@Column(ignore = true)
private String displayPublishStatus;
@Column(ignore = true)
private String createdByName;
@Column(ignore = true)
private List<AgentToolBinding> toolBindings;
@Column(ignore = true)
private List<AgentKnowledgeBinding> knowledgeBindings;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getDeptId() { return deptId; }
public void setDeptId(BigInteger deptId) { this.deptId = deptId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getAvatar() { return avatar; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public BigInteger getCategoryId() { return categoryId; }
public void setCategoryId(BigInteger categoryId) { this.categoryId = categoryId; }
public BigInteger getModelId() { return modelId; }
public void setModelId(BigInteger modelId) { this.modelId = modelId; }
public Map<String, Object> getModelConfigJson() { return modelConfigJson; }
public void setModelConfigJson(Map<String, Object> modelConfigJson) { this.modelConfigJson = modelConfigJson == null ? new LinkedHashMap<>() : modelConfigJson; }
public Map<String, Object> getGenerationConfigJson() { return generationConfigJson; }
public void setGenerationConfigJson(Map<String, Object> generationConfigJson) { this.generationConfigJson = generationConfigJson == null ? new LinkedHashMap<>() : generationConfigJson; }
public Map<String, Object> getPromptConfigJson() { return promptConfigJson; }
public void setPromptConfigJson(Map<String, Object> promptConfigJson) { this.promptConfigJson = promptConfigJson == null ? new LinkedHashMap<>() : promptConfigJson; }
public Map<String, Object> getMemoryConfigJson() { return memoryConfigJson; }
public void setMemoryConfigJson(Map<String, Object> memoryConfigJson) { this.memoryConfigJson = memoryConfigJson == null ? new LinkedHashMap<>() : memoryConfigJson; }
public Map<String, Object> getExecutionConfigJson() { return executionConfigJson; }
public void setExecutionConfigJson(Map<String, Object> executionConfigJson) { this.executionConfigJson = executionConfigJson == null ? new LinkedHashMap<>() : executionConfigJson; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getVisibilityScope() { return visibilityScope; }
public void setVisibilityScope(String visibilityScope) { this.visibilityScope = visibilityScope; }
public String getPublishStatus() { return publishStatus; }
public void setPublishStatus(String publishStatus) { this.publishStatus = publishStatus; }
public BigInteger getCurrentApprovalInstanceId() { return currentApprovalInstanceId; }
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { this.currentApprovalInstanceId = currentApprovalInstanceId; }
public Map<String, Object> getPublishedSnapshotJson() { return publishedSnapshotJson; }
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) { this.publishedSnapshotJson = publishedSnapshotJson == null ? new LinkedHashMap<>() : publishedSnapshotJson; }
public Date getPublishedAt() { return publishedAt; }
public void setPublishedAt(Date publishedAt) { this.publishedAt = publishedAt; }
public BigInteger getPublishedBy() { return publishedBy; }
public void setPublishedBy(BigInteger publishedBy) { this.publishedBy = publishedBy; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
public Boolean getApprovalPending() { return approvalPending; }
public void setApprovalPending(Boolean approvalPending) { this.approvalPending = approvalPending; }
public String getCurrentApprovalActionType() { return currentApprovalActionType; }
public void setCurrentApprovalActionType(String currentApprovalActionType) { this.currentApprovalActionType = currentApprovalActionType; }
public String getDisplayPublishStatus() { return displayPublishStatus; }
public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; }
public String getCreatedByName() { return createdByName; }
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
public List<AgentToolBinding> getToolBindings() { return toolBindings; }
public void setToolBindings(List<AgentToolBinding> toolBindings) { this.toolBindings = toolBindings; }
public List<AgentKnowledgeBinding> getKnowledgeBindings() { return knowledgeBindings; }
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) { this.knowledgeBindings = knowledgeBindings; }
}

View File

@@ -0,0 +1,48 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import tech.easyflow.common.entity.DateEntity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* Agent 分类实体。
*/
@Table("tb_agent_category")
public class AgentCategory extends DateEntity implements Serializable {
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private String categoryName;
private Integer sortNo;
private Integer status;
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public String getCategoryName() { return categoryName; }
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
public Integer getSortNo() { return sortNo; }
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
}

View File

@@ -0,0 +1,89 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import tech.easyflow.common.entity.DateEntity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 工具审批挂起态实体。
*/
@Table("tb_agent_hitl_pending")
public class AgentHitlPending extends DateEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger agentId;
private BigInteger chatSessionId;
private String runtimeSessionId;
private String requestId;
private String resumeToken;
private String toolCallId;
private String toolName;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> toolInputJson = new LinkedHashMap<>();
private String status;
private String rejectReason;
private Date expiresAt;
private Date consumedAt;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> metadataJson = new LinkedHashMap<>();
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
private Integer isDeleted;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getAgentId() { return agentId; }
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
public BigInteger getChatSessionId() { return chatSessionId; }
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
public String getRuntimeSessionId() { return runtimeSessionId; }
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
public String getResumeToken() { return resumeToken; }
public void setResumeToken(String resumeToken) { this.resumeToken = resumeToken; }
public String getToolCallId() { return toolCallId; }
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
public String getToolName() { return toolName; }
public void setToolName(String toolName) { this.toolName = toolName; }
public Map<String, Object> getToolInputJson() { return toolInputJson; }
public void setToolInputJson(Map<String, Object> toolInputJson) { this.toolInputJson = toolInputJson == null ? new LinkedHashMap<>() : toolInputJson; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getRejectReason() { return rejectReason; }
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
public Date getExpiresAt() { return expiresAt; }
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
public Date getConsumedAt() { return consumedAt; }
public void setConsumedAt(Date consumedAt) { this.consumedAt = consumedAt; }
public Map<String, Object> getMetadataJson() { return metadataJson; }
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
}

View File

@@ -0,0 +1,72 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import tech.easyflow.common.entity.DateEntity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 知识库绑定实体。
*/
@Table("tb_agent_knowledge_binding")
public class AgentKnowledgeBinding extends DateEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger agentId;
private BigInteger knowledgeId;
private String retrievalMode;
private Boolean enabled;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> optionsJson = new LinkedHashMap<>();
@Column(ignore = true)
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
@Column(ignore = true)
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
private Integer sortNo;
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getAgentId() { return agentId; }
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
public BigInteger getKnowledgeId() { return knowledgeId; }
public void setKnowledgeId(BigInteger knowledgeId) { this.knowledgeId = knowledgeId; }
public String getRetrievalMode() { return retrievalMode; }
public void setRetrievalMode(String retrievalMode) { this.retrievalMode = retrievalMode; }
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
public Map<String, Object> getOptionsJson() { return optionsJson; }
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
public Map<String, Object> getResourceSummary() { return resourceSummary; }
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
public Integer getSortNo() { return sortNo; }
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
}

View File

@@ -0,0 +1,76 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 运行事件摘要实体。
*/
@Table("tb_agent_run_event")
public class AgentRunEventRecord implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger agentId;
private BigInteger chatSessionId;
private BigInteger roundId;
private Integer roundNo;
private Integer variantIndex;
private String requestId;
private String eventId;
private String eventType;
private String eventPhase;
private String toolCallId;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> payloadJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> metadataJson = new LinkedHashMap<>();
private Date created;
private BigInteger createdBy;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getAgentId() { return agentId; }
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
public BigInteger getChatSessionId() { return chatSessionId; }
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
public BigInteger getRoundId() { return roundId; }
public void setRoundId(BigInteger roundId) { this.roundId = roundId; }
public Integer getRoundNo() { return roundNo; }
public void setRoundNo(Integer roundNo) { this.roundNo = roundNo; }
public Integer getVariantIndex() { return variantIndex; }
public void setVariantIndex(Integer variantIndex) { this.variantIndex = variantIndex; }
public String getRequestId() { return requestId; }
public void setRequestId(String requestId) { this.requestId = requestId; }
public String getEventId() { return eventId; }
public void setEventId(String eventId) { this.eventId = eventId; }
public String getEventType() { return eventType; }
public void setEventType(String eventType) { this.eventType = eventType; }
public String getEventPhase() { return eventPhase; }
public void setEventPhase(String eventPhase) { this.eventPhase = eventPhase; }
public String getToolCallId() { return toolCallId; }
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
public Map<String, Object> getPayloadJson() { return payloadJson; }
public void setPayloadJson(Map<String, Object> payloadJson) { this.payloadJson = payloadJson == null ? new LinkedHashMap<>() : payloadJson; }
public Map<String, Object> getMetadataJson() { return metadataJson; }
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
public Date getCreated() { return created; }
public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
}

View File

@@ -0,0 +1,76 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import tech.easyflow.common.entity.DateEntity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* AgentScope 会话状态实体。
*/
@Table("tb_agent_session")
public class AgentSession extends DateEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger agentId;
private BigInteger chatSessionId;
private String runtimeSessionId;
private String sessionKey;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> stateJson = new LinkedHashMap<>();
private Long version;
private Long cacheVersion;
private Date lastAccessAt;
private Date expiresAt;
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
private Integer isDeleted;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getAgentId() { return agentId; }
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
public BigInteger getChatSessionId() { return chatSessionId; }
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
public String getRuntimeSessionId() { return runtimeSessionId; }
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
public String getSessionKey() { return sessionKey; }
public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; }
public Map<String, Object> getStateJson() { return stateJson; }
public void setStateJson(Map<String, Object> stateJson) { this.stateJson = stateJson == null ? new LinkedHashMap<>() : stateJson; }
public Long getVersion() { return version; }
public void setVersion(Long version) { this.version = version; }
public Long getCacheVersion() { return cacheVersion; }
public void setCacheVersion(Long cacheVersion) { this.cacheVersion = cacheVersion; }
public Date getLastAccessAt() { return lastAccessAt; }
public void setLastAccessAt(Date lastAccessAt) { this.lastAccessAt = lastAccessAt; }
public Date getExpiresAt() { return expiresAt; }
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
public Integer getIsDeleted() { return isDeleted; }
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
}

View File

@@ -0,0 +1,82 @@
package tech.easyflow.agent.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import tech.easyflow.common.entity.DateEntity;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 工具绑定实体。
*/
@Table("tb_agent_tool_binding")
public class AgentToolBinding extends DateEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
@Column(tenantId = true)
private BigInteger tenantId;
private BigInteger agentId;
private String toolType;
private BigInteger targetId;
private String toolName;
private Boolean enabled;
private Boolean hitlEnabled;
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> hitlConfigJson = new LinkedHashMap<>();
@Column(typeHandler = FastjsonTypeHandler.class)
private Map<String, Object> optionsJson = new LinkedHashMap<>();
@Column(ignore = true)
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
@Column(ignore = true)
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
private Integer sortNo;
private Date created;
private BigInteger createdBy;
private Date modified;
private BigInteger modifiedBy;
public BigInteger getId() { return id; }
public void setId(BigInteger id) { this.id = id; }
public BigInteger getTenantId() { return tenantId; }
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
public BigInteger getAgentId() { return agentId; }
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
public String getToolType() { return toolType; }
public void setToolType(String toolType) { this.toolType = toolType; }
public BigInteger getTargetId() { return targetId; }
public void setTargetId(BigInteger targetId) { this.targetId = targetId; }
public String getToolName() { return toolName; }
public void setToolName(String toolName) { this.toolName = toolName; }
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
public Boolean getHitlEnabled() { return hitlEnabled; }
public void setHitlEnabled(Boolean hitlEnabled) { this.hitlEnabled = hitlEnabled; }
public Map<String, Object> getHitlConfigJson() { return hitlConfigJson; }
public void setHitlConfigJson(Map<String, Object> hitlConfigJson) { this.hitlConfigJson = hitlConfigJson == null ? new LinkedHashMap<>() : hitlConfigJson; }
public Map<String, Object> getOptionsJson() { return optionsJson; }
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
public Map<String, Object> getResourceSummary() { return resourceSummary; }
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
public Integer getSortNo() { return sortNo; }
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
@Override public Date getCreated() { return created; }
@Override public void setCreated(Date created) { this.created = created; }
public BigInteger getCreatedBy() { return createdBy; }
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
@Override public Date getModified() { return modified; }
@Override public void setModified(Date modified) { this.modified = modified; }
public BigInteger getModifiedBy() { return modifiedBy; }
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
}

View File

@@ -0,0 +1,26 @@
package tech.easyflow.agent.enums;
import java.util.Locale;
/**
* Agent 工具绑定类型。
*/
public enum AgentToolType {
WORKFLOW,
PLUGIN,
MCP;
/**
* 解析工具类型。
*
* @param value 类型值
* @return 工具类型
*/
public static AgentToolType from(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("toolType 不能为空");
}
return AgentToolType.valueOf(value.trim().toUpperCase(Locale.ROOT));
}
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentCategory;
/**
* Agent 分类 Mapper。
*/
public interface AgentCategoryMapper extends BaseMapper<AgentCategory> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentHitlPending;
/**
* Agent 工具审批挂起态 Mapper。
*/
public interface AgentHitlPendingMapper extends BaseMapper<AgentHitlPending> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
/**
* Agent 知识库绑定 Mapper。
*/
public interface AgentKnowledgeBindingMapper extends BaseMapper<AgentKnowledgeBinding> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.Agent;
/**
* Agent Mapper。
*/
public interface AgentMapper extends BaseMapper<Agent> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentRunEventRecord;
/**
* Agent 运行事件摘要 Mapper。
*/
public interface AgentRunEventRecordMapper extends BaseMapper<AgentRunEventRecord> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentSession;
/**
* AgentScope 会话状态 Mapper。
*/
public interface AgentSessionMapper extends BaseMapper<AgentSession> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.agent.entity.AgentToolBinding;
/**
* Agent 工具绑定 Mapper。
*/
public interface AgentToolBindingMapper extends BaseMapper<AgentToolBinding> {
}

View File

@@ -0,0 +1,168 @@
package tech.easyflow.agent.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.publish.AbstractAiResourceLifecycleHandler;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.approval.service.ApprovalInstanceService;
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.Date;
import java.util.Map;
/**
* Agent 审批资源处理器。
*/
@Component
public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Agent> {
private final AgentService agentService;
private final AgentToolBindingService agentToolBindingService;
private final AgentKnowledgeBindingService agentKnowledgeBindingService;
private final ResourceAccessService resourceAccessService;
/**
* 创建 Agent 审批资源处理器。
*
* @param approvalInstanceService 审批实例服务
* @param objectMapper JSON 映射器
* @param agentService Agent 服务
* @param agentToolBindingService Agent 工具绑定服务
* @param agentKnowledgeBindingService Agent 知识库绑定服务
* @param resourceAccessService 资源访问服务
*/
public AgentApprovalSubjectHandler(ApprovalInstanceService approvalInstanceService,
ObjectMapper objectMapper,
AgentService agentService,
AgentToolBindingService agentToolBindingService,
AgentKnowledgeBindingService agentKnowledgeBindingService,
ResourceAccessService resourceAccessService) {
super(approvalInstanceService, objectMapper);
this.agentService = agentService;
this.agentToolBindingService = agentToolBindingService;
this.agentKnowledgeBindingService = agentKnowledgeBindingService;
this.resourceAccessService = resourceAccessService;
}
/**
* {@inheritDoc}
*/
@Override
public String resourceType() {
return ApprovalResourceType.AGENT.getCode();
}
/**
* {@inheritDoc}
*/
@Override
public void assertPublishedAccess(Object identifier, String denyMessage) {
Agent agent = agentService.getById(String.valueOf(identifier));
if (agent == null || !PublishStatus.from(agent.getPublishStatus()).isExternallyVisible()
|| agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException(denyMessage);
}
}
@Override
protected Agent requireResource(BigInteger resourceId) {
Agent agent = agentService.getById(resourceId);
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
@Override
protected void assertManagePermission(Agent resource) {
resourceAccessService.assertAccess(CategoryResourceType.AGENT, resource, ResourceAction.MANAGE, "无权限管理该 Agent");
}
@Override
protected BigInteger getCategoryId(Agent resource) {
return resource.getCategoryId();
}
@Override
protected BigInteger getDeptId(Agent resource) {
return resource.getDeptId();
}
@Override
protected String getTitle(Agent resource) {
return resource.getName();
}
@Override
protected PublishStatus getCurrentStatus(Agent resource) {
return PublishStatus.from(resource.getPublishStatus());
}
@Override
protected Map<String, Object> getPublishedSnapshot(Agent resource) {
return resource.getPublishedSnapshotJson();
}
@Override
protected Map<String, Object> buildResourceSnapshot(Agent resource) {
return agentService.buildPublishSnapshot(resource);
}
@Override
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(publishStatus.getCode());
agent.setCurrentApprovalInstanceId(currentApprovalInstanceId);
agentService.updateById(agent);
}
@Override
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
agent.setPublishedSnapshotJson(resourceSnapshot);
agent.setPublishedAt(new Date());
agent.setPublishedBy(operatorId);
agent.setCurrentApprovalInstanceId(null);
agentService.updateById(agent);
}
@Override
protected void markResourceOffline(BigInteger resourceId) {
Agent agent = new Agent();
agent.setId(resourceId);
agent.setPublishStatus(PublishStatus.OFFLINE.getCode());
agent.setCurrentApprovalInstanceId(null);
agentService.updateById(agent);
}
@Override
protected void removeResource(BigInteger resourceId) {
agentService.removeById(resourceId);
}
@Override
protected void beforeRemove(BigInteger resourceId) {
agentToolBindingService.remove(QueryWrapper.create().eq(AgentToolBinding::getAgentId, resourceId));
agentKnowledgeBindingService.remove(QueryWrapper.create().eq(AgentKnowledgeBinding::getAgentId, resourceId));
}
@Override
protected String resourceLabel() {
return "Agent";
}
}

View File

@@ -0,0 +1,71 @@
package tech.easyflow.agent.publish;
import org.springframework.stereotype.Service;
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.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
/**
* Agent 发布生命周期应用服务。
*/
@Service
public class AgentPublishAppService {
private final AiResourceLifecycleService aiResourceLifecycleService;
/**
* 创建 Agent 发布应用服务。
*
* @param aiResourceLifecycleService AI 资源生命周期服务
*/
public AgentPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
this.aiResourceLifecycleService = aiResourceLifecycleService;
}
/**
* 提交 Agent 发布审批。
*
* @param id Agent ID
* @return 审批动作结果
*/
public ApprovalActionResult submitPublishApproval(BigInteger id) {
return submit(id, ApprovalActionType.PUBLISH);
}
/**
* 提交 Agent 下线审批。
*
* @param id Agent ID
* @return 审批动作结果
*/
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
return submit(id, ApprovalActionType.OFFLINE);
}
/**
* 提交 Agent 删除审批。
*
* @param id Agent ID
* @return 审批动作结果
*/
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
return submit(id, ApprovalActionType.DELETE);
}
private ApprovalActionResult submit(BigInteger id, ApprovalActionType actionType) {
if (id == null) {
throw new BusinessException("Agent 审批时资源ID不能为空");
}
return aiResourceLifecycleService.submitAction(
ApprovalResourceType.AGENT.getCode(),
id,
actionType.getCode(),
SaTokenUtil.getLoginAccount().getId()
);
}
}

View File

@@ -0,0 +1,55 @@
package tech.easyflow.agent.runtime;
import java.math.BigInteger;
/**
* Agent 管理端运行请求。
*/
public class AgentChatRequest {
private BigInteger agentId;
private BigInteger sessionId;
private String prompt;
/**
* 获取 Agent ID。
*
* @return Agent ID
*/
public BigInteger getAgentId() { return agentId; }
/**
* 设置 Agent ID。
*
* @param agentId Agent ID
*/
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
/**
* 获取会话 ID。
*
* @return 会话 ID
*/
public BigInteger getSessionId() { return sessionId; }
/**
* 设置会话 ID。
*
* @param sessionId 会话 ID
*/
public void setSessionId(BigInteger sessionId) { this.sessionId = sessionId; }
/**
* 获取用户纯文本输入。
*
* @return 用户输入
*/
public String getPrompt() { return prompt; }
/**
* 设置用户纯文本输入。
*
* @param prompt 用户输入
*/
public void setPrompt(String prompt) { this.prompt = prompt; }
}

View File

@@ -0,0 +1,674 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentDefinition;
import com.easyagents.agent.runtime.AgentExecutionOptions;
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgePolicy;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
import com.easyagents.agent.runtime.memory.AgentMemoryType;
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
import com.easyagents.agent.runtime.model.AgentModelProviderType;
import com.easyagents.agent.runtime.model.AgentModelSpec;
import com.easyagents.agent.runtime.tool.AgentToolCategory;
import com.easyagents.agent.runtime.tool.AgentToolResult;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import com.easyagents.core.document.Document;
import com.easyagents.core.model.chat.tool.Parameter;
import com.easyagents.core.model.chat.tool.Tool;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
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.ai.easyagents.tool.ChatToolNameHelper;
import tech.easyflow.ai.easyagents.tool.McpTool;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
import tech.easyflow.ai.service.*;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.time.Duration;
import java.util.*;
/**
* 将 Agent 发布快照编译为 easy-agents-agent-runtime 可执行定义。
*/
@Component
public class AgentDefinitionCompiler {
private static final Logger LOG = LoggerFactory.getLogger(AgentDefinitionCompiler.class);
private static final int LOG_TEXT_MAX_LENGTH = 500;
@Resource
private ModelService modelService;
@Resource
private WorkflowService workflowService;
@Resource
private PluginItemService pluginItemService;
@Resource
private McpService mcpService;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private ObjectMapper objectMapper;
/**
* 编译 Agent 运行时定义和调用器。
*
* @param agent 已发布 Agent 视图
* @return 运行时编译结果
*/
public AgentRuntimeBundle compile(Agent agent) {
if (agent == null || agent.getId() == null) {
throw new BusinessException("Agent 运行定义不能为空");
}
AgentRuntimeBundle bundle = new AgentRuntimeBundle();
AgentDefinition definition = new AgentDefinition();
definition.setAgentId(agent.getId().toString());
definition.setAgentName(agent.getName());
definition.setDescription(agent.getDescription());
definition.setSystemPrompt(stringValue(agent.getPromptConfigJson(), "systemPrompt", stringValue(agent.getPromptConfigJson(), "prompt", "")));
definition.setModelSpec(buildModelSpec(agent));
definition.setGenerationOptions(buildGenerationOptions(agent.getGenerationConfigJson()));
definition.setExecutionOptions(buildExecutionOptions(agent.getExecutionConfigJson()));
definition.setMemoryPolicy(buildMemoryPolicy(agent.getMemoryConfigJson()));
bundle.setDefinition(definition);
compileTools(agent, definition, bundle);
compileKnowledge(agent, definition, bundle);
return bundle;
}
private AgentModelSpec buildModelSpec(Agent agent) {
Model model = modelService.getModelInstance(agent.getModelId());
if (model == null) {
throw new BusinessException("Agent 模型不存在");
}
Map<String, Object> config = agent.getModelConfigJson();
AgentModelSpec spec = new AgentModelSpec();
String providerType = stringValue(config, "providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType());
spec.setProviderType(parseProviderType(providerType));
spec.setModelName(stringValue(config, "modelName", model.getModelName()));
spec.setBaseUrl(stringValue(config, "baseUrl", model.getEndpoint()));
spec.setEndpointPath(stringValue(config, "endpointPath", model.getRequestPath()));
spec.setApiKey(stringValue(config, "apiKey", model.getApiKey()));
spec.getMetadata().put("modelId", model.getId());
return spec;
}
private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) {
AgentGenerationOptions options = new AgentGenerationOptions();
options.setTemperature(doubleValue(config, "temperature"));
options.setTopP(doubleValue(config, "topP"));
options.setTopK(intValue(config, "topK"));
options.setMaxTokens(intValue(config, "maxTokens"));
options.setMaxCompletionTokens(intValue(config, "maxCompletionTokens"));
options.setThinkingBudget(intValue(config, "thinkingBudget"));
options.setReasoningEffort(stringValue(config, "reasoningEffort", stringValue(config, "thinkingLevel", null)));
options.setThinkingEnabled(booleanValue(config, "thinkingEnabled"));
Boolean stream = booleanValue(config, "stream");
if (stream != null) {
options.setStream(stream);
}
options.setAdditionalBodyParams(mapValue(config, "additionalBodyParams"));
options.setAdditionalHeaders(stringMapValue(config, "additionalHeaders"));
options.setAdditionalQueryParams(stringMapValue(config, "additionalQueryParams"));
return options;
}
private AgentExecutionOptions buildExecutionOptions(Map<String, Object> config) {
AgentExecutionOptions options = new AgentExecutionOptions();
Integer maxIters = intValue(config, "maxIters");
if (maxIters != null) {
options.setMaxIters(maxIters);
}
Integer timeoutSeconds = intValue(config, "timeoutSeconds");
if (timeoutSeconds != null) {
options.setTimeout(Duration.ofSeconds(timeoutSeconds));
}
Boolean reasoningEnabled = booleanValue(config, "reasoningEnabled");
if (reasoningEnabled != null) {
options.setReasoningEnabled(reasoningEnabled);
}
Boolean toolCallingEnabled = booleanValue(config, "toolCallingEnabled");
if (toolCallingEnabled != null) {
options.setToolCallingEnabled(toolCallingEnabled);
}
return options;
}
private AgentMemoryPolicy buildMemoryPolicy(Map<String, Object> config) {
AgentMemoryPolicy policy = new AgentMemoryPolicy();
policy.setType(memoryTypeValue(config, "type"));
Map<String, Object> compressionConfig = mapValue(config, "compressionParameter");
if (compressionConfig.isEmpty()) {
compressionConfig = mapValue(config, "autoContext");
}
AgentMemoryCompressionParameter parameter = new AgentMemoryCompressionParameter();
Boolean enabled = booleanValue(compressionConfig, "enabled");
if (enabled != null) {
parameter.setEnabled(enabled);
}
Integer msgThreshold = intValue(compressionConfig, "msgThreshold");
if (msgThreshold == null) {
msgThreshold = intValue(config, "maxAttachedMessageCount");
}
if (msgThreshold == null) {
msgThreshold = intValue(config, "historyLimit");
}
if (msgThreshold != null) {
parameter.setMsgThreshold(msgThreshold);
policy.setMaxAttachedMessageCount(msgThreshold);
}
Integer lastKeep = intValue(compressionConfig, "lastKeep");
if (lastKeep != null) {
parameter.setLastKeep(lastKeep);
}
Double tokenRatio = doubleValue(compressionConfig, "tokenRatio");
if (tokenRatio != null) {
parameter.setTokenRatio(tokenRatio);
}
Long maxToken = longValue(compressionConfig, "maxToken");
if (maxToken != null) {
parameter.setMaxToken(maxToken);
}
Long largePayloadThreshold = longValue(compressionConfig, "largePayloadThreshold");
if (largePayloadThreshold != null) {
parameter.setLargePayloadThreshold(largePayloadThreshold);
}
Integer minCompressionTokenThreshold = intValue(compressionConfig, "minCompressionTokenThreshold");
if (minCompressionTokenThreshold != null) {
parameter.setMinCompressionTokenThreshold(minCompressionTokenThreshold);
}
Double currentRoundCompressionRatio = doubleValue(compressionConfig, "currentRoundCompressionRatio");
if (currentRoundCompressionRatio != null) {
parameter.setCurrentRoundCompressionRatio(currentRoundCompressionRatio);
}
Integer minConsecutiveToolMessages = intValue(compressionConfig, "minConsecutiveToolMessages");
if (minConsecutiveToolMessages != null) {
parameter.setMinConsecutiveToolMessages(minConsecutiveToolMessages);
}
policy.setCompressionParameter(parameter);
return policy;
}
private void compileTools(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
if (agent.getToolBindings() == null) {
return;
}
List<AgentToolSpec> specs = new ArrayList<>();
Map<String, com.easyagents.agent.runtime.tool.AgentToolInvoker> invokers = new LinkedHashMap<>();
for (AgentToolBinding binding : agent.getToolBindings()) {
if (!Boolean.TRUE.equals(binding.getEnabled())) {
continue;
}
Tool tool = buildTool(binding);
AgentToolSpec spec = toToolSpec(tool, binding);
specs.add(spec);
invokers.put(spec.getName(), (arguments, context) -> invokeTool(tool, arguments));
}
definition.setToolSpecs(specs);
bundle.setToolInvokers(invokers);
}
private Tool buildTool(AgentToolBinding binding) {
AgentToolType type = AgentToolType.from(binding.getToolType());
if (type == AgentToolType.WORKFLOW) {
Workflow workflow = snapshotOrPublishedWorkflow(binding);
if (workflow == null) {
throw new BusinessException("绑定工作流不存在");
}
return new WorkflowTool(
workflow,
true,
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
);
}
if (type == AgentToolType.PLUGIN) {
PluginItem pluginItem = snapshotOrCurrentPlugin(binding);
if (pluginItem == null) {
throw new BusinessException("绑定插件不存在");
}
return pluginItem.toFunction();
}
Mcp mcp = snapshotOrCurrentMcp(binding);
if (mcp == null) {
throw new BusinessException("绑定 MCP 不存在");
}
McpTool tool = new McpTool();
tool.setMcpId(mcp.getId());
tool.setName(binding.getToolName());
tool.setDescription(mcp.getDescription());
tool.setParameters(new Parameter[0]);
return tool;
}
private AgentToolSpec toToolSpec(Tool tool, AgentToolBinding binding) {
AgentToolSpec spec = new AgentToolSpec();
String name = resolveRuntimeToolName(tool, binding);
spec.setName(name);
spec.setDescription(safeDescription(tool == null ? null : tool.getDescription()));
spec.setCategory(AgentToolCategory.valueOf(AgentToolType.from(binding.getToolType()).name()));
spec.setParametersSchema(toSchema(tool == null ? null : tool.getParameters()));
spec.setApprovalRequired(Boolean.TRUE.equals(binding.getHitlEnabled()));
if (Boolean.TRUE.equals(binding.getHitlEnabled())) {
AgentToolApprovalRequest request = new AgentToolApprovalRequest();
request.setApprovalPrompt(stringValue(binding.getHitlConfigJson(), "prompt", "是否批准执行工具:" + name));
Map<String, Object> metadata = sanitizedHitlMetadata(binding.getHitlConfigJson());
metadata.put("toolType", binding.getToolType());
metadata.put("bindingId", binding.getId());
metadata.put("targetId", binding.getTargetId());
request.setMetadata(metadata);
spec.setApprovalRequest(request);
}
spec.getMetadata().put("bindingId", binding.getId());
spec.getMetadata().put("targetId", binding.getTargetId());
return spec;
}
private Map<String, Object> sanitizedHitlMetadata(Map<String, Object> config) {
Map<String, Object> metadata = new LinkedHashMap<>();
if (config != null) {
config.forEach((key, value) -> {
if (!isHitlPromptKey(key)) {
metadata.put(key, value);
}
});
}
return metadata;
}
private boolean isHitlPromptKey(String key) {
if (key == null) {
return false;
}
String normalized = key.trim();
return "prompt".equalsIgnoreCase(normalized)
|| "question".equalsIgnoreCase(normalized)
|| "approvalPrompt".equalsIgnoreCase(normalized);
}
private AgentToolResult invokeTool(Tool tool, Map<String, Object> arguments) {
String toolName = tool == null ? null : tool.getName();
LOG.info("Agent tool invoke started, toolName={}, arguments={}", toolName, arguments);
try {
Object result = tool.invoke(arguments == null ? Map.of() : arguments);
String resultText = result == null ? "" : String.valueOf(result);
LOG.info("Agent tool invoke completed, toolName={}, result={}", toolName, truncate(resultText));
return AgentToolResult.success(resultText);
} catch (Exception e) {
LOG.error("Agent tool invoke failed, toolName={}, message={}", toolName, e.getMessage(), e);
return AgentToolResult.failure(e.getMessage() == null ? "工具执行失败" : e.getMessage());
}
}
private String resolveRuntimeToolName(Tool tool, AgentToolBinding binding) {
String bindingName = binding == null ? null : binding.getToolName();
if (ChatToolNameHelper.isSafeToolName(bindingName)) {
return bindingName;
}
String toolName = tool == null ? null : tool.getName();
if (ChatToolNameHelper.isSafeToolName(toolName)) {
return toolName;
}
BigInteger targetId = binding == null ? null : binding.getTargetId();
return ChatToolNameHelper.buildFallbackName("tool", targetId);
}
private void compileKnowledge(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
if (agent.getKnowledgeBindings() == null) {
return;
}
List<AgentKnowledgeSpec> specs = new ArrayList<>();
Map<String, com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever> retrievers = new LinkedHashMap<>();
for (AgentKnowledgeBinding binding : agent.getKnowledgeBindings()) {
if (!Boolean.TRUE.equals(binding.getEnabled())) {
continue;
}
DocumentCollection knowledge = snapshotOrPublishedKnowledge(binding);
if (knowledge == null) {
throw new BusinessException("绑定知识库不存在");
}
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
spec.setKnowledgeId(binding.getKnowledgeId().toString());
spec.setName(knowledge.getTitle());
spec.setDescription(knowledge.getDescription());
spec.setRetrievalMode(AgentKnowledgePolicy.AGENTIC);
spec.getMetadata().put("knowledgeType", knowledge.getCollectionType());
spec.getMetadata().put("faqCollection", knowledge.isFaqCollection());
Integer limit = intValue(binding.getOptionsJson(), "limit");
spec.setLimit(limit == null ? 5 : limit);
Double threshold = doubleValue(binding.getOptionsJson(), "scoreThreshold");
if (threshold != null) {
spec.setScoreThreshold(threshold);
}
specs.add(spec);
retrievers.put(spec.getKnowledgeId(), request -> retrieveKnowledge(binding, request.getQuery(), request.getLimit(), request.getScoreThreshold()));
}
definition.setKnowledgeSpecs(specs);
bundle.setKnowledgeRetrievers(retrievers);
}
private AgentKnowledgeRetrievalResult retrieveKnowledge(AgentKnowledgeBinding binding, String query, int limit, double scoreThreshold) {
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
request.setKnowledgeId(binding.getKnowledgeId());
request.setQuery(query);
request.setLimit(limit <= 0 ? null : limit);
request.setMinSimilarity(scoreThreshold <= 0D ? null : scoreThreshold);
request.setRetrievalMode(KnowledgeRetrievalModes.parse(binding.getRetrievalMode()));
request.setCallerType("AGENT_KNOWLEDGE");
request.setCallerId(binding.getAgentId() == null ? null : binding.getAgentId().toString());
LOG.info(
"Agent knowledge retrieval started, agentId={}, knowledgeId={}, query={}, limit={}, scoreThreshold={}, retrievalMode={}",
request.getCallerId(),
request.getKnowledgeId(),
request.getQuery(),
request.getLimit(),
request.getMinSimilarity(),
request.getRetrievalMode()
);
List<Document> documents = documentCollectionService.search(request);
LOG.info(
"Agent knowledge retrieval completed, agentId={}, knowledgeId={}, query={}, documentCount={}, documents={}",
request.getCallerId(),
request.getKnowledgeId(),
request.getQuery(),
documents == null ? 0 : documents.size(),
summarizeDocuments(documents)
);
List<AgentKnowledgeDocument> mapped = new ArrayList<>();
if (documents != null) {
for (Document document : documents) {
AgentKnowledgeDocument item = new AgentKnowledgeDocument();
item.setDocumentId(firstNonBlank(
metadataString(document.getMetadataMap(), "documentId"),
document.getId() == null ? null : String.valueOf(document.getId())
));
item.setDocumentName(firstNonBlank(
metadataString(document.getMetadataMap(), "sourceFileName"),
document.getTitle()
));
item.setChunkId(firstNonBlank(
metadataString(document.getMetadataMap(), "chunkId"),
document.getId() == null ? null : String.valueOf(document.getId())
));
item.setContent(document.getContent());
item.setScore(document.getScore());
item.setMetadata(document.getMetadataMap());
item.setSourceUri(metadataString(document.getMetadataMap(), "sourceUri"));
mapped.add(item);
}
}
return AgentKnowledgeRetrievalResult.of(mapped);
}
/**
* 构建用于日志排查的知识库命中摘要,避免完整内容撑爆日志。
*
* @param documents 知识库检索命中文档
* @return 文档摘要列表
*/
private List<Map<String, Object>> summarizeDocuments(List<Document> documents) {
List<Map<String, Object>> summaries = new ArrayList<>();
if (documents == null) {
return summaries;
}
for (Document document : documents) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("id", document.getId());
summary.put("title", document.getTitle());
summary.put("score", document.getScore());
summary.put("content", truncate(document.getContent()));
summary.put("metadata", document.getMetadataMap());
summaries.add(summary);
}
return summaries;
}
/**
* 截断日志文本,保留排查所需的前缀内容。
*
* @param text 原始文本
* @return 截断后的文本
*/
private String truncate(String text) {
if (text == null || text.length() <= LOG_TEXT_MAX_LENGTH) {
return text;
}
return text.substring(0, LOG_TEXT_MAX_LENGTH) + "...";
}
private Workflow snapshotOrPublishedWorkflow(AgentToolBinding binding) {
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
Workflow workflow = objectMapper.convertValue(binding.getResourceSnapshot(), Workflow.class);
workflow.setId(firstNonNull(workflow.getId(), binding.getTargetId()));
return workflow;
}
return workflowService.getPublishedById(binding.getTargetId());
}
private PluginItem snapshotOrCurrentPlugin(AgentToolBinding binding) {
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
PluginItem pluginItem = objectMapper.convertValue(binding.getResourceSnapshot(), PluginItem.class);
pluginItem.setId(firstNonNull(pluginItem.getId(), binding.getTargetId()));
return pluginItem;
}
return pluginItemService.getById(binding.getTargetId());
}
private Mcp snapshotOrCurrentMcp(AgentToolBinding binding) {
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
Mcp mcp = objectMapper.convertValue(binding.getResourceSnapshot(), Mcp.class);
mcp.setId(firstNonNull(mcp.getId(), binding.getTargetId()));
return mcp;
}
return mcpService.getById(binding.getTargetId());
}
private DocumentCollection snapshotOrPublishedKnowledge(AgentKnowledgeBinding binding) {
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
DocumentCollection knowledge = objectMapper.convertValue(binding.getResourceSnapshot(), DocumentCollection.class);
knowledge.setId(firstNonNull(knowledge.getId(), binding.getKnowledgeId()));
return knowledge;
}
return documentCollectionService.getPublishedById(binding.getKnowledgeId());
}
private BigInteger firstNonNull(BigInteger first, BigInteger second) {
return first == null ? second : first;
}
private String firstNonBlank(String first, String second) {
return first == null || first.isBlank() ? second : first;
}
private String metadataString(Map<String, Object> metadata, String key) {
Object value = metadata == null ? null : metadata.get(key);
return value == null ? null : String.valueOf(value);
}
private Map<String, Object> toSchema(Parameter[] parameters) {
Map<String, Object> schema = new LinkedHashMap<>();
Map<String, Object> properties = new LinkedHashMap<>();
List<String> required = new ArrayList<>();
if (parameters != null) {
for (Parameter parameter : parameters) {
properties.put(parameter.getName(), parameterSchema(parameter));
if (parameter.isRequired()) {
required.add(parameter.getName());
}
}
}
schema.put("type", "object");
schema.put("properties", properties);
schema.put("required", required);
return schema;
}
private Map<String, Object> parameterSchema(Parameter parameter) {
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", parameter.getType() == null ? "string" : parameter.getType());
putOptionalString(schema, "description", parameter.getDescription());
if (parameter.getChildren() != null && !parameter.getChildren().isEmpty()) {
Map<String, Object> children = new LinkedHashMap<>();
for (Parameter child : parameter.getChildren()) {
if (child != null && child.getName() != null && !child.getName().isBlank()) {
children.put(child.getName(), parameterSchema(child));
}
}
if ("array".equalsIgnoreCase(parameter.getType())) {
schema.put("items", firstArrayItemSchema(parameter.getChildren()));
} else {
schema.put("properties", children);
}
}
return schema;
}
private Map<String, Object> firstArrayItemSchema(List<Parameter> children) {
return children.stream()
.filter(Objects::nonNull)
.findFirst()
.map(this::parameterSchema)
.orElse(Map.of("type", "string"));
}
/**
* 写入非空字符串字段,避免向模型 function schema 输出 null。
*
* @param target 目标 schema
* @param key 字段名
* @param value 字段值
*/
private void putOptionalString(Map<String, Object> target, String key, String value) {
if (value != null && !value.isBlank()) {
target.put(key, value);
}
}
/**
* 将工具描述规整为模型协议可接受的字符串。
*
* @param description 原始描述
* @return 非 null 描述
*/
private String safeDescription(String description) {
return description == null ? "" : description;
}
private AgentModelProviderType parseProviderType(String providerType) {
if (providerType == null || providerType.isBlank()) {
return AgentModelProviderType.OPENAI_COMPATIBLE;
}
try {
return AgentModelProviderType.valueOf(providerType.trim().toUpperCase());
} catch (IllegalArgumentException ignored) {
return AgentModelProviderType.OPENAI_COMPATIBLE;
}
}
private AgentMemoryType memoryTypeValue(Map<String, Object> map, String key) {
String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name());
try {
return AgentMemoryType.valueOf(value.trim().toUpperCase());
} catch (IllegalArgumentException e) {
throw new BusinessException("不支持的 Agent 记忆策略:" + value);
}
}
private String stringValue(Map<String, Object> map, String key, String defaultValue) {
Object value = map == null ? null : map.get(key);
return value == null ? defaultValue : String.valueOf(value);
}
private Integer intValue(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value instanceof Number number) {
return number.intValue();
}
if (value == null) {
return null;
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException e) {
throw new BusinessException("Agent 配置字段必须是整数:" + key);
}
}
private Long longValue(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value instanceof Number number) {
return number.longValue();
}
if (value == null) {
return null;
}
try {
return Long.parseLong(String.valueOf(value));
} catch (NumberFormatException e) {
throw new BusinessException("Agent 配置字段必须是长整数:" + key);
}
}
private Double doubleValue(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value instanceof Number number) {
return number.doubleValue();
}
if (value == null) {
return null;
}
try {
return Double.parseDouble(String.valueOf(value));
} catch (NumberFormatException e) {
throw new BusinessException("Agent 配置字段必须是数字:" + key);
}
}
private Boolean booleanValue(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value instanceof Boolean bool) {
return bool;
}
return value == null ? null : Boolean.parseBoolean(String.valueOf(value));
}
@SuppressWarnings("unchecked")
private Map<String, Object> mapValue(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value == null) {
return new LinkedHashMap<>();
}
if (value instanceof Map<?, ?> rawMap) {
Map<String, Object> result = new LinkedHashMap<>();
rawMap.forEach((rawKey, rawValue) -> result.put(String.valueOf(rawKey), rawValue));
return result;
}
throw new BusinessException("Agent 配置字段必须是对象:" + key);
}
private Map<String, String> stringMapValue(Map<String, Object> map, String key) {
Map<String, Object> rawMap = mapValue(map, key);
Map<String, String> result = new LinkedHashMap<>();
rawMap.forEach((rawKey, rawValue) -> {
if (rawValue != null) {
result.put(rawKey, String.valueOf(rawValue));
}
});
return result;
}
}

View File

@@ -0,0 +1,109 @@
package tech.easyflow.agent.runtime;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import java.util.List;
/**
* Agent 草稿态纯文本试用请求。
*/
public class AgentDraftChatRequest {
private Agent agent;
private List<AgentToolBinding> toolBindings;
private List<AgentKnowledgeBinding> knowledgeBindings;
private String sessionId;
private String prompt;
/**
* 获取 Agent 草稿快照。
*
* @return Agent 草稿快照
*/
public Agent getAgent() {
return agent;
}
/**
* 设置 Agent 草稿快照。
*
* @param agent Agent 草稿快照
*/
public void setAgent(Agent agent) {
this.agent = agent;
}
/**
* 获取工具绑定快照。
*
* @return 工具绑定快照
*/
public List<AgentToolBinding> getToolBindings() {
return toolBindings;
}
/**
* 设置工具绑定快照。
*
* @param toolBindings 工具绑定快照
*/
public void setToolBindings(List<AgentToolBinding> toolBindings) {
this.toolBindings = toolBindings;
}
/**
* 获取知识库绑定快照。
*
* @return 知识库绑定快照
*/
public List<AgentKnowledgeBinding> getKnowledgeBindings() {
return knowledgeBindings;
}
/**
* 设置知识库绑定快照。
*
* @param knowledgeBindings 知识库绑定快照
*/
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) {
this.knowledgeBindings = knowledgeBindings;
}
/**
* 获取草稿试运行会话 ID。
*
* @return 会话 ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置草稿试运行会话 ID。
*
* @param sessionId 会话 ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取用户纯文本输入。
*
* @return 用户输入
*/
public String getPrompt() {
return prompt;
}
/**
* 设置用户纯文本输入。
*
* @param prompt 用户输入
*/
public void setPrompt(String prompt) {
this.prompt = prompt;
}
}

View File

@@ -0,0 +1,491 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentResumeRequest;
import com.easyagents.agent.runtime.AgentRuntime;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.hitl.AgentResumeToken;
import org.springframework.stereotype.Component;
import reactor.core.Disposable;
import tech.easyflow.agent.runtime.lock.AgentRunLock;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* Agent 运行态注册表。
*/
@Component
public class AgentRunRegistry {
private final Map<String, AgentRunContext> runs = new ConcurrentHashMap<>();
private final Map<String, String> sessionRuns = new ConcurrentHashMap<>();
private final Map<String, String> resumeTokenIndex = new ConcurrentHashMap<>();
private final Map<String, Set<String>> requestTokens = new ConcurrentHashMap<>();
private final Map<String, RunOwner> owners = new ConcurrentHashMap<>();
/**
* 注册运行态。
*
* @param context 运行态上下文
*/
public void register(AgentRunContext context) {
if (context == null || context.requestId() == null || context.requestId().isBlank()) {
throw new BusinessException("Agent 运行请求 ID 不能为空");
}
if (context.sessionId() == null || context.sessionId().isBlank()) {
throw new BusinessException("Agent 会话 ID 不能为空");
}
String existingRequestId = sessionRuns.putIfAbsent(context.sessionId(), context.requestId());
if (existingRequestId != null && !existingRequestId.equals(context.requestId())) {
throw new BusinessException("当前 Agent 会话已有运行中的请求,请稍后再试");
}
AgentRunContext existing = runs.putIfAbsent(context.requestId(), context);
if (existing != null) {
sessionRuns.remove(context.sessionId(), context.requestId());
throw new BusinessException("当前 Agent 运行请求已存在");
}
owners.put(context.requestId(), context.owner());
}
/**
* 绑定运行订阅。
*
* @param requestId 请求 ID
* @param subscription Reactor 订阅
*/
public void bindSubscription(String requestId, Disposable subscription) {
AgentRunContext context = runs.get(requestId);
if (context != null) {
context.setSubscription(subscription);
}
}
/**
* 获取运行态。
*
* @param requestId 请求 ID
* @return 运行态
*/
public AgentRunContext get(String requestId) {
return requestId == null ? null : runs.get(requestId);
}
/**
* 取消并移除指定会话当前活跃运行。
*
* <p>草稿试运行清理会删除 AgentScope session。若同一会话仍有 SSE 运行中,
* 必须先取消 runtime 订阅并关闭 SSE避免旧运行继续向已清空的会话写入状态。</p>
*
* @param sessionId 会话 ID
*/
public void cancelSession(String sessionId) {
cancelSession(sessionId, null);
}
/**
* 取消并移除指定会话当前活跃运行。
*
* @param sessionId 会话 ID
* @param userId 当前用户 ID非空时会校验运行归属
*/
public void cancelSession(String sessionId, String userId) {
if (sessionId == null || sessionId.isBlank()) {
return;
}
String requestId = sessionRuns.get(sessionId);
if (requestId == null) {
return;
}
assertOwner(requestId, userId);
AgentRunContext context = runs.get(requestId);
if (context != null) {
context.cancelAndComplete();
}
remove(requestId);
}
/**
* 记录等待审批的恢复令牌。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
*/
public void registerResumeToken(String requestId, String resumeToken) {
if (requestId != null && resumeToken != null && !resumeToken.isBlank()) {
resumeTokenIndex.put(resumeToken, requestId);
requestTokens.computeIfAbsent(requestId, ignored -> ConcurrentHashMap.newKeySet()).add(resumeToken);
}
}
/**
* 运行结束后移除运行态。
*
* @param requestId 请求 ID
*/
public synchronized void remove(String requestId) {
if (requestId == null) {
return;
}
AgentRunContext context = runs.remove(requestId);
if (context != null) {
sessionRuns.remove(context.sessionId(), requestId);
context.releaseLock();
}
owners.remove(requestId);
Set<String> tokens = requestTokens.remove(requestId);
if (tokens != null) {
tokens.forEach(resumeTokenIndex::remove);
}
}
/**
* 批准工具执行。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param userId 当前用户 ID
*/
public void approve(String requestId, String resumeToken, String userId) {
submit(requestId, resumeToken, userId, true, null);
}
/**
* 批准工具执行,并在恢复 runtime 前执行持久化消费动作。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param userId 当前用户 ID
* @param beforeResume runtime resume 前的持久化消费动作
*/
public void approve(String requestId, String resumeToken, String userId, Runnable beforeResume) {
submit(requestId, resumeToken, userId, true, null, beforeResume);
}
/**
* 拒绝工具执行。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param userId 当前用户 ID
* @param reason 拒绝原因
*/
public void reject(String requestId, String resumeToken, String userId, String reason) {
submit(requestId, resumeToken, userId, false, reason);
}
/**
* 拒绝工具执行,并在恢复 runtime 前执行持久化消费动作。
*
* @param requestId 请求 ID
* @param resumeToken 恢复令牌
* @param userId 当前用户 ID
* @param reason 拒绝原因
* @param beforeResume runtime resume 前的持久化消费动作
*/
public void reject(String requestId, String resumeToken, String userId, String reason, Runnable beforeResume) {
submit(requestId, resumeToken, userId, false, reason, beforeResume);
}
/**
* 当前进程是否存在指定 token 对应的活跃运行态。
*
* @param requestId 请求 ID可为空
* @param resumeToken 恢复令牌
* @return true 表示当前节点可直接恢复
*/
public boolean containsResumeTarget(String requestId, String resumeToken) {
try {
String resolvedRequestId = resolveRequestId(requestId, resumeToken);
return runs.containsKey(resolvedRequestId);
} catch (BusinessException ignored) {
return false;
}
}
private void submit(String requestId, String resumeToken, String userId, boolean approved, String reason) {
submit(requestId, resumeToken, userId, approved, reason, null);
}
private synchronized void submit(String requestId,
String resumeToken,
String userId,
boolean approved,
String reason,
Runnable beforeResume) {
String resolvedRequestId = resolveRequestId(requestId, resumeToken);
AgentRunContext context = runs.get(resolvedRequestId);
if (context == null) {
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
}
assertOwner(resolvedRequestId, userId);
assertResumeTokenBelongsToRequest(resolvedRequestId, resumeToken);
if (beforeResume != null) {
beforeResume.run();
}
Set<String> tokens = requestTokens.get(resolvedRequestId);
if (tokens != null) {
tokens.remove(resumeToken);
}
resumeTokenIndex.remove(resumeToken);
AgentResumeToken token = new AgentResumeToken();
token.setValue(resumeToken);
AgentResumeRequest request = new AgentResumeRequest();
request.setResumeToken(token);
request.setApproved(approved);
request.setRejectReason(reason);
request.getMetadata().put("requestId", resolvedRequestId);
request.getMetadata().put("operatorId", userId);
context.resume(request);
}
private String resolveRequestId(String requestId, String resumeToken) {
if (requestId != null && !requestId.isBlank()) {
return requestId;
}
String resolved = resumeTokenIndex.get(resumeToken);
if (resolved == null || resolved.isBlank()) {
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
}
return resolved;
}
private void assertResumeTokenBelongsToRequest(String requestId, String resumeToken) {
if (resumeToken == null || resumeToken.isBlank()) {
throw new BusinessException("Agent 恢复令牌不能为空");
}
Set<String> tokens = requestTokens.get(requestId);
if (tokens == null || !tokens.contains(resumeToken)) {
throw new BusinessException("Agent 审批请求已失效");
}
}
private void assertOwner(String requestId, String userId) {
if (userId == null || userId.isBlank()) {
return;
}
RunOwner owner = owners.get(requestId);
if (owner == null) {
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
}
if (owner.userId() != null && !owner.userId().equals(userId)) {
throw new BusinessException("无权处理该 Agent 运行审批");
}
}
/**
* 当前运行归属信息。
*
* @param agentId Agent ID
* @param sessionId 会话 ID
* @param userId 用户 ID
*/
public record RunOwner(String agentId, String sessionId, String userId) {
}
/**
* 单机内存运行态。
*
*/
public static final class AgentRunContext {
private final String requestId;
private final String sessionId;
private final AgentRuntime runtime;
private final ChatSseEmitter chatSseEmitter;
private final ChatRuntimeContext chatContext;
private final StringBuilder answer;
private final ChatAssistantAccumulator assistantAccumulator;
private final AtomicBoolean finished;
private final boolean persistChatlog;
private final RunOwner owner;
private final AgentRunLock.Handle lockHandle;
private final Consumer<AgentRuntimeEvent> eventConsumer;
private final Consumer<Throwable> errorConsumer;
private final Runnable completionHandler;
private final AtomicBoolean suspended = new AtomicBoolean(false);
private final AtomicReference<Disposable> subscription = new AtomicReference<>();
/**
* 创建运行态。
*
* @param requestId 请求 ID
* @param sessionId 会话 ID
* @param runtime 有状态运行时
* @param chatSseEmitter SSE 连接
* @param chatContext 聊天上下文
* @param answer 助手正文累计缓冲
* @param assistantAccumulator 助手结构化累计器
* @param finished 运行收口标记
* @param persistChatlog 是否持久化聊天日志
* @param owner 运行归属
* @param eventConsumer 运行事件处理器
* @param errorConsumer 错误处理器
* @param completionHandler 完成处理器
*/
public AgentRunContext(String requestId,
String sessionId,
AgentRuntime runtime,
ChatSseEmitter chatSseEmitter,
ChatRuntimeContext chatContext,
StringBuilder answer,
ChatAssistantAccumulator assistantAccumulator,
AtomicBoolean finished,
boolean persistChatlog,
RunOwner owner,
AgentRunLock.Handle lockHandle,
Consumer<AgentRuntimeEvent> eventConsumer,
Consumer<Throwable> errorConsumer,
Runnable completionHandler) {
this.requestId = requestId;
this.sessionId = sessionId;
this.runtime = runtime;
this.chatSseEmitter = chatSseEmitter;
this.chatContext = chatContext;
this.answer = answer;
this.assistantAccumulator = assistantAccumulator;
this.finished = finished;
this.persistChatlog = persistChatlog;
this.owner = owner;
this.lockHandle = lockHandle;
this.eventConsumer = eventConsumer;
this.errorConsumer = errorConsumer;
this.completionHandler = completionHandler;
}
/**
* 获取请求 ID。
*
* @return 请求 ID
*/
public String requestId() {
return requestId;
}
/**
* 获取会话 ID。
*
* @return 会话 ID
*/
public String sessionId() {
return sessionId;
}
/**
* 获取运行归属。
*
* @return 运行归属
*/
public RunOwner owner() {
return owner;
}
/**
* 获取运行事件处理器。
*
* @return 运行事件处理器
*/
public Consumer<AgentRuntimeEvent> eventConsumer() {
return eventConsumer;
}
/**
* 获取错误处理器。
*
* @return 错误处理器
*/
public Consumer<Throwable> errorConsumer() {
return errorConsumer;
}
/**
* 获取完成处理器。
*
* @return 完成处理器
*/
public Runnable completionHandler() {
return completionHandler;
}
/**
* 标记当前运行已进入 HITL 挂起态。
*/
public void markSuspended() {
suspended.set(true);
}
/**
* 当前运行是否处于 HITL 挂起态。
*
* @return true 表示等待审批恢复
*/
public boolean isSuspended() {
return suspended.get();
}
/**
* 绑定运行订阅。
*
* @param subscription Reactor 订阅
*/
public void setSubscription(Disposable subscription) {
if (subscription == null) {
return;
}
Disposable previous = this.subscription.getAndSet(subscription);
if (previous != null && !previous.isDisposed()) {
previous.dispose();
}
}
/**
* 取消当前运行订阅。
*/
public void cancel() {
Disposable subscription = this.subscription.getAndSet(null);
if (subscription != null && !subscription.isDisposed()) {
subscription.dispose();
}
}
/**
* 取消当前运行并关闭 SSE。
*
* <p>该方法用于调用方主动清理会话的场景。它不通过 runtime 事件链发送取消事件,
* 因为调用方此时已经明确要求丢弃当前草稿会话。</p>
*/
public void cancelAndComplete() {
cancel();
if (finished.compareAndSet(false, true) && chatSseEmitter != null) {
chatSseEmitter.complete();
}
}
/**
* 释放当前运行持有的分布式锁。
*/
public void releaseLock() {
if (lockHandle != null) {
lockHandle.release();
}
}
/**
* 通过同一个 runtime 恢复挂起运行,事件继续写入原 SSE。
*
* @param request 恢复请求
*/
public void resume(AgentResumeRequest request) {
suspended.set(false);
Disposable subscription = runtime.resume(request).subscribe(eventConsumer, errorConsumer, completionHandler);
setSubscription(subscription);
}
}
}

View File

@@ -0,0 +1,72 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentDefinition;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 运行时编译结果。
*/
public class AgentRuntimeBundle {
private AgentDefinition definition;
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
/**
* 获取 Agent 定义。
*
* @return Agent 定义
*/
public AgentDefinition getDefinition() {
return definition;
}
/**
* 设置 Agent 定义。
*
* @param definition Agent 定义
*/
public void setDefinition(AgentDefinition definition) {
this.definition = definition;
}
/**
* 获取工具调用器。
*
* @return 工具调用器
*/
public Map<String, AgentToolInvoker> getToolInvokers() {
return toolInvokers;
}
/**
* 设置工具调用器。
*
* @param toolInvokers 工具调用器
*/
public void setToolInvokers(Map<String, AgentToolInvoker> toolInvokers) {
this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers;
}
/**
* 获取知识库检索器。
*
* @return 知识库检索器
*/
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
return knowledgeRetrievers;
}
/**
* 设置知识库检索器。
*
* @param knowledgeRetrievers 知识库检索器
*/
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> knowledgeRetrievers) {
this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
}
}

View File

@@ -0,0 +1,16 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentRuntime;
/**
* Agent 运行时工厂
*/
public interface AgentRuntimeFactory {
/**
* 创建新的有状态 Agent 运行时实例。
*
* @return Agent 运行时实例
*/
AgentRuntime create();
}

View File

@@ -0,0 +1,49 @@
package tech.easyflow.agent.runtime;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
import tech.easyflow.agent.runtime.session.EasyFlowAgentSessionStore;
import java.math.BigInteger;
/**
* Agent 运行态持久化状态清理服务。
*/
@Service
public class AgentRuntimeStateCleanupService {
private final AgentRunRegistry agentRunRegistry;
private final EasyFlowAgentSessionStore sessionStore;
private final AgentHitlPendingService pendingService;
/**
* 创建清理服务。
*
* @param agentRunRegistry 当前节点运行态注册表
* @param sessionStore AgentScope session store
* @param pendingService HITL pending 服务
*/
public AgentRuntimeStateCleanupService(AgentRunRegistry agentRunRegistry,
EasyFlowAgentSessionStore sessionStore,
AgentHitlPendingService pendingService) {
this.agentRunRegistry = agentRunRegistry;
this.sessionStore = sessionStore;
this.pendingService = pendingService;
}
/**
* 清理指定正式聊天会话关联的 Agent 运行态。
*
* @param chatSessionId chatlog 会话 ID
* @param userId 当前用户 ID
*/
public void clearChatSession(BigInteger chatSessionId, BigInteger userId) {
if (chatSessionId == null) {
return;
}
String runtimeSessionId = chatSessionId.toString();
agentRunRegistry.cancelSession(runtimeSessionId, userId == null ? null : userId.toString());
sessionStore.deleteByChatSessionId(chatSessionId);
pendingService.deleteByChatSessionId(chatSessionId);
}
}

View File

@@ -0,0 +1,22 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentRuntime;
import com.easyagents.agent.runtime.agentscope.AgentScopeReActRuntime;
import org.springframework.stereotype.Component;
/**
* ReActAgent 运行时工厂。
*/
@Component
public class AgentScopeRuntimeFactory implements AgentRuntimeFactory {
/**
* 创建新的 ReAct 运行时实例。
*
* @return 新的运行时实例
*/
@Override
public AgentRuntime create() {
return new AgentScopeReActRuntime();
}
}

View File

@@ -0,0 +1,220 @@
package tech.easyflow.agent.runtime;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Agent 工具调用人工确认事件载荷。
*/
public class AgentToolHitlPayload {
private String requestId;
private String resumeToken;
private String sessionId;
private String agentId;
private String toolCallId;
private String toolName;
private String toolDisplayName;
private String toolType;
private Map<String, Object> input = new LinkedHashMap<>();
private String expiresAt;
private Map<String, Object> metadata = new LinkedHashMap<>();
/**
* 获取运行请求 ID。
*
* @return 运行请求 ID
*/
public String getRequestId() {
return requestId;
}
/**
* 设置运行请求 ID。
*
* @param requestId 运行请求 ID
*/
public void setRequestId(String requestId) {
this.requestId = requestId;
}
/**
* 获取恢复令牌。
*
* @return 恢复令牌
*/
public String getResumeToken() {
return resumeToken;
}
/**
* 设置恢复令牌。
*
* @param resumeToken 恢复令牌
*/
public void setResumeToken(String resumeToken) {
this.resumeToken = resumeToken;
}
/**
* 获取会话 ID。
*
* @return 会话 ID
*/
public String getSessionId() {
return sessionId;
}
/**
* 设置会话 ID。
*
* @param sessionId 会话 ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* 获取 Agent ID。
*
* @return Agent ID
*/
public String getAgentId() {
return agentId;
}
/**
* 设置 Agent ID。
*
* @param agentId Agent ID
*/
public void setAgentId(String agentId) {
this.agentId = agentId;
}
/**
* 获取工具调用 ID。
*
* @return 工具调用 ID
*/
public String getToolCallId() {
return toolCallId;
}
/**
* 设置工具调用 ID。
*
* @param toolCallId 工具调用 ID
*/
public void setToolCallId(String toolCallId) {
this.toolCallId = toolCallId;
}
/**
* 获取工具名称。
*
* @return 工具名称
*/
public String getToolName() {
return toolName;
}
/**
* 设置工具名称。
*
* @param toolName 工具名称
*/
public void setToolName(String toolName) {
this.toolName = toolName;
}
/**
* 获取工具展示名称。
*
* @return 工具展示名称
*/
public String getToolDisplayName() {
return toolDisplayName;
}
/**
* 设置工具展示名称。
*
* @param toolDisplayName 工具展示名称
*/
public void setToolDisplayName(String toolDisplayName) {
this.toolDisplayName = toolDisplayName;
}
/**
* 获取工具类型。
*
* @return 工具类型
*/
public String getToolType() {
return toolType;
}
/**
* 设置工具类型。
*
* @param toolType 工具类型
*/
public void setToolType(String toolType) {
this.toolType = toolType;
}
/**
* 获取工具入参。
*
* @return 工具入参
*/
public Map<String, Object> getInput() {
return input;
}
/**
* 设置工具入参。
*
* @param input 工具入参
*/
public void setInput(Map<String, Object> input) {
this.input = input == null ? new LinkedHashMap<>() : input;
}
/**
* 获取过期时间。
*
* @return 过期时间
*/
public String getExpiresAt() {
return expiresAt;
}
/**
* 设置过期时间。
*
* @param expiresAt 过期时间
*/
public void setExpiresAt(String expiresAt) {
this.expiresAt = expiresAt;
}
/**
* 获取扩展元数据。
*
* @return 扩展元数据
*/
public Map<String, Object> getMetadata() {
return metadata;
}
/**
* 设置扩展元数据。
*
* @param metadata 扩展元数据
*/
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
}
}

View File

@@ -0,0 +1,19 @@
package tech.easyflow.agent.runtime.event;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import tech.easyflow.core.runtime.ChatRuntimeContext;
/**
* Agent 运行事件落库记录器。
*/
public interface AgentRunEventRecorder {
/**
* 记录运行事件。
*
* @param requestId 请求 ID
* @param chatContext 聊天上下文
* @param event 运行时事件
*/
void record(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event);
}

View File

@@ -0,0 +1,127 @@
package tech.easyflow.agent.runtime.event;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.entity.AgentRunEventRecord;
import tech.easyflow.agent.mapper.AgentRunEventRecordMapper;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* MySQL Agent 运行事件记录器。
*/
@Service
public class MySqlAgentRunEventRecorder implements AgentRunEventRecorder {
private final AgentRunEventRecordMapper eventRecordMapper;
/**
* 创建记录器。
*
* @param eventRecordMapper 事件 Mapper
*/
public MySqlAgentRunEventRecorder(AgentRunEventRecordMapper eventRecordMapper) {
this.eventRecordMapper = eventRecordMapper;
}
@Override
public void record(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
if (event == null || event.getEventType() == null || !shouldPersist(event.getEventType())) {
return;
}
AgentRunEventRecord record = new AgentRunEventRecord();
record.setTenantId(chatContext == null ? null : chatContext.getTenantId());
record.setAgentId(resolveAgentId(event, chatContext));
record.setChatSessionId(chatContext == null ? null : chatContext.getSessionId());
record.setRoundId(resolveNumber(chatContext, ChatRuntimeExtKeys.CURRENT_ROUND_ID));
record.setRoundNo(resolveInteger(chatContext, ChatRuntimeExtKeys.CURRENT_ROUND_NO));
record.setVariantIndex(resolveInteger(chatContext, ChatRuntimeExtKeys.CURRENT_VARIANT_INDEX, 1));
record.setRequestId(firstText(requestId, stringValue(event.getPayload().get("requestId"))));
record.setEventId(event.getEventId());
record.setEventType(event.getEventType().name());
record.setEventPhase(stringValue(event.getMetadata().get("phase")));
record.setToolCallId(firstText(event.getToolCallId(), stringValue(event.getPayload().get("toolCallId"))));
record.setPayloadJson(new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload()));
record.setMetadataJson(new LinkedHashMap<>(event.getMetadata() == null ? Map.of() : event.getMetadata()));
record.setCreated(toDate(event.getCreatedAt()));
record.setCreatedBy(chatContext == null ? null : chatContext.getUserId());
eventRecordMapper.insert(record);
}
private boolean shouldPersist(AgentRuntimeEventType type) {
return type != AgentRuntimeEventType.MESSAGE_DELTA
&& type != AgentRuntimeEventType.REASONING_DELTA
&& type != AgentRuntimeEventType.STARTED
&& type != AgentRuntimeEventType.COMPLETED;
}
private BigInteger resolveAgentId(AgentRuntimeEvent event, ChatRuntimeContext chatContext) {
String agentId = firstText(event.getAgentId(), stringValue(event.getPayload().get("agentId")));
if (agentId != null && !agentId.isBlank()) {
try {
return new BigInteger(agentId);
} catch (NumberFormatException ignored) {
return chatContext == null ? null : chatContext.getAssistantId();
}
}
return chatContext == null ? null : chatContext.getAssistantId();
}
private BigInteger resolveNumber(ChatRuntimeContext context, String key) {
Object value = context == null || context.getExt() == null ? null : context.getExt().get(key);
if (value instanceof BigInteger bigInteger) {
return bigInteger;
}
if (value instanceof Number number) {
return BigInteger.valueOf(number.longValue());
}
String text = stringValue(value);
if (text == null || text.isBlank()) {
return null;
}
try {
return new BigInteger(text);
} catch (NumberFormatException ignored) {
return null;
}
}
private Integer resolveInteger(ChatRuntimeContext context, String key) {
return resolveInteger(context, key, null);
}
private Integer resolveInteger(ChatRuntimeContext context, String key, Integer defaultValue) {
Object value = context == null || context.getExt() == null ? null : context.getExt().get(key);
if (value instanceof Number number) {
return number.intValue();
}
String text = stringValue(value);
if (text == null || text.isBlank()) {
return defaultValue;
}
try {
return Integer.parseInt(text);
} catch (NumberFormatException ignored) {
return defaultValue;
}
}
private Date toDate(Instant instant) {
return instant == null ? new Date() : Date.from(instant);
}
private String firstText(String left, String right) {
return left != null && !left.isBlank() ? left : right;
}
private String stringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
}

View File

@@ -0,0 +1,45 @@
package tech.easyflow.agent.runtime.hitl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.entity.AgentHitlPending;
import java.util.List;
/**
* Agent 工具审批 pending 过期清理任务。
*/
@Component
public class AgentHitlPendingExpirationTask {
private static final Logger LOG = LoggerFactory.getLogger(AgentHitlPendingExpirationTask.class);
private static final int BATCH_SIZE = 100;
private final AgentHitlPendingService pendingService;
/**
* 创建任务。
*
* @param pendingService pending 服务
*/
public AgentHitlPendingExpirationTask(AgentHitlPendingService pendingService) {
this.pendingService = pendingService;
}
/**
* 定期将超时 pending 标记为 EXPIRED。
*/
@Scheduled(fixedDelayString = "${easyflow.agent.runtime.hitl-expire-scan-delay:60000}", initialDelay = 60000L)
public void expirePending() {
try {
List<AgentHitlPending> expired = pendingService.expirePending(BATCH_SIZE);
if (!expired.isEmpty()) {
LOG.info("Expired Agent HITL pending records, count={}", expired.size());
}
} catch (RuntimeException e) {
LOG.warn("Expire Agent HITL pending records failed, message={}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,72 @@
package tech.easyflow.agent.runtime.hitl;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import tech.easyflow.agent.entity.AgentHitlPending;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.math.BigInteger;
import java.util.List;
/**
* Agent 工具审批 pending 持久化服务。
*/
public interface AgentHitlPendingService {
/**
* 从 TOOL_APPROVAL_REQUIRED 事件创建或刷新 pending。
*
* @param requestId 请求 ID
* @param chatContext 聊天上下文
* @param event 运行时事件
*/
void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event);
/**
* 批准并原子消费 pending。
*
* @param resumeToken 恢复令牌
* @param operatorId 操作人 ID
* @return pending 记录
*/
AgentHitlPending approve(String resumeToken, BigInteger operatorId);
/**
* 拒绝并原子消费 pending。
*
* @param resumeToken 恢复令牌
* @param operatorId 操作人 ID
* @param reason 拒绝原因
* @return pending 记录
*/
AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason);
/**
* 取消指定请求的 pending。
*
* @param requestId 请求 ID
* @param reason 取消原因
*/
void cancelByRequestId(String requestId, String reason);
/**
* 删除指定聊天会话的 pending。
*
* @param chatSessionId 聊天会话 ID
*/
void deleteByChatSessionId(BigInteger chatSessionId);
/**
* 删除指定运行会话的 pending。
*
* @param runtimeSessionId 运行会话 ID
*/
void deleteByRuntimeSessionId(String runtimeSessionId);
/**
* 过期 pending 并返回被过期的记录。
*
* @param limit 每批数量
* @return 过期记录
*/
List<AgentHitlPending> expirePending(int limit);
}

View File

@@ -0,0 +1,279 @@
package tech.easyflow.agent.runtime.hitl;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.agent.entity.AgentHitlPending;
import tech.easyflow.agent.mapper.AgentHitlPendingMapper;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Agent 工具审批 pending 持久化服务实现。
*/
@Service
public class AgentHitlPendingServiceImpl implements AgentHitlPendingService {
private final AgentHitlPendingMapper pendingMapper;
private final AgentRuntimeProperties properties;
/**
* 创建服务。
*
* @param pendingMapper pending Mapper
* @param properties Agent 运行态配置
*/
public AgentHitlPendingServiceImpl(AgentHitlPendingMapper pendingMapper,
AgentRuntimeProperties properties) {
this.pendingMapper = pendingMapper;
this.properties = properties;
}
@Override
public void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
if (event == null || event.getPayload() == null) {
return;
}
String resumeToken = stringValue(event.getPayload().get("resumeToken"));
if (!StringUtils.hasText(resumeToken)) {
return;
}
AgentHitlPending pending = findByToken(resumeToken);
Date now = new Date();
if (pending == null) {
pending = new AgentHitlPending();
pending.setResumeToken(resumeToken);
pending.setCreated(now);
pending.setCreatedBy(chatContext == null ? null : chatContext.getUserId());
pending.setIsDeleted(0);
}
pending.setTenantId(chatContext == null ? null : chatContext.getTenantId());
pending.setAgentId(resolveAgentId(event, chatContext));
pending.setChatSessionId(chatContext == null ? null : chatContext.getSessionId());
pending.setRuntimeSessionId(firstText(event.getSessionId(), stringValue(event.getPayload().get("sessionId"))));
pending.setRequestId(requestId);
pending.setToolCallId(firstText(event.getToolCallId(), stringValue(event.getPayload().get("toolCallId"))));
pending.setToolName(stringValue(event.getPayload().get("toolName")));
pending.setToolInputJson(mapValue(firstNonNull(event.getPayload().get("toolInput"), event.getPayload().get("input"))));
pending.setStatus(AgentHitlPendingStatus.PENDING.name());
pending.setExpiresAt(resolveExpiresAt(event));
pending.setMetadataJson(metadata(event));
pending.setModified(now);
pending.setModifiedBy(chatContext == null ? null : chatContext.getUserId());
pendingMapper.insertOrUpdate(pending);
}
@Override
@Transactional(rollbackFor = Exception.class)
public AgentHitlPending approve(String resumeToken, BigInteger operatorId) {
return consume(resumeToken, operatorId, AgentHitlPendingStatus.APPROVED, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason) {
return consume(resumeToken, operatorId, AgentHitlPendingStatus.REJECTED, reason);
}
@Override
public void cancelByRequestId(String requestId, String reason) {
if (!StringUtils.hasText(requestId)) {
return;
}
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
.eq("request_id", requestId)
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0));
Date now = new Date();
for (AgentHitlPending record : records) {
record.setStatus(AgentHitlPendingStatus.CANCELLED.name());
record.setRejectReason(reason);
record.setConsumedAt(now);
record.setModified(now);
pendingMapper.update(record);
}
}
@Override
public void deleteByChatSessionId(BigInteger chatSessionId) {
if (chatSessionId == null) {
return;
}
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
.eq("chat_session_id", chatSessionId)
.eq("is_deleted", 0));
softDelete(records);
}
@Override
public void deleteByRuntimeSessionId(String runtimeSessionId) {
if (!StringUtils.hasText(runtimeSessionId)) {
return;
}
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
.eq("runtime_session_id", runtimeSessionId)
.eq("is_deleted", 0));
softDelete(records);
}
private void softDelete(List<AgentHitlPending> records) {
if (records == null || records.isEmpty()) {
return;
}
Date now = new Date();
for (AgentHitlPending record : records) {
record.setIsDeleted(1);
record.setModified(now);
pendingMapper.update(record);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<AgentHitlPending> expirePending(int limit) {
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0)
.le("expires_at", new Date())
.limit(Math.max(1, limit)));
Date now = new Date();
for (AgentHitlPending record : records) {
record.setStatus(AgentHitlPendingStatus.EXPIRED.name());
record.setRejectReason("审批超时,已自动拒绝");
record.setConsumedAt(now);
record.setModified(now);
pendingMapper.update(record);
}
return records;
}
private AgentHitlPending consume(String resumeToken,
BigInteger operatorId,
AgentHitlPendingStatus targetStatus,
String reason) {
if (!StringUtils.hasText(resumeToken)) {
throw new BusinessException("Agent 恢复令牌不能为空");
}
AgentHitlPending pending = findByToken(resumeToken);
if (pending == null || Integer.valueOf(1).equals(pending.getIsDeleted())) {
throw new BusinessException("Agent 审批请求不存在或已失效");
}
if (!AgentHitlPendingStatus.PENDING.name().equals(pending.getStatus())) {
throw new BusinessException("Agent 审批请求已处理");
}
if (pending.getExpiresAt() != null && pending.getExpiresAt().before(new Date())) {
markConsumed(pending, operatorId, AgentHitlPendingStatus.EXPIRED, "审批超时,已自动拒绝");
throw new BusinessException("Agent 审批请求已过期");
}
if (!markConsumed(pending, operatorId, targetStatus, reason)) {
throw new BusinessException("Agent 审批请求已处理");
}
pending.setStatus(targetStatus.name());
pending.setRejectReason(reason);
pending.setConsumedAt(new Date());
pending.setModifiedBy(operatorId);
return pending;
}
private boolean markConsumed(AgentHitlPending pending,
BigInteger operatorId,
AgentHitlPendingStatus targetStatus,
String reason) {
Date now = new Date();
AgentHitlPending update = new AgentHitlPending();
update.setStatus(targetStatus.name());
update.setRejectReason(reason);
update.setConsumedAt(now);
update.setModified(now);
update.setModifiedBy(operatorId);
// 用 status=PENDING 作为消费条件,避免两个审批请求同时把同一个 token 消费两次。
return pendingMapper.updateByQuery(update, QueryWrapper.create()
.eq("id", pending.getId())
.eq("status", AgentHitlPendingStatus.PENDING.name())
.eq("is_deleted", 0)) > 0;
}
private AgentHitlPending findByToken(String resumeToken) {
return pendingMapper.selectOneByQuery(QueryWrapper.create()
.eq("resume_token", resumeToken)
.eq("is_deleted", 0)
.limit(1));
}
private BigInteger resolveAgentId(AgentRuntimeEvent event, ChatRuntimeContext chatContext) {
String agentId = firstText(event.getAgentId(), stringValue(event.getPayload().get("agentId")));
if (StringUtils.hasText(agentId)) {
try {
return new BigInteger(agentId);
} catch (NumberFormatException ignored) {
return chatContext == null ? null : chatContext.getAssistantId();
}
}
return chatContext == null ? null : chatContext.getAssistantId();
}
private Map<String, Object> metadata(AgentRuntimeEvent event) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.putAll(event.getMetadata() == null ? Map.of() : event.getMetadata());
Object approvalMetadata = event.getPayload().get("approvalMetadata");
if (approvalMetadata instanceof Map<?, ?> map) {
map.forEach((key, value) -> metadata.put(String.valueOf(key), value));
}
return metadata;
}
@SuppressWarnings("unchecked")
private Map<String, Object> mapValue(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> result = new LinkedHashMap<>();
map.forEach((key, item) -> result.put(String.valueOf(key), item));
return result;
}
return new LinkedHashMap<>();
}
private Date dateValue(Object value) {
if (value instanceof Date date) {
return date;
}
String text = stringValue(value);
if (!StringUtils.hasText(text)) {
return null;
}
try {
return Date.from(Instant.parse(text));
} catch (RuntimeException ignored) {
return null;
}
}
private Date resolveExpiresAt(AgentRuntimeEvent event) {
Date eventExpiresAt = dateValue(event.getPayload().get("expiresAt"));
if (eventExpiresAt != null) {
return eventExpiresAt;
}
return Date.from(Instant.now().plus(properties.getHitlPendingTimeout()));
}
private Object firstNonNull(Object left, Object right) {
return left == null ? right : left;
}
private String firstText(String left, String right) {
return StringUtils.hasText(left) ? left : right;
}
private String stringValue(Object value) {
return value == null ? null : String.valueOf(value);
}
}

View File

@@ -0,0 +1,31 @@
package tech.easyflow.agent.runtime.hitl;
/**
* Agent HITL pending 状态。
*/
public enum AgentHitlPendingStatus {
/**
* 等待审批。
*/
PENDING,
/**
* 已批准。
*/
APPROVED,
/**
* 已拒绝。
*/
REJECTED,
/**
* 已过期。
*/
EXPIRED,
/**
* 已取消。
*/
CANCELLED
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.agent.runtime.lock;
import java.math.BigInteger;
/**
* Agent 会话级运行锁。
*/
public interface AgentRunLock {
/**
* 获取指定 Agent 会话的运行锁。
*
* @param agentId Agent ID
* @param sessionId 运行时会话 ID
* @return 锁句柄
*/
Handle acquire(BigInteger agentId, String sessionId);
/**
* Agent 运行锁句柄。
*/
interface Handle extends AutoCloseable {
/**
* 续期锁。
*
* @return 续期成功时为 true
*/
boolean renew();
/**
* 释放锁。
*/
void release();
@Override
default void close() {
release();
}
}
}

View File

@@ -0,0 +1,92 @@
package tech.easyflow.agent.runtime.lock;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 基于 Redis 的 Agent 会话级运行锁。
*/
@Component
public class RedisAgentRunLock implements AgentRunLock {
private static final String LOCK_PREFIX = "easyflow:agent:run:";
private static final ScheduledExecutorService RENEW_EXECUTOR = Executors.newSingleThreadScheduledExecutor(task -> {
Thread thread = new Thread(task, "agent-run-lock-renew");
thread.setDaemon(true);
return thread;
});
private final RedisLockExecutor redisLockExecutor;
private final AgentRuntimeProperties properties;
/**
* 创建 Redis Agent 运行锁。
*
* @param redisLockExecutor Redis 锁执行器
* @param properties Agent 运行态配置
*/
public RedisAgentRunLock(RedisLockExecutor redisLockExecutor, AgentRuntimeProperties properties) {
this.redisLockExecutor = redisLockExecutor;
this.properties = properties;
}
@Override
public Handle acquire(BigInteger agentId, String sessionId) {
try {
RedisLockExecutor.LockHandle handle = redisLockExecutor.acquire(
lockKey(agentId, sessionId),
properties.getLockWaitTimeout(),
properties.getLockLeaseTimeout());
return new RedisHandle(handle, scheduleRenew(handle));
} catch (IllegalStateException e) {
throw new BusinessException("当前 Agent 会话正在运行,请稍后再试");
}
}
private ScheduledFuture<?> scheduleRenew(RedisLockExecutor.LockHandle handle) {
long intervalMillis = Math.max(1000L, properties.getLockRenewInterval().toMillis());
return RENEW_EXECUTOR.scheduleAtFixedRate(handle::renew, intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);
}
private String lockKey(BigInteger agentId, String sessionId) {
return LOCK_PREFIX + "agent:" + (agentId == null ? "unknown" : agentId)
+ ":session:" + (sessionId == null ? "unknown" : sessionId);
}
private static final class RedisHandle implements Handle {
private final RedisLockExecutor.LockHandle delegate;
private final ScheduledFuture<?> renewTask;
private final AtomicBoolean released = new AtomicBoolean(false);
private RedisHandle(RedisLockExecutor.LockHandle delegate, ScheduledFuture<?> renewTask) {
this.delegate = delegate;
this.renewTask = renewTask;
}
@Override
public boolean renew() {
return delegate.renew();
}
@Override
public void release() {
if (!released.compareAndSet(false, true)) {
return;
}
if (renewTask != null) {
renewTask.cancel(false);
}
delegate.release();
}
}
}

View File

@@ -0,0 +1,348 @@
package tech.easyflow.agent.runtime.session;
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
import com.mybatisflex.core.query.QueryWrapper;
import io.agentscope.core.state.State;
import io.agentscope.core.util.JsonUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.agent.entity.AgentSession;
import tech.easyflow.agent.mapper.AgentSessionMapper;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* session store 持久化实现
*/
@Service
public class EasyFlowAgentSessionStore implements AgentSessionStore {
private static final String REDIS_PREFIX = "easyflow:agent:session:";
private static final String ENVELOPE_VERSION = "1";
private static final String SINGLE_STATES = "singleStates";
private static final String LIST_STATES = "listStates";
private final AgentSessionMapper agentSessionMapper;
private final StringRedisTemplate stringRedisTemplate;
private final AgentRuntimeProperties properties;
/**
* 创建 EasyFlow Agent session store。
*
* @param agentSessionMapper session Mapper
* @param stringRedisTemplate Redis 模板
* @param properties Agent 运行态配置
*/
public EasyFlowAgentSessionStore(AgentSessionMapper agentSessionMapper,
StringRedisTemplate stringRedisTemplate,
AgentRuntimeProperties properties) {
this.agentSessionMapper = agentSessionMapper;
this.stringRedisTemplate = stringRedisTemplate;
this.properties = properties;
}
/**
* 绑定业务会话元信息。
*
* <p>AgentScope Session API 不会把 EasyFlow 的 {@code agentId/chatSessionId/tenantId} 传入
* {@code save(...)},因此运行入口必须先调用本方法建立或刷新元信息。后续 state 写入会复用
* 这些字段,避免表里只剩裸 sessionKey。</p>
*
* @param metadata 业务会话元信息
*/
public void bindSession(AgentSessionMetadata metadata) {
if (metadata == null || !StringUtils.hasText(metadata.sessionKey())) {
return;
}
AgentSession session = findBySessionKey(metadata.sessionKey());
Date now = new Date();
if (session == null) {
session = new AgentSession();
session.setTenantId(metadata.tenantId());
session.setAgentId(metadata.agentId());
session.setChatSessionId(metadata.chatSessionId());
session.setRuntimeSessionId(metadata.runtimeSessionId());
session.setSessionKey(metadata.sessionKey());
session.setStateJson(emptyEnvelope());
session.setVersion(0L);
session.setCacheVersion(0L);
session.setCreated(now);
session.setCreatedBy(metadata.operatorId());
session.setIsDeleted(0);
}
session.setTenantId(firstNonNull(metadata.tenantId(), session.getTenantId()));
session.setAgentId(firstNonNull(metadata.agentId(), session.getAgentId()));
session.setChatSessionId(firstNonNull(metadata.chatSessionId(), session.getChatSessionId()));
session.setRuntimeSessionId(firstText(metadata.runtimeSessionId(), session.getRuntimeSessionId()));
session.setLastAccessAt(now);
session.setModified(now);
session.setModifiedBy(metadata.operatorId());
agentSessionMapper.insertOrUpdate(session);
writeCache(session.getSessionKey(), session.getStateJson());
}
/**
* 按聊天会话清理 AgentScope session。
*
* @param chatSessionId 聊天会话 ID
*/
public void deleteByChatSessionId(BigInteger chatSessionId) {
if (chatSessionId == null) {
return;
}
List<AgentSession> sessions = agentSessionMapper.selectListByQuery(QueryWrapper.create()
.eq("chat_session_id", chatSessionId)
.eq("is_deleted", 0));
Date now = new Date();
for (AgentSession session : sessions) {
session.setIsDeleted(1);
session.setModified(now);
agentSessionMapper.update(session);
deleteCache(session.getSessionKey());
}
}
@Override
public void save(String sessionKey, String name, State state) {
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || state == null) {
return;
}
Map<String, Object> envelope = loadEnvelope(sessionKey);
singleStates(envelope).put(name, JsonUtils.getJsonCodec().toJson(state));
persistEnvelope(sessionKey, envelope);
}
@Override
public void saveList(String sessionKey, String name, List<? extends State> states) {
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name)) {
return;
}
List<String> values = new ArrayList<>();
if (states != null) {
for (State state : states) {
values.add(JsonUtils.getJsonCodec().toJson(state));
}
}
Map<String, Object> envelope = loadEnvelope(sessionKey);
listStates(envelope).put(name, values);
persistEnvelope(sessionKey, envelope);
}
@Override
public <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || type == null) {
return Optional.empty();
}
Object json = singleStates(loadEnvelope(sessionKey)).get(name);
if (!(json instanceof String text) || text.isBlank()) {
return Optional.empty();
}
return Optional.of(JsonUtils.getJsonCodec().fromJson(text, type));
}
@Override
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || itemType == null) {
return List.of();
}
Object raw = listStates(loadEnvelope(sessionKey)).get(name);
if (!(raw instanceof List<?> values) || values.isEmpty()) {
return List.of();
}
List<T> result = new ArrayList<>();
for (Object value : values) {
if (value instanceof String text && !text.isBlank()) {
result.add(JsonUtils.getJsonCodec().fromJson(text, itemType));
}
}
return result;
}
@Override
public boolean exists(String sessionKey) {
if (!StringUtils.hasText(sessionKey)) {
return false;
}
if (readCache(sessionKey) != null) {
return true;
}
return findBySessionKey(sessionKey) != null;
}
@Override
public void delete(String sessionKey) {
if (!StringUtils.hasText(sessionKey)) {
return;
}
AgentSession session = findBySessionKey(sessionKey);
if (session != null) {
session.setIsDeleted(1);
session.setModified(new Date());
agentSessionMapper.update(session);
}
deleteCache(sessionKey);
}
@Override
public Set<String> listSessionKeys() {
List<AgentSession> sessions = agentSessionMapper.selectListByQuery(QueryWrapper.create()
.eq("is_deleted", 0)
.select("session_key"));
Set<String> keys = new LinkedHashSet<>();
for (AgentSession session : sessions) {
if (StringUtils.hasText(session.getSessionKey())) {
keys.add(session.getSessionKey());
}
}
return keys;
}
private void persistEnvelope(String sessionKey, Map<String, Object> envelope) {
AgentSession session = findBySessionKey(sessionKey);
Date now = new Date();
if (session == null) {
session = new AgentSession();
session.setRuntimeSessionId(sessionKey);
session.setSessionKey(sessionKey);
session.setCreated(now);
session.setVersion(0L);
session.setCacheVersion(0L);
session.setIsDeleted(0);
}
long nextVersion = session.getVersion() == null ? 1L : session.getVersion() + 1L;
session.setStateJson(envelope);
session.setVersion(nextVersion);
session.setCacheVersion(nextVersion);
session.setLastAccessAt(now);
session.setModified(now);
session.setIsDeleted(0);
// 同步写会话状态
agentSessionMapper.insertOrUpdate(session);
// 同步写缓存
writeCache(sessionKey, envelope);
}
private Map<String, Object> loadEnvelope(String sessionKey) {
Map<String, Object> cached = readCache(sessionKey);
if (cached != null) {
return cached;
}
AgentSession session = findBySessionKey(sessionKey);
if (session == null || session.getStateJson() == null || session.getStateJson().isEmpty()) {
return emptyEnvelope();
}
writeCache(sessionKey, session.getStateJson());
return deepCopy(session.getStateJson());
}
private AgentSession findBySessionKey(String sessionKey) {
return agentSessionMapper.selectOneByQuery(QueryWrapper.create()
.eq("session_key", sessionKey)
.eq("is_deleted", 0)
.limit(1));
}
private Map<String, Object> emptyEnvelope() {
Map<String, Object> envelope = new LinkedHashMap<>();
envelope.put("version", ENVELOPE_VERSION);
envelope.put(SINGLE_STATES, new LinkedHashMap<String, Object>());
envelope.put(LIST_STATES, new LinkedHashMap<String, Object>());
return envelope;
}
@SuppressWarnings("unchecked")
private Map<String, Object> singleStates(Map<String, Object> envelope) {
return (Map<String, Object>) envelope.computeIfAbsent(SINGLE_STATES, key -> new LinkedHashMap<String, Object>());
}
@SuppressWarnings("unchecked")
private Map<String, Object> listStates(Map<String, Object> envelope) {
return (Map<String, Object>) envelope.computeIfAbsent(LIST_STATES, key -> new LinkedHashMap<String, Object>());
}
@SuppressWarnings("unchecked")
private Map<String, Object> readCache(String sessionKey) {
try {
String value = stringRedisTemplate.opsForValue().get(cacheKey(sessionKey));
if (!StringUtils.hasText(value)) {
return null;
}
return JsonUtils.getJsonCodec().fromJson(value, Map.class);
} catch (RuntimeException e) {
return null;
}
}
private void writeCache(String sessionKey, Map<String, Object> envelope) {
try {
long seconds = Math.max(1L, properties.getSessionCacheTtl().toSeconds());
stringRedisTemplate.opsForValue().set(cacheKey(sessionKey), JsonUtils.getJsonCodec().toJson(envelope),
seconds, TimeUnit.SECONDS);
} catch (RuntimeException e) {
// MySQL 是 AgentScope session 的真相源。Redis 只做热缓存,写缓存失败不能掩盖已完成的持久化写入。
}
}
private void deleteCache(String sessionKey) {
try {
stringRedisTemplate.delete(cacheKey(sessionKey));
} catch (RuntimeException ignored) {
// 清理缓存失败不应掩盖 MySQL 删除结果,后续 TTL 会自然回收。
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> deepCopy(Map<String, Object> source) {
if (source == null || source.isEmpty()) {
return emptyEnvelope();
}
return JsonUtils.getJsonCodec().fromJson(JsonUtils.getJsonCodec().toJson(source), Map.class);
}
private String cacheKey(String sessionKey) {
return REDIS_PREFIX + hash(sessionKey);
}
private String hash(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
} catch (NoSuchAlgorithmException e) {
return value.replace(':', '_');
}
}
private <T> T firstNonNull(T left, T right) {
return left == null ? right : left;
}
private String firstText(String left, String right) {
return StringUtils.hasText(left) ? left : right;
}
/**
* AgentScope session 绑定的 EasyFlow 业务元信息。
*
* @param sessionKey AgentScope session key
* @param runtimeSessionId runtime session ID
* @param agentId Agent ID
* @param chatSessionId chatlog 会话 ID
* @param tenantId 租户 ID
* @param operatorId 操作人 ID
*/
public record AgentSessionMetadata(String sessionKey,
String runtimeSessionId,
BigInteger agentId,
BigInteger chatSessionId,
BigInteger tenantId,
BigInteger operatorId) {
}
}

View File

@@ -0,0 +1,25 @@
package tech.easyflow.agent.service;
import tech.easyflow.agent.entity.Agent;
import java.util.Collection;
/**
* Agent 审批状态派生服务。
*/
public interface AgentApprovalStateService {
/**
* 填充单个 Agent 的审批展示状态。
*
* @param agent Agent 资源
*/
void fillAgentApprovalState(Agent agent);
/**
* 批量填充 Agent 的审批展示状态。
*
* @param agents Agent 资源集合
*/
void fillAgentApprovalState(Collection<Agent> agents);
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.agent.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.agent.entity.AgentCategory;
/**
* Agent 分类服务。
*/
public interface AgentCategoryService extends IService<AgentCategory> {
}

View File

@@ -0,0 +1,30 @@
package tech.easyflow.agent.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import java.math.BigInteger;
import java.util.List;
/**
* Agent 知识库绑定服务。
*/
public interface AgentKnowledgeBindingService extends IService<AgentKnowledgeBinding> {
/**
* 替换 Agent 知识库绑定。
*
* @param agentId Agent ID
* @param bindings 新绑定列表
* @return 保存后的绑定列表
*/
List<AgentKnowledgeBinding> replaceBindings(BigInteger agentId, List<AgentKnowledgeBinding> bindings);
/**
* 查询 Agent 启用知识库绑定。
*
* @param agentId Agent ID
* @return 启用绑定列表
*/
List<AgentKnowledgeBinding> listEnabled(BigInteger agentId);
}

View File

@@ -0,0 +1,61 @@
package tech.easyflow.agent.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.agent.entity.Agent;
import java.math.BigInteger;
import java.util.Map;
/**
* Agent 配置服务。
*/
public interface AgentService extends IService<Agent> {
/**
* 获取 Agent 详情。
*
* @param id Agent ID
* @return Agent 详情
*/
Agent getDetail(BigInteger id);
/**
* 保存草稿 Agent。
*
* @param agent Agent 草稿
* @return 保存后的 Agent
*/
Agent saveDraft(Agent agent);
/**
* 更新草稿 Agent。
*
* @param agent Agent 草稿
* @return 更新后的 Agent
*/
Agent updateDraft(Agent agent);
/**
* 获取已发布运行视图。
*
* @param id Agent ID
* @return 已发布运行视图
*/
Agent getPublishedView(BigInteger id);
/**
* 构建发布快照。
*
* @param agent Agent 当前草稿
* @return 发布快照
*/
Map<String, Object> buildPublishSnapshot(Agent agent);
/**
* 从快照还原运行视图。
*
* @param snapshot 发布快照
* @return Agent 运行视图
*/
Agent fromSnapshot(Map<String, Object> snapshot);
}

View File

@@ -0,0 +1,30 @@
package tech.easyflow.agent.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.agent.entity.AgentToolBinding;
import java.math.BigInteger;
import java.util.List;
/**
* Agent 工具绑定服务。
*/
public interface AgentToolBindingService extends IService<AgentToolBinding> {
/**
* 替换 Agent 工具绑定。
*
* @param agentId Agent ID
* @param bindings 新绑定列表
* @return 保存后的绑定列表
*/
List<AgentToolBinding> replaceBindings(BigInteger agentId, List<AgentToolBinding> bindings);
/**
* 查询 Agent 启用工具绑定。
*
* @param agentId Agent ID
* @return 启用绑定列表
*/
List<AgentToolBinding> listEnabled(BigInteger agentId);
}

View File

@@ -0,0 +1,116 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentApprovalStateService;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.enums.ApprovalActionType;
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Agent 审批状态派生服务实现。
*/
@Service
public class AgentApprovalStateServiceImpl implements AgentApprovalStateService {
private final ApprovalInstanceMapper approvalInstanceMapper;
/**
* 创建 Agent 审批状态派生服务。
*
* @param approvalInstanceMapper 审批实例 Mapper
*/
public AgentApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
this.approvalInstanceMapper = approvalInstanceMapper;
}
/**
* {@inheritDoc}
*/
@Override
public void fillAgentApprovalState(Agent agent) {
fillAgentApprovalState(agent == null ? List.of() : List.of(agent));
}
/**
* {@inheritDoc}
*/
@Override
public void fillAgentApprovalState(Collection<Agent> agents) {
if (CollectionUtils.isEmpty(agents)) {
return;
}
List<Agent> validAgents = agents.stream().filter(Objects::nonNull).toList();
if (validAgents.isEmpty()) {
return;
}
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validAgents, Agent::getCurrentApprovalInstanceId);
for (Agent agent : validAgents) {
fillOne(agent, instanceMap.get(agent.getCurrentApprovalInstanceId()));
}
}
private void fillOne(Agent agent, ApprovalInstance instance) {
PublishStatus currentStatus = PublishStatus.from(agent.getPublishStatus());
if (!isValidCurrentInstance(instance)) {
agent.setApprovalPending(false);
agent.setCurrentApprovalActionType(null);
agent.setDisplayPublishStatus(currentStatus.getCode());
return;
}
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
if (instanceStatus.isFinished()) {
agent.setApprovalPending(false);
agent.setCurrentApprovalActionType(null);
agent.setDisplayPublishStatus(currentStatus.getCode());
return;
}
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
agent.setApprovalPending(true);
agent.setCurrentApprovalActionType(actionType.getCode());
agent.setDisplayPublishStatus(resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
}
private Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<Agent> agents,
Function<Agent, BigInteger> instanceIdGetter) {
Set<BigInteger> instanceIds = agents.stream()
.map(instanceIdGetter)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (instanceIds.isEmpty()) {
return Collections.emptyMap();
}
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
);
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
}
private boolean isValidCurrentInstance(ApprovalInstance instance) {
return instance != null && ApprovalResourceType.AGENT.getCode().equals(instance.getResourceType());
}
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
ApprovalActionType actionType) {
if (currentStatus == PublishStatus.PUBLISH_PENDING
|| currentStatus == PublishStatus.OFFLINE_PENDING
|| currentStatus == PublishStatus.DELETE_PENDING) {
return currentStatus;
}
return switch (actionType) {
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
case DELETE -> PublishStatus.DELETE_PENDING;
};
}
}

View File

@@ -0,0 +1,14 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.entity.AgentCategory;
import tech.easyflow.agent.mapper.AgentCategoryMapper;
import tech.easyflow.agent.service.AgentCategoryService;
/**
* Agent 分类服务实现。
*/
@Service
public class AgentCategoryServiceImpl extends ServiceImpl<AgentCategoryMapper, AgentCategory> implements AgentCategoryService {
}

View File

@@ -0,0 +1,123 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
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.mapper.AgentKnowledgeBindingMapper;
import tech.easyflow.agent.mapper.AgentMapper;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.enums.PublishStatus;
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;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Agent 知识库绑定服务实现。
*/
@Service
public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledgeBindingMapper, AgentKnowledgeBinding>
implements AgentKnowledgeBindingService {
private static final String DEFAULT_RETRIEVAL_MODE = "HYBRID";
@Resource
private AgentMapper agentMapper;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private ResourceAccessService resourceAccessService;
/**
* {@inheritDoc}
*/
@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);
}
/**
* {@inheritDoc}
*/
@Override
public List<AgentKnowledgeBinding> listEnabled(BigInteger agentId) {
return list(QueryWrapper.create()
.where("agent_id = ?", agentId)
.and("enabled = ?", true)
.orderBy("sort_no asc, id asc"));
}
private Agent requireAgent(BigInteger agentId) {
Agent agent = agentMapper.selectOneById(agentId);
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateBinding(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, "无权限绑定该知识库");
}
private void applyBindingDefaults(Agent agent, AgentKnowledgeBinding binding, int index) {
LoginAccount account = requireCurrentLoginAccount();
Date now = new Date();
binding.setId(null);
binding.setTenantId(agent.getTenantId());
binding.setAgentId(agent.getId());
if (binding.getRetrievalMode() == null || binding.getRetrievalMode().isBlank()) {
binding.setRetrievalMode(DEFAULT_RETRIEVAL_MODE);
} else {
binding.setRetrievalMode(binding.getRetrievalMode().trim().toUpperCase());
}
binding.setEnabled(binding.getEnabled() == null || binding.getEnabled());
binding.setSortNo(binding.getSortNo() == null ? index : binding.getSortNo());
binding.setCreated(now);
binding.setCreatedBy(account.getId());
binding.setModified(now);
binding.setModifiedBy(account.getId());
}
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
} catch (Exception e) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}
}

View File

@@ -0,0 +1,391 @@
package tech.easyflow.agent.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.mapper.AgentMapper;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.agent.service.AgentToolBindingService;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.*;
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.enums.VisibilityScope;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Agent 配置服务实现。
*/
@Service
public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements AgentService {
private static final TypeReference<List<AgentToolBinding>> TOOL_BINDING_LIST_TYPE = new TypeReference<>() {};
private static final TypeReference<List<AgentKnowledgeBinding>> KNOWLEDGE_BINDING_LIST_TYPE = new TypeReference<>() {};
@Resource
private AgentToolBindingService agentToolBindingService;
@Resource
private AgentKnowledgeBindingService agentKnowledgeBindingService;
@Resource
private ModelService modelService;
@Resource
private WorkflowService workflowService;
@Resource
private PluginItemService pluginItemService;
@Resource
private McpService mcpService;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public Agent getDetail(BigInteger id) {
Agent agent = requireAgent(id);
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.READ, "无权限查看该 Agent");
agent.setToolBindings(agentToolBindingService.listEnabled(id));
agent.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(id));
return agent;
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Agent saveDraft(Agent agent) {
validateDraft(agent);
applyDraftDefaults(agent);
save(agent);
return getDetail(agent.getId());
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Agent updateDraft(Agent agent) {
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());
}
/**
* {@inheritDoc}
*/
@Override
public Agent getPublishedView(BigInteger id) {
Agent agent = requireAgent(id);
PublishStatus status = PublishStatus.from(agent.getPublishStatus());
if (!status.isExternallyVisible() || agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException("Agent 未发布");
}
return fromSnapshot(agent.getPublishedSnapshotJson());
}
/**
* {@inheritDoc}
*/
@Override
public Map<String, Object> buildPublishSnapshot(Agent agent) {
Agent detail = getDetail(agent.getId());
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("id", detail.getId());
snapshot.put("tenantId", detail.getTenantId());
snapshot.put("deptId", detail.getDeptId());
snapshot.put("createdBy", detail.getCreatedBy());
snapshot.put("name", detail.getName());
snapshot.put("description", detail.getDescription());
snapshot.put("avatar", detail.getAvatar());
snapshot.put("categoryId", detail.getCategoryId());
snapshot.put("modelId", detail.getModelId());
snapshot.put("modelConfigJson", detail.getModelConfigJson());
snapshot.put("generationConfigJson", detail.getGenerationConfigJson());
snapshot.put("promptConfigJson", detail.getPromptConfigJson());
snapshot.put("memoryConfigJson", detail.getMemoryConfigJson());
snapshot.put("executionConfigJson", detail.getExecutionConfigJson());
snapshot.put("visibilityScope", detail.getVisibilityScope());
snapshot.put("toolBindings", snapshotToolBindings(detail.getToolBindings()));
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail.getKnowledgeBindings()));
snapshot.put("basicSummary", basicSummary(detail));
snapshot.put("modelSummary", modelSummary(detail.getModelId()));
snapshot.put("parameterSummary", parameterSummary(detail));
snapshot.put("promptSummary", promptSummary(detail));
snapshot.put("toolSummaries", toolSummaries(detail.getToolBindings()));
snapshot.put("knowledgeSummaries", knowledgeSummaries(detail.getKnowledgeBindings()));
snapshot.put("snapshotAt", new Date());
return snapshot;
}
/**
* {@inheritDoc}
*/
@Override
public Agent fromSnapshot(Map<String, Object> snapshot) {
if (snapshot == null || snapshot.isEmpty()) {
throw new BusinessException("Agent 发布快照为空");
}
Agent agent = objectMapper.convertValue(snapshot, Agent.class);
agent.setId(toBigInteger(snapshot.get("id")));
agent.setTenantId(toBigInteger(snapshot.get("tenantId")));
agent.setDeptId(toBigInteger(snapshot.get("deptId")));
agent.setCreatedBy(toBigInteger(snapshot.get("createdBy")));
agent.setModelId(toBigInteger(snapshot.get("modelId")));
agent.setCategoryId(toBigInteger(snapshot.get("categoryId")));
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
agent.setPublishedSnapshotJson(snapshot);
agent.setToolBindings(objectMapper.convertValue(snapshot.get("toolBindings"), TOOL_BINDING_LIST_TYPE));
agent.setKnowledgeBindings(objectMapper.convertValue(snapshot.get("knowledgeBindings"), KNOWLEDGE_BINDING_LIST_TYPE));
return agent;
}
private Agent requireAgent(BigInteger id) {
Agent agent = getById(id);
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateDraft(Agent agent) {
if (agent == null) {
throw new BusinessException("Agent 不能为空");
}
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 模型不存在");
}
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
}
private void applyDraftDefaults(Agent agent) {
LoginAccount account = requireCurrentLoginAccount();
Date now = new Date();
agent.setTenantId(account.getTenantId());
agent.setDeptId(account.getDeptId());
agent.setCreated(now);
agent.setCreatedBy(account.getId());
agent.setModified(now);
agent.setModifiedBy(account.getId());
agent.setStatus(agent.getStatus() == null ? 1 : agent.getStatus());
agent.setPublishStatus(PublishStatus.DRAFT.getCode());
}
private Map<String, Object> basicSummary(Agent agent) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("id", agent.getId());
summary.put("name", agent.getName());
summary.put("description", agent.getDescription());
summary.put("avatar", agent.getAvatar());
summary.put("categoryId", agent.getCategoryId());
summary.put("status", agent.getStatus());
summary.put("publishStatus", PublishStatus.PUBLISHED.getCode());
summary.put("visibilityScope", agent.getVisibilityScope());
return summary;
}
private void applyDraftUpdate(Agent existing, Agent incoming) {
LoginAccount account = requireCurrentLoginAccount();
existing.setName(incoming.getName());
existing.setDescription(incoming.getDescription());
existing.setAvatar(incoming.getAvatar());
existing.setCategoryId(incoming.getCategoryId());
existing.setModelId(incoming.getModelId());
existing.setModelConfigJson(incoming.getModelConfigJson());
existing.setGenerationConfigJson(incoming.getGenerationConfigJson());
existing.setPromptConfigJson(incoming.getPromptConfigJson());
existing.setMemoryConfigJson(incoming.getMemoryConfigJson());
existing.setExecutionConfigJson(incoming.getExecutionConfigJson());
existing.setStatus(incoming.getStatus() == null ? 1 : incoming.getStatus());
existing.setVisibilityScope(incoming.getVisibilityScope());
existing.setModified(new Date());
existing.setModifiedBy(account.getId());
}
private Map<String, Object> modelSummary(BigInteger modelId) {
Model model = modelService.getModelInstance(modelId);
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("id", model.getId());
summary.put("title", model.getTitle());
summary.put("modelName", model.getModelName());
summary.put("providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType());
return summary;
}
private Map<String, Object> parameterSummary(Agent agent) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("generationConfigJson", agent.getGenerationConfigJson());
summary.put("modelConfigJson", agent.getModelConfigJson());
summary.put("memoryConfigJson", agent.getMemoryConfigJson());
summary.put("executionConfigJson", agent.getExecutionConfigJson());
return summary;
}
private Map<String, Object> promptSummary(Agent agent) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("promptConfigJson", agent.getPromptConfigJson());
summary.put("systemPrompt", agent.getPromptConfigJson() == null ? null : agent.getPromptConfigJson().get("systemPrompt"));
summary.put("prompt", agent.getPromptConfigJson() == null ? null : agent.getPromptConfigJson().get("prompt"));
return summary;
}
private List<Map<String, Object>> toolSummaries(List<AgentToolBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(this::toolSummary).toList();
}
private Map<String, Object> toolSummary(AgentToolBinding binding) {
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("bindingId", binding.getId());
summary.put("toolType", binding.getToolType());
summary.put("targetId", binding.getTargetId());
summary.put("toolName", binding.getToolName());
summary.put("enabled", Boolean.TRUE.equals(binding.getEnabled()));
summary.put("hitlEnabled", Boolean.TRUE.equals(binding.getHitlEnabled()));
summary.put("hitlConfigJson", binding.getHitlConfigJson());
summary.put("sortNo", binding.getSortNo());
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
Workflow workflow = workflowService.getById(binding.getTargetId());
summary.put("title", workflow == null ? null : workflow.getTitle());
} else if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
summary.put("title", pluginItem == null ? null : pluginItem.getName());
} else {
Mcp mcp = mcpService.getById(binding.getTargetId());
summary.put("title", mcp == null ? null : mcp.getTitle());
}
return summary;
}
private List<AgentToolBinding> snapshotToolBindings(List<AgentToolBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(binding -> {
AgentToolBinding snapshot = objectMapper.convertValue(binding, AgentToolBinding.class);
snapshot.setResourceSummary(toolSummary(binding));
snapshot.setResourceSnapshot(toolResourceSnapshot(binding));
return snapshot;
}).toList();
}
private Map<String, Object> toolResourceSnapshot(AgentToolBinding binding) {
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
Workflow workflow = workflowService.getPublishedById(binding.getTargetId());
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
throw new BusinessException("绑定工作流不存在或未发布");
}
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("绑定插件不存在");
}
return objectMapper.convertValue(pluginItem, new TypeReference<Map<String, Object>>() {});
}
Mcp mcp = mcpService.getById(binding.getTargetId());
if (mcp == null) {
throw new BusinessException("绑定 MCP 不存在");
}
return objectMapper.convertValue(mcp, new TypeReference<Map<String, Object>>() {});
}
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(binding -> {
AgentKnowledgeBinding snapshot = objectMapper.convertValue(binding, AgentKnowledgeBinding.class);
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("绑定知识库不存在或未发布");
}
return objectMapper.convertValue(knowledge, new TypeReference<Map<String, Object>>() {});
}
private List<Map<String, Object>> knowledgeSummaries(List<AgentKnowledgeBinding> bindings) {
if (bindings == null) {
return List.of();
}
return bindings.stream().map(this::knowledgeSummary).toList();
}
private Map<String, Object> knowledgeSummary(AgentKnowledgeBinding binding) {
DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("bindingId", binding.getId());
summary.put("knowledgeId", binding.getKnowledgeId());
summary.put("retrievalMode", binding.getRetrievalMode());
summary.put("enabled", Boolean.TRUE.equals(binding.getEnabled()));
summary.put("optionsJson", binding.getOptionsJson());
summary.put("sortNo", binding.getSortNo());
summary.put("title", knowledge == null ? null : knowledge.getTitle());
return summary;
}
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
} catch (Exception e) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}
private BigInteger toBigInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof BigInteger bigInteger) {
return bigInteger;
}
if (value instanceof Number number) {
return BigInteger.valueOf(number.longValue());
}
return new BigInteger(String.valueOf(value));
}
}

View File

@@ -0,0 +1,139 @@
package tech.easyflow.agent.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.agent.entity.Agent;
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.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.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 javax.annotation.Resource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Date;
import java.util.List;
/**
* Agent 工具绑定服务实现。
*/
@Service
public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMapper, AgentToolBinding>
implements AgentToolBindingService {
@Resource
private AgentMapper agentMapper;
@Resource
private WorkflowService workflowService;
@Resource
private PluginItemService pluginItemService;
@Resource
private McpService mcpService;
@Resource
private ResourceAccessService resourceAccessService;
/**
* {@inheritDoc}
*/
@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);
}
/**
* {@inheritDoc}
*/
@Override
public List<AgentToolBinding> listEnabled(BigInteger agentId) {
return list(QueryWrapper.create()
.where("agent_id = ?", agentId)
.and("enabled = ?", true)
.orderBy("sort_no asc, id asc"));
}
private Agent requireAgent(BigInteger agentId) {
Agent agent = agentMapper.selectOneById(agentId);
if (agent == null) {
throw new BusinessException("Agent 不存在");
}
return agent;
}
private void validateBinding(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, "无权限绑定该工作流");
return;
}
if (type == AgentToolType.PLUGIN) {
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
if (pluginItem == null || pluginItem.getStatus() == null || pluginItem.getStatus() != 1) {
throw new BusinessException("绑定插件不存在或未启用");
}
return;
}
Mcp mcp = mcpService.getById(binding.getTargetId());
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
throw new BusinessException("绑定 MCP 不存在或未启用");
}
}
private void applyBindingDefaults(Agent agent, AgentToolBinding binding, int index) {
LoginAccount account = requireCurrentLoginAccount();
Date now = new Date();
binding.setId(null);
binding.setTenantId(agent.getTenantId());
binding.setAgentId(agent.getId());
binding.setEnabled(binding.getEnabled() == null || binding.getEnabled());
binding.setHitlEnabled(Boolean.TRUE.equals(binding.getHitlEnabled()));
binding.setSortNo(binding.getSortNo() == null ? index : binding.getSortNo());
binding.setCreated(now);
binding.setCreatedBy(account.getId());
binding.setModified(now);
binding.setModifiedBy(account.getId());
}
private LoginAccount requireCurrentLoginAccount() {
try {
return SaTokenUtil.getLoginAccount();
} catch (Exception e) {
throw new BusinessException("当前登录状态失效,请重新登录后再试");
}
}
}

View File

@@ -0,0 +1,59 @@
package tech.easyflow.agent.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
import tech.easyflow.agent.service.AgentToolBindingService;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicInteger;
/**
* {@link AgentApprovalSubjectHandler} 单元测试。
*/
public class AgentApprovalSubjectHandlerTest {
/**
* 审批删除 Agent 前必须同步清理工具绑定和知识库绑定,避免留下孤儿数据。
*/
@Test
public void beforeRemoveShouldCleanAgentBindings() {
AtomicInteger toolRemoveCalls = new AtomicInteger();
AtomicInteger knowledgeRemoveCalls = new AtomicInteger();
AgentToolBindingService toolBindingService = proxy(AgentToolBindingService.class, toolRemoveCalls);
AgentKnowledgeBindingService knowledgeBindingService = proxy(AgentKnowledgeBindingService.class, knowledgeRemoveCalls);
AgentApprovalSubjectHandler handler = new AgentApprovalSubjectHandler(
null,
new ObjectMapper(),
null,
toolBindingService,
knowledgeBindingService,
null
);
handler.beforeRemove(BigInteger.valueOf(1001));
Assert.assertEquals(1, toolRemoveCalls.get());
Assert.assertEquals(1, knowledgeRemoveCalls.get());
}
@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, AtomicInteger removeCalls) {
return (T) Proxy.newProxyInstance(
type.getClassLoader(),
new Class<?>[]{type},
(proxy, method, args) -> {
if ("remove".equals(method.getName()) && args != null && args.length == 1) {
removeCalls.incrementAndGet();
return true;
}
if (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class) {
return false;
}
return null;
}
);
}
}

View File

@@ -0,0 +1,192 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.AgentInitRequest;
import com.easyagents.agent.runtime.AgentResumeRequest;
import com.easyagents.agent.runtime.AgentRuntime;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import org.junit.Assert;
import org.junit.Test;
import reactor.core.publisher.Flux;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Agent 运行态注册表测试。
*/
public class AgentRunRegistryTest {
/**
* 验证批准请求会恢复当前运行时。
*/
@Test
public void approveShouldResumeRuntime() {
AgentRunRegistry registry = new AgentRunRegistry();
CapturingRuntime runtime = new CapturingRuntime();
registry.register(context("request-1", "session-1", "user-1", runtime));
registry.registerResumeToken("request-1", "token-1");
registry.approve("request-1", "token-1", "user-1");
AgentResumeRequest request = runtime.resumeRequest.get();
Assert.assertNotNull(request);
Assert.assertTrue(request.isApproved());
Assert.assertEquals("token-1", request.getResumeToken().getValue());
}
/**
* 验证拒绝请求支持通过恢复令牌反查运行时。
*/
@Test
public void rejectShouldResolveRequestByResumeToken() {
AgentRunRegistry registry = new AgentRunRegistry();
CapturingRuntime runtime = new CapturingRuntime();
registry.register(context("request-2", "session-2", "user-1", runtime));
registry.registerResumeToken("request-2", "token-2");
registry.reject(null, "token-2", "user-1", "denied");
AgentResumeRequest request = runtime.resumeRequest.get();
Assert.assertNotNull(request);
Assert.assertFalse(request.isApproved());
Assert.assertEquals("denied", request.getRejectReason());
}
/**
* 验证运行结束后清理运行态与恢复令牌索引。
*/
@Test
public void removeShouldClearRuntimeAndResumeTokens() {
AgentRunRegistry registry = new AgentRunRegistry();
registry.register(context("request-3", "session-3", "user-1", new CapturingRuntime()));
registry.registerResumeToken("request-3", "token-3");
registry.remove("request-3");
Assert.assertThrows(BusinessException.class, () -> registry.approve(null, "token-3", "user-1"));
}
/**
* 验证运行审批只能由运行发起人处理。
*/
@Test
public void approveShouldRejectDifferentOwner() {
AgentRunRegistry registry = new AgentRunRegistry();
CapturingRuntime runtime = new CapturingRuntime();
registry.register(context("request-4", "session-4", "user-1", runtime));
registry.registerResumeToken("request-4", "token-4");
Assert.assertThrows(BusinessException.class, () -> registry.approve("request-4", "token-4", "user-2"));
Assert.assertNull(runtime.resumeRequest.get());
registry.approve("request-4", "token-4", "user-1");
Assert.assertTrue(runtime.resumeRequest.get().isApproved());
}
/**
* 验证传入 requestId 时仍会校验恢复令牌归属,避免错误令牌打断挂起运行。
*/
@Test
public void approveShouldRejectTokenNotBelongingToRequest() {
AgentRunRegistry registry = new AgentRunRegistry();
CapturingRuntime runtime = new CapturingRuntime();
registry.register(context("request-5", "session-5", "user-1", runtime));
Assert.assertThrows(BusinessException.class,
() -> registry.approve("request-5", "wrong-token", "user-1"));
Assert.assertNull(runtime.resumeRequest.get());
}
/**
* 验证同一会话同一时刻只允许一个运行态。
*/
@Test
public void registerShouldRejectActiveRunInSameSession() {
AgentRunRegistry registry = new AgentRunRegistry();
registry.register(context("request-6", "session-6", "user-1", new CapturingRuntime()));
Assert.assertThrows(BusinessException.class,
() -> registry.register(context("request-7", "session-6", "user-1", new CapturingRuntime())));
}
/**
* 验证按会话清理会取消当前运行并释放同会话运行锁。
*/
@Test
public void cancelSessionShouldRemoveActiveRun() {
AgentRunRegistry registry = new AgentRunRegistry();
AgentRunRegistry.AgentRunContext context = context("request-8", "session-8", "user-1", new CapturingRuntime());
registry.register(context);
registry.cancelSession("session-8");
Assert.assertNull(registry.get("request-8"));
registry.register(context("request-9", "session-8", "user-1", new CapturingRuntime()));
Assert.assertNotNull(registry.get("request-9"));
}
/**
* 验证按会话清理时会校验运行归属。
*/
@Test
public void cancelSessionShouldRejectDifferentOwner() {
AgentRunRegistry registry = new AgentRunRegistry();
registry.register(context("request-10", "session-10", "user-1", new CapturingRuntime()));
Assert.assertThrows(BusinessException.class, () -> registry.cancelSession("session-10", "user-2"));
Assert.assertNotNull(registry.get("request-10"));
}
private AgentRunRegistry.AgentRunContext context(String requestId,
String sessionId,
String userId,
AgentRuntime runtime) {
return new AgentRunRegistry.AgentRunContext(
requestId,
sessionId,
runtime,
null,
new ChatRuntimeContext(),
new StringBuilder(),
new ChatAssistantAccumulator(),
new AtomicBoolean(false),
false,
new AgentRunRegistry.RunOwner("agent-1", sessionId, userId),
null,
event -> {
},
error -> {
},
() -> {
}
);
}
private static final class CapturingRuntime implements AgentRuntime {
private final AtomicReference<AgentResumeRequest> resumeRequest = new AtomicReference<>();
@Override
public void init(AgentInitRequest request) {
// 测试桩无需初始化。
}
@Override
public Flux<AgentRuntimeEvent> stream(com.easyagents.agent.runtime.message.AgentMessage userMessage) {
return Flux.empty();
}
@Override
public Flux<AgentRuntimeEvent> resume(AgentResumeRequest request) {
resumeRequest.set(request);
return Flux.empty();
}
}
}

View File

@@ -0,0 +1,660 @@
package tech.easyflow.agent.runtime;
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
import com.easyagents.agent.runtime.message.AgentMessage;
import com.easyagents.agent.runtime.message.AgentMessageRole;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.agent.entity.AgentToolBinding;
import tech.easyflow.agent.runtime.lock.AgentRunLock;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.core.chat.protocol.ChatDomain;
import tech.easyflow.core.chat.protocol.ChatEnvelope;
import tech.easyflow.core.chat.protocol.ChatType;
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
import tech.easyflow.core.runtime.ChatRuntimeContext;
import tech.easyflow.core.runtime.ChatRuntimeManager;
import tech.easyflow.core.runtime.ChatRuntimeMessage;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Agent 草稿试用与 HITL 事件映射测试。
*/
public class AgentRunServiceDraftAndHitlTest {
/**
* 验证工具 HITL 事件会映射为显式前端载荷。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void buildToolHitlPayloadShouldExposeStableFieldsWithoutPrompt() throws Exception {
AgentRunService service = new AgentRunService();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
event.setToolCallId("call-1");
event.getPayload().put("resumeToken", "token-1");
event.getPayload().put("sessionId", "session-1");
event.getPayload().put("agentId", "agent-1");
event.getPayload().put("toolName", "search");
event.getPayload().put("toolType", "PLUGIN");
event.getPayload().put("approvalPrompt", "不应透出");
event.getPayload().put("toolInput", Map.of("keyword", "EasyFlow"));
event.getPayload().put("approvalMetadata", Map.of(
"risk", "low",
"prompt", "不应透出",
"toolType", "WORKFLOW"
));
AgentToolHitlPayload payload = invoke(service, "buildToolHitlPayload",
new Class<?>[]{String.class, AgentRuntimeEvent.class}, "request-1", event);
Assert.assertEquals("request-1", payload.getRequestId());
Assert.assertEquals("token-1", payload.getResumeToken());
Assert.assertEquals("session-1", payload.getSessionId());
Assert.assertEquals("agent-1", payload.getAgentId());
Assert.assertEquals("call-1", payload.getToolCallId());
Assert.assertEquals("search", payload.getToolName());
Assert.assertEquals("PLUGIN", payload.getToolType());
Assert.assertEquals("EasyFlow", payload.getInput().get("keyword"));
Assert.assertEquals("low", payload.getMetadata().get("risk"));
Assert.assertEquals("PLUGIN", payload.getMetadata().get("toolType"));
Assert.assertFalse(payload.getMetadata().containsKey("prompt"));
}
/**
* 验证工具事件发送给前端时会携带稳定工具调用 ID。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void buildToolEventPayloadShouldExposeRuntimeToolCallId() throws Exception {
AgentRunService service = new AgentRunService();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_RESULT);
event.setToolCallId("call-runtime");
event.getPayload().put("toolName", "search");
event.getPayload().put("text", "ok");
Map<String, Object> payload = invoke(service, "buildToolEventPayload",
new Class<?>[]{AgentRuntimeEvent.class}, event);
Assert.assertEquals("call-runtime", payload.get("toolCallId"));
Assert.assertEquals("search", payload.get("toolName"));
Assert.assertEquals("ok", payload.get("text"));
}
/**
* 验证思考事件会优先读取 reasoning 字段。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void stringPayloadShouldExposeReasoningValue() throws Exception {
AgentRunService service = new AgentRunService();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.REASONING_DELTA);
event.getPayload().put("reasoning", "思考中");
String reasoning = invoke(service, "stringPayload",
new Class<?>[]{AgentRuntimeEvent.class, String.class}, event, "reasoning");
String fallback = invoke(service, "firstText",
new Class<?>[]{String.class, String.class}, reasoning, "正文");
Assert.assertEquals("思考中", fallback);
}
/**
* 验证思考事件会作为增量发送给前端。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldSendReasoningDeltaAsThinkingEnvelope() throws Exception {
AgentRunService service = new AgentRunService();
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.REASONING_DELTA);
event.getPayload().put("reasoning", "思考增量");
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-1", emitter, new StringBuilder(), new ChatAssistantAccumulator(),
chatContext(), new AtomicBoolean(false), false);
Assert.assertEquals(1, emitter.envelopes.size());
Assert.assertEquals(ChatDomain.LLM, emitter.envelopes.get(0).getDomain());
Assert.assertEquals(ChatType.THINKING, emitter.envelopes.get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
Assert.assertEquals("思考增量", payload.get("reasoning"));
Assert.assertEquals("思考增量", payload.get("delta"));
}
/**
* 验证正文事件会作为增量发送给前端并累计到持久化缓冲。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldSendMessageDeltaAndAccumulateAnswer() throws Exception {
AgentRunService service = new AgentRunService();
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
StringBuilder answer = new StringBuilder();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
event.getPayload().put("text", "正文增量");
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
chatContext(), new AtomicBoolean(false), false);
Assert.assertEquals("正文增量", answer.toString());
Assert.assertEquals(1, emitter.envelopes.size());
Assert.assertEquals(ChatDomain.LLM, emitter.envelopes.get(0).getDomain());
Assert.assertEquals(ChatType.MESSAGE, emitter.envelopes.get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
Assert.assertEquals("正文增量", payload.get("delta"));
}
/**
* 验证自动上下文压缩事件会作为业务状态发送给前端。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldSendMemoryCompressionAsStatusEnvelope() throws Exception {
AgentRunService service = new AgentRunService();
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED);
event.getPayload().put("statusKey", "memory-compression");
event.getPayload().put("status", "running");
event.getPayload().put("label", "正在整理上下文");
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-1", emitter, new StringBuilder(), new ChatAssistantAccumulator(),
chatContext(), new AtomicBoolean(false), false);
Assert.assertEquals(1, emitter.envelopes.size());
Assert.assertEquals(ChatDomain.BUSINESS, emitter.envelopes.get(0).getDomain());
Assert.assertEquals(ChatType.STATUS, emitter.envelopes.get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
Assert.assertEquals("memory-compression", payload.get("statusKey"));
Assert.assertEquals("running", payload.get("status"));
Assert.assertEquals("正在整理上下文", payload.get("label"));
}
/**
* 验证完成事件不会再次发送正文消息,只用于最终收口。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldNotSendMessageEnvelopeOnCompleted() throws Exception {
AgentRunService service = new AgentRunService();
setField(service, "agentRunRegistry", new AgentRunRegistry());
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
StringBuilder answer = new StringBuilder("流式正文");
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
event.getPayload().put("text", "最终正文");
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
chatContext(), new AtomicBoolean(false), false);
Assert.assertEquals("最终正文", answer.toString());
Assert.assertTrue(emitter.envelopes.stream().noneMatch(envelope ->
envelope.getDomain() == ChatDomain.LLM && envelope.getType() == ChatType.MESSAGE));
Assert.assertTrue(emitter.envelopes.stream().anyMatch(envelope ->
envelope.getDomain() == ChatDomain.SYSTEM && envelope.getType() == ChatType.DONE));
}
/**
* 验证挂起事件后自然完成不会关闭 SSE 或清理运行态。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void finishIfNeededShouldKeepRuntimeWhenSuspended() throws Exception {
AgentRunService service = new AgentRunService();
AgentRunRegistry registry = new AgentRunRegistry();
setField(service, "agentRunRegistry", registry);
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
AtomicBoolean finished = new AtomicBoolean(false);
AgentRunRegistry.AgentRunContext runContext = new AgentRunRegistry.AgentRunContext(
"request-suspended",
"session-suspended",
new NoopRuntime(),
emitter,
chatContext(),
new StringBuilder(),
new ChatAssistantAccumulator(),
finished,
false,
new AgentRunRegistry.RunOwner("agent-1", "session-suspended", "user-1"),
null,
event -> {
},
error -> {
},
() -> {
}
);
registry.register(runContext);
runContext.markSuspended();
invoke(service, "finishIfNeeded",
new Class<?>[]{String.class, ChatSseEmitter.class, ChatRuntimeContext.class, StringBuilder.class,
ChatAssistantAccumulator.class, AtomicBoolean.class, boolean.class},
"request-suspended", emitter, chatContext(), new StringBuilder(),
new ChatAssistantAccumulator(), finished, false);
Assert.assertFalse(finished.get());
Assert.assertNotNull(registry.get("request-suspended"));
Assert.assertTrue(emitter.envelopes.isEmpty());
}
/**
* 验证取消事件作为业务状态收口,不按系统错误发送。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldSendCancelledAsStatusAndDone() throws Exception {
AgentRunService service = new AgentRunService();
setField(service, "agentRunRegistry", new AgentRunRegistry());
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
setField(service, "chatRuntimeManager", chatRuntimeManager);
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
StringBuilder answer = new StringBuilder("取消前正文");
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.CANCELLED);
event.getPayload().put("reason", "用户拒绝执行");
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
chatContext(), new AtomicBoolean(false), true);
Assert.assertEquals(2, emitter.envelopes.size());
Assert.assertEquals(ChatDomain.BUSINESS, emitter.envelopes.get(0).getDomain());
Assert.assertEquals(ChatType.STATUS, emitter.envelopes.get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
Assert.assertEquals("agent-cancelled", payload.get("statusKey"));
Assert.assertEquals("cancelled", payload.get("status"));
Assert.assertEquals("用户拒绝执行", payload.get("message"));
Assert.assertEquals(ChatDomain.SYSTEM, emitter.envelopes.get(1).getDomain());
Assert.assertEquals(ChatType.DONE, emitter.envelopes.get(1).getType());
Assert.assertEquals(1, chatRuntimeManager.recordAssistantCompletedCount);
Assert.assertEquals("取消前正文", chatRuntimeManager.lastAssistantMessage.getContentText());
Assert.assertEquals(1, chatRuntimeManager.recordFailureCount);
}
/**
* 验证最终知识库引用会保留命中分片原文。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void buildKnowledgeCitationPayloadShouldExposeChunkContent() throws Exception {
AgentRunService service = new AgentRunService();
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "answer");
AgentKnowledgeReference reference = new AgentKnowledgeReference();
reference.setKnowledgeId("kb-1");
reference.setKnowledgeName("学生事务 FAQ");
reference.setDocumentId("faq-1");
reference.setDocumentName("faq-1.md");
reference.setChunkId("chunk-1");
reference.setChunkContent("暑假安排原文");
reference.setScore(0.91D);
reference.getMetadata().put("knowledgeType", "FAQ");
reference.getMetadata().put("faqCollection", true);
message.setKnowledgeReferences(List.of(reference));
event.setMessage(message);
List<Map<String, Object>> payload = invoke(service, "buildKnowledgeCitationPayload",
new Class<?>[]{AgentRuntimeEvent.class}, event);
Assert.assertEquals(1, payload.size());
Assert.assertEquals("学生事务 FAQ", payload.get(0).get("knowledgeName"));
Assert.assertEquals("暑假安排原文", payload.get(0).get("chunkContent"));
Assert.assertEquals("FAQ", payload.get(0).get("knowledgeType"));
Assert.assertEquals(Boolean.TRUE, payload.get(0).get("faqCollection"));
}
/**
* 验证未保存草稿会生成临时 Agent ID并把绑定指向该运行 ID。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void buildDraftAgentShouldGenerateRuntimeIdForUnsavedAgent() throws Exception {
AgentRunService service = new AgentRunService();
AgentDraftChatRequest request = new AgentDraftChatRequest();
Agent agent = new Agent();
agent.setModelId(BigInteger.valueOf(10));
request.setAgent(agent);
AgentToolBinding toolBinding = new AgentToolBinding();
toolBinding.setToolType("PLUGIN");
toolBinding.setTargetId(BigInteger.valueOf(20));
toolBinding.setResourceSnapshot(Map.of("name", "client-forged"));
toolBinding.setResourceSummary(Map.of("name", "client-forged"));
request.setToolBindings(List.of(toolBinding));
AgentKnowledgeBinding knowledgeBinding = new AgentKnowledgeBinding();
knowledgeBinding.setKnowledgeId(BigInteger.valueOf(30));
knowledgeBinding.setResourceSnapshot(Map.of("title", "client-forged"));
knowledgeBinding.setResourceSummary(Map.of("title", "client-forged"));
request.setKnowledgeBindings(List.of(knowledgeBinding));
LoginAccount account = new LoginAccount();
account.setId(BigInteger.ONE);
account.setTenantId(BigInteger.valueOf(2));
account.setDeptId(BigInteger.valueOf(3));
Agent draftAgent = invoke(service, "buildDraftAgent",
new Class<?>[]{AgentDraftChatRequest.class, LoginAccount.class}, request, account);
Assert.assertNotNull(draftAgent.getId());
Assert.assertEquals(BigInteger.valueOf(2), draftAgent.getTenantId());
Assert.assertEquals(draftAgent.getId(), draftAgent.getToolBindings().get(0).getAgentId());
Assert.assertEquals(draftAgent.getId(), draftAgent.getKnowledgeBindings().get(0).getAgentId());
Assert.assertTrue(draftAgent.getToolBindings().get(0).getEnabled());
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getEnabled());
Assert.assertTrue(draftAgent.getToolBindings().get(0).getResourceSnapshot().isEmpty());
Assert.assertTrue(draftAgent.getToolBindings().get(0).getResourceSummary().isEmpty());
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getResourceSnapshot().isEmpty());
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getResourceSummary().isEmpty());
}
/**
* 验证正式聊天在获取运行锁失败时不会提前写入用户消息。
*
* <p>运行锁是 AgentScope session 与 chatlog 的一致性入口。若同会话并发请求抢锁失败,
* 必须在 prepareSession 和 recordUserMessage 之前失败,避免 chatlog 出现没有真实运行的用户消息。</p>
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void runShouldAcquireLockBeforePersistingUserMessage() throws Exception {
AgentRunService service = new AgentRunService();
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
setField(service, "chatRuntimeManager", chatRuntimeManager);
setField(service, "agentRunLock", new RejectingAgentRunLock());
Agent agent = new Agent();
agent.setId(BigInteger.valueOf(100));
ChatRuntimeContext context = chatContext();
Exception thrown = Assert.assertThrows(Exception.class, () -> invoke(service, "run",
new Class<?>[]{Agent.class, String.class, String.class, String.class, String.class,
String.class, ChatRuntimeContext.class, boolean.class},
agent, "你好", "request-lock", "trace-lock", "session-lock", "AGENT", context, true));
Assert.assertTrue(rootCause(thrown) instanceof BusinessException);
Assert.assertEquals(0, chatRuntimeManager.prepareSessionCount);
Assert.assertEquals(0, chatRuntimeManager.recordUserMessageCount);
}
/**
* 验证正式聊天会在会话准备完成后向前端返回真实会话 ID。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void sendSessionCreatedShouldExposeSessionId() throws Exception {
AgentRunService service = new AgentRunService();
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
Boolean sent = invoke(service, "sendSessionCreated",
new Class<?>[]{ChatSseEmitter.class, BigInteger.class}, emitter, BigInteger.valueOf(123));
Assert.assertTrue(sent);
Assert.assertEquals(1, emitter.envelopes.size());
Assert.assertEquals(ChatDomain.SYSTEM, emitter.envelopes.get(0).getDomain());
Assert.assertEquals(ChatType.SESSION_CREATED, emitter.envelopes.get(0).getType());
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
Assert.assertEquals("123", payload.get("sessionId"));
}
/**
* 验证正式聊天只有新会话首轮会自动设置默认标题。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void applyFormalSessionTitleShouldOnlyUseFirstPromptForNewSession() throws Exception {
AgentRunService service = new AgentRunService();
ChatRuntimeContext newContext = chatContext();
ChatRuntimeContext existingContext = chatContext();
ChatSessionSummary existingSession = new ChatSessionSummary();
existingSession.setId(BigInteger.valueOf(123));
existingSession.setTitle("用户改过的标题");
existingSession.setMessageCount(2);
invoke(service, "applyFormalSessionTitle",
new Class<?>[]{ChatRuntimeContext.class, String.class, ChatSessionSummary.class},
newContext, "第一句话", null);
invoke(service, "applyFormalSessionTitle",
new Class<?>[]{ChatRuntimeContext.class, String.class, ChatSessionSummary.class},
existingContext, "后续消息", existingSession);
Assert.assertEquals("第一句话", newContext.getSessionTitle());
Assert.assertNull(existingContext.getSessionTitle());
}
/**
* 验证正式聊天 SSE 断开后会取消运行并保存断开前已输出的 assistant 内容。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void handleRuntimeEventShouldCancelAndRecordFailureWhenSseDisconnected() throws Exception {
AgentRunService service = new AgentRunService();
AgentRunRegistry registry = new AgentRunRegistry();
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
setField(service, "agentRunRegistry", registry);
setField(service, "chatRuntimeManager", chatRuntimeManager);
AtomicBoolean finished = new AtomicBoolean(false);
ChatRuntimeContext context = chatContext();
AgentRunRegistry.AgentRunContext runContext = new AgentRunRegistry.AgentRunContext(
"request-disconnected",
"session-disconnected",
new NoopRuntime(),
new FailingChatSseEmitter(),
context,
new StringBuilder(),
new ChatAssistantAccumulator(),
finished,
true,
new AgentRunRegistry.RunOwner("agent-1", "session-disconnected", "user-1"),
null,
ignored -> {
},
ignored -> {
},
() -> {
}
);
registry.register(runContext);
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
event.getPayload().put("text", "断连前正文");
StringBuilder answer = new StringBuilder();
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
invoke(service, "handleRuntimeEvent",
runtimeEventParameterTypes(),
event, "request-disconnected", new FailingChatSseEmitter(), answer,
assistantAccumulator, context, finished, true);
Assert.assertTrue(finished.get());
Assert.assertNull(registry.get("request-disconnected"));
Assert.assertEquals(1, chatRuntimeManager.recordAssistantCompletedCount);
Assert.assertEquals("断连前正文", chatRuntimeManager.lastAssistantMessage.getContentText());
Assert.assertEquals(1, chatRuntimeManager.recordFailureCount);
Assert.assertEquals(0, chatRuntimeManager.recordCompletedCount);
}
@SuppressWarnings("unchecked")
private <T> T invoke(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws Exception {
Method method = target.getClass().getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return (T) method.invoke(target, args);
}
private void setField(Object target, String fieldName, Object value) throws Exception {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
private Class<?>[] runtimeEventParameterTypes() {
return new Class<?>[]{AgentRuntimeEvent.class, String.class, ChatSseEmitter.class, StringBuilder.class,
ChatAssistantAccumulator.class,
ChatRuntimeContext.class, AtomicBoolean.class, boolean.class};
}
private ChatRuntimeContext chatContext() {
ChatRuntimeContext context = new ChatRuntimeContext();
context.setAssistantId(BigInteger.valueOf(100));
context.setAssistantName("Agent");
context.setUserId(BigInteger.valueOf(101));
context.setUserName("用户");
return context;
}
private Throwable rootCause(Throwable throwable) {
Throwable current = throwable;
while (current.getCause() != null) {
current = current.getCause();
}
return current;
}
/**
* 记录发送内容的 SSE 测试桩。
*/
private static class RecordingChatSseEmitter extends ChatSseEmitter {
private final List<ChatEnvelope<?>> envelopes = new java.util.ArrayList<>();
@Override
public boolean send(ChatEnvelope<?> envelope) {
envelopes.add(envelope);
return true;
}
@Override
public boolean sendDone(ChatEnvelope<?> envelope) {
envelopes.add(envelope);
return true;
}
}
private static class FailingChatSseEmitter extends ChatSseEmitter {
@Override
public boolean send(ChatEnvelope<?> envelope) {
return false;
}
@Override
public boolean sendDone(ChatEnvelope<?> envelope) {
return false;
}
}
private static class NoopRuntime implements com.easyagents.agent.runtime.AgentRuntime {
@Override
public void init(com.easyagents.agent.runtime.AgentInitRequest request) {
// 测试桩无需初始化。
}
@Override
public reactor.core.publisher.Flux<AgentRuntimeEvent> stream(AgentMessage userMessage) {
return reactor.core.publisher.Flux.empty();
}
@Override
public reactor.core.publisher.Flux<AgentRuntimeEvent> resume(com.easyagents.agent.runtime.AgentResumeRequest request) {
return reactor.core.publisher.Flux.empty();
}
}
/**
* 记录 chatlog 写入动作的测试桩。
*/
private static class RecordingChatRuntimeManager implements ChatRuntimeManager {
private int prepareSessionCount;
private int recordUserMessageCount;
private int recordAssistantCompletedCount;
private int recordFailureCount;
private int recordCompletedCount;
private ChatRuntimeMessage lastAssistantMessage;
@Override
public void prepareSession(ChatRuntimeContext context) {
prepareSessionCount++;
}
@Override
public void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
recordUserMessageCount++;
}
@Override
public void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
// 测试桩无需记录。
}
@Override
public void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
recordAssistantCompletedCount++;
lastAssistantMessage = message;
}
@Override
public void recordFailure(ChatRuntimeContext context, Throwable throwable) {
recordFailureCount++;
}
@Override
public void recordCompleted(ChatRuntimeContext context) {
recordCompletedCount++;
}
@Override
public List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
return List.of();
}
}
/**
* 模拟同会话运行锁已被占用的测试桩。
*/
private static class RejectingAgentRunLock implements AgentRunLock {
@Override
public Handle acquire(BigInteger agentId, String sessionId) {
throw new BusinessException("当前 Agent 会话已有运行中的请求,请稍后再试");
}
}
}