fix: 完成系统向智能体数据链路切换
- 切换工作台、聊天历史、资源候选与公共调用到 Agent - 加固资源绑定、删除保护及发布运行并发控制 - 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentCategory;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mapper.PluginMapper;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Agent 依赖资源的租户、权限和可用性校验服务。
|
||||
*/
|
||||
@Service
|
||||
public class AgentDependencyAccessService {
|
||||
|
||||
private final ModelService modelService;
|
||||
private final WorkflowService workflowService;
|
||||
private final PluginItemService pluginItemService;
|
||||
private final PluginMapper pluginMapper;
|
||||
private final PluginVisibilityService pluginVisibilityService;
|
||||
private final McpService mcpService;
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final AgentCategoryService agentCategoryService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 依赖资源校验服务。
|
||||
*
|
||||
* @param modelService 模型服务
|
||||
* @param workflowService 工作流服务
|
||||
* @param pluginItemService 插件工具服务
|
||||
* @param pluginMapper 插件 Mapper
|
||||
* @param pluginVisibilityService 插件可见性服务
|
||||
* @param mcpService MCP 服务
|
||||
* @param documentCollectionService 知识库服务
|
||||
* @param agentCategoryService Agent 分类服务
|
||||
* @param categoryPermissionService 分类权限服务
|
||||
* @param resourceAccessService 资源权限服务
|
||||
*/
|
||||
public AgentDependencyAccessService(ModelService modelService,
|
||||
WorkflowService workflowService,
|
||||
PluginItemService pluginItemService,
|
||||
PluginMapper pluginMapper,
|
||||
PluginVisibilityService pluginVisibilityService,
|
||||
McpService mcpService,
|
||||
DocumentCollectionService documentCollectionService,
|
||||
AgentCategoryService agentCategoryService,
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.modelService = modelService;
|
||||
this.workflowService = workflowService;
|
||||
this.pluginItemService = pluginItemService;
|
||||
this.pluginMapper = pluginMapper;
|
||||
this.pluginVisibilityService = pluginVisibilityService;
|
||||
this.mcpService = mcpService;
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.agentCategoryService = agentCategoryService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Agent 模型并锁定模型行。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param modelId 模型 ID
|
||||
* @return 模型
|
||||
*/
|
||||
public Model requireModel(Agent agent, BigInteger modelId) {
|
||||
if (modelId == null) {
|
||||
throw new BusinessException("Agent 模型不能为空");
|
||||
}
|
||||
Model model = modelService.getOne(QueryWrapper.create()
|
||||
.eq(Model::getId, modelId)
|
||||
.forUpdate());
|
||||
if (model == null) {
|
||||
throw new BusinessException("Agent 模型不存在");
|
||||
}
|
||||
assertSameTenant(agent, model.getTenantId(), "无权限使用该模型");
|
||||
if (!Model.MODEL_TYPES[0].equals(model.getModelType())) {
|
||||
throw new BusinessException("Agent 仅支持聊天模型");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定工作流。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 已发布工作流
|
||||
*/
|
||||
public Workflow requireWorkflow(Agent agent, BigInteger workflowId) {
|
||||
Workflow workflow = workflowService.getOne(QueryWrapper.create()
|
||||
.eq(Workflow::getId, workflowId)
|
||||
.forUpdate());
|
||||
if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定工作流不存在或未发布");
|
||||
}
|
||||
assertSameTenant(agent, workflow.getTenantId(), "无权限绑定该工作流");
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限绑定该工作流");
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验插件工具及其所属插件,并按父子顺序锁定资源行。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param pluginItemId 插件工具 ID
|
||||
* @return 插件工具
|
||||
*/
|
||||
public PluginItem requirePluginItem(Agent agent, BigInteger pluginItemId) {
|
||||
PluginItem current = pluginItemService.getById(pluginItemId);
|
||||
if (current == null || current.getPluginId() == null) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
}
|
||||
Plugin plugin = pluginMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq(Plugin::getId, current.getPluginId())
|
||||
.forUpdate());
|
||||
PluginItem pluginItem = pluginItemService.getOne(QueryWrapper.create()
|
||||
.eq(PluginItem::getId, pluginItemId)
|
||||
.forUpdate());
|
||||
if (plugin == null || pluginItem == null || !Objects.equals(plugin.getId(), pluginItem.getPluginId())) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(pluginItem.getStatus())) {
|
||||
throw new BusinessException("绑定插件未启用");
|
||||
}
|
||||
assertSameTenant(agent, plugin.getTenantId(), "无权限绑定该插件");
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限绑定该插件");
|
||||
return pluginItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定 MCP。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param mcpId MCP ID
|
||||
* @return MCP
|
||||
*/
|
||||
public Mcp requireMcp(Agent agent, BigInteger mcpId) {
|
||||
Mcp mcp = mcpService.getOne(QueryWrapper.create()
|
||||
.eq(Mcp::getId, mcpId)
|
||||
.forUpdate());
|
||||
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
|
||||
throw new BusinessException("绑定 MCP 不存在或未启用");
|
||||
}
|
||||
assertSameTenant(agent, mcp.getTenantId(), "无权限绑定该 MCP");
|
||||
return mcp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并锁定知识库。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 已发布知识库
|
||||
*/
|
||||
public DocumentCollection requireKnowledge(Agent agent, BigInteger knowledgeId) {
|
||||
DocumentCollection knowledge = documentCollectionService.getOne(QueryWrapper.create()
|
||||
.eq(DocumentCollection::getId, knowledgeId)
|
||||
.forUpdate());
|
||||
if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定知识库不存在或未发布");
|
||||
}
|
||||
assertSameTenant(agent, knowledge.getTenantId(), "无权限绑定该知识库");
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限绑定该知识库");
|
||||
return knowledge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Agent 分类属于当前租户并在当前账号授权范围内。
|
||||
*
|
||||
* @param agent Agent
|
||||
*/
|
||||
public void validateCategory(Agent agent) {
|
||||
if (agent == null || agent.getCategoryId() == null) {
|
||||
return;
|
||||
}
|
||||
AgentCategory category = agentCategoryService.getById(agent.getCategoryId());
|
||||
if (category == null || !Integer.valueOf(1).equals(category.getStatus())) {
|
||||
throw new BusinessException("Agent 分类不存在或未启用");
|
||||
}
|
||||
assertSameTenant(agent, category.getTenantId(), "无权限使用该 Agent 分类");
|
||||
RoleCategoryAccessSnapshot access =
|
||||
categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||
if (access.isRestricted() && !access.getCategoryIds().contains(agent.getCategoryId())) {
|
||||
throw new BusinessException("无权限使用该 Agent 分类");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验资源与 Agent 属于同一租户。
|
||||
*
|
||||
* @param agent Agent
|
||||
* @param resourceTenantId 资源租户 ID
|
||||
* @param message 拒绝消息
|
||||
*/
|
||||
private void assertSameTenant(Agent agent, Object resourceTenantId, String message) {
|
||||
BigInteger agentTenantId = agent == null ? null : agent.getTenantId();
|
||||
if (agentTenantId == null) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
agentTenantId = account == null ? null : account.getTenantId();
|
||||
}
|
||||
if (agentTenantId == null || resourceTenantId == null
|
||||
|| !agentTenantId.toString().equals(String.valueOf(resourceTenantId))) {
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Agent 与设计器依赖资源的安全选项查询服务。
|
||||
*/
|
||||
@Service
|
||||
public class AgentOptionQueryService {
|
||||
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final AgentService agentService;
|
||||
private final ModelService modelService;
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final WorkflowService workflowService;
|
||||
private final PluginService pluginService;
|
||||
private final PluginItemService pluginItemService;
|
||||
private final PluginVisibilityService pluginVisibilityService;
|
||||
private final McpService mcpService;
|
||||
private final AgentVisibilityQueryHelper agentVisibilityQueryHelper;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 创建 Agent 安全选项查询服务。
|
||||
*
|
||||
* @param agentService Agent 服务
|
||||
* @param modelService 模型服务
|
||||
* @param documentCollectionService 知识库服务
|
||||
* @param workflowService 工作流服务
|
||||
* @param pluginService 插件服务
|
||||
* @param pluginItemService 插件工具服务
|
||||
* @param pluginVisibilityService 插件可见性服务
|
||||
* @param mcpService MCP 服务
|
||||
* @param agentVisibilityQueryHelper Agent 可见性查询助手
|
||||
* @param resourceAccessService 资源访问服务
|
||||
* @param objectMapper JSON 映射器
|
||||
*/
|
||||
public AgentOptionQueryService(AgentService agentService,
|
||||
ModelService modelService,
|
||||
DocumentCollectionService documentCollectionService,
|
||||
WorkflowService workflowService,
|
||||
PluginService pluginService,
|
||||
PluginItemService pluginItemService,
|
||||
PluginVisibilityService pluginVisibilityService,
|
||||
McpService mcpService,
|
||||
AgentVisibilityQueryHelper agentVisibilityQueryHelper,
|
||||
ResourceAccessService resourceAccessService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.agentService = agentService;
|
||||
this.modelService = modelService;
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.workflowService = workflowService;
|
||||
this.pluginService = pluginService;
|
||||
this.pluginItemService = pluginItemService;
|
||||
this.pluginVisibilityService = pluginVisibilityService;
|
||||
this.mcpService = mcpService;
|
||||
this.agentVisibilityQueryHelper = agentVisibilityQueryHelper;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可见的 Agent 安全选项。
|
||||
*
|
||||
* @param publishedOnly 是否仅查询已发布 Agent
|
||||
* @return Agent 选项
|
||||
*/
|
||||
public List<AgentOptionView> listAgentOptions(boolean publishedOnly) {
|
||||
LoginAccount account = requireAccount();
|
||||
QueryWrapper wrapper = QueryWrapper.create();
|
||||
agentVisibilityQueryHelper.applyReadableAccess(wrapper);
|
||||
wrapper.orderBy(Agent::getModified, false);
|
||||
if (publishedOnly) {
|
||||
wrapper.eq(Agent::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.eq(Agent::getStatus, 1);
|
||||
}
|
||||
List<AgentOptionView> result = new ArrayList<>();
|
||||
ResourceAction action = publishedOnly ? ResourceAction.USE : ResourceAction.READ;
|
||||
for (Agent agent : agentService.list(wrapper)) {
|
||||
if (!resourceAccessService.canAccess(CategoryResourceType.AGENT, agent, action)) {
|
||||
continue;
|
||||
}
|
||||
result.add(toAgentOption(agent, publishedOnly));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 设计器所需的安全资源选项。
|
||||
*
|
||||
* @return 资源选项集合
|
||||
*/
|
||||
public AgentResourceOptionsView listDesignerResourceOptions() {
|
||||
LoginAccount account = requireAccount();
|
||||
return new AgentResourceOptionsView(
|
||||
listModelOptions(account),
|
||||
listKnowledgeOptions(account),
|
||||
listWorkflowOptions(account),
|
||||
listPluginToolOptions(account),
|
||||
listMcpOptions(account)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可用于 Agent 会话的知识库安全选项。
|
||||
*
|
||||
* @return 知识库选项
|
||||
*/
|
||||
public List<AgentResourceOptionsView.ResourceOption> listKnowledgeOptions() {
|
||||
return listKnowledgeOptions(requireAccount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 MCP 的安全工具列表。
|
||||
*
|
||||
* @param mcpId MCP ID
|
||||
* @return MCP 工具选项
|
||||
*/
|
||||
public List<AgentResourceOptionsView.McpToolOption> listMcpTools(BigInteger mcpId) {
|
||||
LoginAccount account = requireAccount();
|
||||
Mcp mcp = mcpService.getOne(QueryWrapper.create()
|
||||
.eq(Mcp::getId, mcpId)
|
||||
.eq(Mcp::getTenantId, account.getTenantId())
|
||||
.eq(Mcp::getStatus, true));
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("MCP 不存在或不可用");
|
||||
}
|
||||
Mcp detail = mcpService.getMcpTools(mcpId.toString());
|
||||
if (detail == null || detail.getTools() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<AgentResourceOptionsView.McpToolOption> result = new ArrayList<>();
|
||||
for (Object tool : detail.getTools()) {
|
||||
Map<String, Object> value = objectMapper.convertValue(tool, MAP_TYPE);
|
||||
result.add(new AgentResourceOptionsView.McpToolOption(
|
||||
text(value.get("name")),
|
||||
text(value.get("description"))
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前租户可用于 Agent 的模型选项。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return 模型选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.ModelOption> listModelOptions(LoginAccount account) {
|
||||
Model query = new Model();
|
||||
query.setTenantId(account.getTenantId());
|
||||
query.setModelType(Model.MODEL_TYPES[0]);
|
||||
return modelService.listSelectableModels(query, false, "id", "desc").stream()
|
||||
.filter(model -> Objects.equals(model.getTenantId(), account.getTenantId()))
|
||||
.map(model -> new AgentResourceOptionsView.ModelOption(
|
||||
model.getId(),
|
||||
model.getTitle(),
|
||||
model.getContextWindowTokens(),
|
||||
model.getMaxOutputTokens()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可使用的已发布知识库。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return 知识库选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.ResourceOption> listKnowledgeOptions(LoginAccount account) {
|
||||
return documentCollectionService.list(QueryWrapper.create()
|
||||
.eq(DocumentCollection::getTenantId, account.getTenantId())
|
||||
.eq(DocumentCollection::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.orderBy(DocumentCollection::getModified, false))
|
||||
.stream()
|
||||
.filter(item -> resourceAccessService.canAccess(
|
||||
CategoryResourceType.KNOWLEDGE, item, ResourceAction.USE))
|
||||
.map(item -> new AgentResourceOptionsView.ResourceOption(
|
||||
item.getId(), item.getTitle(), item.getDescription(), null))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可使用的已发布工作流。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return 工作流选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.ResourceOption> listWorkflowOptions(LoginAccount account) {
|
||||
return workflowService.list(QueryWrapper.create()
|
||||
.eq(Workflow::getTenantId, account.getTenantId())
|
||||
.eq(Workflow::getPublishStatus, PublishStatus.PUBLISHED.getCode())
|
||||
.eq(Workflow::getStatus, 1)
|
||||
.orderBy(Workflow::getModified, false))
|
||||
.stream()
|
||||
.filter(item -> resourceAccessService.canAccess(
|
||||
CategoryResourceType.WORKFLOW, item, ResourceAction.USE))
|
||||
.map(item -> new AgentResourceOptionsView.ResourceOption(
|
||||
item.getId(), item.getTitle(), item.getDescription(), item.getEnglishName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可使用的插件工具。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return 插件工具选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.PluginToolOption> listPluginToolOptions(LoginAccount account) {
|
||||
List<Plugin> plugins = pluginService.list(QueryWrapper.create()
|
||||
.eq(Plugin::getTenantId, account.getTenantId()))
|
||||
.stream()
|
||||
.filter(plugin -> pluginVisibilityService.canAccessPlugin(
|
||||
plugin.getCreatedBy(), plugin.getId()))
|
||||
.toList();
|
||||
if (plugins.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<BigInteger, String> pluginNames = new LinkedHashMap<>();
|
||||
plugins.forEach(plugin -> pluginNames.put(plugin.getId(), plugin.getName()));
|
||||
return pluginItemService.list(QueryWrapper.create()
|
||||
.in(PluginItem::getPluginId, pluginNames.keySet())
|
||||
.eq(PluginItem::getStatus, 1)
|
||||
.orderBy(PluginItem::getId, false))
|
||||
.stream()
|
||||
.map(item -> new AgentResourceOptionsView.PluginToolOption(
|
||||
item.getId(),
|
||||
item.getName(),
|
||||
item.getDescription(),
|
||||
item.getEnglishName(),
|
||||
pluginNames.get(item.getPluginId())
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前租户可使用的 MCP。
|
||||
*
|
||||
* @param account 当前登录账号
|
||||
* @return MCP 选项
|
||||
*/
|
||||
private List<AgentResourceOptionsView.McpOption> listMcpOptions(LoginAccount account) {
|
||||
return mcpService.list(QueryWrapper.create()
|
||||
.eq(Mcp::getTenantId, account.getTenantId())
|
||||
.eq(Mcp::getStatus, true)
|
||||
.orderBy(Mcp::getModified, false))
|
||||
.stream()
|
||||
.map(item -> new AgentResourceOptionsView.McpOption(
|
||||
item.getId(),
|
||||
item.getTitle(),
|
||||
item.getDescription(),
|
||||
item.getApprovalRequired()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Agent 转换为不包含运行配置的安全选项。
|
||||
*
|
||||
* @param agent Agent 数据
|
||||
* @param publishedOnly 是否读取发布快照中的展示信息
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
private AgentOptionView toAgentOption(Agent agent, boolean publishedOnly) {
|
||||
Map<String, Object> snapshot = publishedOnly ? agent.getPublishedSnapshotJson() : Map.of();
|
||||
Map<String, Object> basic = snapshot == null ? Map.of() : map(snapshot.get("basicSummary"));
|
||||
Map<String, Object> model = snapshot == null ? Map.of() : map(snapshot.get("modelSummary"));
|
||||
Map<String, Object> interaction = publishedOnly
|
||||
? map(snapshot == null ? null : snapshot.get("interactionConfigJson"))
|
||||
: agent.getInteractionConfigJson();
|
||||
return new AgentOptionView(
|
||||
agent.getId(),
|
||||
firstText(text(basic.get("name")), agent.getName()),
|
||||
firstText(text(basic.get("description")), agent.getDescription()),
|
||||
firstText(text(basic.get("avatar")), agent.getAvatar()),
|
||||
interaction,
|
||||
publishedOnly ? booleanValue(model.get("supportImage")) : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象安全转换为字符串键 Map。
|
||||
*
|
||||
* @param value 待转换值
|
||||
* @return Map;非 Map 值返回空 Map
|
||||
*/
|
||||
private Map<String, Object> map(Object value) {
|
||||
if (!(value instanceof Map<?, ?> raw)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
raw.forEach((key, item) -> result.put(String.valueOf(key), item));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换可空布尔值。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 布尔值或 null
|
||||
*/
|
||||
private Boolean booleanValue(Object value) {
|
||||
return value == null ? null : Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回首个非空文本。
|
||||
*
|
||||
* @param value 首选文本
|
||||
* @param fallback 备用文本
|
||||
* @return 最终文本
|
||||
*/
|
||||
private String firstText(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为可空文本。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 文本或 null
|
||||
*/
|
||||
private String text(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带租户信息的当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录状态无效时抛出
|
||||
*/
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@ import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.mapper.AgentKnowledgeBindingMapper;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.service.AgentDependencyAccessService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
@@ -22,9 +21,13 @@ import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent 知识库绑定服务实现。
|
||||
@@ -38,9 +41,11 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
@Resource
|
||||
private AgentMapper agentMapper;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private AgentBindingLockExecutor agentBindingLockExecutor;
|
||||
@Resource
|
||||
private AgentDependencyAccessService agentDependencyAccessService;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -48,19 +53,21 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentKnowledgeBinding> replaceBindings(BigInteger agentId, List<AgentKnowledgeBinding> bindings) {
|
||||
Agent agent = requireAgent(agentId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
AgentKnowledgeBinding binding = bindings.get(i);
|
||||
validateBinding(binding);
|
||||
applyBindingDefaults(agent, binding, i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
return agentBindingLockExecutor.execute(agentId, () -> {
|
||||
Agent agent = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
validateBindings(agent, bindings);
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
applyBindingDefaults(agent, bindings.get(i), i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,26 +81,70 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
private Agent requireAgent(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneById(agentId);
|
||||
/**
|
||||
* 锁定并加载待修改的 Agent。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 已锁定 Agent
|
||||
* @throws BusinessException Agent 不存在时抛出
|
||||
*/
|
||||
private Agent requireAgentForUpdate(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq(Agent::getId, agentId)
|
||||
.forUpdate());
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateBinding(AgentKnowledgeBinding binding) {
|
||||
/**
|
||||
* 校验知识库绑定并锁定目标知识库到当前事务结束。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param binding 知识库绑定
|
||||
* @throws BusinessException 绑定参数无效或知识库不可用时抛出
|
||||
*/
|
||||
private void validateBinding(Agent agent, AgentKnowledgeBinding binding) {
|
||||
if (binding == null || binding.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库绑定参数不完整");
|
||||
}
|
||||
DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
|
||||
if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定知识库不存在或未发布");
|
||||
}
|
||||
KnowledgeRetrievalModes.parse(binding.getRetrievalMode());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限绑定该知识库");
|
||||
agentDependencyAccessService.requireKnowledge(agent, binding.getKnowledgeId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按知识库 ID 的稳定顺序校验绑定并锁定关联资源。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param bindings 知识库绑定
|
||||
*/
|
||||
private void validateBindings(Agent agent, List<AgentKnowledgeBinding> bindings) {
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<BigInteger> knowledgeIds = new LinkedHashSet<>();
|
||||
for (AgentKnowledgeBinding binding : bindings) {
|
||||
if (binding != null && binding.getKnowledgeId() != null
|
||||
&& !knowledgeIds.add(binding.getKnowledgeId())) {
|
||||
throw new BusinessException("同一知识库不能重复绑定");
|
||||
}
|
||||
}
|
||||
List<AgentKnowledgeBinding> validationOrder = new ArrayList<>(bindings);
|
||||
validationOrder.sort(Comparator.comparing(binding ->
|
||||
binding == null || binding.getKnowledgeId() == null
|
||||
? BigInteger.ZERO
|
||||
: binding.getKnowledgeId()));
|
||||
validationOrder.forEach(binding -> validateBinding(agent, binding));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入知识库绑定的归属、审计与排序默认值。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param binding 知识库绑定
|
||||
* @param index 绑定顺序
|
||||
*/
|
||||
private void applyBindingDefaults(Agent agent, AgentKnowledgeBinding binding, int index) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
@@ -113,10 +164,23 @@ public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledge
|
||||
binding.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof BusinessException businessException) {
|
||||
throw businessException;
|
||||
}
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.ai.service.AgentResourceBindingProvider;
|
||||
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent 对共享 AI 资源的绑定查询与解绑实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentResourceBindingProviderImpl implements AgentResourceBindingProvider {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentToolBindingService agentToolBindingService;
|
||||
private final AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
private final AgentBindingLockExecutor agentBindingLockExecutor;
|
||||
|
||||
/**
|
||||
* 创建 Agent 资源绑定提供者。
|
||||
*
|
||||
* @param agentService Agent 服务
|
||||
* @param agentToolBindingService Agent 工具绑定服务
|
||||
* @param agentKnowledgeBindingService Agent 知识库绑定服务
|
||||
* @param agentBindingLockExecutor Agent 绑定锁执行器
|
||||
*/
|
||||
public AgentResourceBindingProviderImpl(AgentService agentService,
|
||||
AgentToolBindingService agentToolBindingService,
|
||||
AgentKnowledgeBindingService agentKnowledgeBindingService,
|
||||
AgentBindingLockExecutor agentBindingLockExecutor) {
|
||||
this.agentService = agentService;
|
||||
this.agentToolBindingService = agentToolBindingService;
|
||||
this.agentKnowledgeBindingService = agentKnowledgeBindingService;
|
||||
this.agentBindingLockExecutor = agentBindingLockExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId) {
|
||||
return listAgents(collectToolResourceAgentIds(AgentToolType.WORKFLOW, workflowId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId) {
|
||||
return listAgents(collectKnowledgeAgentIds(knowledgeId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listAgentsByPluginItemId(BigInteger pluginItemId) {
|
||||
return listAgents(collectToolResourceAgentIds(AgentToolType.PLUGIN, pluginItemId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listAgentsByMcpId(BigInteger mcpId) {
|
||||
return listAgents(collectToolResourceAgentIds(AgentToolType.MCP, mcpId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<OfflineImpactBindingVo> listAgentsByModelId(BigInteger modelId) {
|
||||
Set<BigInteger> agentIds = collectAgentIdsFromAgents(agentService.list(QueryWrapper.create()
|
||||
.select(Agent::getId)
|
||||
.eq(Agent::getModelId, modelId)));
|
||||
for (Agent agent : listPublishedSnapshotAgents()) {
|
||||
if (sameId(agent.getPublishedSnapshotJson().get("modelId"), modelId)) {
|
||||
agentIds.add(agent.getId());
|
||||
}
|
||||
}
|
||||
return listAgents(agentIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindWorkflow(BigInteger workflowId) {
|
||||
Set<BigInteger> agentIds = collectToolResourceAgentIds(
|
||||
AgentToolType.WORKFLOW, workflowId, true);
|
||||
for (BigInteger agentId : sortedAgentIds(agentIds)) {
|
||||
agentBindingLockExecutor.execute(agentId, () -> {
|
||||
agentToolBindingService.remove(QueryWrapper.create()
|
||||
.eq(AgentToolBinding::getAgentId, agentId)
|
||||
.eq(AgentToolBinding::getToolType, AgentToolType.WORKFLOW.name())
|
||||
.eq(AgentToolBinding::getTargetId, workflowId));
|
||||
trimPublishedSnapshot(
|
||||
agentId,
|
||||
workflowId,
|
||||
AgentToolType.WORKFLOW.name(),
|
||||
"toolBindings",
|
||||
"toolSummaries"
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbindKnowledge(BigInteger knowledgeId) {
|
||||
Set<BigInteger> agentIds = collectKnowledgeAgentIds(knowledgeId, true);
|
||||
for (BigInteger agentId : sortedAgentIds(agentIds)) {
|
||||
agentBindingLockExecutor.execute(agentId, () -> {
|
||||
agentKnowledgeBindingService.remove(QueryWrapper.create()
|
||||
.eq(AgentKnowledgeBinding::getAgentId, agentId)
|
||||
.eq(AgentKnowledgeBinding::getKnowledgeId, knowledgeId));
|
||||
trimPublishedSnapshot(
|
||||
agentId,
|
||||
knowledgeId,
|
||||
null,
|
||||
"knowledgeBindings",
|
||||
"knowledgeSummaries"
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 摘要并保持绑定中首次出现的顺序。
|
||||
*
|
||||
* @param agentIds Agent ID 集合
|
||||
* @return Agent 摘要列表
|
||||
*/
|
||||
private List<OfflineImpactBindingVo> listAgents(Set<BigInteger> agentIds) {
|
||||
if (agentIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<BigInteger, Agent> agentMap = new LinkedHashMap<>();
|
||||
for (Agent agent : agentService.listByIds(agentIds)) {
|
||||
agentMap.put(agent.getId(), agent);
|
||||
}
|
||||
List<OfflineImpactBindingVo> result = new ArrayList<>(agentIds.size());
|
||||
for (BigInteger agentId : agentIds) {
|
||||
Agent agent = agentMap.get(agentId);
|
||||
OfflineImpactBindingVo item = new OfflineImpactBindingVo();
|
||||
item.setId(agentId);
|
||||
item.setTitle(agent == null ? "已删除智能体(悬空绑定)" : agent.getName());
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集草稿绑定和已发布快照中引用指定工具资源的 Agent ID。
|
||||
*
|
||||
* @param toolType 工具类型
|
||||
* @param resourceId 资源 ID
|
||||
* @param lockBindings 是否锁定实时绑定行
|
||||
* @return Agent ID 集合
|
||||
*/
|
||||
private Set<BigInteger> collectToolResourceAgentIds(AgentToolType toolType,
|
||||
BigInteger resourceId,
|
||||
boolean lockBindings) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(AgentToolBinding::getToolType, toolType.name())
|
||||
.eq(AgentToolBinding::getTargetId, resourceId);
|
||||
if (lockBindings) {
|
||||
wrapper.forUpdate();
|
||||
}
|
||||
Set<BigInteger> agentIds = collectAgentIdsFromToolBindings(
|
||||
agentToolBindingService.list(wrapper));
|
||||
for (Agent agent : listPublishedSnapshotAgents()) {
|
||||
if (snapshotContainsToolResource(agent.getPublishedSnapshotJson(), toolType, resourceId)) {
|
||||
agentIds.add(agent.getId());
|
||||
}
|
||||
}
|
||||
return agentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集草稿绑定和已发布快照中引用指定知识库的 Agent ID。
|
||||
*
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @param lockBindings 是否锁定实时绑定行
|
||||
* @return Agent ID 集合
|
||||
*/
|
||||
private Set<BigInteger> collectKnowledgeAgentIds(BigInteger knowledgeId, boolean lockBindings) {
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(AgentKnowledgeBinding::getKnowledgeId, knowledgeId);
|
||||
if (lockBindings) {
|
||||
wrapper.forUpdate();
|
||||
}
|
||||
Set<BigInteger> agentIds = collectAgentIdsFromKnowledgeBindings(
|
||||
agentKnowledgeBindingService.list(wrapper));
|
||||
for (Agent agent : listPublishedSnapshotAgents()) {
|
||||
if (snapshotContainsKnowledge(agent.getPublishedSnapshotJson(), knowledgeId)) {
|
||||
agentIds.add(agent.getId());
|
||||
}
|
||||
}
|
||||
return agentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询带发布快照的 Agent 最小字段。
|
||||
*
|
||||
* @return Agent 发布快照记录
|
||||
*/
|
||||
private List<Agent> listPublishedSnapshotAgents() {
|
||||
return agentService.list(QueryWrapper.create()
|
||||
.select(
|
||||
Agent::getId,
|
||||
Agent::getName,
|
||||
Agent::getModelId,
|
||||
Agent::getPublishedSnapshotJson
|
||||
)
|
||||
.isNotNull(Agent::getPublishedSnapshotJson));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断发布快照是否引用指定工具资源。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @param toolType 工具类型
|
||||
* @param resourceId 资源 ID
|
||||
* @return 是否引用
|
||||
*/
|
||||
private boolean snapshotContainsToolResource(Map<String, Object> snapshot,
|
||||
AgentToolType toolType,
|
||||
BigInteger resourceId) {
|
||||
return snapshotListContains(snapshot, "toolBindings", resourceId, toolType.name())
|
||||
|| snapshotListContains(snapshot, "toolSummaries", resourceId, toolType.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断发布快照是否引用指定知识库。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @param knowledgeId 知识库 ID
|
||||
* @return 是否引用
|
||||
*/
|
||||
private boolean snapshotContainsKnowledge(Map<String, Object> snapshot, BigInteger knowledgeId) {
|
||||
return snapshotListContains(snapshot, "knowledgeBindings", knowledgeId, null)
|
||||
|| snapshotListContains(snapshot, "knowledgeSummaries", knowledgeId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断快照列表中是否存在指定资源。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @param key 列表字段
|
||||
* @param resourceId 资源 ID
|
||||
* @param toolType 工具类型;知识库为空
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean snapshotListContains(Map<String, Object> snapshot,
|
||||
String key,
|
||||
BigInteger resourceId,
|
||||
String toolType) {
|
||||
if (snapshot == null || snapshot.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Object value = snapshot.get(key);
|
||||
if (!(value instanceof List<?> items)) {
|
||||
return false;
|
||||
}
|
||||
return items.stream().anyMatch(item -> matchesResourceBinding(item, resourceId, toolType));
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集工具绑定中的 Agent ID。
|
||||
*
|
||||
* @param bindings 工具绑定
|
||||
* @return 去重后的 Agent ID
|
||||
*/
|
||||
private Set<BigInteger> collectAgentIdsFromToolBindings(List<AgentToolBinding> bindings) {
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
if (bindings == null) {
|
||||
return result;
|
||||
}
|
||||
for (AgentToolBinding binding : bindings) {
|
||||
if (binding != null && binding.getAgentId() != null) {
|
||||
result.add(binding.getAgentId());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集知识库绑定中的 Agent ID。
|
||||
*
|
||||
* @param bindings 知识库绑定
|
||||
* @return 去重后的 Agent ID
|
||||
*/
|
||||
private Set<BigInteger> collectAgentIdsFromKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
if (bindings == null) {
|
||||
return result;
|
||||
}
|
||||
for (AgentKnowledgeBinding binding : bindings) {
|
||||
if (binding != null && binding.getAgentId() != null) {
|
||||
result.add(binding.getAgentId());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集 Agent 实体中的 ID。
|
||||
*
|
||||
* @param agents Agent 列表
|
||||
* @return Agent ID 集合
|
||||
*/
|
||||
private Set<BigInteger> collectAgentIdsFromAgents(List<Agent> agents) {
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
if (agents == null) {
|
||||
return result;
|
||||
}
|
||||
for (Agent agent : agents) {
|
||||
if (agent != null && agent.getId() != null) {
|
||||
result.add(agent.getId());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 Agent ID 升序返回锁定顺序,避免并发批量解绑以相反顺序持锁。
|
||||
*
|
||||
* @param agentIds Agent ID 集合
|
||||
* @return 稳定排序后的 Agent ID
|
||||
*/
|
||||
private List<BigInteger> sortedAgentIds(Set<BigInteger> agentIds) {
|
||||
if (agentIds == null || agentIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return agentIds.stream()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Agent 发布快照中移除指定资源绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param resourceId 资源 ID
|
||||
* @param toolType 工具类型;知识库绑定时为空
|
||||
* @param bindingsKeys 快照绑定字段
|
||||
*/
|
||||
private void trimPublishedSnapshot(BigInteger agentId,
|
||||
BigInteger resourceId,
|
||||
String toolType,
|
||||
String... bindingsKeys) {
|
||||
Agent agent = agentService.getOne(QueryWrapper.create()
|
||||
.eq(Agent::getId, agentId)
|
||||
.forUpdate());
|
||||
if (agent == null || agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>(agent.getPublishedSnapshotJson());
|
||||
boolean changed = false;
|
||||
for (String bindingsKey : bindingsKeys) {
|
||||
Object rawBindings = snapshot.get(bindingsKey);
|
||||
if (!(rawBindings instanceof List<?> bindings)) {
|
||||
continue;
|
||||
}
|
||||
List<Object> filtered = new ArrayList<>(bindings.size());
|
||||
for (Object item : bindings) {
|
||||
if (matchesResourceBinding(item, resourceId, toolType)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
filtered.add(item);
|
||||
}
|
||||
snapshot.put(bindingsKey, filtered);
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
// 仅更新发布快照,避免并发草稿编辑被旧实体中的其他字段覆盖。
|
||||
agentService.updateChain()
|
||||
.set(Agent::getPublishedSnapshotJson, snapshot)
|
||||
.eq(Agent::getId, agentId)
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断快照项是否指向指定资源。
|
||||
*
|
||||
* @param item 快照项
|
||||
* @param resourceId 资源 ID
|
||||
* @param toolType 工具类型;知识库绑定时为空
|
||||
* @return 是否匹配
|
||||
*/
|
||||
private boolean matchesResourceBinding(Object item, BigInteger resourceId, String toolType) {
|
||||
if (!(item instanceof Map<?, ?> binding)) {
|
||||
return false;
|
||||
}
|
||||
Object currentId = toolType == null ? binding.get("knowledgeId") : binding.get("targetId");
|
||||
if (!Objects.equals(String.valueOf(currentId), String.valueOf(resourceId))) {
|
||||
return false;
|
||||
}
|
||||
return toolType == null || toolType.equalsIgnoreCase(String.valueOf(binding.get("toolType")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较快照中的资源 ID 与数据库 ID。
|
||||
*
|
||||
* @param snapshotId 快照 ID
|
||||
* @param resourceId 数据库 ID
|
||||
* @return 是否相同
|
||||
*/
|
||||
private boolean sameId(Object snapshotId, BigInteger resourceId) {
|
||||
return snapshotId != null
|
||||
&& resourceId != null
|
||||
&& Objects.equals(String.valueOf(snapshotId), resourceId.toString());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -10,9 +11,12 @@ import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.runtime.AgentRuntimeCompiler;
|
||||
import tech.easyflow.agent.service.AgentDependencyAccessService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.*;
|
||||
@@ -58,6 +62,12 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
@Resource
|
||||
private AgentDependencyAccessService agentDependencyAccessService;
|
||||
@Resource
|
||||
private AgentBindingLockExecutor agentBindingLockExecutor;
|
||||
@Resource
|
||||
private AgentRuntimeCompiler agentRuntimeCompiler;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -77,8 +87,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Agent saveDraft(Agent agent) {
|
||||
validateDraft(agent);
|
||||
applyDraftDefaults(agent);
|
||||
validateDraft(agent);
|
||||
save(agent);
|
||||
return getDetail(agent.getId());
|
||||
}
|
||||
@@ -92,12 +102,16 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
if (agent == null || agent.getId() == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
Agent existing = requireAgent(agent.getId());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
validateDraft(agent);
|
||||
applyDraftUpdate(existing, agent);
|
||||
updateById(existing);
|
||||
return getDetail(existing.getId());
|
||||
return agentBindingLockExecutor.execute(agent.getId(), () -> {
|
||||
Agent existing = requireAgentForUpdate(agent.getId());
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
agent.setTenantId(existing.getTenantId());
|
||||
validateDraft(agent);
|
||||
applyDraftUpdate(existing, agent);
|
||||
updateById(existing);
|
||||
return getDetail(existing.getId());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,8 +131,27 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> buildPublishSnapshot(Agent agent) {
|
||||
Agent detail = getDetail(agent.getId());
|
||||
if (agent == null || agent.getId() == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
return agentBindingLockExecutor.execute(agent.getId(), () -> buildPublishSnapshotLocked(agent.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Agent 锁和数据库行锁内构建并校验发布快照。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 发布快照
|
||||
*/
|
||||
private Map<String, Object> buildPublishSnapshotLocked(BigInteger agentId) {
|
||||
Agent detail = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, detail, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
detail.setToolBindings(agentToolBindingService.listEnabled(agentId));
|
||||
detail.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(agentId));
|
||||
validateDraft(detail);
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", detail.getId());
|
||||
snapshot.put("tenantId", detail.getTenantId());
|
||||
@@ -136,8 +169,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
snapshot.put("executionConfigJson", detail.getExecutionConfigJson());
|
||||
snapshot.put("interactionConfigJson", detail.getInteractionConfigJson());
|
||||
snapshot.put("visibilityScope", detail.getVisibilityScope());
|
||||
snapshot.put("toolBindings", snapshotToolBindings(detail.getToolBindings()));
|
||||
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail.getKnowledgeBindings()));
|
||||
snapshot.put("toolBindings", snapshotToolBindings(detail, detail.getToolBindings()));
|
||||
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail, detail.getKnowledgeBindings()));
|
||||
snapshot.put("basicSummary", basicSummary(detail));
|
||||
snapshot.put("modelSummary", modelSummary(detail.getModelId()));
|
||||
snapshot.put("parameterSummary", parameterSummary(detail));
|
||||
@@ -145,6 +178,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
snapshot.put("toolSummaries", toolSummaries(detail.getToolBindings()));
|
||||
snapshot.put("knowledgeSummaries", knowledgeSummaries(detail.getKnowledgeBindings()));
|
||||
snapshot.put("snapshotAt", new Date());
|
||||
// 发布前完整编译一次,提前暴露工具运行名冲突和运行定义错误。
|
||||
agentRuntimeCompiler.compile(fromSnapshot(snapshot));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@@ -179,6 +214,22 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询并锁定 Agent 数据行。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return Agent
|
||||
*/
|
||||
private Agent requireAgentForUpdate(BigInteger id) {
|
||||
Agent agent = getOne(QueryWrapper.create()
|
||||
.eq(Agent::getId, id)
|
||||
.forUpdate());
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateDraft(Agent agent) {
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不能为空");
|
||||
@@ -186,13 +237,8 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
if (agent.getName() == null || agent.getName().isBlank()) {
|
||||
throw new BusinessException("Agent 名称不能为空");
|
||||
}
|
||||
if (agent.getModelId() == null) {
|
||||
throw new BusinessException("Agent 模型不能为空");
|
||||
}
|
||||
Model model = modelService.getModelInstance(agent.getModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("Agent 模型不存在");
|
||||
}
|
||||
agentDependencyAccessService.requireModel(agent, agent.getModelId());
|
||||
agentDependencyAccessService.validateCategory(agent);
|
||||
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
|
||||
agent.setInteractionConfigJson(AgentInteractionConfigSupport.normalize(agent.getInteractionConfigJson()));
|
||||
agent.setExecutionConfigJson(normalizeExecutionConfig(agent.getExecutionConfigJson()));
|
||||
@@ -334,57 +380,47 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<AgentToolBinding> snapshotToolBindings(List<AgentToolBinding> bindings) {
|
||||
private List<AgentToolBinding> snapshotToolBindings(Agent agent, List<AgentToolBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(binding -> {
|
||||
AgentToolBinding snapshot = objectMapper.convertValue(binding, AgentToolBinding.class);
|
||||
snapshot.setResourceSnapshot(toolResourceSnapshot(agent, binding));
|
||||
snapshot.setResourceSummary(toolSummary(binding));
|
||||
snapshot.setResourceSnapshot(toolResourceSnapshot(binding));
|
||||
return snapshot;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> toolResourceSnapshot(AgentToolBinding binding) {
|
||||
private Map<String, Object> toolResourceSnapshot(Agent agent, AgentToolBinding binding) {
|
||||
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
|
||||
Workflow workflow = workflowService.getPublishedById(binding.getTargetId());
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
|
||||
throw new BusinessException("绑定工作流不存在或未发布");
|
||||
}
|
||||
Workflow workflow = agentDependencyAccessService.requireWorkflow(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(workflow, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
|
||||
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
|
||||
if (pluginItem == null) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
}
|
||||
PluginItem pluginItem = agentDependencyAccessService.requirePluginItem(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(pluginItem, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
Mcp mcp = mcpService.getById(binding.getTargetId());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("绑定 MCP 不存在");
|
||||
}
|
||||
Mcp mcp = agentDependencyAccessService.requireMcp(agent, binding.getTargetId());
|
||||
return objectMapper.convertValue(mcp, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
|
||||
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
|
||||
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(
|
||||
Agent agent, List<AgentKnowledgeBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(binding -> {
|
||||
AgentKnowledgeBinding snapshot = objectMapper.convertValue(binding, AgentKnowledgeBinding.class);
|
||||
snapshot.setResourceSnapshot(knowledgeResourceSnapshot(agent, binding));
|
||||
snapshot.setResourceSummary(knowledgeSummary(binding));
|
||||
snapshot.setResourceSnapshot(knowledgeResourceSnapshot(binding));
|
||||
return snapshot;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> knowledgeResourceSnapshot(AgentKnowledgeBinding binding) {
|
||||
DocumentCollection knowledge = documentCollectionService.getPublishedById(binding.getKnowledgeId());
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
|
||||
throw new BusinessException("绑定知识库不存在或未发布");
|
||||
}
|
||||
private Map<String, Object> knowledgeResourceSnapshot(Agent agent, AgentKnowledgeBinding binding) {
|
||||
DocumentCollection knowledge =
|
||||
agentDependencyAccessService.requireKnowledge(agent, binding.getKnowledgeId());
|
||||
return objectMapper.convertValue(knowledge, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
|
||||
@@ -408,10 +444,23 @@ public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof BusinessException businessException) {
|
||||
throw businessException;
|
||||
}
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,9 @@ import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.mapper.AgentToolBindingMapper;
|
||||
import tech.easyflow.agent.service.AgentDependencyAccessService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.agent.support.AgentBindingLockExecutor;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
@@ -26,9 +21,13 @@ import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定服务实现。
|
||||
@@ -40,13 +39,11 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
@Resource
|
||||
private AgentMapper agentMapper;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private McpService mcpService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private AgentBindingLockExecutor agentBindingLockExecutor;
|
||||
@Resource
|
||||
private AgentDependencyAccessService agentDependencyAccessService;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -54,19 +51,21 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentToolBinding> replaceBindings(BigInteger agentId, List<AgentToolBinding> bindings) {
|
||||
Agent agent = requireAgent(agentId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
AgentToolBinding binding = bindings.get(i);
|
||||
validateBinding(binding);
|
||||
applyBindingDefaults(agent, binding, i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
return agentBindingLockExecutor.execute(agentId, () -> {
|
||||
Agent agent = requireAgentForUpdate(agentId);
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
validateBindings(agent, bindings);
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
applyBindingDefaults(agent, bindings.get(i), i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,40 +79,89 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
private Agent requireAgent(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneById(agentId);
|
||||
/**
|
||||
* 锁定并加载待修改的 Agent。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 已锁定 Agent
|
||||
* @throws BusinessException Agent 不存在时抛出
|
||||
*/
|
||||
private Agent requireAgentForUpdate(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq(Agent::getId, agentId)
|
||||
.forUpdate());
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateBinding(AgentToolBinding binding) {
|
||||
/**
|
||||
* 校验单个工具绑定,并锁定目标资源到当前事务结束。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param binding 工具绑定
|
||||
* @throws BusinessException 绑定参数无效或目标资源不可用时抛出
|
||||
*/
|
||||
private void validateBinding(Agent agent, AgentToolBinding binding) {
|
||||
if (binding == null || binding.getTargetId() == null || binding.getToolType() == null) {
|
||||
throw new BusinessException("工具绑定参数不完整");
|
||||
}
|
||||
AgentToolType type = AgentToolType.from(binding.getToolType());
|
||||
if (type == AgentToolType.WORKFLOW) {
|
||||
Workflow workflow = workflowService.getById(binding.getTargetId());
|
||||
if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定工作流不存在或未发布");
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限绑定该工作流");
|
||||
agentDependencyAccessService.requireWorkflow(agent, binding.getTargetId());
|
||||
return;
|
||||
}
|
||||
if (type == AgentToolType.PLUGIN) {
|
||||
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
|
||||
if (pluginItem == null || pluginItem.getStatus() == null || pluginItem.getStatus() != 1) {
|
||||
throw new BusinessException("绑定插件不存在或未启用");
|
||||
}
|
||||
agentDependencyAccessService.requirePluginItem(agent, binding.getTargetId());
|
||||
return;
|
||||
}
|
||||
Mcp mcp = mcpService.getById(binding.getTargetId());
|
||||
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
|
||||
throw new BusinessException("绑定 MCP 不存在或未启用");
|
||||
}
|
||||
agentDependencyAccessService.requireMcp(agent, binding.getTargetId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按稳定顺序校验绑定并锁定关联资源,降低并发替换产生数据库死锁的概率。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param bindings 工具绑定
|
||||
*/
|
||||
private void validateBindings(Agent agent, List<AgentToolBinding> bindings) {
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> resourceKeys = new LinkedHashSet<>();
|
||||
Set<String> declaredToolNames = new LinkedHashSet<>();
|
||||
for (AgentToolBinding binding : bindings) {
|
||||
if (binding == null || binding.getToolType() == null || binding.getTargetId() == null) {
|
||||
continue;
|
||||
}
|
||||
String resourceKey = AgentToolType.from(binding.getToolType()).name() + ":" + binding.getTargetId();
|
||||
if (!resourceKeys.add(resourceKey)) {
|
||||
throw new BusinessException("同一工具资源不能重复绑定");
|
||||
}
|
||||
String toolName = binding.getToolName();
|
||||
if (toolName != null && !toolName.isBlank() && !declaredToolNames.add(toolName.trim())) {
|
||||
throw new BusinessException("Agent 工具运行名冲突:" + toolName.trim() + ",请调整工具名称");
|
||||
}
|
||||
}
|
||||
List<AgentToolBinding> validationOrder = new ArrayList<>(bindings);
|
||||
validationOrder.sort(Comparator
|
||||
.comparing((AgentToolBinding binding) ->
|
||||
binding == null || binding.getToolType() == null ? "" : binding.getToolType())
|
||||
.thenComparing(binding ->
|
||||
binding == null || binding.getTargetId() == null
|
||||
? BigInteger.ZERO
|
||||
: binding.getTargetId()));
|
||||
validationOrder.forEach(binding -> validateBinding(agent, binding));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入工具绑定的归属、审计与排序默认值。
|
||||
*
|
||||
* @param agent 当前 Agent
|
||||
* @param binding 工具绑定
|
||||
* @param index 绑定顺序
|
||||
*/
|
||||
private void applyBindingDefaults(Agent agent, AgentToolBinding binding, int index) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
@@ -129,10 +177,23 @@ public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMap
|
||||
binding.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof BusinessException businessException) {
|
||||
throw businessException;
|
||||
}
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user