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

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

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

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

View File

@@ -1,14 +1,11 @@
package tech.easyflow.ai.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.DocumentCollectionCategory;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ModelService;
@@ -37,7 +34,6 @@ public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycle
private final DocumentCollectionService documentCollectionService;
private final ResourceAccessService resourceAccessService;
private final BotDocumentCollectionService botDocumentCollectionService;
private final ModelService modelService;
private final DocumentCollectionCategoryService documentCollectionCategoryService;
private final SysDeptService sysDeptService;
@@ -46,7 +42,6 @@ public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycle
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
ResourceAccessService resourceAccessService,
ApprovalInstanceService approvalInstanceService,
BotDocumentCollectionService botDocumentCollectionService,
ModelService modelService,
DocumentCollectionCategoryService documentCollectionCategoryService,
SysDeptService sysDeptService,
@@ -55,7 +50,6 @@ public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycle
super(approvalInstanceService, objectMapper);
this.documentCollectionService = documentCollectionService;
this.resourceAccessService = resourceAccessService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.modelService = modelService;
this.documentCollectionCategoryService = documentCollectionCategoryService;
this.sysDeptService = sysDeptService;
@@ -200,26 +194,22 @@ public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycle
if (!impact.isCanProceed()) {
throw new BusinessException(buildWorkflowUsageBlockMessage(impact));
}
if (impact.isHasBotBindings()) {
snapshot.put("botBindings", impact.getBotBindings());
if (impact.isHasAgentBindings()) {
snapshot.put("agentBindings", impact.getAgentBindings());
}
}
@Override
protected void validateDelete(DocumentCollection resource, PublishStatus currentStatus) {
if (hasBotBinding(resource.getId())) {
throw new BusinessException("此知识库还关联着bot请先取消关联");
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkKnowledgeImpact(resource.getId());
if (impact.isHasAgentBindings()) {
throw new BusinessException("此知识库仍被智能体使用,请先取消绑定后再删除");
}
}
@Override
protected void afterOffline(BigInteger resourceId) {
resourceOfflineImpactService.unbindKnowledgeFromBots(resourceId);
}
private boolean hasBotBinding(BigInteger knowledgeId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
return botDocumentCollectionService.exists(queryWrapper);
resourceOfflineImpactService.unbindKnowledgeFromAgents(resourceId);
}
private String buildWorkflowUsageBlockMessage(OfflineImpactCheckVo impact) {

View File

@@ -1,14 +1,11 @@
package tech.easyflow.ai.publish;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.plugin.workflow.binding.WorkflowPluginBindingService;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.ResourceOfflineImpactService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
@@ -31,7 +28,6 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
private final WorkflowService workflowService;
private final ResourceAccessService resourceAccessService;
private final BotWorkflowService botWorkflowService;
private final ResourceOfflineImpactService resourceOfflineImpactService;
private final WorkflowPluginBindingService workflowPluginBindingService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
@@ -39,7 +35,6 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
ResourceAccessService resourceAccessService,
ApprovalInstanceService approvalInstanceService,
BotWorkflowService botWorkflowService,
ResourceOfflineImpactService resourceOfflineImpactService,
WorkflowPluginBindingService workflowPluginBindingService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
@@ -47,7 +42,6 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
super(approvalInstanceService, objectMapper);
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.botWorkflowService = botWorkflowService;
this.resourceOfflineImpactService = resourceOfflineImpactService;
this.workflowPluginBindingService = workflowPluginBindingService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
@@ -178,8 +172,8 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
@Override
protected void enrichOfflineSnapshot(Workflow resource, Map<String, Object> snapshot) {
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
if (impact.isHasBotBindings()) {
snapshot.put("botBindings", impact.getBotBindings());
if (impact.isHasAgentBindings()) {
snapshot.put("agentBindings", impact.getAgentBindings());
}
if (impact.isHasPluginBindings()) {
snapshot.put("pluginBindings", impact.getPluginBindings());
@@ -188,18 +182,14 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
@Override
protected void validateDelete(Workflow resource, PublishStatus currentStatus) {
if (hasBotBinding(resource.getId())) {
throw new BusinessException("此工作流还关联有bot请先取消关联后再删除");
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
if (impact.isHasAgentBindings()) {
throw new BusinessException("此工作流仍被智能体使用,请先取消绑定后再删除");
}
}
@Override
protected void afterOffline(BigInteger resourceId) {
resourceOfflineImpactService.unbindWorkflowFromBots(resourceId);
}
private boolean hasBotBinding(BigInteger workflowId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
return botWorkflowService.exists(queryWrapper);
resourceOfflineImpactService.unbindWorkflowFromAgents(resourceId);
}
}

View File

@@ -0,0 +1,68 @@
package tech.easyflow.ai.service;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import java.math.BigInteger;
import java.util.List;
/**
* Agent 对共享 AI 资源的绑定查询与解绑契约。
*
* <p>契约定义在 AI 模块中,由 Agent 模块实现,避免共享资源生命周期反向依赖 Agent 实体。</p>
*/
public interface AgentResourceBindingProvider {
/**
* 查询绑定指定工作流的 Agent。
*
* @param workflowId 工作流 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId);
/**
* 查询绑定指定知识库的 Agent。
*
* @param knowledgeId 知识库 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId);
/**
* 查询绑定指定插件工具的 Agent。
*
* @param pluginItemId 插件工具 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByPluginItemId(BigInteger pluginItemId);
/**
* 查询绑定指定 MCP 的 Agent。
*
* @param mcpId MCP ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByMcpId(BigInteger mcpId);
/**
* 查询使用指定模型的 Agent。
*
* @param modelId 模型 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByModelId(BigInteger modelId);
/**
* 解绑指定工作流及 Agent 发布快照中的对应绑定。
*
* @param workflowId 工作流 ID
*/
void unbindWorkflow(BigInteger workflowId);
/**
* 解绑指定知识库及 Agent 发布快照中的对应绑定。
*
* @param knowledgeId 知识库 ID
*/
void unbindKnowledge(BigInteger knowledgeId);
}

View File

@@ -0,0 +1,64 @@
package tech.easyflow.ai.service;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
/**
* Agent 对共享 AI 资源的统一引用查询服务。
*/
public interface AgentResourceReferenceService {
/**
* 查询引用指定工作流的 Agent。
*
* @param workflowId 工作流 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId);
/**
* 查询引用指定知识库的 Agent。
*
* @param knowledgeId 知识库 ID
* @return Agent 摘要列表
*/
List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId);
/**
* 校验插件工具没有被 Agent 草稿或发布快照引用。
*
* @param pluginItemIds 插件工具 ID 集合
*/
void assertPluginItemsUnused(Collection<BigInteger> pluginItemIds);
/**
* 校验 MCP 没有被 Agent 草稿或发布快照引用。
*
* @param mcpId MCP ID
*/
void assertMcpUnused(BigInteger mcpId);
/**
* 校验模型没有被 Agent 草稿或发布快照引用。
*
* @param modelIds 模型 ID 集合
*/
void assertModelsUnused(Collection<BigInteger> modelIds);
/**
* 从 Agent 草稿绑定和发布快照中解绑工作流。
*
* @param workflowId 工作流 ID
*/
void unbindWorkflow(BigInteger workflowId);
/**
* 从 Agent 草稿绑定和发布快照中解绑知识库。
*
* @param knowledgeId 知识库 ID
*/
void unbindKnowledge(BigInteger knowledgeId);
}

View File

@@ -1,6 +1,5 @@
package tech.easyflow.ai.service;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Workflow;
@@ -42,17 +41,4 @@ public interface AiResourceApprovalStateService {
*/
void fillKnowledgeApprovalState(Collection<DocumentCollection> collections);
/**
* 填充聊天助手审批展示状态。
*
* @param bot 聊天助手
*/
void fillBotApprovalState(Bot bot);
/**
* 批量填充聊天助手审批展示状态。
*
* @param bots 聊天助手集合
*/
void fillBotApprovalState(Collection<Bot> bots);
}

View File

@@ -0,0 +1,25 @@
package tech.easyflow.ai.service;
import tech.easyflow.ai.entity.Bot;
import java.util.Collection;
/**
* 旧 Bot 审批展示状态派生服务。
*/
public interface BotApprovalStateService {
/**
* 填充 Bot 审批展示状态。
*
* @param bot Bot
*/
void fillApprovalState(Bot bot);
/**
* 批量填充 Bot 审批展示状态。
*
* @param bots Bot 集合
*/
void fillApprovalState(Collection<Bot> bots);
}

View File

@@ -3,6 +3,7 @@ package tech.easyflow.ai.service;
import com.mybatisflex.core.service.IService;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import java.math.BigInteger;
import java.util.List;
@@ -22,4 +23,21 @@ public interface BotPluginService extends IService<BotPlugin> {
List<BigInteger> getBotPluginToolIds(String botId);
void saveBotAndPluginTool(BigInteger botId, BigInteger[] pluginToolIds);
/**
* 查询插件工具,并标记指定 Bot 已绑定的工具。
*
* @param pluginId 插件 ID
* @param botId Bot ID
* @return 插件工具列表
*/
List<PluginItem> searchPluginTools(BigInteger pluginId, BigInteger botId);
/**
* 查询指定 Bot 已绑定的插件工具。
*
* @param botId Bot ID
* @return 已绑定插件工具列表
*/
List<PluginItem> getPluginTools(BigInteger botId);
}

View File

@@ -4,11 +4,11 @@ import com.easyagents.core.model.chat.tool.Tool;
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.service.IService;
import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.common.domain.Result;
import java.io.Serializable;
import java.math.BigInteger;
/**
* 服务层。
@@ -24,7 +24,14 @@ public interface McpService extends IService<Mcp> {
void removeMcp(Serializable id);
Tool toFunction(BotMcp botMcp);
/**
* 将指定 MCP 工具转换为运行时工具。
*
* @param mcpId MCP ID
* @param mcpToolName MCP 工具名称
* @return 运行时工具,不存在时返回 {@code null}
*/
Tool toFunction(BigInteger mcpId, String mcpToolName);
Result<Page<Mcp>> pageMcp(Result<Page<Mcp>> page);

View File

@@ -21,10 +21,6 @@ public interface PluginItemService extends IService<PluginItem> {
boolean updatePlugin(PluginItem pluginItem);
List<PluginItem> searchPluginToolByPluginId(BigInteger pluginId, BigInteger botId);
List<PluginItem> getPluginToolList(BigInteger botId);
Result pluginToolTest(String inputData, BigInteger pluginToolId);
List<PluginItem> getByPluginId(String id);

View File

@@ -26,16 +26,16 @@ public interface ResourceOfflineImpactService {
OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId);
/**
* 工作流下线后,静默解绑所有关联 Bot。
* 工作流下线后,静默解绑所有关联 Agent。
*
* @param workflowId 工作流 ID
*/
void unbindWorkflowFromBots(BigInteger workflowId);
void unbindWorkflowFromAgents(BigInteger workflowId);
/**
* 知识库下线后,静默解绑所有关联 Bot。
* 知识库下线后,静默解绑所有关联 Agent。
*
* @param knowledgeId 知识库 ID
*/
void unbindKnowledgeFromBots(BigInteger knowledgeId);
void unbindKnowledgeFromAgents(BigInteger knowledgeId);
}

View File

@@ -0,0 +1,158 @@
package tech.easyflow.ai.service.impl;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.service.AgentResourceBindingProvider;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* Agent 资源引用查询服务实现。
*/
@Service
public class AgentResourceReferenceServiceImpl implements AgentResourceReferenceService {
private final List<AgentResourceBindingProvider> providers;
/**
* 创建 Agent 资源引用查询服务。
*
* @param providers Agent 资源绑定提供者
*/
public AgentResourceReferenceServiceImpl(List<AgentResourceBindingProvider> providers) {
this.providers = providers == null ? List.of() : List.copyOf(providers);
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId) {
return merge(provider -> provider.listAgentsByWorkflowId(workflowId));
}
/**
* {@inheritDoc}
*/
@Override
public List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId) {
return merge(provider -> provider.listAgentsByKnowledgeId(knowledgeId));
}
/**
* {@inheritDoc}
*/
@Override
public void assertPluginItemsUnused(Collection<BigInteger> pluginItemIds) {
if (pluginItemIds == null || pluginItemIds.isEmpty()) {
return;
}
for (BigInteger pluginItemId : pluginItemIds) {
assertUnused(
merge(provider -> provider.listAgentsByPluginItemId(pluginItemId)),
"插件工具"
);
}
}
/**
* {@inheritDoc}
*/
@Override
public void assertMcpUnused(BigInteger mcpId) {
assertUnused(merge(provider -> provider.listAgentsByMcpId(mcpId)), "MCP");
}
/**
* {@inheritDoc}
*/
@Override
public void assertModelsUnused(Collection<BigInteger> modelIds) {
if (modelIds == null || modelIds.isEmpty()) {
return;
}
for (BigInteger modelId : modelIds) {
assertUnused(merge(provider -> provider.listAgentsByModelId(modelId)), "模型");
}
}
/**
* {@inheritDoc}
*/
@Override
public void unbindWorkflow(BigInteger workflowId) {
for (AgentResourceBindingProvider provider : requireProviders()) {
provider.unbindWorkflow(workflowId);
}
}
/**
* {@inheritDoc}
*/
@Override
public void unbindKnowledge(BigInteger knowledgeId) {
for (AgentResourceBindingProvider provider : requireProviders()) {
provider.unbindKnowledge(knowledgeId);
}
}
/**
* 汇总所有提供者返回的 Agent 摘要。
*
* @param loader 单个提供者查询函数
* @return 按 Agent ID 去重后的摘要
*/
private List<OfflineImpactBindingVo> merge(
Function<AgentResourceBindingProvider, List<OfflineImpactBindingVo>> loader) {
Map<BigInteger, OfflineImpactBindingVo> merged = new LinkedHashMap<>();
for (AgentResourceBindingProvider provider : requireProviders()) {
List<OfflineImpactBindingVo> bindings = loader.apply(provider);
if (bindings == null) {
continue;
}
for (OfflineImpactBindingVo binding : bindings) {
if (binding != null && binding.getId() != null) {
merged.putIfAbsent(binding.getId(), binding);
}
}
}
return new ArrayList<>(merged.values());
}
/**
* 校验资源未被任何 Agent 引用。
*
* @param bindings Agent 引用摘要
* @param resourceLabel 资源名称
*/
private void assertUnused(List<OfflineImpactBindingVo> bindings, String resourceLabel) {
if (bindings == null || bindings.isEmpty()) {
return;
}
String agentTitle = bindings.get(0).getTitle();
throw new BusinessException(
resourceLabel + "仍被智能体“" + (agentTitle == null ? "未命名智能体" : agentTitle)
+ "”使用,请先取消绑定或重新发布智能体后再删除"
);
}
/**
* 获取已注册提供者;缺失时阻止破坏性资源操作。
*
* @return Agent 资源绑定提供者
*/
private List<AgentResourceBindingProvider> requireProviders() {
if (providers.isEmpty()) {
throw new BusinessException("Agent 资源引用检查服务不可用,请稍后重试");
}
return providers;
}
}

View File

@@ -3,7 +3,6 @@ package tech.easyflow.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
@@ -88,31 +87,6 @@ public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalSta
);
}
/**
* {@inheritDoc}
*/
@Override
public void fillBotApprovalState(Bot bot) {
fillBotApprovalState(bot == null ? List.of() : List.of(bot));
}
/**
* {@inheritDoc}
*/
@Override
public void fillBotApprovalState(Collection<Bot> bots) {
fillApprovalState(
bots,
ApprovalResourceType.BOT.getCode(),
Bot::getCurrentApprovalInstanceId,
bot -> PublishStatus.from(bot.getPublishStatus()),
Bot::getPublishedSnapshotJson,
Bot::setApprovalPending,
Bot::setCurrentApprovalActionType,
Bot::setDisplayPublishStatus
);
}
/**
* 统一派生审批展示状态。
*
@@ -126,7 +100,7 @@ public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalSta
* @param displaySetter 展示状态写入器
* @param <T> 资源类型
*/
private <T> void fillApprovalState(Collection<T> resources,
<T> void fillApprovalState(Collection<T> resources,
String resourceType,
Function<T, BigInteger> instanceIdGetter,
Function<T, PublishStatus> statusGetter,

View File

@@ -0,0 +1,53 @@
package tech.easyflow.ai.service.impl;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.BotApprovalStateService;
import tech.easyflow.approval.enums.ApprovalResourceType;
import java.util.Collection;
import java.util.List;
/**
* 旧 Bot 审批展示状态派生服务实现。
*/
@Service
public class BotApprovalStateServiceImpl implements BotApprovalStateService {
private final AiResourceApprovalStateServiceImpl approvalStateService;
/**
* 创建 Bot 审批展示状态派生服务。
*
* @param approvalStateService 通用 AI 资源审批状态服务
*/
public BotApprovalStateServiceImpl(AiResourceApprovalStateServiceImpl approvalStateService) {
this.approvalStateService = approvalStateService;
}
/**
* {@inheritDoc}
*/
@Override
public void fillApprovalState(Bot bot) {
fillApprovalState(bot == null ? List.of() : List.of(bot));
}
/**
* {@inheritDoc}
*/
@Override
public void fillApprovalState(Collection<Bot> bots) {
approvalStateService.fillApprovalState(
bots,
ApprovalResourceType.BOT.getCode(),
Bot::getCurrentApprovalInstanceId,
bot -> PublishStatus.from(bot.getPublishStatus()),
Bot::getPublishedSnapshotJson,
Bot::setApprovalPending,
Bot::setCurrentApprovalActionType,
Bot::setDisplayPublishStatus
);
}
}

View File

@@ -6,7 +6,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.mapper.BotPluginMapper;
import tech.easyflow.ai.mapper.PluginItemMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.common.cache.RedisLockExecutor;
@@ -40,6 +42,9 @@ public class BotPluginServiceImpl extends ServiceImpl<BotPluginMapper, BotPlugin
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginItemMapper pluginItemMapper;
@Resource
private RedisLockExecutor redisLockExecutor;
@@ -97,4 +102,50 @@ public class BotPluginServiceImpl extends ServiceImpl<BotPluginMapper, BotPlugin
this.saveBatch(list);
});
}
/**
* {@inheritDoc}
*/
@Override
public List<PluginItem> searchPluginTools(BigInteger pluginId, BigInteger botId) {
List<PluginItem> pluginItems = pluginItemMapper.selectListByQuery(
QueryWrapper.create().eq(PluginItem::getPluginId, pluginId)
);
if (pluginItems == null || pluginItems.isEmpty() || botId == null) {
return pluginItems == null ? List.of() : pluginItems;
}
List<BigInteger> boundToolIdList = botPluginMapper.selectListByQueryAs(
QueryWrapper.create()
.select(BOT_PLUGIN.PLUGIN_ITEM_ID)
.where(BOT_PLUGIN.BOT_ID.eq(botId)),
BigInteger.class
);
Set<BigInteger> boundToolIds = boundToolIdList == null
? Set.of()
: new LinkedHashSet<>(boundToolIdList);
for (PluginItem pluginItem : pluginItems) {
pluginItem.setJoinBot(boundToolIds.contains(pluginItem.getId()));
}
return pluginItems;
}
/**
* {@inheritDoc}
*/
@Override
public List<PluginItem> getPluginTools(BigInteger botId) {
if (botId == null) {
return List.of();
}
List<BigInteger> pluginToolIds = botPluginMapper.selectListByQueryAs(
QueryWrapper.create()
.select(BOT_PLUGIN.PLUGIN_ITEM_ID)
.where(BOT_PLUGIN.BOT_ID.eq(botId)),
BigInteger.class
);
if (pluginToolIds == null || pluginToolIds.isEmpty()) {
return List.of();
}
return pluginItemMapper.selectListByIds(pluginToolIds);
}
}

View File

@@ -568,7 +568,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
queryWrapper.eq(BotMcp::getBotId, botId);
List<BotMcp> botMcpList = botMcpService.getMapper().selectListWithRelationsByQuery(queryWrapper);
botMcpList.forEach(botMcp -> {
Tool tool = mcpService.toFunction(botMcp);
Tool tool = mcpService.toFunction(botMcp.getMcpId(), botMcp.getMcpToolName());
functionList.add(tool);
});

View File

@@ -540,7 +540,7 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
public boolean updateById(DocumentCollection entity) {
DocumentCollection documentCollection = getById(entity.getId());
if (documentCollection == null) {
throw new BusinessException("bot 不存在");
throw new BusinessException("知识库不存在");
}
CustomBeanUtils.copyPropertiesIgnoreNull(entity, documentCollection);

View File

@@ -15,7 +15,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.easyagents.tool.McpTool;
import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.mapper.McpMapper;
import tech.easyflow.ai.mcp.McpTransportType;
@@ -29,6 +28,7 @@ import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.*;
/**
@@ -163,15 +163,15 @@ public class McpServiceImpl extends ServiceImpl<McpMapper, Mcp> implements McpS
}
@Override
public Tool toFunction(BotMcp botMcp) {
Mcp mcpInfo = this.getById(botMcp.getMcpId());
public Tool toFunction(BigInteger mcpId, String mcpToolName) {
Mcp mcpInfo = this.getById(mcpId);
String configJson = mcpInfo.getConfigJson();
String mcpServerName = getFirstMcpServerName(configJson);
if (StringUtil.hasText(mcpServerName)) {
McpSyncClient mcpClient = mcpClientManager.getMcpClient(mcpServerName);
List<McpSchema.Tool> tools = mcpClient.listTools().tools();
for (McpSchema.Tool tool : tools) {
if (tool.name().equals(botMcp.getMcpToolName())) {
if (tool.name().equals(mcpToolName)) {
Map<String, Object> properties = tool.inputSchema().properties();
List<String> required = tool.inputSchema().required();
McpTool mcpTool = new McpTool();

View File

@@ -200,7 +200,10 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
@Override
public void removeByEntity(Model entity) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(Model::getProviderId, entity.getProviderId()).eq(Model::getGroupName, entity.getGroupName());
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(Model::getProviderId, entity.getProviderId())
.eq(Model::getGroupName, entity.getGroupName())
.eq(Model::getTenantId, entity.getTenantId());
modelMapper.deleteByQuery(queryWrapper);
}

View File

@@ -3,7 +3,7 @@ package tech.easyflow.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.BotPlugin;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
@@ -11,10 +11,10 @@ import tech.easyflow.ai.enums.PluginType;
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityDecision;
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityService;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.mapper.BotPluginMapper;
import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.mapper.PluginItemMapper;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.entity.LoginAccount;
@@ -26,8 +26,6 @@ import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.*;
import static tech.easyflow.ai.entity.table.BotPluginTableDef.BOT_PLUGIN;
/**
* 服务层实现。
*
@@ -43,19 +41,35 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
@Resource
private PluginMapper pluginMapper;
@Resource
private BotPluginMapper botPluginMapper;
@Resource
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
@Resource
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
@Resource
private WorkflowService workflowService;
@Resource
private PluginVisibilityService pluginVisibilityService;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean savePluginTool(PluginItem pluginItem) {
Plugin plugin = pluginMapper.selectOneById(pluginItem.getPluginId());
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
if (pluginItem == null || pluginItem.getPluginId() == null) {
throw new BusinessException("插件工具参数不完整");
}
Plugin plugin = pluginMapper.selectOneByQuery(QueryWrapper.create()
.eq(Plugin::getId, pluginItem.getPluginId())
.forUpdate());
if (plugin == null) {
throw new BusinessException("插件不存在");
}
LoginAccount account = SaTokenUtil.getLoginAccount();
if (account == null || account.getTenantId() == null || plugin.getTenantId() == null
|| !account.getTenantId().toString().equals(plugin.getTenantId().toString())) {
throw new BusinessException("无权限新增该插件工具");
}
pluginVisibilityService.assertPluginVisible(
plugin.getCreatedBy(), plugin.getId(), "无权限新增该插件工具");
if (PluginType.isWorkflow(plugin.getType())) {
throw new BusinessException("工作流插件工具由系统自动维护,不支持手动新增");
}
pluginItem.setCreated(new Date());
@@ -100,41 +114,6 @@ public class PluginItemServiceImpl extends ServiceImpl<PluginItemMapper, PluginI
return true;
}
@Override
public List<PluginItem> searchPluginToolByPluginId(BigInteger pluginId, BigInteger botId) {
QueryWrapper queryAiPluginToolWrapper = QueryWrapper.create()
.select()
.eq(PluginItem::getPluginId, pluginId);
List<PluginItem> pluginItems = pluginItemMapper.selectListByQueryAs(queryAiPluginToolWrapper, PluginItem.class);
// 查询当前bot有哪些插件工具方法
QueryWrapper queryBotPluginTools = QueryWrapper.create()
.select()
.eq(BotPlugin::getBotId, botId);
List<BigInteger> aiBotPluginToolIds = botPluginMapper.selectListWithRelationsByQueryAs(queryBotPluginTools, BigInteger.class);
aiBotPluginToolIds.forEach(botPluginTooId -> {
pluginItems.forEach(item -> {
if (Objects.equals(botPluginTooId, item.getId())) {
item.setJoinBot(true);
}
});
});
return pluginItems;
}
@Override
public List<PluginItem> getPluginToolList(BigInteger botId) {
QueryWrapper queryAiPluginToolWrapper = QueryWrapper.create()
.select(BOT_PLUGIN.PLUGIN_ITEM_ID)
.from(BOT_PLUGIN)
.where(BOT_PLUGIN.BOT_ID.eq(botId));
List<BigInteger> pluginToolIds = botPluginMapper.selectListByQueryAs(queryAiPluginToolWrapper, BigInteger.class);
if (pluginToolIds == null || pluginToolIds.isEmpty()) {
return Collections.emptyList();
}
// 查询当前bots对应的有哪些pluginTool
return pluginItemMapper.selectListByIds(pluginToolIds);
}
@Override
public Result<?> pluginToolTest(String inputData, BigInteger pluginToolId) {
PluginItem pluginItem = pluginItemMapper.selectOneById(pluginToolId);

View File

@@ -15,10 +15,10 @@ import tech.easyflow.ai.mapper.PluginMapper;
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityDecision;
import tech.easyflow.ai.plugin.workflow.availability.WorkflowPluginAvailabilityService;
import tech.easyflow.ai.plugin.workflow.binding.WorkflowPluginBindingService;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.entity.LoginAccount;
@@ -57,9 +57,6 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
@Resource
PluginCategoryMappingMapper pluginCategoryMappingMapper;
@Resource
private BotPluginService botPluginService;
@Resource
private PluginItemService pluginItemService;
@Resource
@@ -70,6 +67,8 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
private WorkflowPluginBindingService workflowPluginBindingService;
@Resource
private WorkflowPluginAvailabilityService workflowPluginAvailabilityService;
@Resource
private AgentResourceReferenceService agentResourceReferenceService;
@Override
public Plugin savePlugin(Plugin plugin) {
@@ -87,34 +86,42 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
}
@Override
@Transactional
@Transactional(rollbackFor = Exception.class)
public boolean removePlugin(String id) {
Plugin plugin = pluginMapper.selectOneByQuery(QueryWrapper.create()
.eq(Plugin::getId, id)
.forUpdate());
if (plugin == null) {
throw new BusinessException("插件不存在");
}
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
if (loginAccount == null || loginAccount.getTenantId() == null || plugin.getTenantId() == null
|| !loginAccount.getTenantId().toString().equals(plugin.getTenantId().toString())) {
throw new BusinessException("无权限删除该插件");
}
pluginVisibilityService.assertPluginVisible(
plugin.getCreatedBy(), plugin.getId(), "无权限删除该插件");
List<PluginItem> pluginItems = pluginItemService.getByPluginId(id);
// 父插件行先于工具行锁定,与工具新增和 Agent 绑定保持一致锁顺序。
List<PluginItem> pluginItems = pluginItemService.list(QueryWrapper.create()
.eq(PluginItem::getPluginId, plugin.getId())
.orderBy(PluginItem::getId, true)
.forUpdate());
List<BigInteger> pluginToolIds = new ArrayList<>();
if (pluginItems != null && !pluginItems.isEmpty()) {
pluginToolIds = pluginItems.stream().map(PluginItem::getId).collect(Collectors.toList());
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in(BotPlugin::getPluginItemId, pluginToolIds);
boolean exists = botPluginService.exists(queryWrapper);
}
if (exists){
throw new BusinessException("插件中有工具还关联着bot请先取消关联");
agentResourceReferenceService.assertPluginItemsUnused(pluginToolIds);
if (!pluginToolIds.isEmpty()) {
boolean result = pluginItemService.removeByIds(pluginToolIds);
if (!result) {
log.error("删除插件工具表结果为0");
throw new BusinessException("删除失败,请稍后重试!");
}
}
if ( !pluginToolIds.isEmpty()) {
boolean result = pluginItemService.removeByIds(pluginToolIds);
if (!result){
log.error("删除插件工具表结果为0");
throw new BusinessException("删除失败,请稍后重试!");
}
}
int remove = pluginMapper.deleteById(id);
if (remove <= 0) {
log.error("删除插件结果为0");
@@ -122,7 +129,6 @@ public class PluginServiceImpl extends ServiceImpl<PluginMapper, Plugin> impleme
}
return true;
}
@Override

View File

@@ -5,65 +5,52 @@ import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
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.ai.entity.Bot;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ResourceOfflineImpactService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* 资源下线影响检查与 Bot 静默解绑实现。
* 资源下线影响检查与 Agent 静默解绑实现。
*/
@Service
public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactService {
private static final String KNOWLEDGE_NODE_TYPE = "knowledgeNode";
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
private final BotWorkflowService botWorkflowService;
private final BotDocumentCollectionService botDocumentCollectionService;
private final BotService botService;
private final WorkflowService workflowService;
private final RedisLockExecutor redisLockExecutor;
private final DocumentCollectionService documentCollectionService;
private final WorkflowPluginDependencyService workflowPluginDependencyService;
private final AgentResourceReferenceService agentResourceReferenceService;
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
BotDocumentCollectionService botDocumentCollectionService,
BotService botService,
WorkflowService workflowService,
RedisLockExecutor redisLockExecutor,
WorkflowPluginDependencyService workflowPluginDependencyService) {
this.botWorkflowService = botWorkflowService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.botService = botService;
/**
* 创建资源下线影响服务。
*
* @param workflowService 工作流服务
* @param documentCollectionService 知识库服务
* @param workflowPluginDependencyService 工作流插件依赖服务
* @param agentResourceReferenceService Agent 资源引用服务
*/
public ResourceOfflineImpactServiceImpl(WorkflowService workflowService,
DocumentCollectionService documentCollectionService,
WorkflowPluginDependencyService workflowPluginDependencyService,
AgentResourceReferenceService agentResourceReferenceService) {
this.workflowService = workflowService;
this.redisLockExecutor = redisLockExecutor;
this.documentCollectionService = documentCollectionService;
this.workflowPluginDependencyService = workflowPluginDependencyService;
this.agentResourceReferenceService = agentResourceReferenceService;
}
/**
@@ -71,17 +58,18 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
*/
@Override
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
List<OfflineImpactBindingVo> pluginBindings = workflowPluginDependencyService.listPluginsByWorkflowId(workflowId);
List<OfflineImpactBindingVo> agentBindings = listAgentsByWorkflowId(workflowId);
List<OfflineImpactBindingVo> pluginBindings =
workflowPluginDependencyService.listPluginsByWorkflowId(workflowId);
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
result.setCanProceed(true);
result.setBotBindings(botBindings);
result.setHasBotBindings(!botBindings.isEmpty());
result.setAgentBindings(agentBindings);
result.setHasAgentBindings(!agentBindings.isEmpty());
result.setPluginBindings(pluginBindings);
result.setHasPluginBindings(!pluginBindings.isEmpty());
result.setWorkflowUsages(Collections.emptyList());
result.setHasWorkflowUsages(false);
result.setMessage(resolveWorkflowOfflineImpactMessage(botBindings, pluginBindings));
result.setMessage(resolveWorkflowOfflineImpactMessage(agentBindings, pluginBindings));
return result;
}
@@ -90,18 +78,18 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
*/
@Override
public OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId) {
List<OfflineImpactBindingVo> botBindings = listBotsByKnowledgeId(knowledgeId);
List<OfflineImpactBindingVo> agentBindings = listAgentsByKnowledgeId(knowledgeId);
List<OfflineImpactBindingVo> workflowUsages = listWorkflowsUsingKnowledge(knowledgeId);
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
result.setBotBindings(botBindings);
result.setHasBotBindings(!botBindings.isEmpty());
result.setAgentBindings(agentBindings);
result.setHasAgentBindings(!agentBindings.isEmpty());
result.setWorkflowUsages(workflowUsages);
result.setHasWorkflowUsages(!workflowUsages.isEmpty());
result.setCanProceed(workflowUsages.isEmpty());
result.setMessage(workflowUsages.isEmpty()
? (botBindings.isEmpty()
? (agentBindings.isEmpty()
? "当前知识库下线后不会影响已有绑定"
: "当前知识库下线成功后,将自动从相关聊天助手中解绑")
: "当前知识库下线成功后,将自动从相关智能体中解绑")
: "当前知识库仍被工作流使用,请先调整工作流后再下线");
return result;
}
@@ -110,84 +98,53 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void unbindWorkflowFromBots(BigInteger workflowId) {
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
.eq(BotWorkflow::getWorkflowId, workflowId));
Set<BigInteger> botIds = collectBotIds(relations, BotWorkflow::getBotId);
for (BigInteger botId : botIds) {
redisLockExecutor.executeWithLock(
BOT_BINDING_LOCK_KEY_PREFIX + botId,
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT,
() -> {
botWorkflowService.remove(QueryWrapper.create()
.eq(BotWorkflow::getBotId, botId)
.eq(BotWorkflow::getWorkflowId, workflowId));
trimPublishedSnapshotBindings(botId, "workflowBindings", "workflowId", workflowId);
}
);
}
public void unbindWorkflowFromAgents(BigInteger workflowId) {
agentResourceReferenceService.unbindWorkflow(workflowId);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void unbindKnowledgeFromBots(BigInteger knowledgeId) {
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
Set<BigInteger> botIds = collectBotIds(relations, BotDocumentCollection::getBotId);
for (BigInteger botId : botIds) {
redisLockExecutor.executeWithLock(
BOT_BINDING_LOCK_KEY_PREFIX + botId,
LOCK_WAIT_TIMEOUT,
LOCK_LEASE_TIMEOUT,
() -> {
botDocumentCollectionService.remove(QueryWrapper.create()
.eq(BotDocumentCollection::getBotId, botId)
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
trimPublishedSnapshotBindings(botId, "knowledgeBindings", "knowledgeId", knowledgeId);
}
);
}
public void unbindKnowledgeFromAgents(BigInteger knowledgeId) {
agentResourceReferenceService.unbindKnowledge(knowledgeId);
}
private List<OfflineImpactBindingVo> listBotsByWorkflowId(BigInteger workflowId) {
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
.eq(BotWorkflow::getWorkflowId, workflowId));
return listBotsByIds(collectBotIds(relations, BotWorkflow::getBotId));
/**
* 汇总绑定指定工作流的 Agent。
*
* @param workflowId 工作流 ID
* @return 去重后的 Agent 摘要
*/
private List<OfflineImpactBindingVo> listAgentsByWorkflowId(BigInteger workflowId) {
return agentResourceReferenceService.listAgentsByWorkflowId(workflowId);
}
private List<OfflineImpactBindingVo> listBotsByKnowledgeId(BigInteger knowledgeId) {
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
return listBotsByIds(collectBotIds(relations, BotDocumentCollection::getBotId));
}
private List<OfflineImpactBindingVo> listBotsByIds(Set<BigInteger> botIds) {
if (botIds.isEmpty()) {
return Collections.emptyList();
}
List<Bot> bots = botService.listByIds(botIds);
Map<BigInteger, Bot> botMap = new HashMap<>();
for (Bot bot : bots) {
botMap.put(bot.getId(), bot);
}
List<OfflineImpactBindingVo> result = new ArrayList<>(botIds.size());
for (BigInteger botId : botIds) {
Bot bot = botMap.get(botId);
if (bot == null) {
continue;
}
result.add(toBindingVo(bot.getId(), bot.getTitle()));
}
return result;
/**
* 汇总绑定指定知识库的 Agent。
*
* @param knowledgeId 知识库 ID
* @return 去重后的 Agent 摘要
*/
private List<OfflineImpactBindingVo> listAgentsByKnowledgeId(BigInteger knowledgeId) {
return agentResourceReferenceService.listAgentsByKnowledgeId(knowledgeId);
}
/**
* 查询仍在设计内容中引用指定知识库的工作流。
*
* @param knowledgeId 知识库 ID
* @return 工作流摘要
*/
private List<OfflineImpactBindingVo> listWorkflowsUsingKnowledge(BigInteger knowledgeId) {
List<Workflow> workflows = workflowService.list();
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
if (knowledge == null) {
throw new BusinessException("知识库不存在,无法检查下线影响");
}
QueryWrapper queryWrapper = QueryWrapper.create()
.select(Workflow::getId, Workflow::getTitle, Workflow::getContent)
.eq(Workflow::getTenantId, knowledge.getTenantId());
List<Workflow> workflows = workflowService.list(queryWrapper);
if (workflows == null || workflows.isEmpty()) {
return Collections.emptyList();
}
@@ -197,26 +154,43 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
continue;
}
if (containsKnowledgeReference(workflow.getContent(), knowledgeId)) {
result.add(toBindingVo(workflow.getId(), workflow.getTitle()));
OfflineImpactBindingVo item = new OfflineImpactBindingVo();
item.setId(workflow.getId());
item.setTitle(workflow.getTitle());
result.add(item);
}
}
return result;
}
private String resolveWorkflowOfflineImpactMessage(List<OfflineImpactBindingVo> botBindings,
/**
* 生成工作流下线影响提示。
*
* @param agentBindings Agent 绑定
* @param pluginBindings 插件绑定
* @return 提示信息
*/
private String resolveWorkflowOfflineImpactMessage(List<OfflineImpactBindingVo> agentBindings,
List<OfflineImpactBindingVo> pluginBindings) {
if (!pluginBindings.isEmpty() && !botBindings.isEmpty()) {
return "当前工作流被插件和聊天助手引用,下线后插件将不可用,聊天助手将自动解绑";
if (!pluginBindings.isEmpty() && !agentBindings.isEmpty()) {
return "当前工作流被插件和智能体引用,下线后插件将不可用,智能体将自动解绑";
}
if (!pluginBindings.isEmpty()) {
return "当前工作流被插件引用,下线后相关插件将不可用";
}
if (!botBindings.isEmpty()) {
return "当前工作流下线成功后,将自动从相关聊天助手中解绑";
if (!agentBindings.isEmpty()) {
return "当前工作流下线成功后,将自动从相关智能体中解绑";
}
return "当前工作流下线后不会影响已有绑定";
}
/**
* 判断工作流内容是否引用指定知识库。
*
* @param content 工作流内容
* @param knowledgeId 知识库 ID
* @return 是否引用
*/
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
if (!StringUtils.hasText(content) || knowledgeId == null) {
return false;
@@ -224,7 +198,7 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
try {
Object parsed = JSON.parse(content);
if (!(parsed instanceof JSONObject root)) {
return false;
throw new BusinessException("工作流定义格式异常,无法确认知识库下线影响");
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null || nodes.isEmpty()) {
@@ -246,73 +220,10 @@ public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactSe
}
}
return false;
} catch (Exception ignored) {
return false;
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException("工作流定义解析失败,无法确认知识库下线影响");
}
}
private void trimPublishedSnapshotBindings(BigInteger botId,
String bindingsKey,
String idKey,
BigInteger resourceId) {
Bot bot = botService.getById(botId);
if (bot == null || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) {
return;
}
Map<String, Object> snapshot = new LinkedHashMap<>(bot.getPublishedSnapshotJson());
Object rawBindings = snapshot.get(bindingsKey);
if (!(rawBindings instanceof List<?> bindings)) {
return;
}
List<Map<String, Object>> filtered = new ArrayList<>();
boolean changed = false;
String expectedId = resourceId == null ? null : resourceId.toString();
for (Object item : bindings) {
if (!(item instanceof Map<?, ?> bindingMap)) {
continue;
}
Object currentId = bindingMap.get(idKey);
if (expectedId != null && currentId != null && expectedId.equals(String.valueOf(currentId))) {
changed = true;
continue;
}
filtered.add(new LinkedHashMap<>((Map<String, Object>) bindingMap));
}
if (!changed) {
return;
}
snapshot.put(bindingsKey, filtered);
Bot update = new Bot();
update.setId(botId);
update.setPublishedSnapshotJson(snapshot);
botService.updateById(update);
}
private <T> Set<BigInteger> collectBotIds(Collection<T> relations, BotIdGetter<T> getter) {
if (relations == null || relations.isEmpty()) {
return Collections.emptySet();
}
Set<BigInteger> result = new LinkedHashSet<>();
for (T relation : relations) {
BigInteger botId = getter.getBotId(relation);
if (botId != null) {
result.add(botId);
}
}
return result;
}
private OfflineImpactBindingVo toBindingVo(BigInteger id, String title) {
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
vo.setId(id);
vo.setTitle(title);
return vo;
}
@FunctionalInterface
private interface BotIdGetter<T> {
BigInteger getBotId(T relation);
}
}

View File

@@ -10,13 +10,13 @@ public class OfflineImpactCheckVo {
private boolean canProceed;
private boolean hasBotBindings;
private boolean hasAgentBindings;
private boolean hasWorkflowUsages;
private boolean hasPluginBindings;
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
private List<OfflineImpactBindingVo> agentBindings = new ArrayList<>();
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
@@ -43,21 +43,21 @@ public class OfflineImpactCheckVo {
}
/**
* 是否存在 Bot 绑定。
* 是否存在 Agent 绑定。
*
* @return 是否存在 Bot 绑定
* @return 是否存在 Agent 绑定
*/
public boolean isHasBotBindings() {
return hasBotBindings;
public boolean isHasAgentBindings() {
return hasAgentBindings;
}
/**
* 设置是否存在 Bot 绑定。
* 设置是否存在 Agent 绑定。
*
* @param hasBotBindings 是否存在 Bot 绑定
* @param hasAgentBindings 是否存在 Agent 绑定
*/
public void setHasBotBindings(boolean hasBotBindings) {
this.hasBotBindings = hasBotBindings;
public void setHasAgentBindings(boolean hasAgentBindings) {
this.hasAgentBindings = hasAgentBindings;
}
/**
@@ -79,21 +79,21 @@ public class OfflineImpactCheckVo {
}
/**
* 获取 Bot 绑定列表。
* 获取 Agent 绑定列表。
*
* @return Bot 绑定列表
* @return Agent 绑定列表
*/
public List<OfflineImpactBindingVo> getBotBindings() {
return botBindings;
public List<OfflineImpactBindingVo> getAgentBindings() {
return agentBindings;
}
/**
* 设置 Bot 绑定列表。
* 设置 Agent 绑定列表。
*
* @param botBindings Bot 绑定列表
* @param agentBindings Agent 绑定列表
*/
public void setBotBindings(List<OfflineImpactBindingVo> botBindings) {
this.botBindings = botBindings;
public void setAgentBindings(List<OfflineImpactBindingVo> agentBindings) {
this.agentBindings = agentBindings;
}
/**

View File

@@ -0,0 +1,128 @@
package tech.easyflow.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Agent 资源下线影响检查测试。
*/
public class ResourceOfflineImpactServiceImplTest {
/**
* 验证工作流影响结果使用 Agent 绑定字段并委托 Agent 解绑。
*/
@Test
public void shouldReportAndUnbindAgentWorkflowBindings() {
WorkflowService workflowService = mock(WorkflowService.class);
DocumentCollectionService documentCollectionService =
mock(DocumentCollectionService.class);
WorkflowPluginDependencyService pluginDependencyService =
mock(WorkflowPluginDependencyService.class);
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
BigInteger workflowId = BigInteger.valueOf(10);
OfflineImpactBindingVo binding = binding(BigInteger.ONE, "测试智能体");
when(referenceService.listAgentsByWorkflowId(workflowId)).thenReturn(List.of(binding));
when(pluginDependencyService.listPluginsByWorkflowId(workflowId))
.thenReturn(Collections.emptyList());
ResourceOfflineImpactServiceImpl service = new ResourceOfflineImpactServiceImpl(
workflowService, documentCollectionService, pluginDependencyService, referenceService);
OfflineImpactCheckVo result = service.checkWorkflowImpact(workflowId);
service.unbindWorkflowFromAgents(workflowId);
Assert.assertTrue(result.isHasAgentBindings());
Assert.assertEquals(List.of(binding), result.getAgentBindings());
Assert.assertTrue(result.getMessage().contains("智能体"));
verify(referenceService).unbindWorkflow(workflowId);
}
/**
* 验证知识库影响结果使用 Agent 绑定字段并委托 Agent 解绑。
*/
@Test
public void shouldReportAndUnbindAgentKnowledgeBindings() {
WorkflowService workflowService = mock(WorkflowService.class);
DocumentCollectionService documentCollectionService =
mock(DocumentCollectionService.class);
WorkflowPluginDependencyService pluginDependencyService =
mock(WorkflowPluginDependencyService.class);
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
BigInteger knowledgeId = BigInteger.valueOf(20);
OfflineImpactBindingVo binding = binding(BigInteger.TWO, "知识智能体");
DocumentCollection knowledge = new DocumentCollection();
knowledge.setTenantId(BigInteger.ONE);
when(documentCollectionService.getById(knowledgeId)).thenReturn(knowledge);
when(referenceService.listAgentsByKnowledgeId(knowledgeId)).thenReturn(List.of(binding));
when(workflowService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
ResourceOfflineImpactServiceImpl service = new ResourceOfflineImpactServiceImpl(
workflowService, documentCollectionService, pluginDependencyService, referenceService);
OfflineImpactCheckVo result = service.checkKnowledgeImpact(knowledgeId);
service.unbindKnowledgeFromAgents(knowledgeId);
Assert.assertTrue(result.isHasAgentBindings());
Assert.assertEquals(List.of(binding), result.getAgentBindings());
Assert.assertTrue(result.getMessage().contains("智能体"));
verify(referenceService).unbindKnowledge(knowledgeId);
}
/**
* 工作流定义损坏时必须阻止知识库下线,避免漏判引用关系。
*/
@Test(expected = BusinessException.class)
public void shouldFailClosedWhenWorkflowContentIsInvalid() {
WorkflowService workflowService = mock(WorkflowService.class);
DocumentCollectionService documentCollectionService =
mock(DocumentCollectionService.class);
WorkflowPluginDependencyService pluginDependencyService =
mock(WorkflowPluginDependencyService.class);
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
BigInteger knowledgeId = BigInteger.valueOf(20);
DocumentCollection knowledge = new DocumentCollection();
knowledge.setTenantId(BigInteger.ONE);
Workflow workflow = new Workflow();
workflow.setId(BigInteger.TEN);
workflow.setContent("{invalid");
when(documentCollectionService.getById(knowledgeId)).thenReturn(knowledge);
when(referenceService.listAgentsByKnowledgeId(knowledgeId))
.thenReturn(Collections.emptyList());
when(workflowService.list(any(QueryWrapper.class))).thenReturn(List.of(workflow));
ResourceOfflineImpactServiceImpl service = new ResourceOfflineImpactServiceImpl(
workflowService, documentCollectionService, pluginDependencyService, referenceService);
service.checkKnowledgeImpact(knowledgeId);
}
/**
* 创建绑定摘要。
*
* @param id 资源 ID
* @param title 标题
* @return 绑定摘要
*/
private OfflineImpactBindingVo binding(BigInteger id, String title) {
OfflineImpactBindingVo binding = new OfflineImpactBindingVo();
binding.setId(id);
binding.setTitle(title);
return binding;
}
}